ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
iec60870.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file iec60870.h
6 * @brief IEC 60870-5-101 / -104 telecontrol (SCADA) codec (PROTOCORE_ENABLE_IEC60870).
7 *
8 * The IEC 60870-5 utility-SCADA protocol in its two common transports:
9 * - **-104** over TCP: the APCI (`68 LEN` + 4 control octets) in its I / S / U formats,
10 * carrying an ASDU. I-format transfers numbered information (send/receive sequence
11 * numbers); S-format acknowledges; U-format does STARTDT / STOPDT / TESTFR.
12 * - **-101** over serial: the FT1.2 link frames - fixed length (`10 C A CS 16`) and variable
13 * length (`68 L L 68 C A <ASDU> CS 16`, 8-bit sum checksum), with a 1-octet link address.
14 * - The shared **ASDU** header (type id, variable structure qualifier, cause of transmission,
15 * common address) and the 3-octet Information Object Address used by both.
16 *
17 * Typed information objects for the common type ids are provided: single-point (M_SP_NA_1), double-point
18 * (M_DP_NA_1), short-float measured value (M_ME_NC_1), single command (C_SC_NA_1), and double command
19 * (C_DC_NA_1), each an IOA + value + quality / qualifier descriptor.
20 * The remaining per-type-id elements are the application's; this is the framing + ASDU layer with named
21 * type-id / COT constants for the common ones. Pure and host-tested. Bridge an RTU / outstation onto Wi-Fi:
22 * run -104 over the shipped TCP stack, or -101 over a UART through an RS-232/485 transceiver.
23 *
24 * @author Douglas Quigg (dstroy0)
25 * @date 2026
26 */
27
28#ifndef PROTOCORE_IEC60870_H
29#define PROTOCORE_IEC60870_H
30
31#include "protocore_config.h" // the entry point: protocore_types.h for the widths
32
33#if PROTOCORE_ENABLE_IEC60870
34
36
37#define IEC_START_104 0x68u ///< -104 APCI start octet (also the -101 variable-frame start)
38#define IEC_START_FIXED 0x10u ///< -101 fixed-length frame start octet
39#define IEC_STOP 0x16u ///< -101 frame stop octet
40#define IEC104_APCI_LEN 6u ///< -104 control field: start + length + 4 control octets
41
42// -104 U-format control commands (octet 1).
43#define IEC104_STARTDT_ACT 0x07u
44#define IEC104_STARTDT_CON 0x0Bu
45#define IEC104_STOPDT_ACT 0x13u
46#define IEC104_STOPDT_CON 0x23u
47#define IEC104_TESTFR_ACT 0x43u
48#define IEC104_TESTFR_CON 0x83u
49
50// Common ASDU type identifications (IEC 60870-5-101 ch. 7.2.1).
51#define IEC_TYPE_M_SP_NA_1 1 ///< single-point information
52#define IEC_TYPE_M_DP_NA_1 3 ///< double-point information
53#define IEC_TYPE_M_ME_NA_1 9 ///< measured value, normalized
54#define IEC_TYPE_M_ME_NB_1 11 ///< measured value, scaled
55#define IEC_TYPE_M_ME_NC_1 13 ///< measured value, short float
56#define IEC_TYPE_M_IT_NA_1 15 ///< integrated totals (counter)
57#define IEC_TYPE_C_SC_NA_1 45 ///< single command
58#define IEC_TYPE_C_DC_NA_1 46 ///< double command
59#define IEC_TYPE_M_EI_NA_1 70 ///< end of initialization
60#define IEC_TYPE_C_IC_NA_1 100 ///< interrogation command
61#define IEC_TYPE_C_CS_NA_1 103 ///< clock synchronization command
62
63// Common causes of transmission (COT, low 6 bits).
64#define IEC_COT_PERIODIC 1
65#define IEC_COT_SPONTANEOUS 3
66#define IEC_COT_REQUEST 5
67#define IEC_COT_ACTIVATION 6
68#define IEC_COT_ACT_CON 7
69#define IEC_COT_DEACTIVATION 8
70#define IEC_COT_ACT_TERM 10
71#define IEC_COT_INTERROGATED 20
72
73// -101 link-layer control-field function codes (low nibble; direction/FCB/FCV in the high bits).
74#define IEC_FC_RESET_LINK 0x00 ///< reset of remote link (SEND/CONFIRM)
75#define IEC_FC_TEST_LINK 0x02 ///< test function for link
76#define IEC_FC_USER_DATA_CONFIRM 0x03 ///< user data, confirmed
77#define IEC_FC_USER_DATA_NOREPLY 0x04 ///< user data, no reply
78#define IEC_FC_REQUEST_CLASS1 0x0A ///< request class 1 data
79#define IEC_FC_REQUEST_CLASS2 0x0B ///< request class 2 data
80
81/** @brief -104 APCI frame formats. */
82typedef enum PROTO_ENUM_PACKED
83{
84 IEC104_I = 0, ///< information transfer (numbered; carries an ASDU)
85 IEC104_S, ///< supervisory (acknowledge only)
86 IEC104_U, ///< unnumbered (STARTDT / STOPDT / TESTFR)
87} Iec104Format;
88
89/** @brief A parsed -104 APCI. */
90typedef struct
91{
92 Iec104Format format;
93 uint16_t ns; ///< send sequence number (I-format)
94 uint16_t nr; ///< receive sequence number (I + S formats)
95 uint8_t u_cmd; ///< U-format command octet
96 const uint8_t *asdu; ///< ASDU slice (I-format), or nullptr
97 size_t asdu_len;
98} Iec104Apci;
99
100/** @brief A parsed ASDU header (the 6 octets before the information objects). */
101typedef struct
102{
103 uint8_t type_id;
104 proto_bool sq; ///< structure qualifier: elements share one base IOA
105 uint8_t count; ///< number of information objects / elements (0..127)
106 proto_bool test; ///< COT test bit
107 proto_bool negative; ///< COT positive/negative confirm bit
108 uint8_t cot; ///< cause of transmission (low 6 bits)
109 uint8_t orig_addr; ///< originator address
110 uint16_t common_addr; ///< common address of the ASDU
111} IecAsduHeader;
112
113// --- IEC 60870-5-104 APCI (over TCP) ---
114
115/** @brief Build an I-format APDU carrying @p asdu (numbered with @p ns / @p nr). */
116size_t protocore_iec104_build_i(uint8_t *buf, size_t cap, uint16_t ns, uint16_t nr, const uint8_t *asdu,
117 size_t asdu_len);
118
119/** @brief Build an S-format (supervisory) APDU acknowledging up to @p nr. */
120size_t protocore_iec104_build_s(uint8_t *buf, size_t cap, uint16_t nr);
121
122/** @brief Build a U-format APDU with command @p u_cmd (IEC104_STARTDT_ACT, ...). */
123size_t protocore_iec104_build_u(uint8_t *buf, size_t cap, uint8_t u_cmd);
124
125/** @brief Parse one -104 APDU. Fills @p out and @p consumed (the whole APDU length). */
126proto_bool protocore_iec104_parse(const uint8_t *buf, size_t len, Iec104Apci *out, size_t *consumed);
127
128// --- ASDU header + Information Object Address (shared by -101 and -104) ---
129
130/** @brief Build the 6-octet ASDU header (type id, VSQ, COT[2], common address[2]). */
131size_t protocore_iec_asdu_build_header(uint8_t *buf, size_t cap, const IecAsduHeader *h);
132
133/** @brief Parse the 6-octet ASDU header; @p consumed is 6 on success. */
134proto_bool protocore_iec_asdu_parse_header(const uint8_t *buf, size_t len, IecAsduHeader *out, size_t *consumed);
135
136/** @brief Write a 3-octet Information Object Address (little-endian). */
137size_t protocore_iec_put_ioa(uint8_t *buf, size_t cap, uint32_t ioa);
138
139/** @brief Read a 3-octet Information Object Address (little-endian). */
140uint32_t protocore_iec_get_ioa(const uint8_t *p);
141
142// --- typed information objects (the value(s) inside an ASDU, without a time tag) ---
143//
144// Each object is an IOA(3) followed by the type-specific value + a quality descriptor. The quality bits
145// are shared: the low bit is value-specific (SPI for a single point, OV overflow for a measured value),
146// and bits 4..7 are BL (blocked) / SB (substituted) / NT (not topical) / IV (invalid).
147
148#define IEC_QUAL_OV 0x01u ///< measured-value overflow (QDS bit 0)
149#define IEC_QUAL_BL 0x10u ///< blocked
150#define IEC_QUAL_SB 0x20u ///< substituted
151#define IEC_QUAL_NT 0x40u ///< not topical
152#define IEC_QUAL_IV 0x80u ///< invalid
153#define IEC_SCO_ON 0x01u ///< single command: command state ON (SCS)
154#define IEC_SCO_SE 0x80u ///< single command: select (1) vs execute (0)
155
156// Double-point value (DPI in a DIQ, DCS in a DCO): the low 2 bits of the octet.
157#define IEC_DP_INDETERMINATE 0u ///< indeterminate or intermediate state
158#define IEC_DP_OFF 1u ///< determined state OFF
159#define IEC_DP_ON 2u ///< determined state ON
160#define IEC_DP_INDETERMINATE_3 3u ///< indeterminate state (alternate encoding)
161#define IEC_DP_MASK 0x03u ///< the DPI / DCS field occupies bits 0..1
162#define IEC_DCO_SE 0x80u ///< double command: select (1) vs execute (0)
163#define IEC_DCO_QU_SHIFT 2u ///< double command: qualifier of command (QU) occupies bits 2..6
164#define IEC_DCO_QU_MASK 0x1Fu ///< the 5-bit qualifier value (after shifting down)
165
166// Binary counter reading (BCR) sequence-notation octet (M_IT_NA_1, type 15).
167#define IEC_BCR_SQ_MASK 0x1Fu ///< the 5-bit sequence number occupies bits 0..4
168#define IEC_BCR_CY 0x20u ///< counter overflow (carry) occurred since the last reading
169#define IEC_BCR_CA 0x40u ///< counter was adjusted since the last reading
170#define IEC_BCR_IV 0x80u ///< the counter reading is invalid
171
172/**
173 * @brief Build a single-point information object (M_SP_NA_1, type 1): IOA(3) + SIQ(1).
174 * @param on the single-point value (SPI bit).
175 * @param quality the quality flags (IEC_QUAL_BL / _SB / _NT / _IV; the low bits are ignored).
176 * @return 4 on success, 0 on overflow / a null buffer.
177 */
178size_t protocore_iec_io_build_sp(uint8_t *buf, size_t cap, uint32_t ioa, proto_bool on, uint8_t quality);
179
180/** @brief Parse a single-point information object into its IOA, value, and quality flags. False if < 4 octets. */
181proto_bool protocore_iec_io_parse_sp(const uint8_t *buf, size_t len, uint32_t *ioa, proto_bool *on, uint8_t *quality);
182
183/**
184 * @brief Build a short-float measured-value object (M_ME_NC_1, type 13): IOA(3) + IEEE-754 float(4, LE) + QDS(1).
185 * @return 8 on success, 0 on overflow / a null buffer.
186 */
187size_t protocore_iec_io_build_float(uint8_t *buf, size_t cap, uint32_t ioa, float value, uint8_t qds);
188
189/** @brief Parse a short-float measured-value object into its IOA, value, and quality byte. False if < 8 octets. */
190proto_bool protocore_iec_io_parse_float(const uint8_t *buf, size_t len, uint32_t *ioa, float *value, uint8_t *qds);
191
192/**
193 * @brief Build a scaled measured-value object (M_ME_NB_1, type 11): IOA(3) + SVA(2, signed 16-bit LE) + QDS(1).
194 * The scaled value is a raw signed integer the receiver rescales by its engineering range.
195 * @return 6 on success, 0 on overflow / a null buffer.
196 */
197size_t protocore_iec_io_build_scaled(uint8_t *buf, size_t cap, uint32_t ioa, int16_t value, uint8_t qds);
198
199/** @brief Parse a scaled measured-value object into its IOA, signed value, and quality byte. False if < 6 octets. */
200proto_bool protocore_iec_io_parse_scaled(const uint8_t *buf, size_t len, uint32_t *ioa, int16_t *value, uint8_t *qds);
201
202/**
203 * @brief Build a normalized measured-value object (M_ME_NA_1, type 9): IOA(3) + NVA(2, signed 16-bit LE) +
204 * QDS(1). @p value is the normalized fraction in [-1, +1), stored as value * 32768 (clamped to the
205 * signed-16-bit range) - the fixed-point companion to the scaled (raw integer) and short-float forms.
206 * @return 6 on success, 0 on overflow / a null buffer.
207 */
208size_t protocore_iec_io_build_normalized(uint8_t *buf, size_t cap, uint32_t ioa, float value, uint8_t qds);
209
210/** @brief Parse a normalized measured-value object into its IOA, the fraction value (NVA / 32768), and the
211 * quality byte. False if < 6 octets. */
212proto_bool protocore_iec_io_parse_normalized(const uint8_t *buf, size_t len, uint32_t *ioa, float *value, uint8_t *qds);
213
214/**
215 * @brief Build an integrated-totals object (M_IT_NA_1, type 15): IOA(3) + BCR(5) = a signed 32-bit counter
216 * value (LE, two's complement) followed by a sequence-notation octet (the 5-bit sequence number in
217 * IEC_BCR_SQ_MASK plus the IEC_BCR_CY / _CA / _IV flags). The counter reading is the metering (energy /
218 * pulse total) companion to the measured values.
219 * @return 8 on success, 0 on overflow / a null buffer.
220 */
221size_t protocore_iec_io_build_counter(uint8_t *buf, size_t cap, uint32_t ioa, int32_t value, uint8_t seq);
222
223/** @brief Parse an integrated-totals object into its IOA, the signed counter value, and the raw sequence-notation
224 * octet (test it with IEC_BCR_SQ_MASK / _CY / _CA / _IV). False if < 8 octets. */
225proto_bool protocore_iec_io_parse_counter(const uint8_t *buf, size_t len, uint32_t *ioa, int32_t *value, uint8_t *seq);
226
227/**
228 * @brief Build a single command object (C_SC_NA_1, type 45): IOA(3) + SCO(1).
229 * @param on the commanded state (SCS bit).
230 * @param select true for a select, false for an execute (S/E bit).
231 * @return 4 on success, 0 on overflow / a null buffer.
232 */
233size_t protocore_iec_io_build_sc(uint8_t *buf, size_t cap, uint32_t ioa, proto_bool on, proto_bool select);
234
235/** @brief Parse a single command object into its IOA, commanded state, and select/execute flag. False if < 4. */
236proto_bool protocore_iec_io_parse_sc(const uint8_t *buf, size_t len, uint32_t *ioa, proto_bool *on, proto_bool *select);
237
238/**
239 * @brief Build a double-point information object (M_DP_NA_1, type 3): IOA(3) + DIQ(1).
240 * @param dpi the 2-bit double-point value (IEC_DP_OFF / _ON / _INDETERMINATE).
241 * @param quality the quality flags (IEC_QUAL_BL / _SB / _NT / _IV; other bits are ignored).
242 * @return 4 on success, 0 on overflow / a null buffer.
243 */
244size_t protocore_iec_io_build_dp(uint8_t *buf, size_t cap, uint32_t ioa, uint8_t dpi, uint8_t quality);
245
246/** @brief Parse a double-point information object into its IOA, 2-bit value, and quality flags. False if < 4. */
247proto_bool protocore_iec_io_parse_dp(const uint8_t *buf, size_t len, uint32_t *ioa, uint8_t *dpi, uint8_t *quality);
248
249/**
250 * @brief Build a double command object (C_DC_NA_1, type 46): IOA(3) + DCO(1).
251 * @param dcs the 2-bit double command state (IEC_DP_OFF / _ON).
252 * @param qu the 5-bit qualifier of command (QU).
253 * @param select true for a select, false for an execute (S/E bit).
254 * @return 4 on success, 0 on overflow / a null buffer.
255 */
256size_t protocore_iec_io_build_dc(uint8_t *buf, size_t cap, uint32_t ioa, uint8_t dcs, uint8_t qu, proto_bool select);
257
258/** @brief Parse a double command object into its IOA, 2-bit state, qualifier, and select/execute flag. False if < 4. */
259proto_bool protocore_iec_io_parse_dc(const uint8_t *buf, size_t len, uint32_t *ioa, uint8_t *dcs, uint8_t *qu,
260 proto_bool *select);
261
262// --- IEC 60870-5-101 FT1.2 link frames (over serial) ---
263
264/** @brief Build a fixed-length frame: 10 C A CS 16 (1-octet link address). */
265size_t protocore_iec101_build_fixed(uint8_t *buf, size_t cap, uint8_t control, uint8_t addr);
266
267/** @brief Build a variable-length frame: 68 L L 68 C A <asdu> CS 16 (1-octet link address). */
268size_t protocore_iec101_build_variable(uint8_t *buf, size_t cap, uint8_t control, uint8_t addr, const uint8_t *asdu,
269 uint8_t asdu_len);
270
271/** @brief A parsed -101 FT1.2 frame. */
272typedef struct
273{
274 proto_bool fixed; ///< true => fixed-length frame (no ASDU)
275 uint8_t control; ///< link control field
276 uint8_t addr; ///< link address
277 const uint8_t *asdu; ///< ASDU slice (variable frame), or nullptr
278 uint8_t asdu_len;
279} Iec101Frame;
280
281/** @brief Parse one -101 FT1.2 frame (validates start/stop, doubled length, sum checksum). */
282proto_bool protocore_iec101_parse(const uint8_t *buf, size_t len, Iec101Frame *out, size_t *consumed);
283
285
286#endif // PROTOCORE_ENABLE_IEC60870
287
288#endif // PROTOCORE_IEC60870_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97