Lely CANopen:CSDO/SSDO 客户端服务器 SDO 状态机、分段与块传输机制

在这里插入图片描述

@[toc]

一句话结论

Lely 将 SDO 实现拆成客户端和服务器两个事件驱动状态机:

  • co_csdo_t 主动发起远程上传或下载,负责构造请求帧、等待响应、维护超时、toggle、块序号、CRC、进度与完成回调;
  • co_ssdo_t 被动接收客户端请求,负责解析命令、定位对象字典、调用 co_sub_dn_ind()/co_sub_up_ind(),并生成成功响应或 SDO abort;
  • 两者都依赖 can_net_t 的 receiver、timer 和 send callback,本身不创建线程,也不直接操作 SocketCAN;
  • 快速传输只有初始化帧,分段传输每帧最多携带 7 字节并交替 toggle bit,块传输以最多 127 个 segment 组成一个 block,通过 ackseqblksize 和可选 CRC 实现 Go-Back-N 式恢复;
  • CSDO 的完成回调在状态机回到空闲态后触发,因此回调中可以提交下一次请求;SSDO 没有服务级完成回调,其应用语义集中在对象字典 indication;
  • 动态修改 0x1200..0x12FF 会中止正在进行的传输并重新注册 CAN receiver,参数更新不是透明操作。

最重要的运行模型是:

1
2
3
4
5
6
7
8
9
10
11
12
13
application request or incoming CAN frame

can_net_t receiver/timer callback

CSDO or SSDO finite-state machine

command validation, timeout, toggle, sequence and CRC

co_sdo_req

co_sub_dn_ind / co_sub_up_ind

object dictionary and application callback

1. 四个核心文件的职责边界

1.1 include/lely/co/csdo.h

该头文件公开 Client-SDO API:

1
2
3
4
5
6
7
8
local object dictionary request helpers
Client-SDO create/start/stop/destroy
parameter and timeout access
request progress callbacks
download/upload confirmation callbacks
normal download/upload requests
block download/upload requests
abort transfer request

它是主站侧或其他 SDO 客户端主动访问远端对象字典的底层 C 接口。

1.2 src/co/csdo.c

该文件实现:

1
2
3
4
5
6
7
8
9
CSDO service object
client-side finite-state machine
CAN request frame construction
response frame validation
timeout handling
expedited and segmented transfer
block transfer, retransmission and CRC
progress and completion callbacks
concise DCF sequential download

1.3 include/lely/co/ssdo.h

该头文件公开 Server-SDO 的生命周期、参数查询和 timeout 配置接口。与 CSDO 相比,它没有“主动上传/下载”API,因为 SSDO 的动作由收到的客户端请求触发。

1.4 src/co/ssdo.c

该文件实现:

1
2
3
4
5
6
7
8
SSDO service object
server-side finite-state machine
request frame dispatch
object dictionary lookup
co_sub_dn_ind / co_sub_up_ind bridge
expedited and segmented response
block transfer, acknowledgment and CRC
timeout and abort response

1.5 与 sdo.c 的分层关系

1
2
3
4
5
flowchart TD
A["CAN frame and timer events"] --> B["csdo.c or ssdo.c protocol state machine"]
B --> C["co_sdo_req segment description"]
C --> D["co_sub_dn_ind or co_sub_up_ind"]
D --> E["object dictionary value and application callback"]

csdo.c/ssdo.c 知道协议命令字、toggle、timeout、block sequence 和 CRC;sdo.c 只负责请求片段、数据聚合和类型序列化。


2. CSDO 与 SSDO 的方向不要混淆

CANopen 使用“上传/下载”描述相对于 Server-SDO 对象字典的数据方向:

操作 数据方向 CSDO 行为 SSDO 行为
SDO download Client → Server 发送本地数据 写远端对象字典
SDO upload Server → Client 接收远端数据 读取本地对象字典

因此:

1
2
3
4
5
6
7
8
9
10
11
co_csdo_dn_req()
client sends bytes to server

co_ssdo_dn_ind()
server writes those bytes into local OD

co_csdo_up_req()
client requests bytes from server

co_ssdo_up_ind()
server reads bytes from local OD

CSDO 是 Client-SDO,SSDO 是 Server-SDO。官方网页的个别文字可能把 Server-SDO 后的缩写误写为 CSDO,但源码类型名和头文件边界是明确的:co_csdo_t 对应客户端,co_ssdo_t 对应服务器。


3. SDO 连接参数与对象字典

3.1 参数对象范围

角色 对象范围 典型对象
Server-SDO 参数 0x1200..0x127F 0x1200 为默认 SSDO
Client-SDO 参数 0x1280..0x12FF 0x1280 为第一个 CSDO

参数记录由 struct co_sdo_par 表示:

1
2
3
4
5
6
struct co_sdo_par {
co_unsigned8_t n;
co_unsigned32_t cobid_req;
co_unsigned32_t cobid_res;
co_unsigned8_t id;
};

字段含义:

字段 含义
n 最高支持子索引
cobid_req Client → Server 请求 COB-ID
cobid_res Server → Client 响应 COB-ID
id 对端 Node-ID

3.2 默认连接集

当 Node-ID 为 N 时,默认 SDO 使用:

1
2
request COB-ID  = 0x600 + N
response COB-ID = 0x580 + N

例如 Node-ID 1:

1
2
Client → Server    0x601
Server → Client 0x581

3.3 位 31 的语义

CO_SDO_COBID_VALID 的名称容易误导。该位被置位时表示对应 COB-ID 无效或服务不存在;请求与响应 COB-ID 的 invalid bit 都清零,服务才可用。


4. 两个服务对象内部保存了什么

4.1 struct __co_csdo

CSDO 的主要字段可分为六组:

组别 字段 作用
连接 netdevnumpar CAN 网络、对象字典和 SDO 参数
事件源 recvtimertimeout 响应接收和协议超时
状态机 stateacidxsubidx 当前状态、abort code 和对象地址
分段 sizetoggle 总长度与 toggle bit
块传输 blksizepstackseqcrc block 协商、恢复和 CRC
缓冲/回调 dn_bufup_bufbuf、各类 callback 下载源、上传目标、序列化和通知

关键区别:

  • dn_buf 通常直接引用用户传给 co_csdo_dn_req() 的缓冲区;
  • up_buf 指向上传结果缓冲区,默认使用 CSDO 自己的 buf
  • buf 也用于 co_csdo_dn_val_req() 把类型值序列化为字节流;
  • 一个 CSDO 同时只允许一个进行中的请求。

4.2 struct __co_ssdo

SSDO 保存:

组别 字段 作用
连接 netdevnumpar 本地服务器和 SDO 参数
事件源 recvtimertimeout 请求接收和协议超时
状态机 stateidxsubidx 当前协议状态和访问地址
分段 togglereq toggle 和对象访问请求
块传输 blksizeackseqgencrccrc block 接收/发送状态
缓冲 bufnbyte segment/block 临时数据与当前 request 消费位置

SSDO 不保存完成回调。完成或失败最终表现为:

1
2
3
CAN response or abort frame
+ object dictionary indication result
+ optional application side effect inside indication

5. Lely 的状态机不是 switch(enum state)

官方 FSM 文档说明,Lely 服务对象使用函数指针驱动的事件状态机。CSDO 和 SSDO 都为每个状态定义一组处理函数:

1
2
3
4
5
on_enter
on_abort
on_time
on_recv
on_leave

CSDO 的状态结构包含完整的 enter/leave 钩子;SSDO 状态结构只包含 abort、time 和 recv 转移。

5.1 CSDO 状态进入逻辑

co_csdo_enter() 支持瞬态状态:

1
2
3
4
5
6
7
set next state

invoke previous state's on_leave

invoke next state's on_enter

if on_enter returns another state, continue immediately

abort_state 就是典型瞬态状态:

  1. 进入时停止 timer;
  2. 立即返回 wait_state
  3. 离开 abort_state 时调用完成 callback。

这意味着完成 callback 被调用时,CSDO 已经回到 idle 状态。应用可以在 callback 内提交下一次 SDO 请求,而不会因“上一请求仍忙”被拒绝。

5.2 SSDO 状态进入逻辑

co_ssdo_enter() 只替换 state 指针。初始化请求不是长期等待状态:SSDO 在 wait_state 收到 initiate frame 后直接调用对应处理函数,再进入 segment 或 block 状态。

这是客户端和服务器状态结构不完全对称的原因:

  • Client 发起请求后必须等待远端 initiate response;
  • Server 收到 initiate request 时可以同步完成解析、对象访问和首个响应。

6. CSDO 状态集合

CSDO 定义的主要状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
stopped
waiting
abort transfer
normal download initiate
normal download segment
normal upload initiate
normal upload segment
block download initiate
block download sub-block
block download end
block upload initiate
block upload sub-block
block upload end

总览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flowchart TD
S0["stopped"] --> S1["waiting"]
S1 --> D0["download initiate"]
D0 --> D1["download segment"]
D1 --> S2["abort or complete"]
S1 --> U0["upload initiate"]
U0 --> U1["upload segment"]
U1 --> S2
S1 --> BD0["block download initiate"]
BD0 --> BD1["block download sub-block"]
BD1 --> BD2["block download end"]
BD2 --> S2
S1 --> BU0["block upload initiate"]
BU0 --> BU1["block upload sub-block"]
BU1 --> BU2["block upload end"]
BU2 --> S2
S2 --> S1

快速传输不单独定义状态,它在 initiate 状态收到响应后直接完成。


7. SSDO 状态集合

SSDO 的长期状态更少:

1
2
3
4
5
6
7
8
stopped
waiting
normal download segment
normal upload segment
block download sub-block
block download end
block upload sub-block
block upload end

download initiateupload initiateblock download initiateblock upload initiate 只是 wait_state 内部直接调用的处理函数,并未实例化为独立长期状态。

1
2
3
4
5
6
7
8
9
10
11
flowchart TD
A["waiting"] -->|"download initiate request"| B["parse and access OD"]
B -->|"expedited complete"| A
B -->|"segmented"| C["download segment"]
C --> A
A -->|"upload initiate request"| D["read OD and choose mode"]
D -->|"expedited complete"| A
D -->|"segmented"| E["upload segment"]
E --> A
A -->|"block request"| F["block sub-block states"]
F --> A

8. 创建、启动、停止和销毁

8.1 CSDO 创建

co_csdo_create(net, dev, num) 最终执行:

1
2
3
4
5
6
7
validate num
find 0x1280 + num - 1 when dev is non-NULL
initialize default SDO parameters
create CAN receiver
create timeout timer
initialize state and buffers
start service

dev == NULL 时,num 不再表示 SDO 编号,而是 Node-ID,范围为 1..127。CSDO 直接使用默认请求/响应 COB-ID,因此主站应用无需先构造 0x1280 参数对象。

dev != NULL 时,num 表示 1..128 的 Client-SDO 编号,对应参数对象必须存在。

8.2 SSDO 创建

co_ssdo_create(net, dev, num) 要求本地 co_dev_t 必须存在。对于第一个 SSDO:

  • 0x1200 参数对象可以不存在;
  • 服务使用本地 Node-ID 推导默认 COB-ID。

对于第 2 到第 128 个 SSDO,对应 0x1200 + num - 1 参数对象必须存在。

8.3 start() 做了什么

CSDO:

1
2
3
4
copy 0x1280..0x12FF parameters
install parameter download indication
enter waiting state
register response COB-ID receiver

SSDO:

1
2
3
4
enter waiting state
copy 0x1200..0x127F parameters if object exists
install parameter download indication
register request COB-ID receiver

8.4 stop() 做了什么

两个服务都会:

  1. CO_SDO_AC_NO_SDO 中止当前传输;
  2. 停止 timer;
  3. 注销 CAN receiver;
  4. 移除参数对象的 download indication;
  5. 进入 stopped 状态。

CSDO 停止时,进行中请求的 confirmation callback 会收到 abort code;SSDO 停止时会清理协议状态,但没有服务级用户 completion callback。


9. can_net_t 是协议栈与平台之间的边界

CSDO/SSDO 都只依赖三个 can_net_t 能力:

1
2
3
can_recv_start / can_recv_stop
can_timer_timeout / can_timer_stop
can_net_send

完整运行链路是:

1
2
3
4
5
6
7
8
9
10
flowchart LR
A["SocketCAN or MCU CAN driver"] --> B["application adapter"]
B --> C["can_net_recv"]
C --> D["CSDO or SSDO receiver callback"]
D --> E["state transition"]
E --> F["can_net_send"]
F --> B
G["system or RTOS time"] --> H["can_net_set_time"]
H --> I["SDO timer callback"]
I --> E

因此创建 CSDO/SSDO 并不代表总线已经工作。应用必须:

  • 把收到的 CAN 帧喂给 can_net_recv()
  • 把协议栈产生的发送帧写到实际 CAN 驱动;
  • 持续调用 can_net_set_time() 推进 timer。

如果只接入 CAN 收发但不推进时间,SDO timeout 永远不会触发。


10. 本地对象访问 API 与远程 CSDO 不同

csdo.h 同时提供:

1
2
co_dev_dn_req / co_dev_dn_val_req / co_dev_dn_dcf_req
co_dev_up_req

这些函数名称位于 CSDO 头文件中,但它们不会发送 CAN 帧,也不会进入 CSDO 状态机。

本地下载流程:

1
2
3
4
find object and sub-object
construct complete co_sdo_req
co_sub_dn_ind
invoke confirmation with sdo == NULL

本地上传流程:

1
2
3
4
find object and sub-object
co_sub_up_ind
collect all request segments into membuf
invoke confirmation with sdo == NULL

重要区别:

维度 co_dev_*_req() co_csdo_*_req()
目标 本地对象字典 远端对象字典
CAN 报文
完成方式 当前调用内同步完成 由后续 CAN/time 事件异步完成
callback 中 sdo NULL 当前 CSDO 指针
timeout 可配置

应用不能根据函数名中的 req 就假定它一定异步。


11. CSDO 提交请求前的统一检查

正常和块传输都先通过内部请求初始化函数:

1
2
3
4
5
6
service must be valid
service must be idle
save index and sub-index
reset abort/toggle/block/CRC state
bind download source or upload destination buffer
save confirmation callback

若连接无效或已有请求正在进行,函数返回 -1 并设置 ERRNUM_INVAL,不会排队等待。

因此一个 co_csdo_t 同时只能处理一个请求。并行访问多个节点或多条 SDO 通道时,应创建多个 CSDO service,或在上层实现请求队列。


12. 快速下载:Client 写入 1 到 4 字节

co_csdo_dn_req() 根据长度选择协议:

1
2
1..4 bytes    expedited download
0 or >4 normal segmented download

快速下载流程:

1
2
3
4
5
6
7
flowchart LR
A["co_csdo_dn_req"] --> B["build initiate download request"]
B --> C["embed 1 to 4 data bytes"]
C --> D["send request"]
D --> E["wait for initiate response"]
E --> F["verify command, index and sub-index"]
F --> G["completion callback"]

典型 4 字节下载命令字为 0x23

1
2
3
4
byte 0      command specifier
byte 1..2 index, little-endian
byte 3 sub-index
byte 4..7 value, little-endian

SSDO 收到快速下载后:

  1. 解析 index/sub-index 和数据长度;
  2. req.buf 直接指向当前 CAN frame 的数据区;
  3. 调用 co_sub_dn_ind()
  4. 成功则发送 download initiate response;
  5. 失败则发送 SDO abort。

req.buf 只在同步 indication 调用期间有效,应用 callback 不能保存该指针供以后使用。


13. 0 字节值为什么走分段下载

CSDO 的快速路径条件是:

1
size != 0 and size <= 4

因此 0 字节对象不会使用 expedited transfer。分段状态会专门发送一个空的最后 segment,并借助 toggle 状态确认它确实已经发送。

这不是普通应用的常见对象类型,但对 DOMAIN、空文件或自定义对象测试很有价值。


14. 普通分段下载状态机

14.1 CSDO 侧

1
2
3
4
5
6
7
8
9
10
11
12
13
send download initiate request with total size

wait for initiate response

send at most 7 bytes in each segment

wait for one response after every segment

toggle bit alternates

last segment acknowledged

completion callback

每次进入 download segment 状态,CSDO 从 dn_buf 计算剩余字节数,最多发送 7 字节,并在等待响应前重新启动 timeout。

14.2 toggle 检查

发送 segment 时:

1
2
command contains current toggle
then local toggle flips

收到 server response 时,源码检查响应 toggle 是否已经变化。若不符合预期,发送 CO_SDO_AC_TOGGLE abort。

14.3 SSDO 侧

SSDO 在 initiate 阶段先调用一次 download indication,使对象回调有机会仅根据:

1
2
3
req.size
req.offset = 0
req.nbyte = 0

预先拒绝非法长度或当前状态下不允许的写入。

随后每收到一个 segment:

1
2
3
4
5
6
7
verify command
verify toggle
extract 0..7 bytes
update req.offset, req.nbyte and req.buf
verify declared total length
co_sub_dn_ind
send segment response

这说明 custom download indication 必须支持“首次只通知总长度,后续再逐段提交数据”的调用模式。


15. 快速上传:Server 返回 1 到 4 字节

CSDO 发送 upload initiate request,SSDO 定位对象并调用 co_sub_up_ind()

req.size1..4

1
2
3
4
5
6
7
8
9
SSDO gathers the complete value

embeds it in upload initiate response

CSDO validates index/sub-index

copies bytes into upload buffer

completion callback

CSDO 的 upload confirmation 获得:

1
2
const void *ptr;
size_t n;

该指针通常指向 CSDO 内部 membuf,其内容可能在下一次上传或对象销毁时失效。需要长期保存时,callback 内必须复制。


16. 普通分段上传状态机

16.1 SSDO 侧数据生产

SSDO 的 co_ssdo_up_buf() 不要求对象 indication 一次返回完整值。它可以反复:

1
2
3
4
5
6
7
consume current req.buf

if current request segment exhausted and not last

call co_sub_up_ind again

continue filling protocol buffer

因此应用可通过多次 upload indication 流式提供较大 DOMAIN 数据。

16.2 CSDO 侧数据聚合

CSDO 在 initiate response 中读取声明的总长度,预留上传缓冲区,然后逐段请求:

1
2
3
4
5
6
send upload segment request
receive up to 7 bytes
verify toggle
append to membuf
if last, verify final length
otherwise request next segment

源码对收到的数据执行上限和最终长度检查:

  • 超过声明长度:CO_SDO_AC_TYPE_LEN_HI
  • last segment 到达但总长度不足:CO_SDO_AC_TYPE_LEN_LO

工程上应保证 Server-SDO 在普通分段上传中提供准确 size indication。当前 CSDO 实现使用该长度完成缓冲预留和边界判断。


17. Client-SDO block download

块下载用于 Client 向 Server 发送大数据。

17.1 初始化

CSDO 发送:

1
2
3
4
block download initiate
index/sub-index
total size
CRC capability

SSDO:

  1. 解析对象地址和总长度;
  2. 选择 block size;
  3. 让 download indication 预检大小;
  4. 返回 block size 和 CRC 支持情况。

17.2 子块发送

CSDO 在一个 block 内连续发送多个 segment,不等待每帧响应:

1
2
3
4
seqno 1
seqno 2
...
seqno blksize

每个 segment 最多 7 字节,第 7 位标记最后 segment,低 7 位为 sequence number。

17.3 Go-Back-N 式恢复

SSDO 只接收连续 sequence:

1
seqno == ackseq + 1

乱序或丢失后的 segment 不会写入对象数据。block 结束时,SSDO 返回最后连续接收成功的 ackseq

若:

1
ackseq < blksize

CSDO 将 dn_buf.cur 回退到缺失位置,并从下一轮 block 重发未确认部分。

17.4 结束与 CRC

最后一个 block 完成后:

  1. Client 发送 end request,携带末段有效字节数和可选 CRC;
  2. Server 检查总长度;
  3. Server 检查最后 segment 有效字节数;
  4. Server 校验 CRC;
  5. Server 将最后数据提交 indication;
  6. Server 返回 end response。
1
2
3
4
5
6
7
8
flowchart TD
A["block initiate"] --> B["server returns blksize"]
B --> C["client sends sequential segments"]
C --> D["server returns ackseq and next blksize"]
D --> E{"all data acknowledged"}
E -->|"no"| C
E -->|"yes"| F["end request with size and optional CRC"]
F --> G["server validates and responds"]

18. Client-SDO block upload

块上传用于 Client 从 Server 读取大数据。

18.1 CSDO 初始参数

co_csdo_blk_up_req() 初始化:

1
2
pst       protocol switch threshold
blksize 127 by default

Client 把最大可接收 block size 发给 Server。

18.2 block size 兼容退让

若 Server 用 CO_SDO_AC_BLK_SIZE 拒绝 block size,CSDO 不立即失败,而是把大小减半后重试:

1
127 → 64 → 32 → 16 → ... → 1

源码特别保证第一次从 127 变为 64。

18.3 Server-induced protocol switch

若 CSDO 请求 block upload,但收到普通 upload initiate response,它会直接转入普通上传处理。这是 Server 主动降级到 expedited/segmented upload 的协议切换。

18.4 子块接收和确认

CSDO 只复制连续 sequence segment:

1
seqno == ackseq + 1

达到 block size 或遇到 last segment 时,Client 返回 ackseq。Server 根据确认:

  • flush 已确认的 segment;
  • 保留未确认数据;
  • 使用新的 blksize 生成下一 block。

18.5 结束校验

CSDO 在 block upload end response 中检查:

1
2
3
total length
last segment byte count
optional CRC

成功后发送 end confirmation,并触发 upload completion callback。


19. Protocol Switch Threshold 的实际作用

PST 只用于 block upload。

Client 提供非零 pst 后,如果 Server 发现对象长度:

1
req.size <= pst

SSDO 可以切换为普通 SDO upload:

  • size <= 4:expedited upload;
  • size > 4:segmented upload。

PST 的目的不是改变对象数据,而是避免小对象承担 block 协议额外开销。


20. timeout 的真实计时边界

CSDO/SSDO 的 timeout 都不是后台线程定时器。它依赖 can_net_t 时间推进。

20.1 CSDO

CSDO 在发送需要响应的请求后启动或重启 timer:

1
2
3
4
5
initiate request
segment request
block initiate
block acknowledgment wait
block end wait

超时后进入当前状态的 on_time(),通常:

1
2
3
send abort with CO_SDO_AC_TIMEOUT
complete local request with timeout
return to waiting

20.2 SSDO

SSDO 在已经建立传输、等待 Client 下一步请求时启动 timer。空闲 waiting 状态不使用传输 timeout。

例如:

  • download initiate response 后等待第一个 segment;
  • upload segment response 后等待下一个 request;
  • block response 后等待下一 block 或 end request。

20.3 每个 CSDO/SSDO 一个 timer

单个服务对象只有一个协议 timer,与“一个服务对象同时只允许一个传输”的设计一致。


21. abort 是协议错误,也是统一完成路径

SDO abort frame 的布局为:

1
2
3
4
byte 0      0x80
byte 1..2 index
byte 3 sub-index
byte 4..7 abort code, little-endian

21.1 CSDO 的两类 abort

1
2
3
4
5
6
7
8
abort indication
remote Server sent abort
or local transfer completed with success/failure

abort response
local Client detects protocol error
sends abort frame
then enters abort indication path

co_csdo_abort_ind() 保存最终 ac 并进入瞬态 abort state。成功完成也使用 ac == 0 经过同一完成路径。

21.2 SSDO 的两类 abort

1
2
3
4
5
6
7
8
co_ssdo_abort_res
log error
send abort frame
clear state

co_ssdo_abort_ind
peer aborted or transfer completed
clear state without sending another abort

成功完成同样通过 co_ssdo_abort_ind() 清理 index、toggle、block、CRC、request 和 buffer,然后回到 waiting。

21.3 常见 abort 来源

Abort code 典型触发点
CO_SDO_AC_NO_CS command specifier、帧长度或 subcommand 非法
CO_SDO_AC_TOGGLE 分段 toggle 不符合预期
CO_SDO_AC_TIMEOUT 等待下一请求或响应超时
CO_SDO_AC_TYPE_LEN_HI 收到数据超过声明长度
CO_SDO_AC_TYPE_LEN_LO last 到达但数据不足
CO_SDO_AC_BLK_SIZE block size 非法或不支持
CO_SDO_AC_BLK_SEQ block sequence number 非法
CO_SDO_AC_BLK_CRC block CRC 不匹配
CO_SDO_AC_NO_OBJ index 不存在
CO_SDO_AC_NO_SUB sub-index 不存在

22. 完成 callback 与进度 indication

22.1 下载确认

1
2
3
4
5
6
co_csdo_dn_con_t(
co_csdo_t *sdo,
co_unsigned16_t idx,
co_unsigned8_t subidx,
co_unsigned32_t ac,
void *data);

ac == 0 表示成功。远程请求时 sdo 为当前服务对象,本地 co_dev_dn_req() 时为 NULL

22.2 上传确认

上传 callback 额外获得结果缓冲区:

1
2
const void *ptr;
size_t n;

失败时源码传递:

1
2
ptr = NULL
n = 0

22.3 进度 indication

co_csdo_ind_t 提供:

1
2
total size
completed bytes

其触发规则:

  • expedited transfer 不产生进度通知;
  • 确定总长度后通知一次;
  • 之后按 block 边界或最多 127 个 segment 的区间通知;
  • 最后一次 progress 在 completion callback 前发生。

进度 callback 应保持轻量,不应阻塞 CAN event loop。


23. completion callback 的重入边界

CSDO 的 callback 在 abort transient state 离开时执行,此时状态已经切换到 waiting。因此下面的模式是可行的:

1
2
3
4
5
6
7
8
9
static void on_upload(...)
{
if (ac == 0) {
// copy result
}

// The CSDO is idle here; submit the next request if needed.
co_csdo_up_req(sdo, next_idx, next_subidx, on_upload, data);
}

但需要避免:

  • 在 callback 内销毁仍被外层调用栈使用的 can_net_t
  • 多线程同时对同一 CSDO 调用 request/stop/destroy;
  • 未复制上传数据就提交下一次可能复用内部 membuf 的请求。

24. 参数对象动态更新

CSDO 给 0x1280..0x12FF 安装 co_1280_dn_ind();SSDO 给 0x1200..0x127F 安装 co_1200_dn_ind()

更新流程:

1
2
3
4
5
6
7
8
9
10
11
SDO or local write to parameter object

co_sdo_req_dn_val

validate COB-ID or Node-ID

store value

abort current transfer with NO_SDO

re-register or stop CAN receiver

24.1 COB-ID 修改限制

源码拒绝:

1
2
3
old COB-ID valid
and new COB-ID valid
and CAN-ID changes

正确顺序是:

1
2
3
set invalid bit
change CAN-ID or frame format
clear invalid bit

24.2 扩展帧检查

若 COB-ID 中出现 11-bit 之外的 CAN-ID 位,却没有设置 frame bit,参数写入返回 CO_SDO_AC_PARAM_VAL

24.3 更新会打断传输

co_csdo_update()co_ssdo_update() 都先中止进行中的传输。应用不应在参数写成功后假定旧请求还能继续。


25. CSDO 下载缓冲区所有权

co_csdo_dn_req() 不复制用户提供的原始下载数据。内部 dn_buf 直接引用:

1
2
const void *ptr;
size_t n;

因此直到 confirmation callback 发生前,应用必须保证:

  • 缓冲区仍存在;
  • 内容不被修改;
  • DMA、其他线程或栈退出不会使地址失效。

错误示例:

1
2
3
4
5
6
7
void send_config(co_csdo_t *sdo)
{
uint8_t tmp[32];
build_config(tmp);
co_csdo_dn_req(sdo, 0x2000, 0, tmp, sizeof(tmp), done, NULL);
// Function returns; tmp is no longer valid while transfer is still running.
}

安全做法是使用静态、堆、对象成员或请求上下文持有的缓冲区,并在 callback 后释放。

co_csdo_dn_val_req() 会先把类型值序列化到 CSDO 内部 buf,因此不直接持有调用者标量地址;但同一 CSDO 仍不能并行提交其他请求。


26. SSDO request buffer 生命周期

SSDO 的 req.buf 可能指向:

1
2
3
incoming CAN frame data
SSDO temporary membuf
a buffer returned by an upload indication

custom indication 只能在当前同步调用内读取该指针。若要交给工作线程处理,必须先复制。

对于异步硬件写入,推荐:

1
2
3
4
5
indication validates and copies bytes

returns success quickly

application task processes copied command

但这意味着 SDO 成功响应只表示“数据已被应用接收”,不一定表示外设操作已经完成。若业务要求远程确认硬件结果,应在 indication 内完成必要操作或设计额外状态对象。


27. 无动态内存配置

27.1 CSDO

LELY_NO_MALLOC 启用时,默认:

1
CO_CSDO_MEMBUF_SIZE = 8 bytes

足以序列化基本数据类型,但不足以自动保存较大的 typed value 或 upload result。应用需要评估并调整宏或避免大对象。

27.2 SSDO

当禁用动态内存时:

1
2
block disabled     default 7 bytes
block enabled CO_SDO_MAX_SEQNO * 7 bytes

最大 sequence number 为 127 时,默认 block buffer 为:

1
127 * 7 = 889 bytes

这对小型 MCU 已经不是可忽略的内存。若减小 CO_SSDO_MEMBUF_SIZECO_SSDO_MAX_SEQNO 也会相应受限,客户端必须接受更小 block size。


28. 条件编译边界

宏或配置项 影响
LELY_NO_CO_CSDO 移除 Client-SDO 支持,同时影响依赖 CSDO 的 master 功能
LELY_NO_CO_SSDO_BLK 移除 Server-SDO block transfer
LELY_NO_MALLOC 使用固定 membuf,限制可处理对象和 block 大小
LELY_NO_CANFD 决定是否编译 CAN FD 帧过滤分支
LELY_NO_STDIO 移除部分 trace 字符串格式化
LELY_NO_ERRNO 降低系统错误到 errno 的细分能力

CSDO/SSDO 均忽略 RTR 帧和 CAN FD format 帧,本文分析的是 CiA 301 经典 CAN SDO。


29. 线程安全和串行化要求

源码未在 CSDO/SSDO 内部提供 mutex。推荐所有下列操作由同一 CAN event loop 线程执行:

1
2
3
4
5
6
7
can_net_recv
can_net_set_time
co_csdo_*_req
co_csdo_abort_req
co_csdo_start/stop/destroy
co_ssdo_start/stop/destroy
SDO parameter object reconfiguration

如果业务线程需要发起 SDO,应向 event loop 投递命令,而不是直接并发调用同一 service。

主要竞态风险:

  • request 提交与 response callback 同时修改 state;
  • parameter update 中止传输时,另一线程仍使用 buffer;
  • stop/destroy 与 receiver/timer callback 并发;
  • completion callback 释放上层上下文,而其他线程仍访问。

30. CSDO 完整下载流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
flowchart TD
A["application calls co_csdo_dn_req"] --> B{"valid and idle"}
B -->|"no"| X["return ERRNUM_INVAL"]
B -->|"yes"| C["bind source buffer and save callback"]
C --> D{"size from 1 to 4"}
D -->|"yes"| E["send expedited initiate request"]
D -->|"no"| F["send normal initiate request with size"]
E --> G["wait response with timeout"]
F --> G
G --> H{"response valid"}
H -->|"no"| I["send or accept abort"]
H -->|"yes and expedited"| J["complete"]
H -->|"yes and segmented"| K["send segments with toggle"]
K --> L{"last segment acknowledged"}
L -->|"no"| K
L -->|"yes"| J
I --> M["enter abort state"]
J --> M
M --> N["return to waiting and invoke callback"]

31. SSDO 完整请求处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
flowchart TD
A["request frame received"] --> B["wait state decodes command specifier"]
B --> C{"request type"}
C -->|"download"| D["parse index, sub-index and size"]
D --> E["co_sub_dn_ind"]
C -->|"upload"| F["parse index and sub-index"]
F --> G["co_sub_up_ind"]
C -->|"block download"| H["negotiate block and receive sequence"]
C -->|"block upload"| I["negotiate block and produce sequence"]
E --> J{"result"}
G --> J
H --> J
I --> J
J -->|"error"| K["send SDO abort"]
J -->|"complete"| L["send success response"]
J -->|"more data"| M["enter segment or block state"]
K --> N["clear transfer and return waiting"]
L --> N
M --> A

38. 完整机制总图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flowchart TD
A["application submits CSDO request"] --> B["CSDO validates idle and connection"]
B --> C["CSDO sends request through can_net_send"]
C --> D["CAN bus"]
D --> E["SSDO receiver callback"]
E --> F["SSDO state machine parses command"]
F --> G["co_sdo_req"]
G --> H["co_sub_dn_ind or co_sub_up_ind"]
H --> I["object dictionary and application"]
I --> J["SSDO response or abort"]
J --> D
D --> K["CSDO receiver callback"]
K --> L["CSDO validates response and advances state"]
L --> M{"transfer complete"}
M -->|"no"| C
M -->|"yes"| N["CSDO enters waiting"]
N --> O["completion callback"]
P["application updates can_net time"] --> Q["CSDO and SSDO timers"]
Q --> L
Q --> F

39. 最终心智模型

可以将 CSDO/SSDO 压缩为五层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第 1 层:平台适配
CAN driver, can_net_recv, can_net_send, can_net_set_time

第 2 层:连接对象
0x1200..0x127F and 0x1280..0x12FF

第 3 层:协议状态机
initiate, segment, block, timeout, abort, toggle, CRC

第 4 层:请求描述
co_sdo_req and membuf

第 5 层:对象语义
co_sub_dn_ind, co_sub_up_ind and application callbacks

最简洁的结论是:

co_csdo_t 是“主动请求、等待响应和通知应用”的客户端状态机;co_ssdo_t 是“接收请求、访问本地对象字典和生成响应”的服务器状态机。两者通过 can_net_t 获得 CAN 与时间事件,通过 co_sdo_req 交换对象值,通过统一 abort code 表达所有协议、长度、权限和应用错误。理解 receiver、timer、state、buffer 和 indication 五个边界后,快速、分段和块 SDO 都只是同一事件状态机上的不同传输路径。


参考源码与资料

核心源码

关联源码

Lely 官方网页

协议参考

  • CiA 301 V4.2.0:7.2.4 SDO 服务与协议;7.5.2.33 Server-SDO 参数;7.5.2.34 Client-SDO 参数。
  • CiA 302-3 V4.1.0:对象 0x1F22 concise DCF。

前置学习笔记

  • Lely_CANopen_co_obj_co_sub_对象字典节点_机制原理与运行流程.md
  • Lely_CANopen_co_dev_DCF_EDS解析_对象字典设备描述_机制原理与运行流程.md
  • Lely_CANopen_co_sdo_req_PDO映射编解码_对象字典访问桥接机制.md
  • Lely_CANopen_RPDO_TPDO_运行期收发_SYNC_事件与定时器_机制原理与运行流程.md