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 模型:

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant C as SDO Client
participant CAN as CAN bus
participant S as SDO Server
participant OD as Object Dictionary

C->>CAN: request(index, subIndex, command)
CAN->>S: client ->> server COB-ID
S->>OD: find index/subIndex + check access
OD-->>S: read/write result
S->>CAN: response or abort
CAN-->>C: server ->> client COB-ID

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
2
3
flowchart LR
C["SDO client"] -- "download<br/>写数据到 server OD" --> S["SDO server"]
S -- "upload<br/>从 server OD 返回数据" --> C

因此:

1
2
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 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
2
3
4
5
6
sequenceDiagram
participant C as Client
participant S as Server / OD

C->>S: initiate download req<br/>Byte4..7 = data
S-->>C: initiate download rsp 0x60

或读对象:

1
2
3
4
5
6
sequenceDiagram
participant C as Client
participant S as Server / OD

C->>S: initiate upload req 0x40
S-->>C: initiate upload rsp<br/>Byte4..7 = data

适用场景: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
2
3
4
5
6
7
8
flowchart TD
A["initiate request<br/>携带 index/subIndex 和 size"] --> B["server 确认"]
B --> C["segment 0<br/>toggle = 0"]
C --> D["segment response<br/>确认 toggle"]
D --> E{"最后一段 c=1 ?"}
E -- "否" --> F["segment 1<br/>toggle 翻转"]
F --> D
E -- "是" --> G["结束,state 回 IDLE"]

这里 toggle 的作用不是加密,也不是序号;它主要用于发现“重复帧或错序帧”。源码里 toggle 在 segment 请求/响应之间来回翻转。

4.3 Block transfer:大块数据降低确认开销

Block transfer 把多个 segment 合并成一个 block,每个 segment 使用 seqno,一个 block 结束后才确认 ackseq。CiA 对 SDO 的公开说明中,block transfer 每块最多 127 个 segment,接收方按块确认,因此适合较大数据。[^cia-sdo]

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant C as Client
participant S as Server

C->>S: block initiate
S-->>C: block initiate response<br/>blksize / CRC capability
loop seqno = 1..blksize
C->>S: sub-block segment<br/>seqno + data[7]
end
S-->>C: sub-block response<br/>ackseq + next blksize
C->>S: block end<br/>noData + CRC
S-->>C: block end response

CANopenNode 中 block transfer 受配置宏约束。server 侧打开 block 时必须同时打开 segmented 和 CRC16;client 侧打开 block 时还要求 FIFO 的 ALT_READCRC16_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 步:

1
2
3
4
5
6
7
8
9
10
flowchart TD
A["CO_SDOserver_init()"] --> B["检查参数<br/>SDO / OD / CANdevRx / CANdevTx"]
B --> C["绑定 OD、nodeId、timeout、state = IDLE"]
C --> D{"OD_1200_SDOsrvPar 是否为空?"}
D -- "空:默认通道" --> E["COB-ID = 0x600/0x580 + nodeId"]
D -- "非空:读取 OD 参数" --> F["读取 0x1200+ sub1/sub2<br/>必要时安装 OD extension"]
E --> G["CO_SDOserver_init_canRxTx()"]
F --> G
G --> H["配置 CANrxBufferInit()<br/>接收 client -> server"]
G --> I["配置 CANtxBufferInit()<br/>发送 server -> client"]

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
2
3
4
5
6
7
8
9
10
11
12
flowchart TD
A["CAN 收到匹配 COB-ID 的 8 字节帧"] --> B{"DLC == 8 ?"}
B -- "否" --> Z["忽略"]
B -- "是" --> C{"data[0] == 0x80 ?"}
C -- "是,client abort" --> D["state = IDLE"]
C -- "否" --> E{"上一帧 CANrxNew 未处理?"}
E -- "是" --> F["忽略,避免覆盖"]
E -- "否" --> G{"block download sub-block ?"}
G -- "是" --> H["直接复制 7 字节数据<br/>检查 seqno / blksize / last"]
G -- "否" --> I["复制到 CANrxData[8]<br/>置 CANrxNew"]
I --> J["可选 pFunctSignalPre() 唤醒任务"]
H --> J

普通 expedited/segmented 帧只是复制到 SDO->CANrxData 并置 CANrxNew。block download 的子块数据较密集,源码在 RX 回调里会直接复制数据到 server 缓冲区,同时根据 seqno 判断是否需要进入响应状态。


9. CO_SDOserver_process():server 协议推进核心

server 的主循环处理可以理解为三段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flowchart TD
A["CO_SDOserver_process()"] --> B{"参数 / NMT / valid 是否允许?"}
B -- "不允许" --> C["清 CANrxNew<br/>state = IDLE"]
B -- "允许" --> D{"是否有新 CAN 帧?"}
D -- "无" --> T["累计 timeout<br/>必要时 timerNext_us"]
D -- "有" --> E{"state == IDLE ?"}
E -- "是" --> F["解析 Byte0<br/>选择 download/upload/block"]
F --> G["从 Byte1..3 取 index/subIndex"]
G --> H["OD_find + OD_getSub"]
H --> I{"访问权限匹配?"}
I -- "否" --> AB["state = ABORT"]
I -- "是" --> J["进入具体状态处理"]
E -- "否" --> J
J --> K{"download / upload / block 分支"}
K --> L["读写 OD 或复制 buffer"]
L --> M["准备 response CAN 帧"]
M --> N{"完成?"}
N -- "是" --> O["state = IDLE<br/>return communicationEnd"]
N -- "否" --> P["等待下一帧或下一轮 process"]
AB --> Q["发送 abort frame<br/>state = IDLE"]

关键点:

  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

1
2
3
4
5
6
7
8
9
sequenceDiagram
participant C as Client
participant S as Server
participant OD as OD entry

C->>S: 0x2F/0x2B/0x23...<br/>index/subIndex + data
S->>OD: OD_getSub() + check writable
S->>OD: OD_IO.write(data)
S-->>C: 0x60 initiate download response

源码中的判断思路:

1
2
3
4
5
6
7
如果 initiate download 中 e=1:
Byte4..7 已包含全部数据
根据 n 计算真实数据长度
写入 OD
finished = true
发送 0x60
state 回 IDLE

10.2 Segmented download

1
2
3
4
5
6
7
8
9
10
11
12
flowchart TD
A["收到 initiate download<br/>e=0,可带 size"] --> B["检查 OD 可写"]
B --> C["server 回 0x60"]
C --> D["等待 DOWNLOAD_SEGMENT_REQ"]
D --> E["检查 toggle"]
E --> F["复制 Byte1..7 到 buffer"]
F --> G{"c == 1 ?"}
G -- "否" --> H["回 segment response<br/>toggle 翻转"]
H --> D
G -- "是" --> I["validateAndWriteToOD()"]
I --> J["OD_IO.write()"]
J --> K["回最后一个 segment response<br/>state = IDLE"]

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
2
3
4
5
6
7
8
9
sequenceDiagram
participant C as Client
participant S as Server
participant OD as OD entry

C->>S: 0x40 + index/subIndex
S->>OD: OD_getSub() + check readable
S->>OD: OD_IO.read(max 4 bytes)
S-->>C: 0x43/0x4B/0x4F... + data

如果对象大小 ≤ 4 字节,server 可以把数据直接放在 upload initiate response 的 Byte4..7 中。

11.2 Segmented upload

1
2
3
4
5
6
7
8
9
10
11
flowchart TD
A["client 发 0x40 upload initiate"] --> B["server 查 OD 并确认可读"]
B --> C{"size <= 4 ?"}
C -- "是" --> D["expedited response<br/>Byte4..7 = data"]
C -- "否" --> E["response 指示 segmented<br/>可携带 size"]
E --> F["client 请求 segment<br/>011t0000"]
F --> G["server 从 OD_IO.read() 取最多 7 字节"]
G --> H["server 回 segment<br/>000tnnnc + data"]
H --> I{"最后一段?"}
I -- "否" --> F
I -- "是" --> J["state = IDLE"]

这里要注意:server 侧不一定一次性把整个 OD 对象读到 RAM。它可以通过 OD_IO.stream 流式读取,尤其适合字符串、数组、domain 或应用自定义 read hook。


12. Client 初始化:从 0x1280 到 FIFO

CO_SDOclient_init() 的主线:

1
2
3
4
5
6
7
8
9
10
11
flowchart TD
A["CO_SDOclient_init()"] --> B["检查 OD_1280_SDOcliPar index 范围"]
B --> C["绑定 CANdevRx/CANdevTx/nodeId"]
C --> D["CO_fifo_init() 初始化内部环形缓冲"]
D --> E["读取 0x1280+ sub0..sub3"]
E --> F{"启用 OD dynamic ?"}
F -- "是" --> G["安装 OD_write_1280 extension"]
F -- "否" --> H["跳过"]
G --> I["CO_SDOclient_setup()"]
H --> I
I --> J["配置接收 server->client<br/>配置发送 client->server"]

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
2
3
4
5
6
7
8
flowchart TD
A["CO_SDOclient_setup()"] --> B["CO_SDOclientDownloadInitiate(index, subIndex, size, timeout, blockEnable)"]
B --> C["CO_SDOclientDownloadBufWrite(data)"]
C --> D["周期调用 CO_SDOclientDownload(dt, ...)"]
D --> E{"返回值 > 0 ?"}
E -- "是" --> D
E -- "否,=0" --> F["写入成功,通信结束"]
E -- "否,<0" --> G["读取 abortCode,处理错误"]

普通 segmented/expedited download 状态推进:

1
2
3
4
5
6
7
8
9
10
11
12
13
sequenceDiagram
participant App as Application
participant C as SDO Client
participant S as SDO Server

App->>C: DownloadInitiate()
App->>C: DownloadBufWrite()
loop cyclic
App->>C: CO_SDOclientDownload(dt)
C->>S: initiate/segment request
S-->>C: response
end
C-->>App: communicationEnd or abortCode

源码中的关键决策:

条件 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
2
3
4
5
6
7
flowchart TD
A["CO_SDOclient_setup()"] --> B["CO_SDOclientUploadInitiate(index, subIndex, timeout, blockEnable)"]
B --> C["周期调用 CO_SDOclientUpload(dt, ...)"]
C --> D{"返回值 > 0 ?"}
D -- "是" --> C
D -- "否,=0" --> E["CO_SDOclientUploadBufRead() 读出数据"]
D -- "否,<0" --> F["读取 abortCode,处理错误"]

upload 和 download 的差别在于数据流向:

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant App as Application
participant C as SDO Client
participant S as SDO Server

App->>C: UploadInitiate(index/subIndex)
loop cyclic
App->>C: CO_SDOclientUpload(dt)
C->>S: initiate or segment request
S-->>C: response with data
end
App->>C: UploadBufRead(buf)

CO_SDOclientUploadBufRead() 可以多次调用。官方文档也说明,数据较长时可以在多个周期里从 client 内部 FIFO 读取。[^canopennode-sdo-client]


15. Abort 与 timeout:错误如何结束

SDO abort 帧格式固定:

1
2
3
4
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 调用模型通常是:

1
2
3
4
5
6
7
flowchart TD
A["CAN RX ISR / driver"] --> B["CO_SDO_receive()<br/>复制帧 + CANrxNew"]
B --> C["可选 callback 唤醒主循环"]
C --> D["主循环 / RTOS task"]
D --> E["CO_SDOserver_process(SDO, NMTisPreOrOperational, dt, timerNext)"]
E --> F["OD read/write"]
F --> G["CO_CANsend(response/abort)"]

伪代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 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

普通从机需要主站能读写它的对象字典,所以要确认:

1
2
3
4
0x1200 SDO server parameter
CO_CONFIG_SDO_SRV
CO_RX/TX buffer for SDO server
CO_SDOserver_process() 被周期调用

只有当前节点要主动访问其他节点对象字典,才看:

1
2
3
4
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. 建议阅读顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart TD
A["CO_SDOserver.h<br/>先读 state / abort / return"] --> B["CO_SDOserver.c::CO_SDOserver_init()"]
B --> C["CO_SDO_receive()"]
C --> D["CO_SDOserver_process()"]
D --> E["download 分支"]
D --> F["upload 分支"]
E --> G["validateAndWriteToOD()"]
F --> H["OD_IO.read() 流式读"]
H --> I["CO_SDOclient.h<br/>理解 client API"]
G --> I
I --> J["CO_SDOclient.c::setup/init"]
J --> K["CO_SDOclientDownload()"]
J --> L["CO_SDOclientUpload()"]

20. 一句话串起运行流程

1
2
3
4
5
6
7
8
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.cCO_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 parameter0x1280..0x12FF SDO client parameter