ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
iec60870.h
Go to the documentation of this file.
1// 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 (PC_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"
32
33#if PC_ENABLE_IEC60870
34
35#include <stddef.h>
36#include <stdint.h>
37
38#define IEC_START_104 0x68u ///< -104 APCI start octet (also the -101 variable-frame start)
39#define IEC_START_FIXED 0x10u ///< -101 fixed-length frame start octet
40#define IEC_STOP 0x16u ///< -101 frame stop octet
41#define IEC104_APCI_LEN 6u ///< -104 control field: start + length + 4 control octets
42
43// -104 U-format control commands (octet 1).
44#define IEC104_STARTDT_ACT 0x07u
45#define IEC104_STARTDT_CON 0x0Bu
46#define IEC104_STOPDT_ACT 0x13u
47#define IEC104_STOPDT_CON 0x23u
48#define IEC104_TESTFR_ACT 0x43u
49#define IEC104_TESTFR_CON 0x83u
50
51// Common ASDU type identifications (IEC 60870-5-101 ch. 7.2.1).
52#define IEC_TYPE_M_SP_NA_1 1 ///< single-point information
53#define IEC_TYPE_M_DP_NA_1 3 ///< double-point information
54#define IEC_TYPE_M_ME_NA_1 9 ///< measured value, normalized
55#define IEC_TYPE_M_ME_NB_1 11 ///< measured value, scaled
56#define IEC_TYPE_M_ME_NC_1 13 ///< measured value, short float
57#define IEC_TYPE_M_IT_NA_1 15 ///< integrated totals (counter)
58#define IEC_TYPE_C_SC_NA_1 45 ///< single command
59#define IEC_TYPE_C_DC_NA_1 46 ///< double command
60#define IEC_TYPE_M_EI_NA_1 70 ///< end of initialization
61#define IEC_TYPE_C_IC_NA_1 100 ///< interrogation command
62#define IEC_TYPE_C_CS_NA_1 103 ///< clock synchronization command
63
64// Common causes of transmission (COT, low 6 bits).
65#define IEC_COT_PERIODIC 1
66#define IEC_COT_SPONTANEOUS 3
67#define IEC_COT_REQUEST 5
68#define IEC_COT_ACTIVATION 6
69#define IEC_COT_ACT_CON 7
70#define IEC_COT_DEACTIVATION 8
71#define IEC_COT_ACT_TERM 10
72#define IEC_COT_INTERROGATED 20
73
74// -101 link-layer control-field function codes (low nibble; direction/FCB/FCV in the high bits).
75#define IEC_FC_RESET_LINK 0x00 ///< reset of remote link (SEND/CONFIRM)
76#define IEC_FC_TEST_LINK 0x02 ///< test function for link
77#define IEC_FC_USER_DATA_CONFIRM 0x03 ///< user data, confirmed
78#define IEC_FC_USER_DATA_NOREPLY 0x04 ///< user data, no reply
79#define IEC_FC_REQUEST_CLASS1 0x0A ///< request class 1 data
80#define IEC_FC_REQUEST_CLASS2 0x0B ///< request class 2 data
81
82/** @brief -104 APCI frame formats. */
83enum class Iec104Format : uint8_t
84{
85 IEC104_I = 0, ///< information transfer (numbered; carries an ASDU)
86 IEC104_S, ///< supervisory (acknowledge only)
87 IEC104_U, ///< unnumbered (STARTDT / STOPDT / TESTFR)
88};
89
90/** @brief A parsed -104 APCI. */
91struct Iec104Apci
92{
93 Iec104Format format;
94 uint16_t ns; ///< send sequence number (I-format)
95 uint16_t nr; ///< receive sequence number (I + S formats)
96 uint8_t u_cmd; ///< U-format command octet
97 const uint8_t *asdu; ///< ASDU slice (I-format), or nullptr
98 size_t asdu_len;
99};
100
101/** @brief A parsed ASDU header (the 6 octets before the information objects). */
102struct IecAsduHeader
103{
104 uint8_t type_id;
105 bool sq; ///< structure qualifier: elements share one base IOA
106 uint8_t count; ///< number of information objects / elements (0..127)
107 bool test; ///< COT test bit
108 bool negative; ///< COT positive/negative confirm bit
109 uint8_t cot; ///< cause of transmission (low 6 bits)
110 uint8_t orig_addr; ///< originator address
111 uint16_t common_addr; ///< common address of the ASDU
112};
113
114// --- IEC 60870-5-104 APCI (over TCP) ---
115
116/** @brief Build an I-format APDU carrying @p asdu (numbered with @p ns / @p nr). */
117size_t pc_iec104_build_i(uint8_t *buf, size_t cap, uint16_t ns, uint16_t nr, const uint8_t *asdu, size_t asdu_len);
118
119/** @brief Build an S-format (supervisory) APDU acknowledging up to @p nr. */
120size_t pc_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 pc_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). */
126bool pc_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 pc_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. */
134bool pc_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 pc_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 pc_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 pc_iec_io_build_sp(uint8_t *buf, size_t cap, uint32_t ioa, 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. */
181bool pc_iec_io_parse_sp(const uint8_t *buf, size_t len, uint32_t *ioa, 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 pc_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. */
190bool pc_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 pc_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. */
200bool pc_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 pc_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. */
212bool pc_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 pc_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. */
225bool pc_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 pc_iec_io_build_sc(uint8_t *buf, size_t cap, uint32_t ioa, bool on, bool select);
234
235/** @brief Parse a single command object into its IOA, commanded state, and select/execute flag. False if < 4. */
236bool pc_iec_io_parse_sc(const uint8_t *buf, size_t len, uint32_t *ioa, bool *on, 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 pc_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. */
247bool pc_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 pc_iec_io_build_dc(uint8_t *buf, size_t cap, uint32_t ioa, uint8_t dcs, uint8_t qu, bool select);
257
258/** @brief Parse a double command object into its IOA, 2-bit state, qualifier, and select/execute flag. False if < 4. */
259bool pc_iec_io_parse_dc(const uint8_t *buf, size_t len, uint32_t *ioa, uint8_t *dcs, uint8_t *qu, bool *select);
260
261// --- IEC 60870-5-101 FT1.2 link frames (over serial) ---
262
263/** @brief Build a fixed-length frame: 10 C A CS 16 (1-octet link address). */
264size_t pc_iec101_build_fixed(uint8_t *buf, size_t cap, uint8_t control, uint8_t addr);
265
266/** @brief Build a variable-length frame: 68 L L 68 C A <asdu> CS 16 (1-octet link address). */
267size_t pc_iec101_build_variable(uint8_t *buf, size_t cap, uint8_t control, uint8_t addr, const uint8_t *asdu,
268 uint8_t asdu_len);
269
270/** @brief A parsed -101 FT1.2 frame. */
271struct Iec101Frame
272{
273 bool fixed; ///< true => fixed-length frame (no ASDU)
274 uint8_t control; ///< link control field
275 uint8_t addr; ///< link address
276 const uint8_t *asdu; ///< ASDU slice (variable frame), or nullptr
277 uint8_t asdu_len;
278};
279
280/** @brief Parse one -101 FT1.2 frame (validates start/stop, doubled length, sum checksum). */
281bool pc_iec101_parse(const uint8_t *buf, size_t len, Iec101Frame *out, size_t *consumed);
282
283#endif // PC_ENABLE_IEC60870
284#endif // PROTOCORE_IEC60870_H
User-facing configuration for ProtoCore.