ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
simatic.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 simatic.h
6 * @brief Siemens SIMATIC serial point-to-point link (PROTOCORE_ENABLE_SIMATIC) - the 3964R link protocol +
7 * the RK512 computer-link telegrams, zero-heap.
8 *
9 * The pre-Ethernet Siemens point-to-point link, two layers:
10 *
11 * - **3964R** - a byte-oriented, half-duplex link protocol (S5/S7 PtP CP modules CP 341 / CP 441 /
12 * CP 524 / CP 525). A block is framed @code STX <data, DLE bytes doubled> DLE ETX [BCC] @endcode
13 * with an interactive per-block handshake: the sender emits STX and waits for the receiver's DLE
14 * (ready) before sending the block, then waits for a final DLE (ok) or NAK (retry). On a simultaneous
15 * STX collision the low-priority station yields. The "R" variant appends a BCC - the longitudinal XOR
16 * (even parity) of every character of the block after STX, i.e. the stuffed data and the terminating
17 * DLE ETX. A payload byte equal to DLE (0x10) is doubled (transparency); a doubled DLE contributes
18 * 0x10 ^ 0x10 = 0 to the XOR, so it does not change the BCC.
19 *
20 * - **RK512** - fixed-header request/reaction telegrams carried as the 3964R block payload: SEND (write
21 * words to the partner) and FETCH (read words from the partner), addressing a data block / flag / I-O
22 * area by number + word offset + count. Siemens words are big-endian.
23 *
24 * Pure, host-tested (native_simatic) against an independent python 3964R+RK512 reference peer; the
25 * RS-232 / RS-485 UART is the application's (like the other serial-bus codecs). Control-char handshake,
26 * QVZ/ZVZ timeouts, priority arbitration, BCC, and the RK512 header layout follow the Siemens "3964(R)
27 * transmission protocol" and "RK 512 computer link" CP-module manuals.
28 *
29 * @author Douglas Quigg (dstroy0)
30 * @date 2026
31 */
32
33#ifndef PROTOCORE_SIMATIC_H
34#define PROTOCORE_SIMATIC_H
35
36#include "protocore_config.h" // the entry point: protocore_types.h for the widths
37
38#if PROTOCORE_ENABLE_SIMATIC
39
41
42// PROTOCORE_SIMATIC_BORROW - the bytes this module runs out of - is stated in protocore_config.h, which sums
43// it into its arena. A caller takes them once and passes the pointer to every call. How they
44// are carved is this module's and is never named here.
45
46// 3964R control characters (wire bytes).
47#define SIMATIC_STX 0x02
48#define SIMATIC_DLE 0x10
49#define SIMATIC_ETX 0x03
50#define SIMATIC_NAK 0x15
51
52// 3964R retry limits: Siemens RepetitionAttempts and BuildupAttempts, both default 6.
53#define SIMATIC_MAX_BLOCK_RETRY 6
54#define SIMATIC_MAX_CONN_RETRY 6
55
56/** @brief Link state (one job in flight; half-duplex). */
57typedef enum PROTO_ENUM_PACKED
58{
59 SIMATIC3964_STATE_IDLE, ///< nothing in flight
60 SIMATIC3964_STATE_TX_AWAIT_CONN, ///< sent STX, awaiting the partner's connect DLE (QVZ)
61 SIMATIC3964_STATE_TX_AWAIT_END, ///< sent the block, awaiting the partner's end DLE / NAK (QVZ)
62 SIMATIC3964_STATE_RX_COLLECT ///< replied DLE to a partner STX, collecting the block (ZVZ per-char)
63} Simatic3964State;
64
65/** @brief Sink for one outbound byte (the state machine writes to the UART through this). */
66typedef void (*Simatic3964TxFn)(void *user, uint8_t byte);
67
68/** @brief Delivery of a fully received, check-valid block payload. */
69typedef void (*Simatic3964RxFn)(void *user, const uint8_t *data, size_t len);
70
71/**
72 * @brief 3964R link owner - all link state in one named context (no file-scope mutable). The tx/rx buffers
73 * are fixed BSS; @p user is threaded to the callbacks.
74 */
75typedef struct
76{
77 Simatic3964State state;
78 proto_bool high_priority; ///< the priority bit; on an STX collision the low-priority side yields to receive
79 proto_bool with_bcc; ///< the "R" (BCC) variant
80 Simatic3964TxFn tx; ///< outbound-byte sink
81 Simatic3964RxFn rx; ///< received-block delivery
82 void *user; ///< passed to tx / rx
83
84 uint8_t txbuf[PROTOCORE_SIMATIC_BLOCK_MAX]; ///< the block body being sent (built once, re-sent on retry)
85 size_t txlen;
86 uint8_t rxbuf[PROTOCORE_SIMATIC_BLOCK_MAX]; ///< raw inbound block body (pre un-stuffing)
87 size_t rxpos;
88
89 uint8_t block_retries; ///< block resends this connection (max 6)
90 uint8_t conn_retries; ///< connection reattempts (max 6)
91 uint32_t deadline_ms; ///< QVZ (handshake) / ZVZ (inter-char) expiry
92 proto_bool prev_dle; ///< rx terminator scan: previous rx byte was an un-paired DLE
93 proto_bool await_bcc; ///< rx: DLE ETX seen, the next byte is the BCC (R variant)
94} Simatic3964Ctx;
95
96/** @brief RK512 job / telegram identifier (the "Kennung" command byte). */
97typedef enum PROTO_ENUM_PACKED
98{
99 RK512_CMD_SEND = 0x00, ///< write words to the partner
100 RK512_CMD_FETCH = 0x01, ///< read words from the partner
101 RK512_CMD_REACTION = 0x02 ///< the partner's reaction (acknowledge) telegram
102} Rk512Cmd;
103
104/** @brief RK512 memory area code (the operand area a job addresses). */
105typedef enum PROTO_ENUM_PACKED
106{
107 RK512_AREA_DB = 0x01, ///< data block (DBNR selects which)
108 RK512_AREA_DX = 0x02, ///< extended data block
109 RK512_AREA_MB = 0x03, ///< flag / marker (M)
110 RK512_AREA_EB = 0x04, ///< process input image (E)
111 RK512_AREA_AB = 0x05, ///< process output image (A)
112 RK512_AREA_PB = 0x06, ///< peripheral / I-O
113 RK512_AREA_ZB = 0x07, ///< counter (Z)
114 RK512_AREA_TB = 0x08 ///< timer (T)
115} Rk512Area;
116
117/** @brief A decoded RK512 header. */
118typedef struct
119{
120 Rk512Cmd cmd;
121 Rk512Area area;
122 uint8_t dbnr; ///< data-block number (when area is DB/DX)
123 uint16_t addr; ///< start word offset (DBADR)
124 uint16_t count; ///< word count (ANZ)
125} Rk512Header;
126
127/** @brief What bcc_3964r takes: data, len. */
128typedef struct
129{
130 const uint8_t *data;
131 size_t len;
132} SimaticBcc3964rArgs;
133
134/** @brief What build_block_3964r takes: buf, cap, data, len, with_bcc. */
135typedef struct
136{
137 uint8_t *buf;
138 size_t cap;
139 const uint8_t *data;
140 size_t len;
141 proto_bool with_bcc;
142} SimaticBuildBlock3964rArgs;
143
144/** @brief What parse_block_3964r takes: buf, len, with_bcc, out, ... */
145typedef struct
146{
147 const uint8_t *buf;
148 size_t len;
149 proto_bool with_bcc;
150 uint8_t *out; ///< receives the de-stuffed payload
151 size_t out_cap; ///< capacity of out
152 size_t *out_len; ///< receives the payload length
153} SimaticParseBlock3964rArgs;
154
155/** @brief What init_3964r takes: ctx, high_priority, with_bcc, tx, ... */
156typedef struct
157{
158 Simatic3964Ctx *ctx;
159 proto_bool high_priority;
160 proto_bool with_bcc;
161 Simatic3964TxFn tx;
162 Simatic3964RxFn rx;
163 void *user;
164} SimaticInit3964rArgs;
165
166/** @brief What send_3964r takes: ctx, data, len, now_ms. */
167typedef struct
168{
169 Simatic3964Ctx *ctx;
170 const uint8_t *data;
171 size_t len;
172 uint32_t now_ms;
173} SimaticSend3964rArgs;
174
175/** @brief What rx_byte_3964r takes: ctx, b, now_ms. */
176typedef struct
177{
178 Simatic3964Ctx *ctx;
179 uint8_t b;
180 uint32_t now_ms;
181} SimaticRxByte3964rArgs;
182
183/** @brief What tick_3964r takes: ctx, now_ms. */
184typedef struct
185{
186 Simatic3964Ctx *ctx;
187 uint32_t now_ms;
188} SimaticTick3964rArgs;
189
190/** @brief What idle_3964r takes: ctx. */
191typedef struct
192{
193 const Simatic3964Ctx *ctx;
194} SimaticIdle3964rArgs;
195
196/** @brief What build_send_rk512 takes: buf, cap, area, dbnr, addr, ... */
197typedef struct
198{
199 uint8_t *buf;
200 size_t cap;
201 Rk512Area area;
202 uint8_t dbnr;
203 uint16_t addr;
204 const uint16_t *words;
205 uint16_t wcount;
206} SimaticBuildSendRk512Args;
207
208/** @brief What build_fetch_rk512 takes: buf, cap, area, dbnr, addr, ... */
209typedef struct
210{
211 uint8_t *buf;
212 size_t cap;
213 Rk512Area area;
214 uint8_t dbnr;
215 uint16_t addr;
216 uint16_t wcount;
217} SimaticBuildFetchRk512Args;
218
219/** @brief What build_reaction_rk512 takes: buf, cap, status. */
220typedef struct
221{
222 uint8_t *buf;
223 size_t cap;
224 uint16_t status;
225} SimaticBuildReactionRk512Args;
226
227/** @brief What parse_header_rk512 takes: buf, len, out. */
228typedef struct
229{
230 const uint8_t *buf;
231 size_t len;
232 Rk512Header *out;
233} SimaticParseHeaderRk512Args;
234
235/** @brief What parse_reaction_rk512 takes: buf, len, status, data, ... */
236typedef struct
237{
238 const uint8_t *buf;
239 size_t len;
240 uint16_t *status;
241 const uint8_t **data;
242 size_t *dlen;
243} SimaticParseReactionRk512Args;
244
245/**
246 * @brief Siemens SIMATIC serial point-to-point link (PROTOCORE_ENABLE_SIMATIC) - the 3964R link protocol + the RK512
247 * computer-link telegrams, zero-heap.
248 *
249 * A caller sets the members a call takes, invokes it through ::Simatic with the bytes it runs
250 * out of, and reads the outcome off the same handle.
251 *
252 * Simatic.bcc_3964r_args.data = ...;
253 * Simatic.bcc_3964r_args.len = ...;
254 * Simatic.bcc_3964r(work);
255 * // Simatic.value is what the call reports
256 *
257 * @var SimaticNs::bcc_3964r_args what bcc_3964r takes: data, len
258 * @var SimaticNs::build_block_3964r_args what build_block_3964r takes: buf, cap, data, len, with_bcc
259 * @var SimaticNs::parse_block_3964r_args what parse_block_3964r takes: buf, len, with_bcc, out,
260 * @var SimaticNs::init_3964r_args what init_3964r takes: ctx, high_priority, with_bcc, tx,
261 * @var SimaticNs::send_3964r_args what send_3964r takes: ctx, data, len, now_ms
262 * @var SimaticNs::rx_byte_3964r_args what rx_byte_3964r takes: ctx, b, now_ms
263 * @var SimaticNs::tick_3964r_args what tick_3964r takes: ctx, now_ms
264 * @var SimaticNs::idle_3964r_args what idle_3964r takes: ctx
265 * @var SimaticNs::build_send_rk512_args what build_send_rk512 takes: buf, cap, area, dbnr, addr,
266 * @var SimaticNs::build_fetch_rk512_args what build_fetch_rk512 takes: buf, cap, area, dbnr, addr,
267 * @var SimaticNs::build_reaction_rk512_args what build_reaction_rk512 takes: buf, cap, status
268 * @var SimaticNs::parse_header_rk512_args what parse_header_rk512 takes: buf, len, out
269 * @var SimaticNs::parse_reaction_rk512_args what parse_reaction_rk512 takes: buf, len, status, data,
270 * @var SimaticNs::ok true on a complete, check-valid block; false on bad framing, a lone ...
271 * @var SimaticNs::value the value a call reports
272 * @var SimaticNs::n octets written, or 0 on overflow / bad input
273 * @var SimaticNs::bcc_3964r 3964R BCC: the longitudinal XOR (even parity) over len bytes at ...
274 * @var SimaticNs::build_block_3964r build the 3964R block body: DLE-stuffed data, then DLE ETX, then ...
275 * @var SimaticNs::parse_block_3964r parse + validate a 3964R block body (the bytes after STX): un-stuff ...
276 * @var SimaticNs::init_3964r initialize the link. high_priority: one end true, the other false ...
277 * @var SimaticNs::send_3964r start sending data (one job in flight). Emits STX and arms the ...
278 * @var SimaticNs::rx_byte_3964r feed one inbound byte at now_ms; drives the handshake / block ...
279 * @var SimaticNs::tick_3964r drive timeouts (QVZ/ZVZ) + retries; call periodically with the ...
280 * @var SimaticNs::idle_3964r true when no job is in flight and no block is being received
281 * @var SimaticNs::build_send_rk512 build a SEND telegram header + the wcount big-endian data words at ...
282 * @var SimaticNs::build_fetch_rk512 build a FETCH telegram header (no data words - the partner returns ...
283 * @var SimaticNs::build_reaction_rk512 build a reaction (acknowledge) telegram carrying status (0 = ok)
284 * @var SimaticNs::parse_header_rk512 parse an RK512 header off a telegram. true on a complete, valid ...
285 * @var SimaticNs::parse_reaction_rk512 parse a reaction telegram: the status word, and (for a FETCH ...
286 *
287 * @c work is PROTOCORE_SIMATIC_BORROW bytes the CALLER took, at an address it knows. It is not held past the call, so
288 * nothing here aliases it. How those bytes are carved is this module's and is never named here.
289 */
290typedef struct
291{
292 SimaticBcc3964rArgs bcc_3964r_args;
293 SimaticBuildBlock3964rArgs build_block_3964r_args;
294 SimaticParseBlock3964rArgs parse_block_3964r_args;
295 SimaticInit3964rArgs init_3964r_args;
296 SimaticSend3964rArgs send_3964r_args;
297 SimaticRxByte3964rArgs rx_byte_3964r_args;
298 SimaticTick3964rArgs tick_3964r_args;
299 SimaticIdle3964rArgs idle_3964r_args;
300 SimaticBuildSendRk512Args build_send_rk512_args;
301 SimaticBuildFetchRk512Args build_fetch_rk512_args;
302 SimaticBuildReactionRk512Args build_reaction_rk512_args;
303 SimaticParseHeaderRk512Args parse_header_rk512_args;
304 SimaticParseReactionRk512Args parse_reaction_rk512_args;
305 proto_bool ok;
306 uint8_t value;
307 size_t n;
308} SimaticVars;
309
310/** @brief The operands and the outcome. */
311extern SimaticVars SimaticV;
312
313/** @brief The entries. */
314typedef struct
315{
316 void (*const bcc_3964r)(uint8_t *work);
317 void (*const build_block_3964r)(uint8_t *work);
318 void (*const parse_block_3964r)(uint8_t *work);
319 void (*const init_3964r)(uint8_t *work);
320 void (*const send_3964r)(uint8_t *work);
321 void (*const rx_byte_3964r)(uint8_t *work);
322 void (*const tick_3964r)(uint8_t *work);
323 void (*const idle_3964r)(uint8_t *work);
324 void (*const build_send_rk512)(uint8_t *work);
325 void (*const build_fetch_rk512)(uint8_t *work);
326 void (*const build_reaction_rk512)(uint8_t *work);
327 void (*const parse_header_rk512)(uint8_t *work);
328 void (*const parse_reaction_rk512)(uint8_t *work);
329} SimaticNs;
330
331// What the table binds, defined once in the .c and taking one parameter each: everything
332// else an entry needs is an operand in SimaticV or a region of the borrow at a fixed offset.
333void protocore_simatic_bcc_3964r(uint8_t *work);
334void protocore_simatic_build_block_3964r(uint8_t *work);
335void protocore_simatic_parse_block_3964r(uint8_t *work);
336void protocore_simatic_init_3964r(uint8_t *work);
337void protocore_simatic_send_3964r(uint8_t *work);
338void protocore_simatic_rx_byte_3964r(uint8_t *work);
339void protocore_simatic_tick_3964r(uint8_t *work);
340void protocore_simatic_idle_3964r(uint8_t *work);
341void protocore_simatic_build_send_rk512(uint8_t *work);
342void protocore_simatic_build_fetch_rk512(uint8_t *work);
343void protocore_simatic_build_reaction_rk512(uint8_t *work);
344void protocore_simatic_parse_header_rk512(uint8_t *work);
345void protocore_simatic_parse_reaction_rk512(uint8_t *work);
346
347// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
348// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
349// `Simatic.bcc_3964r(work)` resolves to a named function and becomes a DIRECT call. An extern table
350// leaves the call indirect and the symbol live at every level, -O2 -flto included.
351static const SimaticNs Simatic __attribute__((unused)) = {
352 .bcc_3964r = protocore_simatic_bcc_3964r,
353 .build_block_3964r = protocore_simatic_build_block_3964r,
354 .parse_block_3964r = protocore_simatic_parse_block_3964r,
355 .init_3964r = protocore_simatic_init_3964r,
356 .send_3964r = protocore_simatic_send_3964r,
357 .rx_byte_3964r = protocore_simatic_rx_byte_3964r,
358 .tick_3964r = protocore_simatic_tick_3964r,
359 .idle_3964r = protocore_simatic_idle_3964r,
360 .build_send_rk512 = protocore_simatic_build_send_rk512,
361 .build_fetch_rk512 = protocore_simatic_build_fetch_rk512,
362 .build_reaction_rk512 = protocore_simatic_build_reaction_rk512,
363 .parse_header_rk512 = protocore_simatic_parse_header_rk512,
364 .parse_reaction_rk512 = protocore_simatic_parse_reaction_rk512,
365};
366
367/**
368 * @brief The PROTOCORE_SIMATIC_BORROW bytes this module's state lives in.
369 *
370 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
371 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
372 * walks, so the state lasts the life of the program.
373 *
374 * @return the span.
375 */
376uint8_t *protocore_simatic_span(void);
377
379
380#endif // PROTOCORE_ENABLE_SIMATIC
381
382#endif // PROTOCORE_SIMATIC_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_SIMATIC_BLOCK_MAX
3964R block-body buffer size (built/received bytes: DLE-stuffed payload + DLE ETX + BCC).
#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