ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
canopen.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 canopen.h
6 * @brief CANopen (CiA 301) application-layer message codec (PC_ENABLE_CANOPEN).
7 *
8 * A pure, zero-heap builder + parser for the CANopen messaging set carried over classic
9 * CAN frames (see shared_primitives/can.h): NMT node control, SYNC, TIME, the
10 * heartbeat / boot-up (NMT error control), EMCY, PDO (process data), and expedited SDO
11 * (service data object) read / write / abort. The 11-bit CAN identifier is a 4-bit
12 * function code plus a 7-bit node id; each builder computes the right COB-ID and each
13 * parser classifies a received frame back to its function + node.
14 *
15 * Scope: the CANopen object dictionary itself is the application's; this is the wire codec.
16 * SDO transfers cover expedited (<= 4 octets) and segmented (CiA 301 §7.2.4.3, larger objects via a
17 * toggling run of 7-octet segments); block SDO is not covered.
18 *
19 * Bridging: pair with the ESP32's TWAI peripheral (or an MCP2515 over SPI) to bridge a
20 * CANopen field bus onto Wi-Fi - expose node state / PDOs over HTTP, MQTT, or a WebSocket.
21 *
22 * @author Douglas Quigg (dstroy0)
23 * @date 2026
24 */
25
26#ifndef PROTOCORE_CANOPEN_H
27#define PROTOCORE_CANOPEN_H
28
29#include "protocore_config.h"
30
31#if PC_ENABLE_CANOPEN
32
34#include <stddef.h>
35#include <stdint.h>
36
37// Function-code COB-ID bases. The 11-bit id is (function-code | node-id); the node id is
38// 1..127 (0 = broadcast for NMT / SYNC / TIME). EMCY shares 0x080 with SYNC: SYNC is the
39// node-id == 0 case, EMCY is 0x081..0x0FF.
40#define CANOPEN_COB_NMT 0x000u ///< NMT node control (broadcast), 2 data octets
41#define CANOPEN_COB_SYNC 0x080u ///< SYNC (broadcast), 0 data octets
42#define CANOPEN_COB_EMCY 0x080u ///< EMCY base (+ node id)
43#define CANOPEN_COB_TIME 0x100u ///< TIME stamp (broadcast)
44#define CANOPEN_COB_TPDO1 0x180u ///< transmit PDO 1 base (+ node id)
45#define CANOPEN_COB_RPDO1 0x200u ///< receive PDO 1 base (+ node id)
46#define CANOPEN_COB_TPDO2 0x280u ///< transmit PDO 2 base
47#define CANOPEN_COB_RPDO2 0x300u ///< receive PDO 2 base
48#define CANOPEN_COB_TPDO3 0x380u ///< transmit PDO 3 base
49#define CANOPEN_COB_RPDO3 0x400u ///< receive PDO 3 base
50#define CANOPEN_COB_TPDO4 0x480u ///< transmit PDO 4 base
51#define CANOPEN_COB_RPDO4 0x500u ///< receive PDO 4 base
52#define CANOPEN_COB_SDO_TX 0x580u ///< SDO server -> client (response), + node id
53#define CANOPEN_COB_SDO_RX 0x600u ///< SDO client -> server (request), + node id
54#define CANOPEN_COB_HEARTBEAT 0x700u ///< NMT error control (heartbeat / boot-up), + node id
55#define CANOPEN_FUNC_MASK 0x780u ///< top 4 bits select the function code
56#define CANOPEN_NODE_MASK 0x07Fu ///< low 7 bits select the node id
57
58// NMT node-control commands (CANOPEN_COB_NMT data[0]).
59#define CANOPEN_NMT_START 0x01u ///< enter Operational
60#define CANOPEN_NMT_STOP 0x02u ///< enter Stopped
61#define CANOPEN_NMT_PRE_OP 0x80u ///< enter Pre-operational
62#define CANOPEN_NMT_RESET_NODE 0x81u ///< reset application
63#define CANOPEN_NMT_RESET_COMM 0x82u ///< reset communication
64
65// NMT states reported in a heartbeat (data[0], low 7 bits).
66#define CANOPEN_STATE_BOOTUP 0x00u
67#define CANOPEN_STATE_STOPPED 0x04u
68#define CANOPEN_STATE_OPERATIONAL 0x05u
69#define CANOPEN_STATE_PRE_OP 0x7Fu
70
71// SDO command specifier (high 3 bits of data[0]).
72#define CANOPEN_SDO_CCS_DOWNLOAD 1u ///< client download initiate (write)
73#define CANOPEN_SDO_CCS_UPLOAD 2u ///< client upload initiate (read)
74#define CANOPEN_SDO_SCS_UPLOAD 2u ///< server upload initiate response
75#define CANOPEN_SDO_SCS_DOWNLOAD 3u ///< server download initiate response (ack)
76#define CANOPEN_SDO_ABORT 4u ///< abort transfer (either direction)
77
78// A few common SDO abort codes (the field is any 32-bit value).
79#define CANOPEN_ABORT_TOGGLE 0x05030000u ///< toggle bit not alternated
80#define CANOPEN_ABORT_TIMEOUT 0x05040000u ///< SDO protocol timed out
81#define CANOPEN_ABORT_NO_OBJECT 0x06020000u ///< object does not exist
82#define CANOPEN_ABORT_NO_SUBINDEX 0x06090011u ///< sub-index does not exist
83#define CANOPEN_ABORT_GENERAL 0x08000000u ///< general error
84
85/** @brief CANopen message classes (the function decoded from the COB-ID). */
86enum class CanopenType : uint8_t
87{
88 CANOPEN_T_UNKNOWN = 0,
89 CANOPEN_T_NMT,
90 CANOPEN_T_SYNC,
91 CANOPEN_T_EMCY,
92 CANOPEN_T_TIME,
93 CANOPEN_T_TPDO,
94 CANOPEN_T_RPDO,
95 CANOPEN_T_SDO_TX,
96 CANOPEN_T_SDO_RX,
97 CANOPEN_T_HEARTBEAT,
98};
99
100/** @brief A classified CANopen frame (the function code + node, from pc_canopen_parse). */
101struct CanopenMsg
102{
103 CanopenType type;
104 uint8_t node_id; ///< 1..127, or 0 for a broadcast (NMT / SYNC / TIME)
105 uint8_t pdo_num; ///< 1..4 for TPDO / RPDO, else 0
106};
107
108/** @brief A decoded SDO initiate response (from pc_canopen_parse_sdo_response). */
109struct CanopenSdoResponse
110{
111 uint16_t index; ///< object index echoed by the server
112 uint8_t sub; ///< sub-index echoed by the server
113 bool is_abort; ///< true => the server aborted the transfer
114 uint32_t abort_code; ///< valid when is_abort
115 bool is_upload; ///< true => upload (read) response; false => download (write) ack
116 bool expedited; ///< true => the payload is inline in data[0..len-1]
117 uint8_t data[4]; ///< expedited upload payload
118 uint8_t len; ///< expedited payload length 0..4
119};
120
121/** @brief Octets in a TIME message (CiA 301 TIME_OF_DAY: 4-octet ms + 2-octet days). */
122#define CANOPEN_TIME_LEN 6
123/** @brief The ms-after-midnight field is 28 bits; the top 4 bits of the U32 are reserved. */
124#define CANOPEN_TIME_MS_MASK 0x0FFFFFFFu
125
126/** @brief Decoded CANopen TIME_OF_DAY (the TIME message payload, CiA 301 §7.2.6). The CANopen epoch is
127 * January 1, 1984, so @c days_since_1984 plus @c ms_since_midnight locate an absolute instant. */
128struct CanopenTime
129{
130 uint32_t ms_since_midnight; ///< milliseconds after midnight (28-bit; 0..86'399'999)
131 uint16_t days_since_1984; ///< days since January 1, 1984
132};
133
134// --- builders: fill *out and return true; false on a bad argument ---
135
136/** @brief NMT node-control frame. @p node_id 0 addresses all nodes. */
137bool pc_canopen_build_nmt(CanFrame *out, uint8_t command, uint8_t node_id);
138
139/** @brief SYNC frame (zero-length, broadcast). */
140bool pc_canopen_build_sync(CanFrame *out);
141
142/** @brief TIME frame (broadcast): the TIME_OF_DAY (@p ms_since_midnight masked to 28 bits, days since
143 * 1984). */
144bool pc_canopen_build_time(CanFrame *out, uint32_t ms_since_midnight, uint16_t days_since_1984);
145
146/** @brief Heartbeat / boot-up frame for @p node_id reporting @p state. */
147bool pc_canopen_build_heartbeat(CanFrame *out, uint8_t node_id, uint8_t state);
148
149/** @brief Emergency (EMCY) frame: 16-bit error code (LE), error register, 5 manufacturer octets. */
150bool pc_canopen_build_emcy(CanFrame *out, uint8_t node_id, uint16_t error_code, uint8_t error_reg,
151 const uint8_t msef[5]);
152
153/** @brief Transmit-PDO frame (@p pdo_num 1..4): up to 8 raw mapped octets. */
154bool pc_canopen_build_tpdo(CanFrame *out, uint8_t pdo_num, uint8_t node_id, const uint8_t *data, uint8_t len);
155
156/** @brief Receive-PDO frame (@p pdo_num 1..4): up to 8 raw mapped octets. */
157bool pc_canopen_build_rpdo(CanFrame *out, uint8_t pdo_num, uint8_t node_id, const uint8_t *data, uint8_t len);
158
159/** @brief SDO expedited upload (read) request for object @p index / @p sub on @p node_id. */
160bool pc_canopen_build_sdo_read(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub);
161
162/** @brief SDO expedited download (write) of @p len (1..4) octets to @p index / @p sub. */
163bool pc_canopen_build_sdo_write(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub, const uint8_t *data,
164 uint8_t len);
165
166/** @brief SDO abort frame. @p to_server true => client->server (0x600), false => server->client (0x580). */
167bool pc_canopen_build_sdo_abort(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub, uint32_t abort_code,
168 bool to_server);
169
170// --- parsers ---
171
172/** @brief Classify any frame by COB-ID into its CANopen function + node. */
173bool pc_canopen_parse(const CanFrame *f, CanopenMsg *out);
174
175/** @brief Decode an EMCY frame (must be a 0x080+node, 8-octet frame). */
176bool pc_canopen_parse_emcy(const CanFrame *f, uint8_t *node_id, uint16_t *error_code, uint8_t *error_reg,
177 uint8_t msef[5]);
178
179/** @brief Decode a heartbeat frame (0x700+node, 1 octet). */
180bool pc_canopen_parse_heartbeat(const CanFrame *f, uint8_t *node_id, uint8_t *state);
181
182/** @brief Decode a TIME frame (0x100, 6 octets) into @p out. @return true iff @p f is the TIME COB with at
183 * least 6 data octets; the reserved top 4 bits of the ms field are masked off. */
184bool pc_canopen_parse_time(const CanFrame *f, CanopenTime *out);
185
186/** @brief Decode an SDO server response (0x580+node): upload data, download ack, or abort. */
187bool pc_canopen_parse_sdo_response(const CanFrame *f, CanopenSdoResponse *out);
188
189// --- segmented SDO (CiA 301 §7.2.4.3): transfers larger than the 4-octet expedited limit ---
190//
191// A segmented transfer is: an initiate frame carrying the object + total size, then a run of segment
192// frames each carrying up to 7 octets with a toggle bit that alternates 0,1,0,1 and a last-segment flag.
193
194/** @brief Octets of object data a single SDO segment carries. */
195#define CANOPEN_SDO_SEG_DATA 7
196
197/**
198 * @brief Build a segmented SDO download (write) initiate: names the object and the total byte count that
199 * the following segments will carry (client -> server, 0x600+node).
200 */
201bool pc_canopen_build_sdo_download_init(CanFrame *out, uint8_t node_id, uint16_t index, uint8_t sub,
202 uint32_t total_size);
203
204/**
205 * @brief Build a segmented SDO download segment carrying @p len (1..7) octets of object data.
206 * @param toggle the toggle bit for this segment (0 for the first, then alternating).
207 * @param last true on the final segment.
208 */
209bool pc_canopen_build_sdo_download_segment(CanFrame *out, uint8_t node_id, bool toggle, const uint8_t *data,
210 uint8_t len, bool last);
211
212/** @brief Build a segmented SDO upload segment request (client asks the server for the next segment). */
213bool pc_canopen_build_sdo_upload_segment_req(CanFrame *out, uint8_t node_id, bool toggle);
214
215/**
216 * @brief Decode an SDO segment frame (either direction) into its toggle, data, length, and last flag.
217 * @return true iff @p f is an 8-octet frame whose command specifier is the segment form (high 3 bits 0).
218 */
219bool pc_canopen_parse_sdo_segment(const CanFrame *f, bool *toggle, uint8_t *data, uint8_t *len, bool *last);
220
221/** @brief Segmented-upload reassembly state (accumulates segment data into a caller buffer). */
222struct CanopenSdoReasm
223{
224 uint8_t *buf; ///< caller-provided accumulation buffer
225 size_t cap; ///< its capacity
226 size_t len; ///< octets accumulated so far
227 bool expect_toggle; ///< the toggle the next segment must carry (starts false)
228 bool done; ///< set once the last segment is accepted
229};
230
231/** @brief Begin a segmented-upload reassembly with a caller-owned buffer. */
232void pc_canopen_sdo_reasm_init(CanopenSdoReasm *r, uint8_t *buf, size_t cap);
233
234/**
235 * @brief Feed one decoded upload segment into the reassembler (append @p len octets).
236 * @return true on success (sets @c done on the last segment); false on a toggle mismatch or a buffer
237 * overflow.
238 */
239bool pc_canopen_sdo_reasm_feed(CanopenSdoReasm *r, const uint8_t *data, uint8_t len, bool toggle, bool last);
240
241#endif // PC_ENABLE_CANOPEN
242#endif // PROTOCORE_CANOPEN_H
Shared CAN 2.0 frame type for the CAN-based industrial codecs (one source of truth).
User-facing configuration for ProtoCore.
Definition can.h:44