『7x24小时有问必答』
  

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 写远端 OD

CO_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 参数;子索引  01h02h  指定两条 SDO COB-ID,客户端参数还包含 server Node-ID。[^cia301-sdo-od]
源码对应关系:
OD 对象
CANopenNode 模块
当前节点角色
0x1200+ SDO server parameterCO_SDOserver
别人访问本节点 OD
0x1280+ SDO client parameterCO_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。它的数值设计很规整:
数值范围
含义
0x00IDLE
  /  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 -> IDLE

upload normal:

IDLE -> UPLOAD_INITIATE_REQ -> UPLOAD_INITIATE_RSP

        -> UPLOAD_SEGMENT_REQ -> UPLOAD_SEGMENT_RSP -> IDLE

block download:

IDLE -> DOWNLOAD_BLK_INITIATE_REQ -> DOWNLOAD_BLK_INITIATE_RSP

        -> DOWNLOAD_BLK_SUBBLOCK_REQ/RSP

        -> DOWNLOAD_BLK_END_REQ/RSP -> IDLE

block 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_RWODA_SDO_RODA_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 回 IDLE

10.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      = 0x80

Byte1..2 = index

Byte3      = subIndex

Byte4..7 = abort code,little endian
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 至少要同时满足:
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 parameter

CO_CONFIG_SDO_SRV

CO_RX/TX buffer for SDO server

CO_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 parameter

CO_CONFIG_SDO_CLI_ENABLE

CO_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 回 IDLE
SDO 的重点不是“怎么发 8 字节 CAN 帧”,而是  CAN 帧、对象字典、访问权限、非阻塞状态机  这四层如何连起来。读源码时只要抓住  stateCANrxNewOD_IOtimeoutTimer  这几个变量,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
  

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码