CANopen 搞懂 SDO client/server 运行流程
@[toc]
先给结论
SDO(Service Data Object)是 CANopen 里用于读写对象字典的标准服务。它不是实时过程数据通道,也不是 Heartbeat 那类状态上报通道。它更像“标准化的配置/诊断访问入口”:
SDO 永远由 client 发起。
client 访问另一个节点的对象字典;拥有对象字典的一方是 server。download / upload 是站在 server 角度命名。
SDO download:client 把数据写入 server 的对象字典。SDO upload:client 从 server 的对象字典读取数据。
普通 CANopen 从机必须有 SDO server;SDO client 通常是可选的。
一个普通 STM32 从机被主站配置,只需要CO_SDOserver;只有当前节点要主动读写别的节点时,才需要CO_SDOclient。CANopenNode 的 SDO 实现是非阻塞状态机。
CAN 接收回调只做预处理和置位;真正协议推进发生在CO_SDOserver_process()、CO_SDOclientDownload()、CO_SDOclientUpload()周期调用里。源码运行主线可以压缩成一句话:
CAN 帧 -> SDO state -> OD_find/OD_getSub -> OD_IO.read/write -> response/abort -> state idle
1. 先看协议:SDO 解决什么问题
CANopen 的对象字典(Object Dictionary,OD)把设备参数、通信参数和应用参数统一放在 index + subIndex 地址空间里。SDO 的作用,就是通过 CAN 帧访问这些对象字典条目。
SDO 采用 client/server 模型:
1 | sequenceDiagram |
CiA 对 SDO 的公开说明中,SDO 用于访问 CANopen CC 设备对象字典的所有条目;一个 SDO 通道由两个不同 CAN identifier 的 CAN 帧组成,并且是确认式通信服务。CANopenNode 官方文档也说明,SDO 可访问对象字典任意条目,并通过 segmented/block 方式传输超过单帧的数据。[^cia-sdo][^canopennode-sdo-server]
1.1 SDO 和 PDO 的边界
| 对比项 | SDO | PDO |
|---|---|---|
| 通信模型 | client/server,请求/响应 | producer/consumer,通常无确认 |
| 主要用途 | 配置、诊断、参数读写、大块数据 | 实时过程数据 |
| 数据地址 | 每次携带 index + subIndex |
由 PDO mapping 预配置 |
| 开销 | 较高,有命令字和确认 | 低,适合周期/事件触发实时数据 |
| 是否适合高频控制环 | 不适合 | 适合 |
1.2 download / upload 的方向不要看反
SDO 的术语容易和“PC 上传/下载”混淆。CANopen 按 server 对象字典视角命名:
1 | flowchart LR |
因此:
1 | CO_SDOclientDownload*() -> client 写远端 OD |
2. SDO 通道和对象字典参数
SDO 通信至少需要两条 CAN 报文方向:
| 方向 | 默认 COB-ID | 语义 |
|---|---|---|
| client -> server | 0x600 + Node-ID |
client 发请求,server 接收 |
| server -> client | 0x580 + Node-ID |
server 发响应,client 接收 |
CiA 301 的对象字典中,0x1200..0x127F 描述 SDO server 参数,0x1280..0x12FF 描述 SDO client 参数;子索引 01h、02h 指定两条 SDO COB-ID,客户端参数还包含 server Node-ID。[^cia301-sdo-od]
源码对应关系:
| OD 对象 | CANopenNode 模块 | 当前节点角色 |
|---|---|---|
0x1200+ SDO server parameter |
CO_SDOserver |
别人访问本节点 OD |
0x1280+ SDO client parameter |
CO_SDOclient |
本节点主动访问别人 OD |
3. SDO 帧格式:先读懂 Byte 0
经典 CAN 帧数据区只有 8 字节。SDO 把前 4 字节固定用于协议寻址和命令控制:
| 字节 | 作用 |
|---|---|
| Byte 0 | command specifier + 标志位 |
| Byte 1 | index 低字节 |
| Byte 2 | index 高字节 |
| Byte 3 | subIndex |
| Byte 4..7 | 数据、长度、CRC 或 abort code |
CANopenNode 的 CO_SDO_state_t 注释直接按这些帧格式描述状态。例如:
| 阶段 | Byte 0 典型形式 | 说明 |
|---|---|---|
| initiate download request | 0010nnes |
写对象;e=1 表示 expedited,s=1 表示大小有效 |
| initiate download response | 01100000 / 0x60 |
server 确认启动写入 |
| download segment request | 000tnnnc |
每段最多 7 字节;t 是 toggle;c=1 表示最后一段 |
| download segment response | 001t0000 |
server 确认 segment |
| initiate upload request | 01000000 / 0x40 |
读对象 |
| initiate upload response | 0100nnes |
server 返回短数据或返回后续分段长度 |
| upload segment request | 011t0000 |
client 请求下一段 |
| upload segment response | 000tnnnc |
server 返回下一段 |
| abort | 10000000 / 0x80 |
Byte 4..7 放 SDO abort code |
4. 三种传输方式
4.1 Expedited transfer:≤ 4 字节直接放启动帧
1 | sequenceDiagram |
或读对象:
1 | sequenceDiagram |
适用场景:UNSIGNED8/16/32、短枚举、控制字、参数标量。
协议开销最小,一次请求 + 一次响应即可完成。
4.2 Segmented transfer:每段最多 7 字节
如果数据超过 4 字节,或者对象类型需要流式读写,SDO 进入 segmented transfer。每个 segment 的 Byte 0 用于协议控制,Byte 1..7 放数据,所以每段最多传 7 字节。CiA 对 SDO 的公开说明也明确指出 normal/segmented transfer 每段最多携带 7 字节应用数据。[^cia-sdo]
1 | flowchart TD |
这里 toggle 的作用不是加密,也不是序号;它主要用于发现“重复帧或错序帧”。源码里 toggle 在 segment 请求/响应之间来回翻转。
4.3 Block transfer:大块数据降低确认开销
Block transfer 把多个 segment 合并成一个 block,每个 segment 使用 seqno,一个 block 结束后才确认 ackseq。CiA 对 SDO 的公开说明中,block transfer 每块最多 127 个 segment,接收方按块确认,因此适合较大数据。[^cia-sdo]
1 | sequenceDiagram |
CANopenNode 中 block transfer 受配置宏约束。server 侧打开 block 时必须同时打开 segmented 和 CRC16;client 侧打开 block 时还要求 FIFO 的 ALT_READ 和 CRC16_CCITT 能力。CANopenNode 官方配置文档也列出了这些依赖关系。[^canopennode-sdo-config]
5. 源码文件分工
本文按下面顺序读源码:
| 文件 | 作用 | 阅读重点 |
|---|---|---|
CO_SDOserver.h |
协议公共定义和 server API | CO_SDO_state_t、abort code、return code、server 对象结构 |
CO_SDOserver.c |
SDO server 实现 | CAN RX 回调、OD 查找、权限检查、download/upload 应答、timeout/abort |
CO_SDOclient.h |
client 对象和 API | FIFO 缓冲区、setup、download/upload 调用模型 |
CO_SDOclient.c |
SDO client 实现 | client 状态机、非阻塞循环、本地传输、block 分支 |
6. CO_SDO_state_t:源码状态机的骨架
CO_SDOserver.h 把 SDO 状态集中定义在 CO_SDO_state_t。它的数值设计很规整:
| 数值范围 | 含义 |
|---|---|
0x00 |
IDLE / ABORT |
0x10 |
download 相关状态 |
0x20 |
upload 相关状态 |
0x40 flag |
block mode 相关状态 |
这和源码里的判断方式一致:先看当前状态属于 download、upload 还是 block,再进入具体分支处理。
几个主状态链路:
1 | download normal: |
7. Server 初始化:从 0x1200 到 CAN RX/TX buffer
CO_SDOserver_init() 必须在 communication reset 阶段调用。它做的事情可以拆成 5 步:
1 | flowchart TD |
CO_SDOserver_init_canRxTx() 会检查 COB-ID bit31 的 valid 位。如果 client-to-server 和 server-to-client 两个 CAN-ID 都有效,SDO->valid = true;否则把 CAN-ID 清零并认为通道无效。
如果启用了 CO_CONFIG_FLAG_OD_DYNAMIC,写 0x1201+ 的 SDO server 参数会调用 OD_write_1201_additional(),再进入 CO_SDOserver_init_canRxTx() 重新配置 CAN RX/TX。
8. Server 接收回调:只做轻量预处理
CO_SDO_receive() 是 CAN RX 回调。它不是完整协议处理函数,只处理“收到帧以后先放哪里”的问题:
1 | flowchart TD |
普通 expedited/segmented 帧只是复制到 SDO->CANrxData 并置 CANrxNew。block download 的子块数据较密集,源码在 RX 回调里会直接复制数据到 server 缓冲区,同时根据 seqno 判断是否需要进入响应状态。
9. CO_SDOserver_process():server 协议推进核心
server 的主循环处理可以理解为三段:
1 | flowchart TD |
关键点:
SDO server 只在 Pre-operational 或 Operational 状态下工作。
NMTisPreOrOperational为 false 时,server 会回到 idle。新请求必须从 idle 开始。
如果 idle 下收到Byte0,源码会识别:0x20mask:download initiate0x40:upload initiate- block download/upload initiate
- 其他命令:abort
对象字典访问先于传输细节。
server 在新请求阶段就会解析index/subIndex,调用OD_find()、OD_getSub(),再检查ODA_SDO_RW、ODA_SDO_R、ODA_SDO_W。读写最终通过 OD interface 完成。
这意味着 SDO server 不直接知道应用变量怎么存,它只通过OD_IO.read()/OD_IO.write()访问对象。
10. Server download:client 写本节点对象字典
download 是“client 把数据下载到 server 的对象字典”。
10.1 Expedited download
1 | sequenceDiagram |
源码中的判断思路:
1 | 如果 initiate download 中 e=1: |
10.2 Segmented download
1 | flowchart TD |
validateAndWriteToOD() 是 download 写入的关键 helper。它主要做:
- 校验最终收到长度是否等于
sizeInd。 - 必要时处理大端平台字节序。
- 对字符串补终止零。
- block 模式下计算或校验 CRC。
- 调用
OD_IO.write()写入对象字典。 - 如果 OD 写入返回错误,转换成 SDO abort code。
11. Server upload:client 读本节点对象字典
upload 是“client 从 server 的对象字典上传数据”。
11.1 Expedited upload
1 | sequenceDiagram |
如果对象大小 ≤ 4 字节,server 可以把数据直接放在 upload initiate response 的 Byte4..7 中。
11.2 Segmented upload
1 | flowchart TD |
这里要注意:server 侧不一定一次性把整个 OD 对象读到 RAM。它可以通过 OD_IO.stream 流式读取,尤其适合字符串、数组、domain 或应用自定义 read hook。
12. Client 初始化:从 0x1280 到 FIFO
CO_SDOclient_init() 的主线:
1 | flowchart TD |
CO_SDOclient_setup() 既可由初始化调用,也可由应用在访问不同远端节点前主动调用。CANopenNode 官方文档给出的典型用法也是先 setup(),再 UploadInitiate() 或 DownloadInitiate(),随后循环调用处理函数直到返回值 <= 0。[^canopennode-sdo-client]
client 对象内部有 CO_fifo_t bufFifo。download 时应用先把要写的数据放入 FIFO;upload 时协议栈把收到的数据放进 FIFO,应用再读出。
13. Client download:本节点主动写远端 OD
client 侧 download 的调用链:
1 | flowchart TD |
普通 segmented/expedited download 状态推进:
1 | sequenceDiagram |
源码中的关键决策:
| 条件 | client 状态 |
|---|---|
nodeIDOfTheSDOServer == nodeId 且启用 local |
CO_SDO_ST_DOWNLOAD_LOCAL_TRANSFER |
blockEnable == true 且数据未知或大于阈值 |
CO_SDO_ST_DOWNLOAD_BLK_INITIATE_REQ |
| 其他 | CO_SDO_ST_DOWNLOAD_INITIATE_REQ |
CO_CONFIG_SDO_CLI_PST 是 client block transfer 的协议切换阈值,默认 21U。也就是说,小数据不值得走 block;大数据才可能启用 block。
14. Client upload:本节点主动读远端 OD
client 侧 upload 的调用链:
1 | flowchart TD |
upload 和 download 的差别在于数据流向:
1 | sequenceDiagram |
CO_SDOclientUploadBufRead() 可以多次调用。官方文档也说明,数据较长时可以在多个周期里从 client 内部 FIFO 读取。[^canopennode-sdo-client]
15. Abort 与 timeout:错误如何结束
SDO abort 帧格式固定:
1 | Byte0 = 0x80 |
server 和 client 都可以发送 abort。源码中的常见触发点:
| 场景 | 结果 |
|---|---|
| command specifier 不合法 | CO_SDO_AB_CMD |
| OD index/subIndex 不存在 | 由 OD_getSDOabCode() 转换 |
| 没有 SDO 读写权限 | read-only / write-only / unsupported access |
| 实际长度和声明长度不一致 | data long / data short |
| segmented toggle 不匹配 | toggle bit error |
| 等待响应超时 | timeout abort |
| CRC 不匹配 | block CRC abort |
timeout 的实现方式也很直接:每轮 process 把 timeDifference_us 累加到 timeoutTimer。如果超过 SDOtimeoutTime_us,就转入 CO_SDO_ST_ABORT,准备发送 abort 帧。
16. CO_CONFIG_SDO_*:编译能力和运行 OD 要同时满足
CANopenNode 的配置宏只决定“代码是否编译进来”,不等价于 OD 一定配置正确。SDO 至少要同时满足:
- 编译宏启用对应能力。
- OD 中存在对应参数对象。
- COB-ID 有效,且不与受限 CAN-ID 冲突。
- CAN RX/TX buffer 数量和下标正确。
- NMT 状态允许 SDO 工作。
常见配置判断:
| 需求 | 建议 |
|---|---|
| 普通 STM32 从机,被主站读写参数 | 保留 SDO server;不启用 SDO client |
| 当前节点要主动配置别的节点 | 启用 CO_CONFIG_SDO_CLI_ENABLE |
| 要读写字符串、数组、domain 等 >4 字节对象 | 启用 segmented |
| 要做固件/大块 domain 传输 | 考虑 block,同时补齐 FIFO/CRC 配置 |
| RTOS 中收到 SDO 后要唤醒任务 | 启用 CO_CONFIG_FLAG_CALLBACK_PRE |
| 调度器想知道下一次最晚调用时间 | 启用 CO_CONFIG_FLAG_TIMERNEXT |
| 允许运行时改 SDO COB-ID | 启用 CO_CONFIG_FLAG_OD_DYNAMIC,并配置对应 OD |
CANopenNode 官方配置文档说明:SDO server 可选 segmented、block、callback、timerNext、OD dynamic;SDO client 可选 enable、segmented、block、local、callback、timerNext、OD dynamic,并说明 block/fifo/buffer 的依赖。[^canopennode-sdo-config]
17. 把源码放进 STM32 从机主循环
一个普通从机的 SDO server 调用模型通常是:
1 | flowchart TD |
伪代码结构:
1 | /* communication reset section */ |
如果当前节点不是配置器/网关,不要为了“支持 SDO”去打开 CO_SDOclient。server 和 client 是两个角色,普通从机被访问只需要 server。
18. 几个工程判断
18.1 “支持 SDO”通常先看 server,不是 client
普通从机需要主站能读写它的对象字典,所以要确认:
1 | 0x1200 SDO server parameter |
只有当前节点要主动访问其他节点对象字典,才看:
1 | 0x1280 SDO client parameter |
18.2 CANrxNew 不是“收到了就清”
server/client 的 CAN RX 回调只设置新帧标志。真正清除发生在 process 函数已经取走并处理该帧之后。这样可以避免主循环还没处理完时被下一帧覆盖。
18.3 block transfer 不只是打开一个宏
block 需要更大的缓冲、CRC/FIFO 支持和更复杂的时序。资源紧张 MCU 上,先跑通 expedited + segmented,再考虑 block。
18.4 OD 权限错误不是 CAN 驱动错误
如果 SDO abort 显示只读、只写、对象不存在、类型不匹配,优先查对象字典生成结果、访问属性和 subIndex,而不是先查 CAN 波特率或滤波器。
19. 建议阅读顺序
1 | flowchart TD |
20. 一句话串起运行流程
1 | client 选择 server Node-ID |
SDO 的重点不是“怎么发 8 字节 CAN 帧”,而是 CAN 帧、对象字典、访问权限、非阻塞状态机 这四层如何连起来。读源码时只要抓住 state、CANrxNew、OD_IO、timeoutTimer 这几个变量,CO_SDOserver.c 和 CO_SDOclient.c 的分支就会清晰很多。
参考资料
[^cia-sdo]: CAN in Automation, “SDO protocol: CAN in Automation”, https://www.can-cia.org/can-knowledge/sdo-protocol
[^canopennode-sdo-server]: CANopenNode Doxygen, “SDO server”, https://canopennode.github.io/CANopenNode/group__CO__SDOserver.html
[^canopennode-sdo-client]: CANopenNode Doxygen, “SDO client”, https://canopennode.github.io/CANopenNode/group__CO__SDOclient.html
[^canopennode-sdo-config]: CANopenNode Doxygen, “SDO server/client configuration”, https://canopennode.github.io/CANopenNode/group__CO__STACK__CONFIG__SDO.html
[^canopennode-github]: CANopenNode GitHub repository, https://github.com/CANopenNode/CANopenNode
[^cia301-sdo-od]: 你上传的《CiA301 V4.2.0(中文注释版)》中,目录列出 7.2.4 服务数据对象(SDO),通信协议对象规范列出 0x1200..0x127F SDO server parameter 与 0x1280..0x12FF SDO client parameter。













