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.ctpdo.c 是 Lely CANopen 中真正负责 PDO 运行期行为的服务层:

  • rpdo.c 注册数据帧接收器,按 transmission type 决定立即处理还是缓存到下一个 SYNC,并通过接收 deadline 和同步窗口定时器报告错误;
  • tpdo.c 统一处理事件、周期定时器、SYNC、RTR、抑制时间和异步采样,再调用 co_pdo_up() 构造并发送 CAN 帧;
  • 两者都把对象 0x1400/0x16000x1800/0x1A00 的运行值复制到服务内部缓存,并安装 download indication,使 SDO 重配置可以立即改变接收器、定时器和状态;
  • co_pdo_dn()co_pdo_up() 只负责对象字典和 payload 之间的转换,真正的 CAN 收发、触发条件和时间状态均由 RPDO/TPDO 服务维护;
  • C++ 头文件没有重新实现协议,只通过 incomplete_c_type、类型 traits 和回调适配器包装 C 服务对象。

最重要的运行模型是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
RPDO CAN frame
↓ CAN receiver
co_rpdo_recv
├─ event-driven → co_rpdo_read_frame immediately
└─ synchronous → keep the last frame until next SYNC

co_rpdo_sync

co_pdo_dn

object dictionary

TPDO trigger
├─ application event
├─ event timer
├─ SYNC
└─ RTR

optional sampling callback

co_tpdo_sample_res

co_tpdo_init_frame

co_pdo_up

can_net_send

1. 六个文件的职责边界

1.1 include/lely/co/rpdo.h

该头文件公开 Receive-PDO 服务的 C API:

1
2
3
4
5
6
7
8
create / destroy
start / stop / is_stopped
network, device and PDO number accessors
cached communication and mapping parameter accessors
receive indication callback
error callback
SYNC actuation entry
RTR request entry

两个回调承担不同职责:

  • 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
2
3
4
5
6
7
8
9
RPDO service lifetime
CAN data-frame receiver registration
communication and mapping parameter interception
synchronous RPDO buffering
SYNC-window filtering
receive-deadline monitoring
payload decoding and OD update
length and MPDO error reporting
RTR transmission request

1.3 include/lely/co/tpdo.h

该头文件公开 Transmit-PDO 服务的 C API:

1
2
3
4
5
6
7
8
9
10
11
create / destroy
start / stop / is_stopped
network, device and PDO number accessors
cached communication and mapping parameter accessors
send indication callback
sampling indication callback
event trigger
SYNC trigger
sampling completion response
next inhibit deadline query
DAM-MPDO and SAM-MPDO event functions

1.4 src/co/tpdo.c

该文件实现:

1
2
3
4
5
6
7
8
9
10
11
TPDO service lifetime
RTR receiver registration
event timer
synchronous window timer
SYNC start and cycle counting
application event processing
inhibit-time enforcement
synchronous and RTR sampling
CAN frame construction and transmission
DAM/SAM-MPDO frame construction
communication and mapping parameter interception

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
2
3
4
5
6
7
8
flowchart TD
A["CAN network and timers"] --> B["rpdo.c / tpdo.c runtime service"]
B --> C["pdo.c mapping and OD bridge"]
C --> D["co_sub_dn_ind / co_sub_up_ind"]
D --> E["object dictionary values and application callbacks"]

F["SDO writes 0x1400/0x1600/0x1800/0x1A00"] --> G["parameter download indications"]
G --> B

职责边界可以概括为:

层级 负责内容
can_net_tcan_recv_tcan_timer_t CAN 帧和时间事件
co_rpdo_tco_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 为什么 0xFE0xFF 在源码中几乎相同

二者的区别主要属于配置来源和协议语义:

1
2
0xFE    manufacturer-specific event
0xFF device-profile/application-profile event

tpdo.crpdo.c 的运行路径不需要知道事件由哪种规范定义,因此将它们按相同的 event-driven 分支处理。


4. struct __co_rpdo:RPDO 运行期状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct __co_rpdo {
can_net_t *net;
co_dev_t *dev;
co_unsigned16_t num;
int stopped;
struct co_pdo_comm_par comm;
struct co_pdo_map_par map;
can_recv_t *recv;
can_timer_t *timer_event;
can_timer_t *timer_swnd;
unsigned int sync : 1;
unsigned int swnd : 1;
struct can_msg msg;
struct co_sdo_req req;
co_rpdo_ind_t *ind;
void *ind_data;
co_rpdo_err_t *err;
void *err_data;
};

4.1 字段分类

分类 字段 作用
外部依赖 netdevnum CAN 网络、对象字典和 PDO 编号
生命周期 stopped 服务是否停止
参数缓存 commmap 运行期使用的通信参数和映射参数副本
CAN 资源 recv RPDO 数据帧接收器
时间资源 timer_event RPDO 接收 deadline
时间资源 timer_swnd 同步窗口结束时间
同步状态 sync 是否存在等待下一个 SYNC 的缓存帧
同步状态 swnd 当前同步窗口是否已经过期
数据缓存 msg 等待下一个 SYNC 的最后一帧 RPDO
OD 桥接 req co_pdo_dn() 写对象字典时复用的请求对象
用户接口 inderr 数据处理结果和通信错误回调

4.2 一个同步 RPDO 只有一帧缓存

msg 不是队列。同步窗口内连续收到多帧相同 RPDO 时:

1
2
3
4
frame 1 → pdo->msg = frame 1
frame 2 → pdo->msg = frame 2
frame 3 → pdo->msg = frame 3
next SYNC → process frame 3

所以同步 RPDO 的语义是“在下一个 SYNC 使用最后收到的有效值”,不是积累并逐帧执行。


5. struct __co_tpdo:TPDO 运行期状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct __co_tpdo {
can_net_t *net;
co_dev_t *dev;
co_unsigned16_t num;
int stopped;
struct co_pdo_comm_par comm;
struct co_pdo_map_par map;
can_recv_t *recv;
can_timer_t *timer_event;
can_timer_t *timer_swnd;
struct can_msg msg;
struct timespec inhibit;
unsigned int event : 1;
unsigned int swnd : 1;
co_unsigned8_t sync;
co_unsigned8_t cnt;
struct co_sdo_req req;
co_tpdo_ind_t *ind;
void *data;
co_tpdo_sample_ind_t *sample_ind;
void *sample_data;
};

5.1 字段分类

分类 字段 作用
参数缓存 commmap 运行期通信和映射配置
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
2
3
4
5
6
7
flowchart TD
A["validate PDO number"] --> B["find communication object"]
B --> C["find mapping object"]
C --> D["allocate CAN receiver and timers"]
D --> E["initialize co_sdo_req"]
E --> F["start service"]
F --> G["return active service"]

必备对象:

服务 通信参数 映射参数
RPDO num 0x1400 + num - 1 0x1600 + num - 1
TPDO num 0x1800 + num - 1 0x1A00 + num - 1

若任一对象不存在,创建失败。

6.2 start() 做的事情

RPDO 和 TPDO 的 start() 都会:

  1. 从对象字典复制 communication parameter;
  2. 从对象字典复制 mapping parameter;
  3. 在参数对象上安装 object-level download indication;
  4. 重置服务内部状态;
  5. 根据有效位、CAN-ID 和 transmission type 注册接收器或启动定时器;
  6. 清除 stopped

6.3 stop() 做的事情

两类服务都会:

1
2
3
4
5
6
stop synchronous-window timer
stop event/deadline timer
stop CAN receiver
remove mapping-object download indication
remove communication-object download indication
set stopped = 1

start()stop() 都是幂等式接口:

  • 已启动时再次 start() 返回成功;
  • 已停止时再次 stop() 直接返回。

6.4 生命周期依赖

服务对象内部只保存 netdev 指针,不拥有它们。因此必须保证:

1
2
can_net_t lifetime > co_rpdo_t / co_tpdo_t lifetime
co_dev_t lifetime > co_rpdo_t / co_tpdo_t lifetime

6.5 同一 PDO 编号不要创建两个服务实例

每个实例都会:

  • 安装同一参数对象的 object-level indication;
  • 注册相同 CAN-ID 的 receiver;
  • 独立维护定时器和参数缓存。

两个实例会互相覆盖对象回调,并可能重复接收或发送。一个 PDO 编号应只存在一个运行期服务对象。


7. 参数缓存与对象字典回调

7.1 为什么服务要复制参数

每次收发都查询对象字典会增加查找和类型转换开销。Lely 在启动时将参数复制为:

1
2
pdo->comm
pdo->map

运行期快速路径直接读取缓存。

7.2 参数更新不能只改对象值

启动后,参数对象上的 download indication 负责同时更新:

1
2
3
4
5
6
object dictionary current value
service cached parameter
CAN receiver registration
event timer
synchronous-window state
inhibit and SYNC counters

因此外部配置应使用:

1
2
3
4
remote SDO download
co_sub_dn_ind()
co_sub_dn_ind_val()
co_dev_cfg_rpdo() / co_dev_cfg_tpdo()

不要绕过 indication 直接修改 current value,否则对象字典与 pdo->commpdo->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
2
3
4
5
6
COB-ID valid bit = 0
→ extract 11-bit or 29-bit CAN-ID
→ register data-frame receiver

COB-ID valid bit = 1
→ stop receiver

接收器的服务契约为经典 CAN PDO:

  • 不接收 RTR frame;
  • 不接收其他 CAN-ID;
  • debug 构建会断言 EDL 未置位,payload 处理仍以经典 CAN 的 8 字节上限为边界。

rpdo.c 本身不检查 NMT state。服务是否应在 Pre-operational、Operational 或 Stopped 状态运行,需要由更高层生命周期控制。


9. RPDO 完整收帧流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sequenceDiagram
participant Net as CAN Network
participant Recv as RPDO Receiver
participant Service as RPDO Service
participant Codec as PDO Decoder
participant OD as Object Dictionary
participant App as Application

Net->>Recv: matching data frame
Recv->>Service: co_rpdo_recv
Service->>Service: reset receive deadline timer
alt synchronous type 0x00..0xF0
Service->>Service: save last frame and set sync flag
else event-driven type 0xFE or 0xFF
Service->>Codec: co_rpdo_read_frame
Codec->>OD: co_pdo_dn
OD->>App: mapped download indications
Service->>App: receive indication and optional error callback
end

9.1 收帧第一步总是重置 deadline

co_rpdo_recv() 进入后首先调用:

1
co_rpdo_init_timer_event(pdo);

因此接收到匹配 CAN-ID 的数据帧就会刷新 deadline,即使该同步帧随后因为同步窗口已经关闭而不被缓存。

9.2 event-driven RPDO

trans >= 0xFE

1
2
3
4
5
6
7
8
9
receive frame

co_rpdo_read_frame

co_pdo_dn

write mapped OD entries

invoke co_rpdo_ind_t

9.3 synchronous RPDO

trans <= 0xF0

  • 同步窗口未过期:复制整帧到 pdo->msg,设置 pdo->sync=1
  • 同步窗口已过期:不缓存,不写对象字典;
  • 不论是否缓存,当前 receiver callback 返回时都没有立即执行应用对象。

10. RPDO 与 SYNC 的执行顺序

co_rpdo_sync() 的流程为:

1
2
3
4
5
6
7
8
9
flowchart TD
A["validate SYNC counter"] --> B{"RPDO is synchronous"}
B -->|no| C["return without action"]
B -->|yes| D["open a new synchronous window"]
D --> E["start 0x1007 window timer"]
E --> F{"buffered frame exists"}
F -->|no| G["return"]
F -->|yes| H["clear pending flag"]
H --> I["decode and apply buffered frame"]

关键时序:

1
2
3
4
5
6
7
8
9
SYNC k arrives
1. open window k
2. apply the last RPDO received in window k-1

RPDO frames arrive during window k
keep only the latest frame

SYNC k+1 arrives
apply the latest frame from window k

这与 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
2
3
4
5
swnd = 0
read 0x1007:00
if RPDO valid, synchronous and window != 0
deadline = now + window microseconds
start timer_swnd

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
2
co_unsigned32_t ac = co_pdo_dn(
&pdo->map, pdo->dev, &pdo->req, msg->data, n, 1);

13.1 返回值

ac 是 SDO abort code,用于表达本地映射或对象访问错误,例如:

1
2
3
4
5
CO_SDO_AC_NO_OBJ
CO_SDO_AC_NO_SUB
CO_SDO_AC_NO_WRITE
CO_SDO_AC_NO_PDO
CO_SDO_AC_PDO_LEN

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
2
3
RPDO writes shadow variables
↓ all mapped entries succeed
application commits shadow group atomically

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.hco_rpdo_err_t 的注释只列出 0x82100x82200x8250,但 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
2
3
CAN-ID = RPDO communication COB-ID 中的 CAN-ID
flags = RTR
length = 0

然后调用 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..0xF00xFE0xFF;拒绝 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
2
3
4
5
1. set COB-ID invalid bit
2. write sub-index 0 = 0
3. modify mapping entries
4. write sub-index 0 = mapping count
5. clear COB-ID invalid bit

17.1 写 sub-index 0

当写入映射数量时:

  • PDO 必须无效;
  • 普通 PDO 数量必须不大于 64;
  • 总映射长度不得超过 64 bit;
  • 每个对象必须存在、可写且允许 RPDO mapping;
  • 启用 MPDO 时也接受 0xFE0xFF 特殊映射类型。

17.2 写映射项

sub-index 1..64 时:

1
2
3
PDO must be invalid
and
cached mapping count must be 0

非零 mapping entry 会立即调用 co_dev_chk_rpdo() 校验。


18. TPDO 启动后的资源配置

co_tpdo_start() 在复制参数后初始化:

1
2
3
4
5
inhibit = current CAN-network time
event = 0
swnd = 1
sync = communication.sync
cnt = 0

然后:

1
2
initialize RTR receiver
initialize event timer

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
2
3
PDO valid
and
CO_PDO_COBID_RTR bit is clear

receiver 只匹配:

1
2
3
configured CAN-ID
CAN_FLAG_RTR
optional CAN_FLAG_IDE

因此 TPDO 的 recv 不是接收过程数据,而是接收消费者发来的 Remote Transmission Request。


20. co_tpdo_event():应用事件入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
A["co_tpdo_event"] --> B{"PDO valid and normal PDO"}
B -->|no| C["ignore and return success"]
B -->|yes| D{"transmission type"}
D -->|0x00| E["set pending event for next SYNC"]
D -->|0xFD| F["refresh buffered frame without sending"]
D -->|0xFE or 0xFF| G["check inhibit deadline"]
G --> H["sample OD and build frame"]
H --> I["send immediately"]
D -->|other| J["ignore synchronous event"]
E --> K["reset event timer when applicable"]
F --> K
I --> L["advance inhibit deadline"]
L --> K

20.1 trans=0x00

调用 co_tpdo_event() 只设置:

1
pdo->event = 1;

真正采样和发送发生在下一次 co_tpdo_sync()

连续发生多个事件不会排队,只保留一个 pending bit。

20.2 trans=0xFE/0xFF

处理顺序:

1
2
3
4
5
6
7
8
9
check inhibit time

co_tpdo_init_frame

co_pdo_up

can_net_send

advance next inhibit deadline

若抑制时间尚未结束:

1
2
3
return -1
set ERRNUM_AGAIN
no send indication callback

调用者必须检查返回值,而不能只依赖 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
2
3
PDO valid
trans >= 0xFE
event timer != 0

因此 event timer 只作用于普通 event-driven TPDO:

1
2
0xFE
0xFF

不会用于:

1
2
3
4
synchronous TPDO
0xFC RTR-only
0xFD RTR-only
MPDO-specific event API

21.2 到期流程

1
2
3
4
5
6
7
8
9
timer_event expires

co_tpdo_timer_event

co_tpdo_event

transmit or report error

restart event timer

如果本次发送失败,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
2
inhibit = 10
minimum interval = 1000 us = 1 ms

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
2
3
4
5
send failed
→ inhibit deadline not advanced

send succeeded
→ inhibit = previous send reference + inhibit * 100 us

22.4 co_tpdo_get_next()

应用可查询下一次允许 event-driven 发送的绝对时间,用于:

  • 调度延迟发送;
  • 避免反复得到 ERRNUM_AGAIN
  • 在上层 event coalescing 中合并多次触发。

23. co_tpdo_sync():SYNC 入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flowchart TD
A["co_tpdo_sync"] --> B["validate counter"]
B --> C{"valid and SYNC-driven"}
C -->|no| D["ignore"]
C -->|yes| E{"wait for SYNC start value"}
E -->|not reached| D
E -->|reached or unused| F["open synchronous window"]
F --> G{"transmission type"}
G -->|0x00| H{"pending event exists"}
H -->|no| D
H -->|yes| I["clear event"]
G -->|1..240| J{"cycle count reached"}
J -->|no| D
J -->|yes| K["reset cycle count"]
G -->|0xFC| L["sample every eligible SYNC"]
I --> M["invoke sampling indication"]
K --> M
L --> M

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
2
3
receive SYNC → ++pdo->cnt
pdo->cnt < trans → no send
pdo->cnt == trans → reset to 0 and sample

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
2
swnd = 0
start timer at now + 0x1007 microseconds

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
2
3
4
trigger ADC conversion
request another task to snapshot data
wait for hardware latch
copy process image

完成后再调用 co_tpdo_sample_res()

24.4 窗口超时

如果异步采样完成时:

1
2
3
swnd == 1
and
trans != 0xFD

co_tpdo_sample_res() 将结果改为:

1
CO_SDO_AC_TIMEOUT

随后:

  • 不发送 TPDO;
  • 调用 co_tpdo_ind_tptr=NULLn=0
  • 返回 0,因为该 abort 已通过 indication 报告。

24.5 并发采样风险

服务结构没有显式的“sampling in progress”标志。若上层允许新的 SYNC 或 RTR 在上一次异步采样完成前再次触发,多个采样完成事件可能共用同一个 pdo->reqpdo->msg

工程上应保证:

1
2
one TPDO service has at most one outstanding sampling operation
sample_res is invoked exactly once for each accepted sample indication

25. co_tpdo_sample_res():采样完成后的统一出口

处理步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
validate mode

reject MPDO path

check synchronous-window timeout

if application abort code != 0
invoke indication and stop

co_tpdo_init_frame

if trans == 0xFC
keep frame buffered and wait for RTR
else
send frame now

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
2
3
4
5
6
7
8
9
SYNC arrives

sample current mapped values

store complete CAN frame in pdo->msg

RTR arrives

send buffered frame without re-sampling

若还没有有效缓存帧,RTR 被忽略。

26.2 0xFD:RTR 到来后采样

1
2
3
4
5
6
7
8
9
RTR arrives

invoke sample_ind

sample_res

co_pdo_up

send response TPDO

消费者得到的是 RTR 时刻触发的新采样,而不是上一个 SYNC 时刻的缓存。

26.3 其他 transmission type

RTR receiver callback 返回 0,不处理该帧。


27. co_tpdo_init_frame():构造 TPDO 帧

1
2
3
4
5
6
7
8
flowchart TD
A["clear CAN message"] --> B["extract 11-bit or 29-bit CAN-ID"]
B --> C["set IDE when needed"]
C --> D["set capacity to 8 bytes"]
D --> E["co_pdo_up"]
E --> F{"mapping and OD upload success"}
F -->|no| G["invoke indication with abort code"]
F -->|yes| H["set actual CAN length"]

27.1 co_pdo_up() 的作用

它依次:

  1. 解析 mapping entry;
  2. 调用映射对象的 upload indication;
  3. 将对象值按 bit length 打包到 CAN payload;
  4. 返回实际 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..0xFB0xFC/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
2
3
4
inhibit = current time
event = 0
sync = configured SYNC start value
cnt = 0

这样旧服务状态不会泄漏到重新启用后的 PDO。

29.2 为什么改 CAN-ID 前必须禁用

源码禁止:

1
2
3
old valid = true
new valid = true
CAN-ID changes

合法方式是:

1
2
3
set invalid bit
change CAN-ID and parameters
clear invalid bit

否则运行中的 receiver、定时器和外部消费者可能在半配置状态下工作。


30. TPDO mapping parameter 动态更新

co_1a00_dn_ind() 与 RPDO mapping 更新顺序相同,但使用:

1
co_dev_chk_tpdo(dev, idx, subidx);

检查目标对象:

1
2
3
4
exists
readable
TPDO mappable
mapping length total <= 64 bits

TPDO 不接受 RPDO dummy mapping 的特殊路径。

普通 mapping 和 MPDO mode 只能在 PDO invalid 时切换。


31. MPDO 运行路径

本文重点是普通 PDO,但 tpdo.crpdo.c 也包含 MPDO 入口。

31.1 DAM-MPDO 发送

co_dam_mpdo_event()

1
2
3
4
5
6
7
validate PDO and DAM mode
check Node-ID
check inhibit time
build fixed 8-byte MPDO
copy caller-provided 4-byte data
send
advance inhibit deadline

31.2 SAM-MPDO 发送

co_sam_mpdo_event()

1
2
3
4
5
6
7
validate PDO and SAM mode
read local Node-ID
check inhibit time
build MPDO address fields
co_sam_mpdo_up reads object value
send
advance inhibit deadline

31.3 MPDO 接收

RPDO 仍由 co_rpdo_read_frame() 调用 co_pdo_dn(),后者根据 map.n 的特殊值进入 DAM/SAM-MPDO 解码。

目标对象不存在时,RPDO error callback 可收到 0x8230


32. C API 最小接线示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <lely/co/rpdo.h>
#include <lely/co/tpdo.h>

static void
on_rpdo(co_rpdo_t *pdo, co_unsigned32_t ac, const void *ptr, size_t n,
void *data)
{
(void)pdo;
(void)ptr;
(void)n;
(void)data;

if (ac) {
/* Record mapping or object-access error. */
}
}

static void
on_rpdo_error(co_rpdo_t *pdo, co_unsigned16_t eec, co_unsigned8_t er,
void *data)
{
(void)pdo;
(void)eec;
(void)er;
(void)data;

/* Forward the PDO communication error to the EMCY/application layer. */
}

static void
on_tpdo(co_tpdo_t *pdo, co_unsigned32_t ac, const void *ptr, size_t n,
void *data)
{
(void)pdo;
(void)ptr;
(void)n;
(void)data;

if (ac) {
/* Record mapping, timeout or CAN I/O error. */
}
}

static int
on_tpdo_sample(co_tpdo_t *pdo, void *data)
{
(void)data;

/* Snapshot application variables before co_pdo_up() reads them. */
return co_tpdo_sample_res(pdo, 0);
}

int
pdo_services_init(can_net_t *net, co_dev_t *dev,
co_rpdo_t **prpdo, co_tpdo_t **ptpdo)
{
co_rpdo_t *rpdo = co_rpdo_create(net, dev, 1);
if (!rpdo)
return -1;

co_tpdo_t *tpdo = co_tpdo_create(net, dev, 1);
if (!tpdo) {
co_rpdo_destroy(rpdo);
return -1;
}

co_rpdo_set_ind(rpdo, &on_rpdo, NULL);
co_rpdo_set_err(rpdo, &on_rpdo_error, NULL);
co_tpdo_set_ind(tpdo, &on_tpdo, NULL);
co_tpdo_set_sample_ind(tpdo, &on_tpdo_sample, NULL);

*prpdo = rpdo;
*ptpdo = tpdo;
return 0;
}

void
pdo_services_on_sync(co_rpdo_t *rpdo, co_tpdo_t *tpdo,
co_unsigned8_t counter)
{
/* Apply the previous synchronous RPDO before sampling the new TPDO. */
(void)co_rpdo_sync(rpdo, counter);
(void)co_tpdo_sync(tpdo, counter);
}

32.1 示例中的顺序

先执行 RPDO,再执行 TPDO,可使:

1
2
3
previous synchronous RPDO becomes active

TPDO samples the updated application state

实际工程中应与系统的 SYNC 分发策略保持一致。

32.2 NMT 生命周期

上述示例没有 NMT gating。集成到完整 CANopen 节点时,应由 NMT 状态转换决定何时 start/stop 或是否向服务分发 SYNC 和应用事件。


33. C++ 包装层

33.1 c_type_traits

CORPDOCOTPDO 为不完整 C 类型提供:

1
2
3
4
alloc
free
init
fini

底层仍调用:

1
2
__co_rpdo_alloc / __co_rpdo_init / __co_rpdo_fini / __co_rpdo_free
__co_tpdo_alloc / __co_tpdo_init / __co_tpdo_fini / __co_tpdo_free

33.2 callback 适配

CORPDO::setInd()CORPDO::setErr()COTPDO::setInd() 支持:

1
2
3
raw C callback + void *data
function object pointer
class object + compile-time member function

适配器最终仍保存为 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
2
3
4
co_tpdo_set_sample_ind(tpdo, sample_ind, data);
co_tpdo_sample_res(tpdo, ac);
co_dam_mpdo_event(tpdo, id, idx, subidx, bytes);
co_sam_mpdo_event(tpdo, idx, subidx);

不要因为 C++ 类没有成员函数就误认为底层服务不支持这些能力。


34. 回调上下文、所有权和线程边界

34.1 回调通常位于 CAN network 执行路径

以下回调均由接收器、timer 或发送路径同步触发:

1
2
3
4
co_rpdo_ind_t
co_rpdo_err_t
co_tpdo_ind_t
co_tpdo_sample_ind_t

回调过长会直接增加 CAN event loop 延迟和 timer 抖动。

34.2 payload 指针生命周期

co_rpdo_ind_tptr 指向接收 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
2
3
callback marks a stop/destroy request

event-loop owner performs stop/destroy after current callback returns

35. 运行期状态与 NMT 的边界

源码中的 RPDO/TPDO service 不读取 NMT state,也不自行判断:

1
2
3
Pre-operational
Operational
Stopped

只要服务 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
flowchart TD
A["RPDO data frame"] --> B["co_rpdo_recv"]
B --> C{"transmission type"}
C -->|synchronous| D["cache last frame"]
D --> E["next co_rpdo_sync"]
C -->|event-driven| F["co_rpdo_read_frame"]
E --> F
F --> G["co_pdo_dn"]
G --> H["object dictionary writes"]
F --> I["RPDO indication and error callback"]

J["application event"] --> K["co_tpdo_event"]
L["event timer"] --> K
M["SYNC"] --> N["co_tpdo_sync"]
O["RTR"] --> P["co_tpdo_recv"]
N --> Q["sampling indication"]
P --> Q
Q --> R["co_tpdo_sample_res"]
K --> S["co_tpdo_init_frame"]
R --> S
S --> T["co_pdo_up"]
T --> U["object dictionary reads"]
S --> V["co_tpdo_send_frame"]
V --> W["CAN network"]

X["SDO writes PDO parameters"] --> Y["0x1400/0x1600/0x1800/0x1A00 indications"]
Y --> B
Y --> K

42. 最终心智模型

可以把 RPDO/TPDO 服务压缩成两个对称但不完全相同的状态机:

1
2
3
4
5
6
7
8
9
10
11
12
RPDO
owns a data-frame receiver
owns one synchronous frame cache
owns receive deadline and window timers
decides when received bytes become application values

TPDO
owns an RTR receiver
owns one output frame cache
owns event and window timers
owns inhibit, event and SYNC counters
decides when application values are sampled and transmitted

最重要的五点是:

  1. 参数对象不是静态配置表。 服务通过 download indication 把对象字典写入转换为 receiver、timer 和状态更新。
  2. 同步 RPDO 是 last-frame-wins。 收到的数据在下一次 SYNC 才执行。
  3. TPDO 的 event timer、inhibit time 和 synchronous window 是三个独立时间机制。
  4. sampling callback 是同步 TPDO 与应用快照之间的边界。 自定义异步采样必须保证单实例、单次完成和窗口约束。
  5. 这些服务本身不检查 NMT state,也不提供通用线程同步。 正确的生命周期、串行化和状态 gating 由更高层集成负责。

理解这两个服务后,PDO 数据路径就不再只是“0x181 发 8 字节、0x201 收 8 字节”,而是一组由对象字典配置驱动、由 CAN receiver 和 timer 执行、通过 SYNC/event/RTR 触发并最终桥接到应用对象的运行期服务。


参考源码与资料

核心源码

前置源码

协议参考

  • 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_对象字典节点_机制原理与运行流程.md
  • Lely_CANopen_co_dev_DCF_EDS解析_对象字典设备描述_机制原理与运行流程.md
  • Lely_CANopen_co_sdo_req_PDO映射编解码_对象字典访问桥接_机制原理与运行流程.md