|
CANopen 搞懂 SDO client/server 运行流程
@[toc] 先给结论 SDO(Service Data Object)是 CANopen 里用于读写对象字典的标准服务。它不是实时过程数据通道,也不是 Heartbeat 那类状态上报通道。它更像“标准化的配置/诊断访问入口”: 1. SDO 永远由 client 发起。client 访问另一个节点的对象字典;拥有对象字典的一方是 server。 2. download / upload 是站在 server 角度命名。 • SDO download:client 把数据写入 server 的对象字典。 • SDO upload:client 从 server 的对象字典读取数据。 3. 普通 CANopen 从机必须有 SDO server;SDO client 通常是可选的。一个普通 STM32 从机被主站配置,只需要 CO_SDOserver;只有当前节点要主动读写别的节点时,才需要 CO_SDOclient。 4. CANopenNode 的 SDO 实现是非阻塞状态机。CAN 接收回调只做预处理和置位;真正协议推进发生在 CO_SDOserver_process()、CO_SDOclientDownload()、CO_SDOclientUpload() 周期调用里。 5. 源码运行主线可以压缩成一句话: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 模型: <tspan dy="0" x="1019"><tspan>Object Dictionary</tspan></tspan><tspan dy="0" x="678"><tspan>SDO Server</tspan></tspan><tspan dy="0" x="418"><tspan>CAN bus</tspan></tspan><tspan dy="0" x="75"><tspan>SDO Client</tspan></tspan><symbol height="24" width="24"> </symbol><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol><symbol height="24" width="24"> </symbol><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker><tspan>request(index, subIndex, command)</tspan><tspan>client ->> server COB-ID</tspan><tspan>find index/subIndex + check access</tspan><tspan>read/write result</tspan><tspan>response or abort</tspan><tspan>server ->> client COB-ID</tspan> 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 对象字典视角命名: <circle r="5" cy="5" cx="5"></circle><circle r="5" cy="5" cx="5"></circle> 因此: CO_SDOclientDownload*() -> client 写远端 ODCO_SDOclientUpload*() -> 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 字节直接放启动帧 <tspan dy="0" x="348"><tspan>Server / OD</tspan></tspan><tspan dy="0" x="75"><tspan>Client</tspan></tspan><symbol height="24" width="24"> </symbol><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol><symbol height="24" width="24"> </symbol><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker><tspan>initiate download req</tspan><tspan>Byte4..7 = data</tspan><tspan>initiate download rsp 0x60</tspan> 或读对象: <tspan dy="0" x="326"><tspan>Server / OD</tspan></tspan><tspan dy="0" x="75"><tspan>Client</tspan></tspan><symbol height="24" width="24"> </symbol><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol><symbol height="24" width="24"> </symbol><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker><tspan>initiate upload req 0x40</tspan><tspan>initiate upload rsp</tspan><tspan>Byte4..7 = data</tspan> 适用场景: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] <circle r="5" cy="5" cx="5"></circle><circle r="5" cy="5" cx="5"></circle> 这里 toggle 的作用不是加密,也不是序号;它主要用于发现“重复帧或错序帧”。源码里 toggle 在 segment 请求/响应之间来回翻转。 4.3 Block transfer:大块数据降低确认开销 Block transfer 把多个 segment 合并成一个 block,每个 segment 使用 seqno,一个 block 结束后才确认 ackseq。CiA 对 SDO 的公开说明中,block transfer 每块最多 127 个 segment,接收方按块确认,因此适合较大数据。[^cia-sdo] <tspan dy="0" x="323"><tspan>Server</tspan></tspan><tspan dy="0" x="75"><tspan>Client</tspan></tspan><symbol height="24" width="24"> </symbol><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol><symbol height="24" width="24"> </symbol><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker> <tspan>loop</tspan><tspan x="224"><tspan>[seqno = 1..blksize]</tspan></tspan><tspan>block initiate</tspan><tspan>block initiate response</tspan><tspan>blksize / CRC capability</tspan><tspan>sub-block segment</tspan><tspan>seqno + data[7]</tspan><tspan>sub-block response</tspan><tspan>ackseq + next blksize</tspan><tspan>block end</tspan><tspan>noData + CRC</tspan><tspan>block end response</tspan> 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,再进入具体分支处理。 几个主状态链路: download normal:IDLE -> DOWNLOAD_INITIATE_REQ -> DOWNLOAD_INITIATE_RSP -> DOWNLOAD_SEGMENT_REQ -> DOWNLOAD_SEGMENT_RSP -> IDLEupload normal:IDLE -> UPLOAD_INITIATE_REQ -> UPLOAD_INITIATE_RSP -> UPLOAD_SEGMENT_REQ -> UPLOAD_SEGMENT_RSP -> IDLEblock download:IDLE -> DOWNLOAD_BLK_INITIATE_REQ -> DOWNLOAD_BLK_INITIATE_RSP -> DOWNLOAD_BLK_SUBBLOCK_REQ/RSP -> DOWNLOAD_BLK_END_REQ/RSP -> IDLEblock upload:IDLE -> UPLOAD_BLK_INITIATE_REQ -> UPLOAD_BLK_INITIATE_RSP -> UPLOAD_BLK_INITIATE_REQ2 -> UPLOAD_BLK_SUBBLOCK_SREQ/CRSP -> UPLOAD_BLK_END_SREQ/CRSP -> IDLE--- 7. Server 初始化:从 0x1200 到 CAN RX/TX buffer CO_SDOserver_init() 必须在 communication reset 阶段调用。它做的事情可以拆成 5 步: <circle r="5" cy="5" cx="5"></circle><circle r="5" cy="5" cx="5"></circle> 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 回调。它不是完整协议处理函数,只处理“收到帧以后先放哪里”的问题: <circle r="5" cy="5" cx="5"></circle><circle r="5" cy="5" cx="5"></circle> 普通 expedited/segmented 帧只是复制到 SDO->CANrxData 并置 CANrxNew。block download 的子块数据较密集,源码在 RX 回调里会直接复制数据到 server 缓冲区,同时根据 seqno 判断是否需要进入响应状态。 --- 9. CO_SDOserver_process():server 协议推进核心 server 的主循环处理可以理解为三段: <circle r="5" cy="5" cx="5"></circle><circle r="5" cy="5" cx="5"></circle> 关键点: 1. SDO server 只在 Pre-operational 或 Operational 状态下工作。NMTisPreOrOperational 为 false 时,server 会回到 idle。 2. 新请求必须从 idle 开始。如果 idle 下收到 Byte0,源码会识别: • 0x20 mask:download initiate • 0x40:upload initiate • block download/upload initiate • 其他命令:abort 3. 对象字典访问先于传输细节。server 在新请求阶段就会解析 index/subIndex,调用 OD_find()、OD_getSub(),再检查 ODA_SDO_RW、ODA_SDO_R、ODA_SDO_W。 4. 读写最终通过 OD interface 完成。这意味着 SDO server 不直接知道应用变量怎么存,它只通过 OD_IO.read() / OD_IO.write() 访问对象。 --- 10. Server download:client 写本节点对象字典 download 是“client 把数据下载到 server 的对象字典”。 10.1 Expedited download <text alignment-baseline="central" dominant-baseline="central" y="341.5" x="692"><tspan dy="0" x="692"><tspan>OD entry</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="341.5" x="393"><tspan dy="0" x="393"><tspan>Server</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="341.5" x="75"><tspan dy="0" x="75"><tspan>Client</tspan></tspan></text><line name="S" stroke="#999" stroke-width="0.5px" y2="309" x2="393" y1="65" x1="393"></line><line name="C" stroke="#999" stroke-width="0.5px" y2="309" x2="75" y1="65" x1="75"></line><defs><symbol height="24" width="24"> </symbol></defs><defs><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol></defs><defs><symbol height="24" width="24"> </symbol></defs><defs><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker></defs><defs><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker></defs><defs><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker></defs><defs><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker></defs><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="80" x="233"><tspan>0x2F/0x2B/0x23...</tspan></text><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="99" x="233"><tspan>index/subIndex + data</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="135" x2="389" y1="135" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="150" x="541"><tspan>OD_getSub() + check writable</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="187" x2="688" y1="187" x1="394"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="202" x="541"><tspan>OD_IO.write(data)</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="239" x2="688" y1="239" x1="394"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="254" x="236"><tspan>0x60 initiate download response</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="289" x2="79" y1="289" x1="392"></line> 源码中的判断思路: <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)"></ellipse><ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)"></ellipse><ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)"></ellipse>如果 initiate download 中 e=1: Byte4..7 已包含全部数据 根据 n 计算真实数据长度 写入 OD finished = true 发送 0x60 state 回 IDLE10.2 Segmented download <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> validateAndWriteToOD() 是 download 写入的关键 helper。它主要做: • 校验最终收到长度是否等于 sizeInd。 • 必要时处理大端平台字节序。 • 对字符串补终止零。 • block 模式下计算或校验 CRC。 • 调用 OD_IO.write() 写入对象字典。 • 如果 OD 写入返回错误,转换成 SDO abort code。 --- 11. Server upload:client 读本节点对象字典 upload 是“client 从 server 的对象字典上传数据”。 11.1 Expedited upload <text alignment-baseline="central" dominant-baseline="central" y="321.5" x="637"><tspan dy="0" x="637"><tspan>OD entry</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="321.5" x="333"><tspan dy="0" x="333"><tspan>Server</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="321.5" x="75"><tspan dy="0" x="75"><tspan>Client</tspan></tspan></text><line name="S" stroke="#999" stroke-width="0.5px" y2="289" x2="333" y1="65" x1="333"></line><line name="C" stroke="#999" stroke-width="0.5px" y2="289" x2="75" y1="65" x1="75"></line><defs><symbol height="24" width="24"> </symbol></defs><defs><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol></defs><defs><symbol height="24" width="24"> </symbol></defs><defs><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker></defs><defs><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker></defs><defs><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker></defs><defs><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker></defs><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="80" x="203"><tspan>0x40 + index/subIndex</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="115" x2="329" y1="115" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="130" x="484"><tspan>OD_getSub() + check readable</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="167" x2="633" y1="167" x1="334"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="182" x="484"><tspan>OD_IO.read(max 4 bytes)</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="219" x2="633" y1="219" x1="334"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="234" x="206"><tspan>0x43/0x4B/0x4F... + data</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="269" x2="79" y1="269" x1="332"></line> 如果对象大小 ≤ 4 字节,server 可以把数据直接放在 upload initiate response 的 Byte4..7 中。 11.2 Segmented upload <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> 这里要注意:server 侧不一定一次性把整个 OD 对象读到 RAM。它可以通过 OD_IO.stream 流式读取,尤其适合字符串、数组、domain 或应用自定义 read hook。 --- 12. Client 初始化:从 0x1280 到 FIFO CO_SDOclient_init() 的主线: <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> 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 的调用链: <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> 普通 segmented/expedited download 状态推进: <text alignment-baseline="central" dominant-baseline="central" y="477.5" x="659"><tspan dy="0" x="659"><tspan>SDO Server</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="477.5" x="401"><tspan dy="0" x="401"><tspan>SDO Client</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="477.5" x="75"><tspan dy="0" x="75"><tspan>Application</tspan></tspan></text><line name="S" stroke="#999" stroke-width="0.5px" y2="445" x2="659" y1="65" x1="659"></line><line name="C" stroke="#999" stroke-width="0.5px" y2="445" x2="401" y1="65" x1="401"></line><line name="App" stroke="#999" stroke-width="0.5px" y2="445" x2="75" y1="65" x1="75"></line><defs><symbol height="24" width="24"> </symbol></defs><defs><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol></defs><defs><symbol height="24" width="24"> </symbol></defs><defs><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker></defs><defs><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker></defs><defs><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker></defs><defs><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker></defs><line y2="175" x2="670" y1="175" x1="64"></line><line y2="375" x2="670" y1="175" x1="670"></line><line y2="375" x2="670" y1="375" x1="64"></line><line y2="375" x2="64" y1="175" x1="64"></line> <text alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="188" x="89"><tspan>loop</tspan></text><text text-anchor="middle" y="193" x="392"><tspan x="392"><tspan>[cyclic]</tspan></tspan></text><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="80" x="237"><tspan>DownloadInitiate()</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="115" x2="397" y1="115" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="130" x="237"><tspan>DownloadBufWrite()</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="165" x2="397" y1="165" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="226" x="237"><tspan>CO_SDOclientDownload(dt)</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="263" x2="397" y1="263" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="278" x="529"><tspan>initiate/segment request</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="315" x2="655" y1="315" x1="402"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="330" x="532"><tspan>response</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="365" x2="405" y1="365" x1="658"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="390" x="240"><tspan>communicationEnd or abortCode</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="425" x2="79" y1="425" x1="400"></line> 源码中的关键决策: 条件 | 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 的调用链: <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> upload 和 download 的差别在于数据流向: <text alignment-baseline="central" dominant-baseline="central" y="427.5" x="659"><tspan dy="0" x="659"><tspan>SDO Server</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="427.5" x="382"><tspan dy="0" x="382"><tspan>SDO Client</tspan></tspan></text><text alignment-baseline="central" dominant-baseline="central" y="427.5" x="75"><tspan dy="0" x="75"><tspan>Application</tspan></tspan></text><line name="S" stroke="#999" stroke-width="0.5px" y2="395" x2="659" y1="65" x1="659"></line><line name="C" stroke="#999" stroke-width="0.5px" y2="395" x2="382" y1="65" x1="382"></line><line name="App" stroke="#999" stroke-width="0.5px" y2="395" x2="75" y1="65" x1="75"></line><defs><symbol height="24" width="24"> </symbol></defs><defs><symbol clip-rule="evenodd" fill-rule="evenodd"> </symbol></defs><defs><symbol height="24" width="24"> </symbol></defs><defs><marker orient="auto-start-reverse" markerheight="12" markerwidth="12" markerunits="userSpaceOnUse" refy="5" refx="7.9"> </marker></defs><defs><marker refy="4.5" refx="4" orient="auto" markerheight="8" markerwidth="15"> </marker></defs><defs><marker orient="auto" markerheight="28" markerwidth="20" refy="7" refx="15.5"> </marker></defs><defs><marker orient="auto" markerheight="40" markerwidth="60" refy="15" refx="15"><circle r="6" cy="15" cx="15"></circle></marker></defs><line y2="125" x2="670" y1="125" x1="64"></line><line y2="325" x2="670" y1="125" x1="670"></line><line y2="325" x2="670" y1="325" x1="64"></line><line y2="325" x2="64" y1="125" x1="64"></line> <text alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="138" x="89"><tspan>loop</tspan></text><text text-anchor="middle" y="143" x="392"><tspan x="392"><tspan>[cyclic]</tspan></tspan></text><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="80" x="227"><tspan>UploadInitiate(index/subIndex)</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="115" x2="378" y1="115" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="176" x="227"><tspan>CO_SDOclientUpload(dt)</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="213" x2="378" y1="213" x1="76"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="228" x="519"><tspan>initiate or segment request</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="265" x2="655" y1="265" x1="383"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="280" x="522"><tspan>response with data</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="315" x2="386" y1="315" x1="658"></line><text dy="1em" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="340" x="227"><tspan>UploadBufRead(buf)</tspan></text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" y2="375" x2="378" y1="375" x1="76"></line> CO_SDOclientUploadBufRead() 可以多次调用。官方文档也说明,数据较长时可以在多个周期里从 client 内部 FIFO 读取。[^canopennode-sdo-client] --- 15. Abort 与 timeout:错误如何结束 SDO abort 帧格式固定: <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)"></ellipse><ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)"></ellipse><ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)"></ellipse>Byte0 = 0x80Byte1..2 = indexByte3 = subIndexByte4..7 = abort code,little endianserver 和 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 至少要同时满足: 1. 编译宏启用对应能力。 2. OD 中存在对应参数对象。 3. COB-ID 有效,且不与受限 CAN-ID 冲突。 4. CAN RX/TX buffer 数量和下标正确。 5. 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 调用模型通常是: <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> 伪代码结构: <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)"></ellipse><ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)"></ellipse><ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)"></ellipse>/* communication reset section */CO_SDOserver_init(...);/* main loop or CANopen task */for (;;) { uint32_t timerNext_us = UINT32_MAX; bool_t nmtOk = (NMTstate == CO_NMT_PRE_OPERATIONAL) || (NMTstate == CO_NMT_OPERATIONAL); (void)CO_SDOserver_process(SDO, nmtOk, timeDifference_us, &timerNext_us); /* sleep until next CAN event or timerNext_us */}如果当前节点不是配置器/网关,不要为了“支持 SDO”去打开 CO_SDOclient。server 和 client 是两个角色,普通从机被访问只需要 server。 --- 18. 几个工程判断 18.1 “支持 SDO”通常先看 server,不是 client 普通从机需要主站能读写它的对象字典,所以要确认: <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)"></ellipse><ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)"></ellipse><ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)"></ellipse>0x1200 SDO server parameterCO_CONFIG_SDO_SRVCO_RX/TX buffer for SDO serverCO_SDOserver_process() 被周期调用只有当前节点要主动访问其他节点对象字典,才看: <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)"></ellipse><ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)"></ellipse><ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)"></ellipse>0x1280 SDO client parameterCO_CONFIG_SDO_CLI_ENABLECO_SDOclient_setup()CO_SDOclientDownload/Upload()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. 建议阅读顺序 <marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="8" markerwidth="8" markerunits="userSpaceOnUse" refy="5" refx="4.5" viewbox="0 0 10 10"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="11" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5" refx="-1" viewbox="0 0 10 10"><circle r="5" cy="5" cx="5"></circle></marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="12" viewbox="0 0 11 11"> </marker><marker orient="auto" markerheight="11" markerwidth="11" markerunits="userSpaceOnUse" refy="5.2" refx="-1" viewbox="0 0 11 11"> </marker> --- 20. 一句话串起运行流程 <ellipse cx="50" cy="65" rx="50" ry="52" stroke="rgb(220,60,54)" stroke-width="2" fill="rgb(237,108,96)"></ellipse><ellipse cx="225" cy="65" rx="50" ry="52" stroke="rgb(218,151,33)" stroke-width="2" fill="rgb(247,193,81)"></ellipse><ellipse cx="400" cy="65" rx="50" ry="52" stroke="rgb(27,161,37)" stroke-width="2" fill="rgb(100,200,86)"></ellipse>client 选择 server Node-ID -> 用 0x600+Node-ID 发起 request -> server 收到后按 Byte0 判断 download/upload/block -> server 用 index/subIndex 找 OD 条目并检查权限 -> expedited 直接读写 Byte4..7 -> segmented/block 通过 buffer/FIFO 分段推进 -> 任何错误都转成 abort frame -> 成功或中止后双方 state 回 IDLESDO 的重点不是“怎么发 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。 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |