ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
simatic.cpp
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 simatic.cpp
6 * @brief Siemens SIMATIC serial: 3964R link protocol + RK512 telegrams. See simatic.h.
7 */
8
10
11#if PC_ENABLE_SIMATIC
12
13#include <string.h>
14
15// ---------------------------------------------------------------------------
16// Big-endian word helpers (Siemens words are big-endian; no stdlib)
17// ---------------------------------------------------------------------------
18
19static inline void wr_u16(uint8_t *p, uint16_t v)
20{
21 p[0] = (uint8_t)(v >> 8);
22 p[1] = (uint8_t)(v & 0xFF);
23}
24
25static inline uint16_t rd_u16(const uint8_t *p)
26{
27 return (uint16_t)((uint16_t)p[0] << 8 | p[1]);
28}
29
30// ---------------------------------------------------------------------------
31// 3964R block framing
32// ---------------------------------------------------------------------------
33
34uint8_t pc_3964r_bcc(const uint8_t *data, size_t len)
35{
36 uint8_t x = 0;
37 for (size_t i = 0; i < len; i++)
38 {
39 x ^= data[i];
40 }
41 return x;
42}
43
44size_t pc_3964r_build_block(uint8_t *buf, size_t cap, const uint8_t *data, size_t len, bool with_bcc)
45{
46 if (!buf || (!data && len))
47 {
48 return 0;
49 }
50 size_t o = 0;
51 for (size_t i = 0; i < len; i++)
52 {
53 if (o >= cap)
54 {
55 return 0;
56 }
57 buf[o++] = data[i];
58 if (data[i] == SIMATIC_DLE) // transparency: a payload DLE is doubled
59 {
60 if (o >= cap)
61 {
62 return 0;
63 }
64 buf[o++] = SIMATIC_DLE;
65 }
66 }
67 if (o + 2 > cap)
68 {
69 return 0;
70 }
71 buf[o++] = SIMATIC_DLE;
72 buf[o++] = SIMATIC_ETX;
73 if (with_bcc)
74 {
75 if (o >= cap)
76 {
77 return 0;
78 }
79 buf[o] = pc_3964r_bcc(buf, o); // XOR over the stuffed data + DLE ETX
80 o++;
81 }
82 return o;
83}
84
85// Append one destuffed payload byte; false when the caller's buffer is full.
86static bool put_byte(uint8_t *out, size_t out_cap, size_t *oo, uint8_t b)
87{
88 if (*oo >= out_cap)
89 {
90 return false;
91 }
92 out[(*oo)++] = b;
93 return true;
94}
95
96// DLE ETX consumed at @p i: the trailing BCC must be present and match the XOR over the
97// stuffed data + DLE ETX.
98static bool bcc_ok(const uint8_t *buf, size_t i, size_t len, bool with_bcc)
99{
100 if (!with_bcc)
101 {
102 return true;
103 }
104 if (i >= len)
105 {
106 return false; // missing BCC
107 }
108 return pc_3964r_bcc(buf, i) == buf[i];
109}
110
111bool pc_3964r_parse_block(const uint8_t *buf, size_t len, bool with_bcc, uint8_t *out, size_t out_cap, size_t *out_len)
112{
113 if (!buf || !out || !out_len)
114 {
115 return false;
116 }
117 size_t oo = 0;
118 size_t i = 0;
119 while (i < len)
120 {
121 uint8_t b = buf[i];
122 if (b != SIMATIC_DLE)
123 {
124 if (!put_byte(out, out_cap, &oo, b))
125 {
126 return false;
127 }
128 i++;
129 continue;
130 }
131 if (i + 1 >= len)
132 {
133 return false; // dangling DLE (truncated)
134 }
135 uint8_t n = buf[i + 1];
136 if (n == SIMATIC_DLE) // doubled -> one literal DLE
137 {
138 if (!put_byte(out, out_cap, &oo, SIMATIC_DLE))
139 {
140 return false;
141 }
142 i += 2;
143 continue;
144 }
145 if (n != SIMATIC_ETX)
146 {
147 return false; // DLE + illegal control byte
148 }
149 i += 2; // terminator
150 if (!bcc_ok(buf, i, len, with_bcc))
151 {
152 return false;
153 }
154 *out_len = oo;
155 return true;
156 }
157 return false; // no DLE ETX terminator
158}
159
160// ---------------------------------------------------------------------------
161// 3964R link state machine
162// ---------------------------------------------------------------------------
163
164static inline void emit(Simatic3964Ctx *ctx, uint8_t b)
165{
166 if (ctx->tx)
167 {
168 ctx->tx(ctx->user, b);
169 }
170}
171
172static void send_stx_await_conn(Simatic3964Ctx *ctx, uint32_t now_ms)
173{
174 emit(ctx, SIMATIC_STX);
175 ctx->state = Simatic3964State::TX_AWAIT_CONN;
176 ctx->deadline_ms = now_ms + PC_SIMATIC_QVZ_MS;
177}
178
179static void send_block(Simatic3964Ctx *ctx, uint32_t now_ms)
180{
181 for (size_t i = 0; i < ctx->txlen; i++)
182 {
183 emit(ctx, ctx->txbuf[i]);
184 }
185 ctx->state = Simatic3964State::TX_AWAIT_END;
186 ctx->deadline_ms = now_ms + PC_SIMATIC_QVZ_MS;
187}
188
189static void begin_receive(Simatic3964Ctx *ctx, uint32_t now_ms)
190{
191 emit(ctx, SIMATIC_DLE); // ready
192 ctx->state = Simatic3964State::RX_COLLECT;
193 ctx->rxpos = 0;
194 ctx->prev_dle = false;
195 ctx->await_bcc = false;
196 ctx->deadline_ms = now_ms + PC_SIMATIC_ZVZ_MS;
197}
198
199void pc_3964r_init(Simatic3964Ctx *ctx, bool high_priority, bool with_bcc, Simatic3964TxFn tx, Simatic3964RxFn rx,
200 void *user)
201{
202 memset(ctx, 0, sizeof(*ctx));
203 ctx->state = Simatic3964State::IDLE;
204 ctx->high_priority = high_priority;
205 ctx->with_bcc = with_bcc;
206 ctx->tx = tx;
207 ctx->rx = rx;
208 ctx->user = user;
209}
210
211bool pc_3964r_send(Simatic3964Ctx *ctx, const uint8_t *data, size_t len, uint32_t now_ms)
212{
213 if (ctx->state != Simatic3964State::IDLE)
214 {
215 return false;
216 }
217 size_t n = pc_3964r_build_block(ctx->txbuf, sizeof(ctx->txbuf), data, len, ctx->with_bcc);
218 if (n == 0)
219 {
220 return false;
221 }
222 ctx->txlen = n;
223 ctx->block_retries = 0;
224 ctx->conn_retries = 0;
225 send_stx_await_conn(ctx, now_ms);
226 return true;
227}
228
229static void deliver_or_nak(Simatic3964Ctx *ctx)
230{
231 uint8_t out[PC_SIMATIC_BLOCK_MAX];
232 size_t olen = 0;
233 if (pc_3964r_parse_block(ctx->rxbuf, ctx->rxpos, ctx->with_bcc, out, sizeof(out), &olen))
234 {
235 emit(ctx, SIMATIC_DLE); // ack the received block
236 // Return to IDLE BEFORE the delivery callback: a request/response peer replies from inside rx (e.g.
237 // an RK512 FETCH -> a reaction telegram), and pc_3964r_send requires an idle link.
238 ctx->state = Simatic3964State::IDLE;
239 if (ctx->rx)
240 {
241 ctx->rx(ctx->user, out, olen);
242 }
243 }
244 else
245 {
246 emit(ctx, SIMATIC_NAK); // bad framing / BCC
247 ctx->state = Simatic3964State::IDLE;
248 }
249}
250
251static void rx_collect_byte(Simatic3964Ctx *ctx, uint8_t b, uint32_t now_ms)
252{
253 if (ctx->rxpos >= sizeof(ctx->rxbuf))
254 {
255 emit(ctx, SIMATIC_NAK); // overflow -> reject
256 ctx->state = Simatic3964State::IDLE;
257 return;
258 }
259 ctx->rxbuf[ctx->rxpos++] = b;
260 ctx->deadline_ms = now_ms + PC_SIMATIC_ZVZ_MS;
261
262 if (ctx->await_bcc) // this byte was the BCC that follows DLE ETX
263 {
264 deliver_or_nak(ctx);
265 return;
266 }
267 if (ctx->prev_dle)
268 {
269 ctx->prev_dle = false;
270 if (b == SIMATIC_DLE)
271 {
272 return; // doubled literal DLE
273 }
274 if (b == SIMATIC_ETX)
275 {
276 if (ctx->with_bcc)
277 {
278 ctx->await_bcc = true; // one more byte (BCC) then finalize
279 }
280 else
281 {
282 deliver_or_nak(ctx);
283 }
284 return;
285 }
286 // DLE + illegal control byte -> framing error
287 emit(ctx, SIMATIC_NAK);
288 ctx->state = Simatic3964State::IDLE;
289 return;
290 }
291 if (b == SIMATIC_DLE)
292 {
293 ctx->prev_dle = true;
294 }
295}
296
297void pc_3964r_rx_byte(Simatic3964Ctx *ctx, uint8_t b, uint32_t now_ms)
298{
299 switch (ctx->state)
300 {
301 case Simatic3964State::IDLE:
302 if (b == SIMATIC_STX)
303 {
304 begin_receive(ctx, now_ms);
305 }
306 break;
307 case Simatic3964State::TX_AWAIT_CONN:
308 if (b == SIMATIC_DLE) // connect acknowledged
309 {
310 send_block(ctx, now_ms);
311 }
312 else if (b == SIMATIC_STX) // collision: low-priority station yields to receive
313 {
314 if (!ctx->high_priority)
315 {
316 begin_receive(ctx, now_ms);
317 }
318 // high-priority: ignore, keep awaiting our connect DLE (the partner yields)
319 }
320 else if (b == SIMATIC_NAK)
321 {
322 if (++ctx->conn_retries < SIMATIC_MAX_CONN_RETRY)
323 {
324 send_stx_await_conn(ctx, now_ms);
325 }
326 else
327 {
328 ctx->state = Simatic3964State::IDLE; // give up
329 }
330 }
331 break;
332 case Simatic3964State::TX_AWAIT_END:
333 if (b == SIMATIC_DLE) // block acknowledged -> done
334 {
335 ctx->state = Simatic3964State::IDLE;
336 }
337 else if (b == SIMATIC_NAK)
338 {
339 if (++ctx->block_retries < SIMATIC_MAX_BLOCK_RETRY)
340 {
341 send_stx_await_conn(ctx, now_ms); // repeat the block (from STX)
342 }
343 else
344 {
345 ctx->state = Simatic3964State::IDLE;
346 }
347 }
348 break;
349 case Simatic3964State::RX_COLLECT:
350 rx_collect_byte(ctx, b, now_ms);
351 break;
352 }
353}
354
355void pc_3964r_tick(Simatic3964Ctx *ctx, uint32_t now_ms)
356{
357 if (ctx->state == Simatic3964State::IDLE)
358 {
359 return;
360 }
361 if ((int32_t)(now_ms - ctx->deadline_ms) < 0)
362 {
363 return; // not yet expired
364 }
365 switch (ctx->state)
366 {
367 case Simatic3964State::TX_AWAIT_CONN:
368 if (++ctx->conn_retries < SIMATIC_MAX_CONN_RETRY)
369 {
370 send_stx_await_conn(ctx, now_ms);
371 }
372 else
373 {
374 ctx->state = Simatic3964State::IDLE;
375 }
376 break;
377 case Simatic3964State::TX_AWAIT_END:
378 if (++ctx->block_retries < SIMATIC_MAX_BLOCK_RETRY)
379 {
380 send_stx_await_conn(ctx, now_ms);
381 }
382 else
383 {
384 ctx->state = Simatic3964State::IDLE;
385 }
386 break;
387 case Simatic3964State::RX_COLLECT:
388 emit(ctx, SIMATIC_NAK); // ZVZ inter-char timeout -> abort receive
389 ctx->state = Simatic3964State::IDLE;
390 break;
391 default:
392 break;
393 }
394}
395
396bool pc_3964r_idle(const Simatic3964Ctx *ctx)
397{
398 return ctx->state == Simatic3964State::IDLE;
399}
400
401// ---------------------------------------------------------------------------
402// RK512 telegrams (big-endian words). Field ORDER + BE encoding are the spec invariants; the exact
403// command / area byte values are verify-against-the-CP-manual (noted in the header + roadmap).
404// ---------------------------------------------------------------------------
405
406// Request header: [cmd, coord=0, area, dbnr, addr_hi, addr_lo, count_hi, count_lo] (8 bytes)
407#define RK512_HDR_LEN 8
408
409size_t pc_rk512_build_send(uint8_t *buf, size_t cap, Rk512Area area, uint8_t dbnr, uint16_t addr, const uint16_t *words,
410 uint16_t wcount)
411{
412 if (!buf || (!words && wcount))
413 {
414 return 0;
415 }
416 size_t need = RK512_HDR_LEN + (size_t)wcount * 2;
417 if (need > cap)
418 {
419 return 0;
420 }
421 buf[0] = (uint8_t)Rk512Cmd::SEND;
422 buf[1] = 0x00; // coordination / follow-up flag (single block)
423 buf[2] = (uint8_t)area;
424 buf[3] = dbnr;
425 wr_u16(buf + 4, addr);
426 wr_u16(buf + 6, wcount);
427 for (uint16_t i = 0; i < wcount; i++)
428 {
429 wr_u16(buf + RK512_HDR_LEN + (size_t)i * 2, words[i]);
430 }
431 return need;
432}
433
434size_t pc_rk512_build_fetch(uint8_t *buf, size_t cap, Rk512Area area, uint8_t dbnr, uint16_t addr, uint16_t wcount)
435{
436 if (!buf || cap < RK512_HDR_LEN)
437 {
438 return 0;
439 }
440 buf[0] = (uint8_t)Rk512Cmd::FETCH;
441 buf[1] = 0x00;
442 buf[2] = (uint8_t)area;
443 buf[3] = dbnr;
444 wr_u16(buf + 4, addr);
445 wr_u16(buf + 6, wcount);
446 return RK512_HDR_LEN;
447}
448
449// Reaction: [cmd=REACTION, status_hi, status_lo] (+ FETCH-response data words appended by the caller)
450size_t pc_rk512_build_reaction(uint8_t *buf, size_t cap, uint16_t status)
451{
452 if (!buf || cap < 3)
453 {
454 return 0;
455 }
456 buf[0] = (uint8_t)Rk512Cmd::REACTION;
457 wr_u16(buf + 1, status);
458 return 3;
459}
460
461static bool area_valid(uint8_t a)
462{
463 return a >= (uint8_t)Rk512Area::DB && a <= (uint8_t)Rk512Area::TB;
464}
465
466bool pc_rk512_parse_header(const uint8_t *buf, size_t len, Rk512Header *out)
467{
468 if (!buf || !out || len < RK512_HDR_LEN)
469 {
470 return false;
471 }
472 uint8_t cmd = buf[0];
473 if (cmd != (uint8_t)Rk512Cmd::SEND && cmd != (uint8_t)Rk512Cmd::FETCH)
474 {
475 return false;
476 }
477 if (!area_valid(buf[2]))
478 {
479 return false;
480 }
481 out->cmd = (Rk512Cmd)cmd;
482 out->area = (Rk512Area)buf[2];
483 out->dbnr = buf[3];
484 out->addr = rd_u16(buf + 4);
485 out->count = rd_u16(buf + 6);
486 return true;
487}
488
489bool pc_rk512_parse_reaction(const uint8_t *buf, size_t len, uint16_t *status, const uint8_t **data, size_t *dlen)
490{
491 if (!buf || !status || len < 3)
492 {
493 return false;
494 }
495 if (buf[0] != (uint8_t)Rk512Cmd::REACTION)
496 {
497 return false;
498 }
499 *status = rd_u16(buf + 1);
500 if (data)
501 {
502 *data = (len > 3) ? buf + 3 : nullptr;
503 }
504 if (dlen)
505 {
506 *dlen = len - 3;
507 }
508 return true;
509}
510
511#endif // PC_ENABLE_SIMATIC
#define PC_SIMATIC_ZVZ_MS
3964R ZVZ (Zeichenverzugszeit): inter-character timeout while receiving a block, ms.
#define PC_SIMATIC_QVZ_MS
3964R QVZ (Quittungsverzugszeit): handshake acknowledge-delay timeout, ms.
#define PC_SIMATIC_BLOCK_MAX
3964R block-body buffer size (built/received bytes: DLE-stuffed payload + DLE ETX + BCC).
Siemens SIMATIC serial point-to-point link (PC_ENABLE_SIMATIC) - the 3964R link protocol + the RK512 ...