Problem
The current DNP3 implementation is based off of Modbus and has some limitations.
Mapping from IO addresses (for example %IW0) is based off of the data type,
which is different from the DNP3 model of making a point available in different
representations (classes). The current approach results in data loss due to
truncation of values. For example, it is possible to have a IEC variable of REAL
type, but that cannot be mapped to a DNP3 point without first converting to fixed
point. This proposal is to address those issues.
Outside of Scope
For the purpose of this work, we assume the following it out of scope
- Mapping variables to different communcation drivers. That is, we will continue
to use a global address space and assume the user only utilizes communcation
protocol.
- Multiple indpendent instances of a communcation driver. For example, you can
only have one DNP3 Outstation.
Solution
In IEC languages, PLC addresses define a direction (I, Q, M) and a data width
(see below). The data width should not be confused with data type.
Data width |
Bits |
Compatible types |
Symbol |
bit |
1 |
BOOL |
X |
byte |
8 |
BYTE, SINT, USINT |
B |
word |
16 |
WORD, INT, UINT |
W |
double word |
32 |
DWORD, DINT, UDINT, REAL |
D |
long word |
64 |
LWORD, LINT, ULINT, LREAL |
L |
Currently, OpenPLC generates glue arrays for some of these types, however that support
is incomplete. In order to support each of these data types without precision loss,
OpenPLC needs to support "glueing" to these types.
Adding support for all of these would substantially increase the memory footprint. Furthmore,
iterating over the arrays would often iterate over values are are un-mapped. Thus, a new glue
structure is proposed where we track both an index and the pointer to the value
template<T>
struct GlueVariable {
std::uint16_t index;
T* value;
... // See below for additional members
};
The index here defines the index for DNP3.
Declaration of the glue arrays need only allocate sufficent space for the mapped values and
writing to the communcation protocol is simply a matter of iterating over the all glue arrays, which
is known at compile time.
Receipt of a new value can be in different formats, for example receving a AnalogOutputDouble64 for
a value that is mapped to an IEC SINT. In this case, we must iterate over all glue arrays, find a matching
index. Because we know which glue array it is in, we can appropriately cast to the IEC data type. The
GlueVariable structure can then define cast operations for each data width available from DNP3.
template<T>
struct GlueVariable {
std::uint16_t index;
T* value;
T as(const std::uint8_t v) { return (T)v; }
... // and so on for other DNP3 types
};
Searching over the arrays incurs a performance penalty which can be addressed in the future by with an
additional array of pointers to appropriate glue variable slots. I believe it is possible to handle the
casting in this case, but that is for future work.
Intergration with Modbus and Periperals
The current design for integration Modbus and periperals is based on glue variables that are of fixed
size (and interation over those in order to discover mappings to values). In order to limit the scope
of this work, we will keep that approach in place and allow DNP3 to work with variables in either
format.
Offsets
The current DNP3 mapping allows for the user to supply an offset - offsets define a runtime operation
to change the mapping index to the glue arrays. This will continue to be supported, however, only for
the data types (not data widths) that current support this type.
DNP3 Types
DNP3 supports different representations for a single data point. For example object group 40 (analog output
status) may be represented as a 32-bit signed integer, 16-bit signed integer, single-precision floating point,
and double-precision floating point. The same logical point can also be represented with object group 41, 42
and 43.
For "simplicity", we define the following as the default object groups when publishing data over DNP3:
Type |
Location Symbol |
Direction |
Object Group |
Variation |
Friendly DNP3 Name |
BOOL |
X |
I |
10 |
1 |
Binary output - packed format (1) |
BOOL |
X |
Q |
1 |
1 |
Binary input - packed format (1) |
SINT |
B |
I |
41 |
2 |
Analog output - 16-bit |
SINT |
B |
Q |
3x |
any |
Analog input |
USINT |
B |
I |
20 |
6 |
Counter - unsigned 16-bit w/o flag |
USINT |
B |
Q |
N/A |
|
|
INT |
W |
I |
41 |
2 |
Analog output - 16-bit |
INT |
W |
Q |
3x |
any |
Analog input |
UINT |
W |
I |
20 |
5 |
Counter - unsigned 32-bit w/o flag |
UINT |
W |
Q |
N/A |
|
|
DINT |
D |
I |
41 |
1 |
Analog output - 32-bit |
DINT |
D |
Q |
3x |
any |
Analog input |
UDINT |
D |
I |
20 |
5 |
Counter - unsigned 32-bit w/o flag |
UDINT |
D |
Q |
N/A |
|
|
REAL |
D |
I |
41 |
3 |
Analog output - single-preicison |
REAL |
D |
Q |
3x |
any |
Analog input |
LREAL |
L |
I |
41 |
4 |
Analog output - double-precision |
LREAL |
L |
Q |
3x |
any |
Analog input |
(1) BOOL values that are only partially mapped are supported for each item in the 8-bit field that has an address.
The following types will not be mapped to DNP3 - consequently there is no glue mapping required for these.
Type |
Location Symbol |
Reason |
BYTE |
B |
Not currently supporting bit fields |
WORD |
W |
Not currently supporting bit fields |
DWORD |
D |
Not currently supporting bit fields |
LWORD |
L |
Not currently supporting bit fields |
LINT |
L |
DNP3 does not support 64-bit integers |
LUINT |
L |
DNP3 does not support 64-bit integers |
Future Work
This proposal clearly does not address all of the needs for a complete DNP3 implementation. The goal is to enable sane defaults with the option of allowing further user-customization in the future either from the OpenPLC web front end, configuration files, or the PLCOpen project file. Furthermore, the focus is on a subset of data types that define some useful applications - these won't address all needs, but can be improved upon in the future.