ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
canopen.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 canopen.cpp
6 * @brief CANopen (CiA 301) message codec (pure, host-tested).
7 */
8
10
11#if PC_ENABLE_CANOPEN
12
13#include <string.h>
14
15// All CANopen default-profile identifiers are 11-bit standard frames.
16static void std_frame(CanFrame *f, uint32_t id, uint8_t dlc)
17{
18 f->id = id & PC_CAN_STD_ID_MASK;
19 f->extended = false;
20 f->rtr = false;
21 f->dlc = dlc;
22 memset(f->data, 0, sizeof(f->data));
23}
24
25static bool valid_node(uint8_t node_id)
26{
27 return node_id >= 1 && node_id <= 127;
28}
29
30bool pc_canopen_build_nmt(CanFrame *out, uint8_t command, uint8_t node_id)
31{
32 if (!out || node_id > 127) // 0 = all nodes
33 {
34 return false;
35 }
36 std_frame(out, CANOPEN_COB_NMT, 2);
37 out->data[0] = command;
38 out->data[1] = node_id;
39 return true;
40}
41
42bool pc_canopen_build_sync(CanFrame *out)
43{
44 if (!out)
45 {
46 return false;
47 }
48 std_frame(out, CANOPEN_COB_SYNC, 0);
49 return true;
50}
51
52bool pc_canopen_build_time(CanFrame *out, uint32_t ms_since_midnight, uint16_t days_since_1984)
53{
54 if (!out)
55 {
56 return false;
57 }
58 std_frame(out, CANOPEN_COB_TIME, CANOPEN_TIME_LEN);
59 uint32_t ms = ms_since_midnight & CANOPEN_TIME_MS_MASK; // 28-bit ms after midnight, top 4 bits reserved
60 out->data[0] = (uint8_t)ms; // little-endian
61 out->data[1] = (uint8_t)(ms >> 8);
62 out->data[2] = (uint8_t)(ms >> 16);
63 out->data[3] = (uint8_t)(ms >> 24);
64 out->data[4] = (uint8_t)days_since_1984; // days since 1984-01-01, little-endian
65 out->data[5] = (uint8_t)(days_since_1984 >> 8);
66 return true;
67}
68
69bool pc_canopen_build_heartbeat(CanFrame *out, uint8_t node_id, uint8_t state)
70{
71 if (!out || !valid_node(node_id))
72 {
73 return false;
74 }
75 std_frame(out, CANOPEN_COB_HEARTBEAT + node_id, 1);
76 out->data[0] = state;
77 return true;
78}
79
80bool pc_canopen_build_emcy(CanFrame *out, uint8_t node_id, uint16_t error_code, uint8_t error_reg,
81 const uint8_t msef[5])
82{
83 if (!out || !valid_node(node_id))
84 {
85 return false;
86 }
87 std_frame(out, CANOPEN_COB_EMCY + node_id, 8);
88 out->data[0] = (uint8_t)error_code; // error code, little-endian
89 out->data[1] = (uint8_t)(error_code >> 8);
90 out->data[2] = error_reg; // object 0x1001 error register
91 if (msef)
92 {
93 memcpy(out->data + 3, msef, 5); // 5 manufacturer-specific error octets
94 }
95 return true;
96}
97
98// Map a PDO number (1..4) to its TPDO / RPDO COB-ID base.
99static bool pdo_base(uint8_t pdo_num, bool transmit, uint32_t *base)
100{
101 if (pdo_num < 1 || pdo_num > 4)
102 {
103 return false;
104 }
105 static const uint32_t tx[4] = {CANOPEN_COB_TPDO1, CANOPEN_COB_TPDO2, CANOPEN_COB_TPDO3, CANOPEN_COB_TPDO4};
106 static const uint32_t rx[4] = {CANOPEN_COB_RPDO1, CANOPEN_COB_RPDO2, CANOPEN_COB_RPDO3, CANOPEN_COB_RPDO4};
107 *base = (transmit ? tx : rx)[pdo_num - 1];
108 return true;
109}
110
111static bool build_pdo(CanFrame *out, uint8_t pdo_num, bool transmit, uint8_t node_id, const uint8_t *data, uint8_t len)
112{
113 uint32_t base;
114 if (!out || !valid_node(node_id) || len > PC_CAN_MAX_DLC || (len && !data) || !pdo_base(pdo_num, transmit, &base))
115 {
116 return false;
117 }
118 std_frame(out, base + node_id, len);
119 if (len)
120 {
121 memcpy(out->data, data, len);
122 }
123 return true;
124}
125
126bool pc_canopen_build_tpdo(CanFrame *out, uint8_t pdo_num, uint8_t node_id, const uint8_t *data, uint8_t len)
127{
128 return build_pdo(out, pdo_num, true, node_id, data, len);
129}
130
131bool pc_canopen_build_rpdo(CanFrame *out, uint8_t pdo_num, uint8_t node_id, const uint8_t *data, uint8_t len)
132{
133 return build_pdo(out, pdo_num, false, node_id, data, len);
134}
135
136// Fill data[1..3] with the object index (LE) + sub-index common to every SDO frame.
137static void sdo_set_object(CanFrame *f, uint16_t index, uint8_t sub)
138{
139 f->data[1] = (uint8_t)index;
140 f->data[2] = (uint8_t)(index >> 8);
141 f->data[3] = sub;
142}
143
144bool pc_canopen_build_sdo_read(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub)
145{
146 if (!out || !valid_node(node_id))
147 {
148 return false;
149 }
150 std_frame(out, CANOPEN_COB_SDO_RX + node_id, 8);
151 out->data[0] = (uint8_t)(CANOPEN_SDO_CCS_UPLOAD << 5); // upload initiate request (0x40)
152 sdo_set_object(out, index, sub);
153 return true;
154}
155
156bool pc_canopen_build_sdo_write(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub, const uint8_t *data,
157 uint8_t len)
158{
159 if (!out || !valid_node(node_id) || len < 1 || len > 4 || !data)
160 {
161 return false;
162 }
163 std_frame(out, CANOPEN_COB_SDO_RX + node_id, 8);
164 // download initiate, expedited (e=1), size indicated (s=1); n = unused octets in data[4..7].
165 out->data[0] = (uint8_t)((CANOPEN_SDO_CCS_DOWNLOAD << 5) | (((4u - len) & 3u) << 2) | 0x03u);
166 sdo_set_object(out, index, sub);
167 memcpy(out->data + 4, data, len);
168 return true;
169}
170
171bool pc_canopen_build_sdo_abort(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub, uint32_t abort_code,
172 bool to_server)
173{
174 if (!out || !valid_node(node_id))
175 {
176 return false;
177 }
178 std_frame(out, (to_server ? CANOPEN_COB_SDO_RX : CANOPEN_COB_SDO_TX) + node_id, 8);
179 out->data[0] = (uint8_t)(CANOPEN_SDO_ABORT << 5); // 0x80
180 sdo_set_object(out, index, sub);
181 out->data[4] = (uint8_t)abort_code; // abort code, little-endian
182 out->data[5] = (uint8_t)(abort_code >> 8);
183 out->data[6] = (uint8_t)(abort_code >> 16);
184 out->data[7] = (uint8_t)(abort_code >> 24);
185 return true;
186}
187
188bool pc_canopen_parse(const CanFrame *f, CanopenMsg *out)
189{
190 if (!f || !out || f->extended)
191 {
192 return false; // CANopen default profile is 11-bit standard frames
193 }
194 uint32_t id = f->id & PC_CAN_STD_ID_MASK;
195 uint32_t func = id & CANOPEN_FUNC_MASK;
196 uint8_t node = (uint8_t)(id & CANOPEN_NODE_MASK);
197 out->type = CanopenType::CANOPEN_T_UNKNOWN;
198 out->node_id = node;
199 out->pdo_num = 0;
200
201 if (id == CANOPEN_COB_NMT)
202 {
203 out->type = CanopenType::CANOPEN_T_NMT;
204 out->node_id = 0;
205 return true;
206 }
207 if (id == CANOPEN_COB_SYNC) // function 0x080 with node 0
208 {
209 out->type = CanopenType::CANOPEN_T_SYNC;
210 out->node_id = 0;
211 return true;
212 }
213 if (id == CANOPEN_COB_TIME)
214 {
215 out->type = CanopenType::CANOPEN_T_TIME;
216 out->node_id = 0;
217 return true;
218 }
219 if (node == 0)
220 {
221 return true; // a function base with node 0 we don't classify further
222 }
223
224 switch (func)
225 {
226 case CANOPEN_COB_EMCY:
227 out->type = CanopenType::CANOPEN_T_EMCY;
228 return true;
229 case CANOPEN_COB_TPDO1:
230 out->type = CanopenType::CANOPEN_T_TPDO;
231 out->pdo_num = 1;
232 return true;
233 case CANOPEN_COB_RPDO1:
234 out->type = CanopenType::CANOPEN_T_RPDO;
235 out->pdo_num = 1;
236 return true;
237 case CANOPEN_COB_TPDO2:
238 out->type = CanopenType::CANOPEN_T_TPDO;
239 out->pdo_num = 2;
240 return true;
241 case CANOPEN_COB_RPDO2:
242 out->type = CanopenType::CANOPEN_T_RPDO;
243 out->pdo_num = 2;
244 return true;
245 case CANOPEN_COB_TPDO3:
246 out->type = CanopenType::CANOPEN_T_TPDO;
247 out->pdo_num = 3;
248 return true;
249 case CANOPEN_COB_RPDO3:
250 out->type = CanopenType::CANOPEN_T_RPDO;
251 out->pdo_num = 3;
252 return true;
253 case CANOPEN_COB_TPDO4:
254 out->type = CanopenType::CANOPEN_T_TPDO;
255 out->pdo_num = 4;
256 return true;
257 case CANOPEN_COB_RPDO4:
258 out->type = CanopenType::CANOPEN_T_RPDO;
259 out->pdo_num = 4;
260 return true;
261 case CANOPEN_COB_SDO_TX:
262 out->type = CanopenType::CANOPEN_T_SDO_TX;
263 return true;
264 case CANOPEN_COB_SDO_RX:
265 out->type = CanopenType::CANOPEN_T_SDO_RX;
266 return true;
267 case CANOPEN_COB_HEARTBEAT:
268 out->type = CanopenType::CANOPEN_T_HEARTBEAT;
269 return true;
270 default:
271 return true; // unknown function code: type stays CanopenType::CANOPEN_T_UNKNOWN
272 }
273}
274
275bool pc_canopen_parse_emcy(const CanFrame *f, uint8_t *node_id, uint16_t *error_code, uint8_t *error_reg,
276 uint8_t msef[5])
277{
278 if (!f || f->extended || f->dlc < 8)
279 {
280 return false;
281 }
282 uint32_t id = f->id & PC_CAN_STD_ID_MASK;
283 uint8_t node = (uint8_t)(id & CANOPEN_NODE_MASK);
284 if ((id & CANOPEN_FUNC_MASK) != CANOPEN_COB_EMCY || node == 0)
285 {
286 return false; // 0x080 with node 0 is SYNC, not EMCY
287 }
288 if (node_id)
289 {
290 *node_id = node;
291 }
292 if (error_code)
293 {
294 *error_code = (uint16_t)(f->data[0] | (f->data[1] << 8));
295 }
296 if (error_reg)
297 {
298 *error_reg = f->data[2];
299 }
300 if (msef)
301 {
302 memcpy(msef, f->data + 3, 5);
303 }
304 return true;
305}
306
307bool pc_canopen_parse_heartbeat(const CanFrame *f, uint8_t *node_id, uint8_t *state)
308{
309 if (!f || f->extended || f->dlc < 1)
310 {
311 return false;
312 }
313 uint32_t id = f->id & PC_CAN_STD_ID_MASK;
314 uint8_t node = (uint8_t)(id & CANOPEN_NODE_MASK);
315 if ((id & CANOPEN_FUNC_MASK) != CANOPEN_COB_HEARTBEAT || node == 0)
316 {
317 return false;
318 }
319 if (node_id)
320 {
321 *node_id = node;
322 }
323 if (state)
324 {
325 *state = (uint8_t)(f->data[0] & 0x7Fu); // bit 7 is the boot toggle in some stacks
326 }
327 return true;
328}
329
330bool pc_canopen_parse_time(const CanFrame *f, CanopenTime *out)
331{
332 if (!f || !out || f->extended || f->dlc < CANOPEN_TIME_LEN)
333 {
334 return false;
335 }
336 if ((f->id & PC_CAN_STD_ID_MASK) != CANOPEN_COB_TIME)
337 {
338 return false;
339 }
340 uint32_t ms = (uint32_t)f->data[0] | ((uint32_t)f->data[1] << 8) | ((uint32_t)f->data[2] << 16) |
341 ((uint32_t)f->data[3] << 24);
342 out->ms_since_midnight = ms & CANOPEN_TIME_MS_MASK; // discard the reserved top 4 bits
343 out->days_since_1984 = (uint16_t)(f->data[4] | (f->data[5] << 8));
344 return true;
345}
346
347bool pc_canopen_parse_sdo_response(const CanFrame *f, CanopenSdoResponse *out)
348{
349 if (!f || !out || f->extended || f->dlc < 8)
350 {
351 return false;
352 }
353 uint32_t id = f->id & PC_CAN_STD_ID_MASK;
354 if ((id & CANOPEN_FUNC_MASK) != CANOPEN_COB_SDO_TX || (id & CANOPEN_NODE_MASK) == 0)
355 {
356 return false;
357 }
358
359 uint8_t cmd = f->data[0];
360 uint8_t scs = (uint8_t)(cmd >> 5);
361 out->index = (uint16_t)(f->data[1] | (f->data[2] << 8));
362 out->sub = f->data[3];
363 out->is_abort = false;
364 out->abort_code = 0;
365 out->is_upload = false;
366 out->expedited = false;
367 out->len = 0;
368 memset(out->data, 0, sizeof(out->data));
369
370 if (scs == CANOPEN_SDO_ABORT)
371 {
372 out->is_abort = true;
373 out->abort_code = (uint32_t)f->data[4] | ((uint32_t)f->data[5] << 8) | ((uint32_t)f->data[6] << 16) |
374 ((uint32_t)f->data[7] << 24);
375 return true;
376 }
377 if (scs == CANOPEN_SDO_SCS_UPLOAD) // upload initiate response
378 {
379 out->is_upload = true;
380 bool e = (cmd & 0x02u) != 0; // expedited
381 bool s = (cmd & 0x01u) != 0; // size indicated
382 if (e)
383 {
384 out->expedited = true;
385 out->len = s ? (uint8_t)(4u - ((cmd >> 2) & 0x03u)) : 4u;
386 memcpy(out->data, f->data + 4, out->len);
387 }
388 return true; // a non-expedited (segmented) response is reported with len 0
389 }
390 if (scs == CANOPEN_SDO_SCS_DOWNLOAD) // download initiate response (write acknowledged)
391 {
392 return true;
393 }
394 return false; // not a recognized server command specifier
395}
396
397// --- segmented SDO (CiA 301 ยง7.2.4.3) ---
398
399bool pc_canopen_build_sdo_download_init(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub,
400 uint32_t total_size)
401{
402 if (!out || !valid_node(node_id))
403 {
404 return false;
405 }
406 std_frame(out, CANOPEN_COB_SDO_RX + node_id, 8);
407 // download initiate, segmented (e=0), size indicated (s=1) -> command 0x21; size in data[4..7] LE.
408 out->data[0] = (uint8_t)((CANOPEN_SDO_CCS_DOWNLOAD << 5) | 0x01u);
409 sdo_set_object(out, index, sub);
410 out->data[4] = (uint8_t)total_size;
411 out->data[5] = (uint8_t)(total_size >> 8);
412 out->data[6] = (uint8_t)(total_size >> 16);
413 out->data[7] = (uint8_t)(total_size >> 24);
414 return true;
415}
416
417bool pc_canopen_build_sdo_download_segment(CanFrame *out, uint8_t node_id, bool toggle, const uint8_t *data,
418 uint8_t len, bool last)
419{
420 if (!out || !valid_node(node_id) || !data || len < 1 || len > CANOPEN_SDO_SEG_DATA)
421 {
422 return false;
423 }
424 std_frame(out, CANOPEN_COB_SDO_RX + node_id, 8);
425 // segment: ccs=0, t=toggle (bit 4), n=unused octets (bits 1..3), c=last (bit 0).
426 uint8_t n = (uint8_t)(CANOPEN_SDO_SEG_DATA - len);
427 out->data[0] = (uint8_t)((toggle ? 0x10u : 0u) | ((n & 0x07u) << 1) | (last ? 0x01u : 0u));
428 memcpy(out->data + 1, data, len);
429 return true;
430}
431
432bool pc_canopen_build_sdo_upload_segment_req(CanFrame *out, uint8_t node_id, bool toggle)
433{
434 if (!out || !valid_node(node_id))
435 {
436 return false;
437 }
438 std_frame(out, CANOPEN_COB_SDO_RX + node_id, 8);
439 // upload segment request: ccs=3, t=toggle (bit 4).
440 out->data[0] = (uint8_t)((3u << 5) | (toggle ? 0x10u : 0u));
441 return true;
442}
443
444bool pc_canopen_parse_sdo_segment(const CanFrame *f, bool *toggle, uint8_t *data, uint8_t *len, bool *last)
445{
446 if (!f || f->dlc < 8 || (f->data[0] & 0xE0u) != 0u) // segment form: command specifier (high 3 bits) is 0
447 {
448 return false;
449 }
450 uint8_t n = (uint8_t)((f->data[0] >> 1) & 0x07u);
451 uint8_t seg_len = (uint8_t)(CANOPEN_SDO_SEG_DATA - n);
452 if (toggle)
453 {
454 *toggle = (f->data[0] & 0x10u) != 0u;
455 }
456 if (last)
457 {
458 *last = (f->data[0] & 0x01u) != 0u;
459 }
460 if (len)
461 {
462 *len = seg_len;
463 }
464 if (data)
465 {
466 memcpy(data, f->data + 1, seg_len);
467 }
468 return true;
469}
470
471void pc_canopen_sdo_reasm_init(CanopenSdoReasm *r, uint8_t *buf, size_t cap)
472{
473 if (!r)
474 {
475 return;
476 }
477 r->buf = buf;
478 r->cap = cap;
479 r->len = 0;
480 r->expect_toggle = false; // the first segment carries toggle 0
481 r->done = false;
482}
483
484bool pc_canopen_sdo_reasm_feed(CanopenSdoReasm *r, const uint8_t *data, uint8_t len, bool toggle, bool last)
485{
486 if (!r || !r->buf || r->done || (len && !data))
487 {
488 return false;
489 }
490 if (toggle != r->expect_toggle) // toggles must alternate 0,1,0,1
491 {
492 return false;
493 }
494 if (len > r->cap - r->len) // would overflow the buffer
495 {
496 return false;
497 }
498 if (len)
499 {
500 memcpy(r->buf + r->len, data, len);
501 }
502 r->len += len;
503 r->expect_toggle = !r->expect_toggle;
504 if (last)
505 {
506 r->done = true;
507 }
508 return true;
509}
510
511#endif // PC_ENABLE_CANOPEN
#define PC_CAN_MAX_DLC
classic CAN carries at most 8 data octets.
Definition can.h:30
#define PC_CAN_STD_ID_MASK
11-bit standard identifier.
Definition can.h:31
CANopen (CiA 301) application-layer message codec (PC_ENABLE_CANOPEN).
Definition can.h:44
uint8_t data[PC_CAN_MAX_DLC]
Definition can.h:49
bool rtr
Definition can.h:47
uint32_t id
Definition can.h:45
uint8_t dlc
Definition can.h:48
bool extended
Definition can.h:46