Lely CANopen:RPDO/TPDO 运行期收发、SYNC、事件与定时器机制原理和运行流程
@[toc]
源码基线:
lely-industries/lely-core,提交620d1858eb8520dbc3dc5e1a7314565becd54199,查阅时间为 2026-07-30。本文核心分析以下文件:
1
2
3
4
5
6 include/lely/co/rpdo.h
include/lely/co/rpdo.hpp
src/co/rpdo.c
include/lely/co/tpdo.h
include/lely/co/tpdo.hpp
src/co/tpdo.c前置文章已经说明
co_pdo_dn()、co_pdo_up()、PDO 映射校验、位级编解码以及对象字典 indication 桥接。本文不重复展开这些基础算法,而是集中分析 RPDO/TPDO 服务对象如何注册 CAN 接收器、维护运行期参数、处理 SYNC、事件定时器、接收超时、同步窗口、抑制时间、RTR 和用户回调。事实边界:函数、字段、条件分支和调用链来自上述源码;并发风险、生命周期约束、测试建议和部分设计评价属于基于源码行为的工程分析。CiA 301 语义参考 V4.2.0 第 7.2.2、7.5.2.35 至 7.5.2.38 节。
一句话结论
rpdo.c 和 tpdo.c 是 Lely CANopen 中真正负责 PDO 运行期行为的服务层:
rpdo.c注册数据帧接收器,按 transmission type 决定立即处理还是缓存到下一个 SYNC,并通过接收 deadline 和同步窗口定时器报告错误;tpdo.c统一处理事件、周期定时器、SYNC、RTR、抑制时间和异步采样,再调用co_pdo_up()构造并发送 CAN 帧;- 两者都把对象
0x1400/0x1600或0x1800/0x1A00的运行值复制到服务内部缓存,并安装 download indication,使 SDO 重配置可以立即改变接收器、定时器和状态; co_pdo_dn()与co_pdo_up()只负责对象字典和 payload 之间的转换,真正的 CAN 收发、触发条件和时间状态均由 RPDO/TPDO 服务维护;- C++ 头文件没有重新实现协议,只通过
incomplete_c_type、类型 traits 和回调适配器包装 C 服务对象。
最重要的运行模型是:
1 | RPDO CAN frame |
1. 六个文件的职责边界
1.1 include/lely/co/rpdo.h
该头文件公开 Receive-PDO 服务的 C API:
1 | create / destroy |
两个回调承担不同职责:
co_rpdo_ind_t:某个 RPDO payload 被解释后调用,携带 SDO abort code 和原始接收字节;co_rpdo_err_t:接收 deadline、payload 过短、payload 过长或 MPDO 目标错误时调用,携带 EMCY error code 和 error register。
1.2 src/co/rpdo.c
该文件实现:
1 | RPDO service lifetime |
1.3 include/lely/co/tpdo.h
该头文件公开 Transmit-PDO 服务的 C API:
1 | create / destroy |
1.4 src/co/tpdo.c
该文件实现:
1 | TPDO service lifetime |
1.5 include/lely/co/rpdo.hpp
CORPDO 是 C++ 包装层,提供:
- C 对象的分配、初始化、终结和释放 traits;
start()、stop()、sync()、rtr()等薄包装;- 普通函数对象和成员函数到 C callback 的适配;
- communication/mapping 参数的引用式访问。
1.6 include/lely/co/tpdo.hpp
COTPDO 同样是薄包装,但当前提交中暴露的接口少于 C API:
- 有
event()、sync()、getNext()和普通 indication; - 没有直接包装
co_tpdo_set_sample_ind()、co_tpdo_sample_res(); - 没有直接包装 DAM/SAM-MPDO 事件函数;
- 没有直接包装
co_tpdo_is_stopped()。
需要这些能力时,C++ 代码仍可直接调用对应 C API。
2. RPDO/TPDO 在 Lely 分层中的位置
1 | flowchart TD |
职责边界可以概括为:
| 层级 | 负责内容 |
|---|---|
can_net_t、can_recv_t、can_timer_t |
CAN 帧和时间事件 |
co_rpdo_t、co_tpdo_t |
PDO 运行期状态、触发条件、接收和发送 |
co_pdo_dn()、co_pdo_up() |
PDO payload 与对象字典之间的位级转换 |
co_sub_dn_ind()、co_sub_up_ind() |
对象访问权限、值转换和应用副作用 |
因此:
rpdo.c/tpdo.c决定“何时处理”;pdo.c决定“如何映射”;对象字典 indication 决定“是否允许以及处理后产生什么应用行为”。
3. CiA 301 transmission type 与源码分支
3.1 RPDO transmission type
| 值 | CiA 301 语义 | rpdo.c 行为 |
|---|---|---|
0x00..0xF0 |
同步 RPDO | 收帧后缓存,下一次 co_rpdo_sync() 才写入对象字典 |
0xF1..0xFD |
保留 | co_1400_dn_ind() 拒绝配置 |
0xFE |
事件驱动,制造商定义 | 收帧后立即处理 |
0xFF |
事件驱动,设备协议或应用协议定义 | 收帧后立即处理 |
Lely 对同步 RPDO 的 0x00..0xF0 没有进一步区分倍率。消费者收到同步 RPDO 后,只在下一次 SYNC 到来时执行最后收到的数据。
3.2 TPDO transmission type
| 值 | CiA 301 语义 | tpdo.c 行为 |
|---|---|---|
0x00 |
同步、非周期 | co_tpdo_event() 置位事件,下一次 SYNC 采样并发送 |
0x01..0xF0 |
同步、周期 | 每收到指定数量的 SYNC 后采样并发送 |
0xF1..0xFB |
保留 | co_1800_dn_ind() 拒绝配置 |
0xFC |
同步 RTR-only | SYNC 时采样并缓存,收到 RTR 后发送缓存帧 |
0xFD |
事件驱动 RTR-only | 收到 RTR 后启动采样并发送响应帧 |
0xFE |
事件驱动,制造商定义 | 事件或 event timer 触发,受 inhibit time 限制 |
0xFF |
事件驱动,设备协议或应用协议定义 | 与 0xFE 使用相同运行路径 |
3.3 为什么 0xFE 和 0xFF 在源码中几乎相同
二者的区别主要属于配置来源和协议语义:
1 | 0xFE manufacturer-specific event |
tpdo.c 和 rpdo.c 的运行路径不需要知道事件由哪种规范定义,因此将它们按相同的 event-driven 分支处理。
4. struct __co_rpdo:RPDO 运行期状态
1 | struct __co_rpdo { |
4.1 字段分类
| 分类 | 字段 | 作用 |
|---|---|---|
| 外部依赖 | net、dev、num |
CAN 网络、对象字典和 PDO 编号 |
| 生命周期 | stopped |
服务是否停止 |
| 参数缓存 | comm、map |
运行期使用的通信参数和映射参数副本 |
| CAN 资源 | recv |
RPDO 数据帧接收器 |
| 时间资源 | timer_event |
RPDO 接收 deadline |
| 时间资源 | timer_swnd |
同步窗口结束时间 |
| 同步状态 | sync |
是否存在等待下一个 SYNC 的缓存帧 |
| 同步状态 | swnd |
当前同步窗口是否已经过期 |
| 数据缓存 | msg |
等待下一个 SYNC 的最后一帧 RPDO |
| OD 桥接 | req |
co_pdo_dn() 写对象字典时复用的请求对象 |
| 用户接口 | ind、err |
数据处理结果和通信错误回调 |
4.2 一个同步 RPDO 只有一帧缓存
msg 不是队列。同步窗口内连续收到多帧相同 RPDO 时:
1 | frame 1 → pdo->msg = frame 1 |
所以同步 RPDO 的语义是“在下一个 SYNC 使用最后收到的有效值”,不是积累并逐帧执行。
5. struct __co_tpdo:TPDO 运行期状态
1 | struct __co_tpdo { |
5.1 字段分类
| 分类 | 字段 | 作用 |
|---|---|---|
| 参数缓存 | comm、map |
运行期通信和映射配置 |
| RTR 接收 | recv |
只接收与本 TPDO COB-ID 匹配的 RTR |
| 定时器 | timer_event |
event-driven TPDO 周期触发 |
| 定时器 | timer_swnd |
同步窗口结束时间 |
| 帧缓存 | msg |
待发送帧或 RTR-only 缓存帧 |
| 抑制状态 | inhibit |
下一次 event-driven TPDO 最早允许发送时间 |
| 事件状态 | event |
trans=0 时等待下一个 SYNC 的事件标志 |
| 同步状态 | swnd |
同步窗口是否过期 |
| 同步状态 | sync |
尚未达到的 SYNC start value |
| 同步状态 | cnt |
1..240 周期倍率计数器 |
| OD 桥接 | req |
co_pdo_up() 采样对象时复用的请求对象 |
| 用户接口 | ind |
发送结果或错误回调 |
| 用户接口 | sample_ind |
SYNC/RTR 触发后的采样入口 |
5.2 inhibit 保存的是绝对时间
inhibit 不是剩余微秒数,而是:
1 | next earliest allowed event-driven transmission time |
发送成功后:
1 | inhibit = send_time + communication_parameter.inhibit * 100 us |
co_tpdo_get_next() 直接返回这个绝对时间点。
6. 服务对象生命周期
6.1 创建即启动
co_rpdo_create() 和 co_tpdo_create() 的公共行为为:
1 | flowchart TD |
必备对象:
| 服务 | 通信参数 | 映射参数 |
|---|---|---|
RPDO num |
0x1400 + num - 1 |
0x1600 + num - 1 |
TPDO num |
0x1800 + num - 1 |
0x1A00 + num - 1 |
若任一对象不存在,创建失败。
6.2 start() 做的事情
RPDO 和 TPDO 的 start() 都会:
- 从对象字典复制 communication parameter;
- 从对象字典复制 mapping parameter;
- 在参数对象上安装 object-level download indication;
- 重置服务内部状态;
- 根据有效位、CAN-ID 和 transmission type 注册接收器或启动定时器;
- 清除
stopped。
6.3 stop() 做的事情
两类服务都会:
1 | stop synchronous-window timer |
start() 和 stop() 都是幂等式接口:
- 已启动时再次
start()返回成功; - 已停止时再次
stop()直接返回。
6.4 生命周期依赖
服务对象内部只保存 net 和 dev 指针,不拥有它们。因此必须保证:
1 | can_net_t lifetime > co_rpdo_t / co_tpdo_t lifetime |
6.5 同一 PDO 编号不要创建两个服务实例
每个实例都会:
- 安装同一参数对象的 object-level indication;
- 注册相同 CAN-ID 的 receiver;
- 独立维护定时器和参数缓存。
两个实例会互相覆盖对象回调,并可能重复接收或发送。一个 PDO 编号应只存在一个运行期服务对象。
7. 参数缓存与对象字典回调
7.1 为什么服务要复制参数
每次收发都查询对象字典会增加查找和类型转换开销。Lely 在启动时将参数复制为:
1 | pdo->comm |
运行期快速路径直接读取缓存。
7.2 参数更新不能只改对象值
启动后,参数对象上的 download indication 负责同时更新:
1 | object dictionary current value |
因此外部配置应使用:
1 | remote SDO download |
不要绕过 indication 直接修改 current value,否则对象字典与 pdo->comm、pdo->map 可能不一致。
7.3 服务会占用整个参数对象的 download indication
源码使用:
1 | co_obj_set_dn_ind(obj, callback, pdo); |
这不是只针对单个子索引,而是整个参数 RECORD。启动服务会覆盖该对象此前安装的 object-level download indication;停止时直接清空,不会自动恢复旧 callback。
如果应用需要叠加自定义逻辑,应在服务回调之外设计明确的转发层,不要让多个模块分别争用同一 object-level indication。
8. RPDO 启动后的 receiver 注册
co_rpdo_init_recv() 的规则很直接:
1 | COB-ID valid bit = 0 |
接收器的服务契约为经典 CAN PDO:
- 不接收 RTR frame;
- 不接收其他 CAN-ID;
- debug 构建会断言 EDL 未置位,payload 处理仍以经典 CAN 的 8 字节上限为边界。
rpdo.c 本身不检查 NMT state。服务是否应在 Pre-operational、Operational 或 Stopped 状态运行,需要由更高层生命周期控制。
9. RPDO 完整收帧流程
1 | sequenceDiagram |
9.1 收帧第一步总是重置 deadline
co_rpdo_recv() 进入后首先调用:
1 | co_rpdo_init_timer_event(pdo); |
因此接收到匹配 CAN-ID 的数据帧就会刷新 deadline,即使该同步帧随后因为同步窗口已经关闭而不被缓存。
9.2 event-driven RPDO
当 trans >= 0xFE:
1 | receive frame |
9.3 synchronous RPDO
当 trans <= 0xF0:
- 同步窗口未过期:复制整帧到
pdo->msg,设置pdo->sync=1; - 同步窗口已过期:不缓存,不写对象字典;
- 不论是否缓存,当前 receiver callback 返回时都没有立即执行应用对象。
10. RPDO 与 SYNC 的执行顺序
co_rpdo_sync() 的流程为:
1 | flowchart TD |
关键时序:
1 | SYNC k arrives |
这与 CiA 301 中“同步 RPDO 在下一次 SYNC 后生效”的消费者模型一致。
10.1 RPDO 不使用 SYNC counter 倍率
co_rpdo_sync(pdo, cnt) 只检查 cnt <= 240,不会按 comm.trans 计算第几个 SYNC。同步 RPDO 的 0..240 在该实现中统一表示:
1 | last accepted frame becomes active on the next SYNC |
11. RPDO 同步窗口 0x1007
11.1 窗口启动
每次 co_rpdo_sync():
1 | swnd = 0 |
11.2 窗口结束
co_rpdo_timer_swnd() 只做一件事:
1 | pdo->swnd = 1; |
窗口结束后到下一次 SYNC 之前收到的同步 RPDO 不会进入缓存。
11.3 0x1007 = 0 的语义
源码不启动 window timer,因此 swnd 在下一次 SYNC 前不会自动变为 1,可理解为不设置有限同步窗口。
11.4 首个 SYNC 之前
服务启动时 RPDO 将 swnd 初始化为 0。若首个 SYNC 之前已经收到同步 RPDO,该帧可以被缓存,并在首个 co_rpdo_sync() 时处理。
12. RPDO receive deadline
12.1 timer_event 在 RPDO 中不是发送事件定时器
同名字段在 RPDO 与 TPDO 中含义不同:
| 服务 | timer_event 含义 |
|---|---|
| RPDO | 接收 deadline:多长时间没有收到下一帧 |
| TPDO | event timer:周期触发发送事件 |
RPDO 的通信参数 sub-index 5 被用作接收 deadline,单位为 ms。
12.2 何时启动
co_rpdo_start() 不启动 deadline timer。它只在收到一帧 RPDO 后调用:
1 | can_timer_timeout(timer_event, net, comm.event); |
因此:
服务自启动以来从未收到过 RPDO 时,当前实现不会产生 deadline timeout;第一次收帧后才开始监控后续间隔。
12.3 超时结果
定时器到期后调用:
1 | co_rpdo_err_t(pdo, 0x8250, 0x10, data) |
普通 co_rpdo_ind_t 不会因 deadline timeout 被调用。
12.4 动态修改 deadline 的细节
co_1400_dn_ind() 修改 sub-index 5 时只更新 pdo->comm.event,不会立即重启当前正在运行的 timer。新值会在下一次 RPDO 收帧并重置 timer 时生效。
这与 TPDO 修改 event timer 后立即重启 timer 的行为不同。
13. co_rpdo_read_frame():从 CAN payload 到对象字典
1 | co_unsigned32_t ac = co_pdo_dn( |
13.1 返回值
ac 是 SDO abort code,用于表达本地映射或对象访问错误,例如:
1 | CO_SDO_AC_NO_OBJ |
RPDO 没有网络确认帧,因此这些 abort code 只用于本地 indication、诊断和 EMCY 路径。
13.2 indication 回调
无论 co_pdo_dn() 成功还是失败,只要进入 co_rpdo_read_frame(),都会调用:
1 | ind(pdo, ac, msg->data, n, ind_data); |
应用可以同时观察:
- 解析结果;
- 原始 payload;
- payload 实际长度。
13.3 映射写入不是事务
co_pdo_dn() 按映射顺序逐项写对象字典。如果第 3 项失败,前两项可能已经更新。co_rpdo_read_frame() 不做回滚。
要求帧内对象原子一致时,应采用:
1 | RPDO writes shadow variables |
14. RPDO 长度和 EMCY 错误路径
co_rpdo_err_t 的实现调用条件为:
| 条件 | EMCY error code | error register |
|---|---|---|
| payload 比映射要求短 | 0x8210 |
0x10 |
| payload 比映射长度长 | 0x8220 |
0x10 |
| DAM-MPDO 目标对象不存在 | 0x8230 |
0x10 |
| RPDO receive deadline timeout | 0x8250 |
0x10 |
14.1 头文件与实现的细节差异
rpdo.h 对 co_rpdo_err_t 的注释只列出 0x8210、0x8220 和 0x8250,但 rpdo.c 在启用 MPDO 时还可能报告 0x8230。
使用回调时不应把错误码枚举硬编码为头文件注释中的三个值。
14.2 payload 过长不是 co_pdo_dn() 的 abort
co_pdo_dn() 能正常处理映射所需的前若干字节时可能返回成功。随后 co_rpdo_read_frame() 重新计算映射 bit 总数,如果:
1 | received bytes > ceil(mapped bits / 8) |
再通过 co_rpdo_err_t 报告 0x8220。
15. co_rpdo_rtr():主动请求远端 TPDO
该函数构造:
1 | CAN-ID = RPDO communication COB-ID 中的 CAN-ID |
然后调用 can_net_send()。
这适用于本地 RPDO 消费者主动请求远端 TPDO 生产者。
源码只检查 PDO valid bit,没有根据 COB-ID bit 30 阻止发送 RTR。应用应根据对端 TPDO 的 RTR 能力和 transmission type 决定是否调用。
16. RPDO communication parameter 动态更新
co_1400_dn_ind() 对各子索引的处理:
| sub-index | 参数 | 规则和副作用 |
|---|---|---|
0 |
highest sub-index | 不可写 |
1 |
COB-ID | 有效状态下不能直接改变 CAN-ID;校验 11/29-bit;重注册 receiver;清除同步缓存和窗口状态 |
2 |
transmission type | 接受 0..0xF0、0xFE、0xFF;拒绝 0xF1..0xFD |
3 |
inhibit time | PDO 有效时不允许修改 |
5 |
event/deadline time | 更新缓存,不立即重启 deadline timer |
| 其他 | — | CO_SDO_AC_NO_SUB |
16.1 RPDO inhibit time 在该文件中没有接收限速作用
comm.inhibit 会被加载、校验和保存,但 co_rpdo_recv()、co_rpdo_sync() 和 co_rpdo_read_frame() 都不读取它。
因此在这个实现中,RPDO inhibit time 不会丢弃高频接收帧,也不会限制对象更新速率。若设备协议赋予其制造商特定行为,需要在应用层另行实现。
17. RPDO mapping parameter 动态更新
co_1600_dn_ind() 强制遵守以下顺序:
1 | 1. set COB-ID invalid bit |
17.1 写 sub-index 0
当写入映射数量时:
- PDO 必须无效;
- 普通 PDO 数量必须不大于 64;
- 总映射长度不得超过 64 bit;
- 每个对象必须存在、可写且允许 RPDO mapping;
- 启用 MPDO 时也接受
0xFE或0xFF特殊映射类型。
17.2 写映射项
写 sub-index 1..64 时:
1 | PDO must be invalid |
非零 mapping entry 会立即调用 co_dev_chk_rpdo() 校验。
18. TPDO 启动后的资源配置
co_tpdo_start() 在复制参数后初始化:
1 | inhibit = current CAN-network time |
然后:
1 | initialize RTR receiver |
18.1 swnd 初值为什么为 1
TPDO 的同步发送必须由 SYNC 打开同步窗口。因此启动后还未收到 SYNC 时:
1 | swnd = expired |
只有 co_tpdo_sync() 才会将其清零并启动窗口 timer。
18.2 inhibit 初值
启动时设为当前时间,意味着 event-driven TPDO 在启动后可立即发送,不需要先等待一个 inhibit interval。
19. TPDO RTR receiver 注册
co_tpdo_init_recv() 只有在以下条件同时满足时注册 receiver:
1 | PDO valid |
receiver 只匹配:
1 | configured CAN-ID |
因此 TPDO 的 recv 不是接收过程数据,而是接收消费者发来的 Remote Transmission Request。
20. co_tpdo_event():应用事件入口
1 | flowchart TD |
20.1 trans=0x00
调用 co_tpdo_event() 只设置:
1 | pdo->event = 1; |
真正采样和发送发生在下一次 co_tpdo_sync()。
连续发生多个事件不会排队,只保留一个 pending bit。
20.2 trans=0xFE/0xFF
处理顺序:
1 | check inhibit time |
若抑制时间尚未结束:
1 | return -1 |
调用者必须检查返回值,而不能只依赖 co_tpdo_ind_t。
20.3 trans=0xFD
当前源码中 co_tpdo_event() 会调用 co_tpdo_init_frame() 更新 pdo->msg,但不主动发送。真正收到 RTR 时,co_tpdo_recv() 会再次启动 sampling path。
这是该提交的具体实现行为,不应把 co_tpdo_event() 理解为 0xFD 的立即发送接口。
21. TPDO event timer
21.1 启动条件
co_tpdo_init_timer_event() 仅在以下条件满足时启动:
1 | PDO valid |
因此 event timer 只作用于普通 event-driven TPDO:
1 | 0xFE |
不会用于:
1 | synchronous TPDO |
21.2 到期流程
1 | timer_event expires |
如果本次发送失败,timer callback 仍会重新启动 event timer,并恢复进入 callback 前保存的 error code。
21.3 event timer 是周期起点,不是精确硬实时发送点
下一周期 timer 在处理完成后重新安排。若采样、对象回调或 CAN send 阻塞,实际周期会包含这些执行时间。
这是基于实现方式的工程结论。需要严格等周期时,应评估 can_timer_timeout() 的具体调度模型和 callback 执行耗时。
22. TPDO inhibit time
22.1 单位
通信参数中的 inhibit time 单位是:
1 | 100 microseconds |
例如:
1 | inhibit = 10 |
22.2 只约束 event-driven 发送
当前实现中 inhibit time 用于:
co_tpdo_event()的0xFE/0xFF;co_dam_mpdo_event();co_sam_mpdo_event()。
它不约束:
- synchronous
0..0xF0; - RTR-only
0xFC/0xFD。
22.3 只在发送成功后推进 deadline
1 | send failed |
22.4 co_tpdo_get_next()
应用可查询下一次允许 event-driven 发送的绝对时间,用于:
- 调度延迟发送;
- 避免反复得到
ERRNUM_AGAIN; - 在上层 event coalescing 中合并多次触发。
23. co_tpdo_sync():SYNC 入口
1 | flowchart TD |
23.1 SYNC start value
服务启动时:
1 | pdo->sync = pdo->comm.sync; |
如果 pdo->sync != 0 且传入的 SYNC counter cnt != 0:
- counter 不等于 start value:忽略本次 SYNC;
- 等于 start value:清除等待状态并从该点开始计数。
若调用者传入 cnt=0,源码不会执行 start-value 比较,等价于不使用 SYNC counter gate。
23.2 transmission type 1..240
内部 pdo->cnt 独立于 SYNC 帧中的 counter:
1 | receive SYNC → ++pdo->cnt |
23.3 transmission type 0
只有此前调用过 co_tpdo_event() 并设置 pending event,下一次 SYNC 才会采样。
23.4 transmission type 0xFC
每次符合 start gate 的 SYNC 都触发采样。采样完成后不立即发送,而是缓存帧等待 RTR。
24. TPDO 同步窗口和异步采样
24.1 SYNC 打开窗口
co_tpdo_sync() 在开始采样前:
1 | swnd = 0 |
24.2 sampling indication
1 | typedef int co_tpdo_sample_ind_t(co_tpdo_t *pdo, void *data); |
回调必须在采样完成后调用一次:
1 | co_tpdo_sample_res(pdo, abort_code); |
默认 callback 立即调用:
1 | co_tpdo_sample_res(pdo, 0); |
因此默认路径是同步采样。
24.3 异步采样的用途
自定义 callback 可以:
1 | trigger ADC conversion |
完成后再调用 co_tpdo_sample_res()。
24.4 窗口超时
如果异步采样完成时:
1 | swnd == 1 |
co_tpdo_sample_res() 将结果改为:
1 | CO_SDO_AC_TIMEOUT |
随后:
- 不发送 TPDO;
- 调用
co_tpdo_ind_t,ptr=NULL、n=0; - 返回 0,因为该 abort 已通过 indication 报告。
24.5 并发采样风险
服务结构没有显式的“sampling in progress”标志。若上层允许新的 SYNC 或 RTR 在上一次异步采样完成前再次触发,多个采样完成事件可能共用同一个 pdo->req 和 pdo->msg。
工程上应保证:
1 | one TPDO service has at most one outstanding sampling operation |
25. co_tpdo_sample_res():采样完成后的统一出口
处理步骤:
1 | validate mode |
25.1 应用提供的 abort code
应用可调用:
1 | co_tpdo_sample_res(pdo, CO_SDO_AC_DATA); |
表示采样失败。服务会通过 co_tpdo_ind_t 上报,但不会发送帧。
25.2 0xFC 的特殊处理
co_tpdo_init_frame() 已经把当前对象值编码到 pdo->msg,但函数直接返回,不调用 can_net_send()。后续 RTR 到来时发送这份缓存快照。
25.3 0xFD 的特殊处理
0xFD 不受同步窗口超时检查,收到 RTR 后采样完成即发送。
26. TPDO RTR 处理
co_tpdo_recv() 只接收 RTR frame,并按 transmission type 分支。
26.1 0xFC:发送同步采样缓存
1 | SYNC arrives |
若还没有有效缓存帧,RTR 被忽略。
26.2 0xFD:RTR 到来后采样
1 | RTR arrives |
消费者得到的是 RTR 时刻触发的新采样,而不是上一个 SYNC 时刻的缓存。
26.3 其他 transmission type
RTR receiver callback 返回 0,不处理该帧。
27. co_tpdo_init_frame():构造 TPDO 帧
1 | flowchart TD |
27.1 co_pdo_up() 的作用
它依次:
- 解析 mapping entry;
- 调用映射对象的 upload indication;
- 将对象值按 bit length 打包到 CAN payload;
- 返回实际 payload 长度。
27.2 失败时没有部分帧发送
即使前几个映射对象已经读取并写入临时 payload,只要后续对象失败,co_tpdo_init_frame() 返回 -1,整帧不会发送。
但前面对象的 upload callback 副作用不会回滚。
28. co_tpdo_send_frame():发送和 indication
1 | int result = can_net_send(pdo->net, msg); |
结果映射:
| 结果 | indication abort code | 数据指针 |
|---|---|---|
| 发送成功 | 0 |
已发送 payload |
| CAN I/O 失败 | CO_SDO_AC_ERROR |
NULL |
需要注意:
- inhibit 尚未到期时函数根本不会进入 send path,因此不会调用 indication;
- mapping error 在
co_tpdo_init_frame()内调用 indication; - async sampling abort 和同步窗口 timeout 在
co_tpdo_sample_res()内调用 indication; - application 必须同时检查 API 返回值和 indication。
29. TPDO communication parameter 动态更新
co_1800_dn_ind() 对各子索引的处理:
| sub-index | 参数 | 规则和副作用 |
|---|---|---|
0 |
highest sub-index | 不可写 |
1 |
COB-ID | 有效状态下不能直接改 CAN-ID;校验 frame bit;重置 event/window;重注册 RTR receiver;重启 event timer |
2 |
transmission type | 拒绝 0xF1..0xFB;0xFC/0xFD 要求允许 RTR;重注册 RTR receiver |
3 |
inhibit time | PDO 有效时不允许修改 |
5 |
event timer | 更新后立即重启 timer |
6 |
SYNC start value | PDO 有效时不允许修改 |
| 其他 | — | CO_SDO_AC_NO_SUB |
29.1 invalid → valid 的额外状态重置
PDO 从禁用变为启用时:
1 | inhibit = current time |
这样旧服务状态不会泄漏到重新启用后的 PDO。
29.2 为什么改 CAN-ID 前必须禁用
源码禁止:
1 | old valid = true |
合法方式是:
1 | set invalid bit |
否则运行中的 receiver、定时器和外部消费者可能在半配置状态下工作。
30. TPDO mapping parameter 动态更新
co_1a00_dn_ind() 与 RPDO mapping 更新顺序相同,但使用:
1 | co_dev_chk_tpdo(dev, idx, subidx); |
检查目标对象:
1 | exists |
TPDO 不接受 RPDO dummy mapping 的特殊路径。
普通 mapping 和 MPDO mode 只能在 PDO invalid 时切换。
31. MPDO 运行路径
本文重点是普通 PDO,但 tpdo.c 和 rpdo.c 也包含 MPDO 入口。
31.1 DAM-MPDO 发送
co_dam_mpdo_event():
1 | validate PDO and DAM mode |
31.2 SAM-MPDO 发送
co_sam_mpdo_event():
1 | validate PDO and SAM mode |
31.3 MPDO 接收
RPDO 仍由 co_rpdo_read_frame() 调用 co_pdo_dn(),后者根据 map.n 的特殊值进入 DAM/SAM-MPDO 解码。
目标对象不存在时,RPDO error callback 可收到 0x8230。
32. C API 最小接线示例
1 |
|
32.1 示例中的顺序
先执行 RPDO,再执行 TPDO,可使:
1 | previous synchronous RPDO becomes active |
实际工程中应与系统的 SYNC 分发策略保持一致。
32.2 NMT 生命周期
上述示例没有 NMT gating。集成到完整 CANopen 节点时,应由 NMT 状态转换决定何时 start/stop 或是否向服务分发 SYNC 和应用事件。
33. C++ 包装层
33.1 c_type_traits
CORPDO 和 COTPDO 为不完整 C 类型提供:
1 | alloc |
底层仍调用:
1 | __co_rpdo_alloc / __co_rpdo_init / __co_rpdo_fini / __co_rpdo_free |
33.2 callback 适配
CORPDO::setInd()、CORPDO::setErr() 和 COTPDO::setInd() 支持:
1 | raw C callback + void *data |
适配器最终仍保存为 C 函数指针和 void *。
33.3 protected destructor
两个包装类的析构函数为 protected,因此不能像普通类型一样直接创建栈对象或使用裸 delete。Lely 提供的典型所有权方式是 make_unique_c<T>()、make_shared_c<T>() 和对应的 delete_c_type;也可以在更高层派生封装中使用。
33.4 COTPDO 接口不完整
当前提交的 C++ wrapper 没有包装异步 sampling 和 MPDO API。需要时可直接调用:
1 | co_tpdo_set_sample_ind(tpdo, sample_ind, data); |
不要因为 C++ 类没有成员函数就误认为底层服务不支持这些能力。
34. 回调上下文、所有权和线程边界
34.1 回调通常位于 CAN network 执行路径
以下回调均由接收器、timer 或发送路径同步触发:
1 | co_rpdo_ind_t |
回调过长会直接增加 CAN event loop 延迟和 timer 抖动。
34.2 payload 指针生命周期
co_rpdo_ind_t 的 ptr 指向接收 CAN message 数据;co_tpdo_ind_t 成功时指向服务的缓存帧数据。
应用不应在 callback 返回后继续保存裸指针。需要异步处理时立即复制。
34.3 co_sdo_req 不可并发复用
每个服务只有一个:
1 | pdo->req |
同一个 RPDO/TPDO 服务不能在无保护条件下并行执行两次 co_pdo_dn() 或 co_pdo_up()。
34.4 callback 内销毁服务的风险
接收器和 timer callback 正在使用 pdo 时调用 co_rpdo_destroy() 或 co_tpdo_destroy(),可能导致 callback 返回路径访问已释放资源。
更安全的方式是:
1 | callback marks a stop/destroy request |
35. 运行期状态与 NMT 的边界
源码中的 RPDO/TPDO service 不读取 NMT state,也不自行判断:
1 | Pre-operational |
只要服务 started 且 COB-ID valid:
- RPDO receiver 可以接收帧;
- TPDO event timer 可以触发;
co_tpdo_event()可以发送;- SYNC 和 RTR 可以驱动处理。
因此完整 CANopen 节点必须由上层 NMT 服务或应用集成层控制这些入口。不能把“PDO 只在 Operational 有效”假定为 rpdo.c/tpdo.c 内部自动保证。
36. 关键差异汇总
| 维度 | RPDO | TPDO |
|---|---|---|
| CAN receiver | 接收数据帧 | 接收 RTR |
| 同步数据 | 缓存最后一帧,下一 SYNC 应用 | SYNC 触发采样和发送/缓存 |
| event timer | 接收 deadline | 周期发送触发 |
| inhibit time | 缓存但运行期未使用 | 限制 event-driven 发送 |
| synchronous window | 决定哪些帧可进入下一 SYNC 缓存 | 决定异步采样完成后是否仍允许发送 |
| OD 桥接 | co_pdo_dn() |
co_pdo_up() |
| 普通 indication | 处理结果和原始接收数据 | 发送结果或本地错误 |
| 专用错误 callback | 有 | 无,统一通过 indication |
| RTR API | 主动发 RTR | 被动接收 RTR 并响应 |
41. 完整机制总图
1 | flowchart TD |
42. 最终心智模型
可以把 RPDO/TPDO 服务压缩成两个对称但不完全相同的状态机:
1 | RPDO |
最重要的五点是:
- 参数对象不是静态配置表。 服务通过 download indication 把对象字典写入转换为 receiver、timer 和状态更新。
- 同步 RPDO 是 last-frame-wins。 收到的数据在下一次 SYNC 才执行。
- TPDO 的 event timer、inhibit time 和 synchronous window 是三个独立时间机制。
- sampling callback 是同步 TPDO 与应用快照之间的边界。 自定义异步采样必须保证单实例、单次完成和窗口约束。
- 这些服务本身不检查 NMT state,也不提供通用线程同步。 正确的生命周期、串行化和状态 gating 由更高层集成负责。
理解这两个服务后,PDO 数据路径就不再只是“0x181 发 8 字节、0x201 收 8 字节”,而是一组由对象字典配置驱动、由 CAN receiver 和 timer 执行、通过 SYNC/event/RTR 触发并最终桥接到应用对象的运行期服务。
参考源码与资料
核心源码
include/lely/co/rpdo.hinclude/lely/co/rpdo.hppsrc/co/rpdo.cinclude/lely/co/tpdo.hinclude/lely/co/tpdo.hppsrc/co/tpdo.c
前置源码
协议参考
- CiA 301 V4.2.0:7.2.2 PDO 服务和协议;7.5.2.35 RPDO 通信参数;7.5.2.36 RPDO 映射参数;7.5.2.37 TPDO 通信参数;7.5.2.38 TPDO 映射参数。
前置学习笔记
Lely_CANopen_co_obj_co_sub_对象字典节点_机制原理与运行流程.mdLely_CANopen_co_dev_DCF_EDS解析_对象字典设备描述_机制原理与运行流程.mdLely_CANopen_co_sdo_req_PDO映射编解码_对象字典访问桥接_机制原理与运行流程.md









