CANopen PDO 运行流程
@[toc]
PDO 不是“带 index/sub-index 的小 SDO”,也不是“谁发给谁的一问一答报文”。它更像一条预先约定好格式的实时数据通道:发送方把对象字典里的几个变量按固定顺序塞进一个 CAN 数据帧,接收方再按自己的 PDO 映射表把这些字节拆回本地对象字典变量。
理解 PDO 时要先把两个问题分开:
- 协议规定 PDO 应该是什么样。 包括它为什么只有过程数据、为什么要靠对象字典配置、为什么要区分 RPDO/TPDO、为什么 transmission type 会影响触发时机。
- CANopenNode 源码怎样实现这些规定。 包括
CO_CANopenInitPDO()如何初始化每个 PDO,CO_PDO_receive()为什么只缓存,CO_RPDO_process()为什么等到主循环才写 OD,CO_TPDO_process()又为什么要同时看 SYNC、event timer、inhibit timer 和sendRequest。
本文按这个顺序讲。前半部分先把协议模型讲清楚,后半部分再对照 CANopenNode 的 CO_PDO.h/.c 看源码运行路径。
1 | flowchart LR |
1. PDO 解决的不是“访问对象字典”,而是“实时搬运过程数据”
CANopen 里有很多通信对象。SDO 适合配置、诊断和读写对象字典;它能带 index/sub-index,语义清楚,但协议交互更重。PDO 的目标相反:少解释、少握手、少字节,把高频过程数据尽快放到总线上。
CiA 对 PDO 的公开说明是:PDO 用于广播高优先级控制与状态信息;在经典 CANopen 中,一个 PDO 是一个 CAN Classic 数据帧,最多携带 8 字节纯应用数据。也就是说,PDO payload 里不再放“我要访问哪个 index/sub-index”这种协议字段。CANopenNode 官方 PDO 文档同样把 PDO 描述为用于实时数据传输、没有协议开销的对象。
这就是读 PDO 源码时最重要的前提:PDO 帧里只有数据,数据含义不在帧内,而在对象字典里的映射表中。
假设一个从站周期上报 3 个量:
1 | Byte0 : digital input 8bit |
实际发到 CAN 总线上的 TPDO 可能只是:
1 | CAN-ID = 0x181 |
这帧里看不到 0x6000:01、0x6411:01、0x6041:00。接收方之所以知道 Byte0 是数字输入、Byte1..2 是模拟量,是因为它的 RPDO mapping 事先写好了“这些字节对应哪些 OD 对象”。
1 | flowchart LR |
这也是为什么初学时容易把 PDO 看乱:
1 | SDO 报文里直接出现 index/sub-index。 |
2. RPDO 和 TPDO 是“从本节点视角命名”的
RPDO 是 Receive PDO,TPDO 是 Transmit PDO。这个名字不是站在整个网络的绝对视角,而是站在当前设备的视角。
同一条 CAN 帧,对发送设备来说是 TPDO,对接收设备来说可以是 RPDO。例如:
1 | 驱动器 A 发出 CAN-ID 0x181 的 TPDO1 |
这里没有“一发一收绑定”的概念。CAN 是广播总线,只要 CAN-ID 匹配,多个节点都可以消费同一个 PDO。CANopenNode 官方文档也强调:一个 TPDO 的 CAN-ID 应该唯一,由单个源产生;零个或多个 RPDO 可以配置成接收这个 TPDO 的 CAN-ID。
对源码来说,这个模型会落成两个方向:
1 | TPDO:从本地 OD 变量读取数据 -> 打包 -> 发 CAN 帧 |
这解释了 CO_TPDOsend() 和 CO_RPDO_process() 的方向差异。TPDO 调 OD_IO.read(),RPDO 调 OD_IO.write()。同样是 mapping entry,方向不同,源码检查的 OD 属性也不同:RPDO 要求目标变量允许 ODA_RPDO,TPDO 要求源变量允许 ODA_TPDO。
3. 一个 PDO 由两组对象字典参数定义
CiA 的 PDO 说明把 PDO 参数分成两类:communication parameter 和 mapping parameter。CANopenNode 头文件注释也采用同一套分法:
1 | flowchart LR |
3.1 communication parameter 决定“这条 PDO 怎么通信”
对 RPDO 来说,0x1400 + n 是第 n 个 RPDO 的通信参数。对 TPDO 来说,0x1800 + n 是第 n 个 TPDO 的通信参数。
这里最核心的是 sub-index 1:COB-ID。它至少表达两个信息:
1 | bit31 : PDO 是否无效,1 表示禁用,0 表示启用 |
在 CANopenNode 经典 11-bit CAN-ID 场景中,源码会检查 bit 11..29 是否非法置位。启用动态配置时,写 OD_write_14xx() 或 OD_write_18xx() 修改 COB-ID,源码不会盲目接受,而是会检查 PDO 是否允许在当前状态下改变、CAN-ID 是否有效、mapping 是否已经配置好。
communication parameter 还会包含 transmission type、event timer、inhibit time、SYNC start value 等字段。它们不改变 payload 的字节含义,但会改变“什么时候处理或发送这帧”。
3.2 mapping parameter 决定“这 8 字节怎么解释”
对 RPDO 来说,0x1600 + n 是第 n 个 RPDO 的映射参数。对 TPDO 来说,0x1A00 + n 是第 n 个 TPDO 的映射参数。
sub-index 0 不代表一个被映射的变量,它表示当前启用多少个映射项。
sub-index 1..n 才是每个映射项。每个映射项是 32-bit,格式如下:
1 | bit31..16 : index |
例如:
1 | 0x62000108 |
这条映射项的含义不是“PDO 报文里会带 0x6200 和 0x01”,而是“PDO 数据区的当前位置对应对象字典 0x6200:01,长度 8 bit”。
如果一个 PDO mapping 是:
1 | sub0 = 3 |
那它的 payload 解释顺序就是:
1 | Byte0 -> 0x6200:01,8 bit |
源码里的 PDO_initMapping() 和 PDOconfigMap() 主要就是把这些 32-bit 映射项解析成运行时可以使用的 OD 访问接口或内存指针。
4. transmission type 决定 PDO 的时机
PDO 的内容由 mapping 决定;PDO 的时机由 transmission type 决定。
常见取值可以这样理解:
| transmission type | 协议含义 | RPDO 侧直观行为 | TPDO 侧直观行为 |
|---|---|---|---|
0 |
同步、非周期 | 收到后缓存,等下一次 SYNC 后处理 | 事件先置请求,下一次 SYNC 才发送 |
1..240 |
同步、周期,每 N 次 SYNC | 收到后缓存,等下一次 SYNC 后处理 | 每 N 次 SYNC 发送一次 |
254 |
事件型,制造商特定 | 收到后不等 SYNC,主循环满足条件即可处理 | 由 event timer、应用请求或 OD flag 触发 |
255 |
事件型,设备/profile 特定 | 收到后不等 SYNC,主循环满足条件即可处理 | 由 event timer、应用请求或 OD flag 触发 |
这里要区分“收到 CAN 帧”和“处理 OD 变量”。对 RPDO 来说,即使 CAN 接收中断已经收到帧,同步 RPDO 仍然不会马上写对象字典,而是等到下一个 SYNC 边界。CANopenNode 的头文件注释也明确写到,同步 RPDO 会在下一次 SYNC 消息之后处理。
对 TPDO 来说,事件型并不等于“立刻发”。如果配置了 inhibit time,发送会被最小间隔限制住;如果配置了 event timer,timer 到期会触发发送请求;如果应用调用 CO_TPDOsendRequest(),也只是把 sendRequest 置位,真正发送仍由 CO_TPDO_process() 判断条件后完成。
5. 动态/可变映射为什么必须按顺序操作
PDO mapping 不能随便一边通信一边改。原因很直接:如果 PDO 正在有效状态,你突然把 Byte0 的含义从“数字输入”改成“控制字低字节”,接收方可能在半配置状态下按错误布局解释数据。
CiA 的 PDO mapping procedure 给出固定步骤:先让 PDO 无效,再让 mapping 无效,修改 mapping,重新启用 mapping,最后重新启用 PDO。CANopenNode 官方文档也给出同样的步骤。
1 | flowchart LR |
CANopenNode 的 OD_write_PDO_mapping() 也按这个思路约束:如果 PDO->valid 仍为真,就不允许写 mapping;如果 mappedObjectsCount 不为 0,又去写具体 mapping 子项,也会被拒绝。换句话说,源码不是只“建议”你按顺序做,而是在动态配置路径里直接检查这个顺序。
6. CO_CONFIG_PDO 不是普通配置表,而是源码裁剪开关
你补充的这段 CO_CONFIG_PDO 很关键。它不是运行时变量,而是编译期能力组合。不同 flag 会改变结构体成员、函数参数、初始化步骤和运行时分支。
1 | flowchart TB |
下面按“打开后源码会多出什么”来讲。
6.1 CO_CONFIG_RPDO_ENABLE:编译接收 PDO
打开它后,CO_RPDO_t、CO_RPDO_init()、CO_RPDO_process()、CO_PDO_receive() 等 RPDO 相关代码才参与编译。
在 CO_CANopenInitPDO() 中,只有满足:
1 |
才会进入 RPDO 初始化循环。这个循环会取 0x1400 和 0x1600 两组 OD entry,然后逐个调用 CO_RPDO_init()。
如果你的 STM32 设备只负责上报状态,不接收主站下发的过程控制量,理论上可以关闭 RPDO,减少对象、buffer 和处理分支。
6.2 CO_CONFIG_TPDO_ENABLE:编译发送 PDO
打开它后,CO_TPDO_t、CO_TPDO_init()、CO_TPDO_process()、CO_TPDOsendRequest()、内部 CO_TPDOsend() 等 TPDO 相关代码才参与编译。
CO_CANopenInitPDO() 中 TPDO 的初始化循环会取 0x1800 和 0x1A00,逐个调用 CO_TPDO_init()。如果设备只接收外部命令而不周期上报过程数据,可以关闭 TPDO。
6.3 CO_CONFIG_RPDO_TIMERS_ENABLE:给 RPDO 加超时监控
RPDO 的 event timer 在接收方向通常被 CANopenNode 用作 timeout 监控。打开这个 flag 后,CO_RPDO_t 会增加:
1 | timeoutTime_us |
CO_RPDO_init() 会读取 RPDO communication parameter 的 event timer,把毫秒换算成微秒。CO_RPDO_process() 每轮会根据 timeDifference_us 累加计时。如果一直没有收到新的 RPDO,超过超时时间后会上报 CO_EM_RPDO_TIME_OUT。
这适合控制类设备。例如电机驱动器期望主站每 10 ms 下发控制字或目标速度。如果主站掉线但 NMT/Heartbeat 还没来得及反映,RPDO timeout 可以作为更贴近过程数据的保护。
6.4 CO_CONFIG_TPDO_TIMERS_ENABLE:给 TPDO 加 inhibit 和 event timer
打开后,CO_TPDO_t 会增加:
1 | inhibitTime_us |
这两个 timer 解决的是不同问题:
1 | event timer : 最长多久主动发一次,避免状态一直不变导致对端长期没新数据 |
在源码里,event timer 到期会让 TPDO 具备发送请求;inhibit timer 未到期时,即使 sendRequest 已经置位,也不会立即发送。它们共同决定事件型 TPDO 的节奏。
6.5 CO_CONFIG_PDO_SYNC_ENABLE:让 PDO 受 SYNC 节拍控制
打开后,RPDO 和 TPDO 结构体中都会出现与 SYNC 相关的字段。
RPDO 侧会多出:
1 | CO_SYNC_t *SYNC; |
并且 CO_RPDO_CAN_BUFFERS_COUNT 会从 1 变成 2。双 buffer 的意义是:同步 RPDO 的接收和处理存在 SYNC 边界,源码需要避免“刚收到的新帧”和“本轮应按上一同步窗口处理的帧”互相覆盖。
TPDO 侧会多出:
1 | CO_SYNC_t *SYNC; |
CO_TPDO_process() 会在 syncWas == true 时才处理同步发送逻辑。transmissionType = 1 表示每次 SYNC 都发,2 表示每 2 次 SYNC 发,直到 240。transmissionType = 0 则是同步非周期:应用或 OD flag 先提出发送请求,真正发送等下一次 SYNC。
如果你的从站不使用同步采样/同步控制,只做事件型 PDO,可以关闭这个能力,减少字段、分支和一个 RPDO buffer。
6.6 CO_CONFIG_PDO_OD_IO_ACCESS:用 OD_IO 读写,而不是直接访问内存
这是 CANopenNode PDO 模块里很重要的设计分叉。
打开它时,CO_PDO_common_t 中保存的是:
1 | OD_IO_t OD_IO[CO_PDO_MAX_MAPPED_ENTRIES]; |
RPDO 写入时调用 OD_IO.write();TPDO 发送时调用 OD_IO.read()。优点是灵活:OD 变量可以不是一个裸内存地址,也可以通过回调访问硬件寄存器、做范围检查、做应用层封装。代价是多一点 RAM 和函数调用开销。
关闭它时,源码走简化路径,mapping 初始化阶段会把 OD 变量的原始地址拆成 mapPointer[]。运行时 RPDO/TPDO 直接按字节复制内存。这更省资源,但不适合需要自定义 OD read/write 行为的对象。
对 STM32 裸机项目来说,如果对象字典变量都只是普通 RAM 变量,直接内存方式更轻;如果你希望 PDO 映射到带副作用的对象,例如读取 ADC 快照、写入经过保护的控制量,OD_IO_ACCESS 更安全、也更符合 CANopenNode 的通用模型。
6.7 CO_CONFIG_PDO_BITWISE_MAPPING:允许按 bit 映射
默认情况下,CANopenNode PDO mapping 是按字节粒度处理的。也就是说,mapping length 通常要是 8 的倍数。
打开 bitwise mapping 后,OD_IO.stream.dataOffset 不再临时保存“字节数”,而是保存“bit 数”。源码会在 RPDO 写入和 TPDO 读取时使用 64-bit 临时缓冲做移位和掩码。
注意它依赖 CO_CONFIG_PDO_OD_IO_ACCESS。源码开头有直接编译期检查:如果开启 bitwise mapping 却没有开启 OD_IO access,会触发 #error。
这个功能适合把多个布尔量或小位宽状态打包进一个 PDO,但它会增加处理复杂度。对初学者和多数 STM32 控制类项目,先用字节映射更容易调试。
6.8 CO_CONFIG_FLAG_CALLBACK_PRE:RPDO 预处理后唤醒任务
这个 flag 会让 RPDO 支持 CO_RPDO_initCallbackPre()。
CAN 驱动收到 RPDO 后,CO_PDO_receive() 复制数据、置位 CANrxNew,然后如果配置了 callback pre,就调用用户注册的回调。它的典型用途不是在回调里直接写 OD,而是唤醒 RTOS 任务,让任务尽快执行 CO_RPDO_process()。
这很适合 FreeRTOS 场景:
1 | CAN RX ISR / 驱动回调 |
6.9 CO_CONFIG_FLAG_TIMERNEXT:让 process 告诉上层下一次最晚何时再调用
CANopenNode 的 process 函数通常会接收 timerNext_us。打开这个 flag 后,PDO 处理函数会根据内部 timer 计算下一次需要被调用的时间。
对裸机超级循环来说,它可以减少无意义轮询。对 RTOS 或低功耗 MCU 来说,它可以帮助决定 task delay 或休眠时间。
注意你贴出的注释写的是 “inside CO_TPDO_process()”,但上传的 CO_PDO.c 中 RPDO timeout 分支也会在相关条件下更新 timerNext_us。实际工程应以当前源码为准。
6.10 CO_CONFIG_FLAG_OD_DYNAMIC:允许运行时通过写 OD 重配 PDO
打开后,CO_PDO_common_t 会保存:
1 | isRPDO |
初始化时,CO_RPDO_init() 和 CO_TPDO_init() 会给 communication parameter 和 mapping parameter 安装 OD extension。之后通过 SDO 写 0x1400/0x1600/0x1800/0x1A00 时,不再只是改 OD 存储值,而会进入 OD_write_14xx()、OD_write_18xx()、OD_write_PDO_mapping(),同步更新 CAN RX/TX buffer、mapping、valid 状态等运行时对象。
如果关闭它,PDO 配置主要在 communication reset 初始化阶段确定,运行时写 OD 不会自动重配底层 CAN buffer。这更简单,也更符合固定从站固件的裁剪思路。
7. CANopenNode 的 PDO 初始化主线
有了上面的协议背景,再看你贴出的 CO_CANopenInitPDO() 就清楚很多。它不是在发送或接收 PDO,而是在“把对象字典中的 PDO 配置翻译成运行时对象”。
1 | flowchart TB |
7.1 为什么要先算 preDefinedCanId
CANopen 预定义连接集中,前四个 PDO 常用 CAN-ID 是:
1 | TPDO1 = 0x180 + Node-ID |
你贴出的 CO_CANopenInitPDO() 会根据 i 和 nodeId 计算这个默认 CAN-ID,再传给 CO_RPDO_init() 或 CO_TPDO_init()。这个值有两个作用:
- 初始化时,如果 OD 中保存的是默认 CAN-ID 的“基值”,源码可以补上当前 Node-ID。
- 动态 OD 读写时,如果识别为预定义 CAN-ID,源码可以把 OD 内部存储和当前 Node-ID 解耦,避免 LSS 改 Node-ID 后默认 CAN-ID 不更新。
这也是为什么 preDefinedCanId 看起来像“多余参数”,但实际和 Node-ID 动态变化相关。
7.2 CO_RPDO_init() 初始化的是接收路径
CO_RPDO_init() 做的事情可以按顺序理解:
1 | 1. 清空 CO_RPDO_t |
这里最关键的是第 6 步。RPDO 初始化会把 CAN 接收 buffer 配好,并把回调函数设为 CO_PDO_receive()。之后硬件或驱动层收到匹配 CAN-ID 的帧,先走这个短路径回调。
7.3 CO_TPDO_init() 初始化的是发送路径
CO_TPDO_init() 的顺序略有不同:
1 | 1. 清空 CO_TPDO_t |
TPDO 初始化时还会把 sendRequest 置为 true。不要把它理解成“初始化马上发 CAN 帧”。真正发送仍然要等 CO_TPDO_process() 在 Operational 状态下判断 transmission type、timer 和 SYNC 条件。
8. mapping 初始化:源码把 32-bit 映射项翻译成运行时访问方式
PDO_initMapping() 是 RPDO 和 TPDO 共用的。它不知道自己最终是接收还是发送,只通过 isRPDO 参数决定检查 ODA_RPDO 还是 ODA_TPDO。
核心流程是:
1 | PDO_initMapping() |
PDOconfigMap() 做的检查非常实际:
- 总长度不能超过
CO_PDO_MAX_SIZE,经典 CAN 默认 8 字节。 - 未启用 bitwise mapping 时,映射长度必须按字节对齐。
- 被映射对象必须带正确方向的 PDO 属性。RPDO 不能写一个只允许 TPDO 的对象;TPDO 也不能读一个没有 TPDO 映射权限的对象。
这解释了一个常见现象:对象字典里有这个变量,不代表它一定能映射到 PDO。 是否可映射要看 OD 属性。
如果启用 CO_CONFIG_PDO_OD_IO_ACCESS,源码会把每个映射项保存成 OD_IO_t,运行时通过 read() 或 write() 访问。如果未启用,源码会在初始化时保存变量内存地址,运行时直接字节复制。
9. RPDO 运行流程:收到帧不是终点,只是开始
RPDO 的运行可以分成两段:CAN 接收短路径和主循环处理路径。
1 | flowchart TB |
9.1 CO_PDO_receive() 为什么不直接写 OD
CO_PDO_receive() 通常由 CAN 接收中断或驱动回调调用。它做的事情很少:
1 | 1. 读取 DLC 和 data 指针 |
它不写 OD,是因为 OD 写入可能涉及锁、回调、应用逻辑、大端转换、bitwise 处理、错误上报等,放在中断里会让实时性和可维护性变差。
CANopenNode 的设计是:中断里只保存最新帧,主循环或 CANopen task 再做较重处理。源码注释也说明,如果新 CAN 帧到来而上一帧还没处理,上一帧会被新帧覆盖。这对 PDO 是合理的:PDO 表示过程数据,通常最新值比历史值更重要。
9.2 CO_RPDO_process() 什么时候才写 OD
CO_RPDO_process() 周期调用,但不是每次都处理。它至少要求:
1 | PDO->valid && NMTisOperational |
如果启用了 SYNC,还要满足:
1 | syncWas || !RPDO->synchronous |
所以:
1 | 事件型 RPDO:收到后,下一次 process 且处于 Operational 就可写 OD。 |
这个行为和 CANopen 的 NMT 状态有关。节点在 Pre-operational 中可以用 SDO 配置 PDO,但 PDO 本身要到 Operational 才正常收发。
9.3 RPDO 写 OD 的细节
进入处理后,源码会遍历 PDO->mappedObjectsCount。每个映射项处理一次:
1 | 取 mappedLength |
这里有一个不直观但很重要的细节:CANopenNode 把 OD_IO.stream.dataOffset 临时用来保存 mapped length。写入 OD 前会把 dataOffset 清 0,调用 write(),写完再恢复成 mapped length。这样同一套 OD_IO 接口既能表达普通 OD 访问,又能在 PDO 场景中携带“本映射项用了多少字节/bit”的信息。
9.4 RPDO timeout 不是“CAN 超时”,而是“过程数据超时”
如果打开 CO_CONFIG_RPDO_TIMERS_ENABLE,并且 RPDO communication parameter 的 event timer 非 0,CO_RPDO_process() 会监控该 RPDO 是否长期没有新数据。
收到新 RPDO 时:
1 | 如果之前已经超时,恢复错误 |
没收到时:
1 | 累加 timeoutTimer |
这和 Heartbeat timeout 不同。Heartbeat 监控节点是否在线,RPDO timeout 监控某条过程数据是否按期更新。实际控制系统里两者都可能需要。
10. TPDO 运行流程:触发条件满足后才打包发送
TPDO 的运行主线和 RPDO 相反。RPDO 是“收到帧 -> 写 OD”;TPDO 是“从 OD 读变量 -> 发帧”。
1 | flowchart TB |
10.1 TPDO 的触发来源有多种
CANopenNode 官方 PDO 文档列出 CO_TPDO_process() 的触发来源:SYNC、event timer、应用调用 CO_TPDOsendRequest()、或者对象字典变量通过 OD_requestTPDO() 请求发送。
可以把它理解成几条线汇合到一个变量:
1 | 应用想发 -> CO_TPDOsendRequest() -> sendRequest = true |
但是触发不等于马上发。CO_TPDO_process() 还会看 NMT 状态、PDO valid、inhibit timer、CAN TX buffer 是否 busy。
10.2 inhibit time 是防刷屏的
事件型 TPDO 很容易被状态变化频繁触发。如果每一次变量抖动都马上发一帧,总线可能被单个节点占满。
inhibit time 就是最小发送间隔。即使 sendRequest 已经为 true,只要 inhibit timer 还没到期,本轮也不会发。
这对实际 STM32 从站很有用。例如一个输入采样值在阈值附近抖动,应用不断请求 TPDO。配置 inhibit time 后,设备可以保证“最多每 N 微秒发一次”,避免总线被抖动事件放大。
10.3 event timer 是保底更新周期
如果过程变量长时间不变,事件型 TPDO 可能一直不发。对接收方来说,这会带来一个问题:不知道发送方是“值没变”,还是“已经不再更新”。
event timer 解决的是保底发送:到了周期,即使没有明显事件,也可以发一次当前值。这样接收方能持续看到新鲜数据。
10.4 同步 TPDO 受 SYNC 节拍控制
同步 TPDO 只在 syncWas == true 的处理周期考虑发送。
transmissionType = 1:每次 SYNC 发。transmissionType = 2:每 2 次 SYNC 发。transmissionType = 240:每 240 次 SYNC 发。transmissionType = 0:应用或 OD flag 先请求发送,但真正发送等下一次 SYNC。
如果配置了 syncStartValue,源码还会等 SYNC counter 到达指定值后才开始同步发送。这用于多个节点在同一个 SYNC 网络里错峰或对齐启动。
10.5 CO_TPDOsend() 才是真正打包函数
CO_TPDOsend() 是内部静态函数。它做的事情和 RPDO 写入相反:
1 | 1. 检查 CANtxBuff 是否 busy |
所以,应用层调用 CO_TPDOsendRequest() 时,不要把它理解成“直接发”。它只是让 CO_TPDO_process() 在合适时机调用 CO_TPDOsend()。
11. 把一个 PDO 例子从协议走到源码
假设节点 1 要用 TPDO1 上报 1 字节数字输入和 2 字节模拟输入。预定义 CAN-ID 是:
1 | TPDO1 = 0x180 + Node-ID = 0x181 |
对象字典可以这样配置:
1 | 0x1800:01 = 0x00000181 ; TPDO1 enabled, CAN-ID 0x181 |
初始化阶段:
1 | CO_CANopenInitPDO() |
运行阶段:
1 | 应用更新 0x6000:01 或 0x6401:01 |
接收方如果希望消费这条数据,就把某个 RPDO 的 COB-ID 配成 0x181,并把 RPDO mapping 配成同样的字节布局。接收方收到 CAN-ID 0x181 后,CO_PDO_receive() 先缓存数据;下一轮 CO_RPDO_process() 再把 Byte0、Byte1..2 写入接收方自己的 OD 变量。
14. 一句话收束
PDO 的本质是:用对象字典提前约定一帧 CAN 数据的字节布局,再用 communication parameter 决定它的 CAN-ID 和触发时机。
CANopenNode 的源码实现也围绕这句话展开:
1 | 初始化阶段:读取 0x14xx/0x16xx/0x18xx/0x1Axx,把协议参数翻译成 CO_RPDO_t / CO_TPDO_t。 |
读源码时只要抓住“communication parameter 管时机与 CAN-ID,mapping parameter 管数据布局,process 函数管运行时节奏”,PDO 就不会再像一堆散乱的宏和分支。
注:本文协议语义参考 CiA PDO 公开说明、CANopenNode 官方 Doxygen 与 CANopenNode 官方 GitHub 仓库;源码解释以当前上传的 CO_PDO.h、CO_PDO.c、CO_config.h 和你贴出的 CO_CANopenInitPDO() 片段为主。若实际工程使用固定旧版本,应以工程内源码和对象字典生成为准。


















