CANopen MPDO:动态对象寻址
摘要:解析 MPDO 的 DAM/SAM 寻址、对象列表与报文流程,并结合 Lely、CANopenNode 和 CanFestival 源码说明实现差异及替代方案。
@[toc]
1. MPDO 解决了普通 PDO 的什么问题
普通 PDO 的优势是协议开销小、实时性高,但它的对象映射通常在通信前确定:一个 PDO COB-ID 对应一组固定的对象字典条目。收到数据帧后,消费者按照 0x1600~0x17FF 中的映射,把第 1 段、第 2 段数据写到预先配置的本地对象。
当设备存在大量低频、稀疏变化的 8/16/32 位对象时,固定映射会遇到三个问题:
- 为每组对象分配 PDO,会增加通信参数、映射对象和 COB-ID 管理量。
- 反复动态重映射 PDO,需要先禁用 PDO、清空映射、修改条目、恢复映射,再重新启用,不能把它当成逐帧选择机制。
- 改用 SDO 虽然可以逐次携带 Index/Sub-index,但每次访问都有请求、响应和协议状态,语义更可靠,实时性和总线效率却不同于 PDO。
MPDO(Multiplexed Process Data Object)在 PDO 数据区中携带 Node-ID、Index 和 Sub-index,使同一个 PDO COB-ID 能够按帧选择不同对象。它可以近似理解为:
1 | PDO 的单帧、无确认、事件驱动传输 |
但这只是帮助理解的类比。MPDO 仍属于 PDO 服务:它使用 PDO 通信参数、遵循生产者/消费者模型,并通过 Scanner List 或 Dispatching List 管理对象路由;它没有 SDO response,也不会返回 SDO abort 帧。
2. 一帧 MPDO 如何携带对象地址
经典 CANopen MPDO 使用 8 字节数据帧,最多携带 4 字节过程数据。
| 字节 | 字段 | 含义 |
|---|---|---|
| Byte 0 | f + addr |
bit7 为寻址模式;bit0~6 为 Node-ID |
| Byte 1 | Index 低字节 | 对象字典 Index 的低 8 位 |
| Byte 2 | Index 高字节 | 对象字典 Index 的高 8 位 |
| Byte 3 | Sub-index | 对象字典子索引 |
| Byte 4~7 | Data | 最多 32 bit,按 CANopen 小端编码 |
Byte 0 的定义为:
f = 0:SAM,addr表示生产者 Node-ID;f = 1:DAM,addr表示消费者 Node-ID;- DAM 中
addr = 0表示面向所有配置为该 DAM-MPDO 消费者的节点; - SAM 中
addr = 0保留; addr = 1~127表示具体 CANopen Node-ID。
例如,DAM-MPDO 向节点 1 的 0x2200:00 写入 0x12345678:
1 | 81 00 22 00 78 56 34 12 |
SAM-MPDO 由节点 5 发布其 0x2100:00 = 100:
1 | 05 00 21 00 64 00 00 00 |
此时 0x05 不是目标地址,而是“这份数据来自 Node-ID 5”。消费者随后使用 Object Dispatching List 判断该远端对象应写入哪个本地对象。
MPDO 的代价也在报文中直接体现:8 字节中有 4 字节用于动态寻址,真正的数据只剩 4 字节。因此它适合短、稀疏、无需逐帧确认的数据,不适合大于 32 位的参数或文件数据。
3. DAM 与 SAM:一个指定目的,一个声明来源
3.1 DAM:帧中直接指定接收方对象
DAM(Destination Address Mode)中的 Node-ID、Index 和 Sub-index描述接收方对象字典。
1 | flowchart LR |
DAM 的语义接近一次“无应答的对象字典下载”:
- 生产者决定目标 Node-ID 和目标对象;
- 消费者不需要预先为每个远端对象建立普通 PDO 映射;
- 没有成功响应帧;
- 目标对象不存在、不可写或长度不兼容时,不能像 SDO 那样返回 abort response;
- 按用户提供的 CiA 301 材料,目标对象不可用对应 EMCY error code
0x8230。
addr = 0 是 DAM 广播语义。它不是“任意分组号”:哪些节点真正处理该帧,仍取决于这些节点是否配置为相应 COB-ID 的 DAM-MPDO 消费者。
3.2 SAM:帧中声明发送方对象
SAM(Source Address Mode)中的 Node-ID、Index 和 Sub-index描述生产者对象字典。按用户提供的 CiA 301 材料,每个 CANopen 设备只允许一个这种 SAM-MPDO 生产者。消费者不是把数据写到帧内指定的本地 Index,而是查询 Dispatching List:
1 | 远端 Node-ID + 远端 Index + 远端 Sub-index |
SAM 需要两类对象字典列表:
| 角色 | 对象范围 | 数据类型 | 作用 |
|---|---|---|---|
| SAM 生产者 | 0x1FA0~0x1FCF |
ARRAY of UNSIGNED32 | Object Scanner List,决定哪些本地对象可被扫描并发送 |
| SAM 消费者 | 0x1FD0~0x1FFF |
ARRAY of UNSIGNED64 | Object Dispatching List,把远端来源对象映射到本地目标对象 |
Scanner List 条目包含块大小、Index 和 Sub-index,可描述连续子索引。Dispatching List 条目同时包含远端 Node-ID、远端 Index/Sub-index、本地 Index/Sub-index 和块大小,用于建立跨节点对象引用。
这与普通 PDO mapping 不同:普通 PDO mapping 描述“数据帧中第几位对应哪个本地对象”;SAM 的两个列表描述“哪个来源对象允许发送,以及它在消费者上应该落到哪里”。
3.3 MPDO 通过 PDO mapping 的特殊值启用
在 RPDO/TPDO mapping parameter 的子索引 0 中,普通 PDO 使用 0x00~0x40 表示有效映射条目数。MPDO 使用特殊值:
| 子索引 0 | 含义 |
|---|---|
0x00 |
映射禁用 |
0x01~0x40 |
普通 PDO,有效固定映射条目数 |
0xFE |
SAM-MPDO |
0xFF |
DAM-MPDO |
因此,“在 EDS 中加入 0x1FA0~0x1FFF”并不足以实现 MPDO。协议栈还必须在 RPDO/TPDO 收发路径中识别 0xFE/0xFF,解析 8 字节 MPDO 头,执行 Scanner/Dispatching List 查找,并接入对象字典读写和错误处理。
4. MPDO、普通 PDO 与 SDO 的边界
| 维度 | 普通 PDO | MPDO | SDO |
|---|---|---|---|
| 对象选择 | 通信前固定映射 | 每帧携带对象地址 | 每次请求携带对象地址 |
| 单帧有效数据 | 经典 CAN 最多 8 字节 | 最多 4 字节 | expedited 最多 4 字节;可分段/块传输任意长度 |
| 成功确认 | 无 | 无 | 有响应和 abort code |
| 触发方式 | 同步、事件、定时器、部分实现支持 RTR | CiA 301 材料限定为事件驱动 | 客户端请求驱动 |
| 配置复杂度 | 映射和 COB-ID 管理 | Scanner/Dispatching List 与动态路由 | SDO client/server 参数和状态机 |
| 典型用途 | 高频固定过程数据 | 大量稀疏变化的短对象 | 参数配置、诊断、大数据和可靠访问 |
| 失败反馈 | 通常通过 EMCY/应用诊断 | 无逐帧响应,必要时 EMCY | SDO abort response |
工程上可以使用以下判断:
- 字段固定、频率高、对时延敏感:普通 PDO。
- 数据超过 32 位,或调用方必须知道成功/失败:SDO。
- 对象很多、变化稀疏、每项不超过 32 位、允许无确认,并且双方协议栈均支持标准 MPDO:MPDO。
MPDO 不是“更多 PDO”的通用替代品。如果对象数量不多,扩展普通 PDO 往往更简单;如果数据必须可靠落盘,MPDO 又不如 SDO 合适。
5. Lely 如何表示和配置 MPDO
Lely 官方的 standards support 页面把 MPDO、0x1FA0~0x1FCF 和 0x1FD0~0x1FFF 列为已实现能力;构建时可以通过 --disable-mpdo 或 LELY_NO_CO_MPDO 去除相关代码。
Lely 没有另建一套完全独立于 PDO 的服务对象。MPDO 仍由:
1 | co_tpdo_t:生产路径 |
协同完成。关键公开符号包括:
1 | CO_PDO_MAP_SAM_MPDO |
Lely 的 pdo.c 还揭示了一个重要设计:普通 PDO 和 MPDO 最终都通过“本地 SDO request”访问对象字典。
co_pdo_dn():通过本地 SDO download request 把普通 RPDO 数据写入对象字典;co_pdo_up():通过本地 SDO upload request 从对象字典构造普通 TPDO;co_sam_mpdo_up():通过本地 SDO upload request 读取一个 SAM-MPDO 对象,输出固定 4 字节缓冲区。
这里的“本地 SDO request”是内部对象访问抽象,不会在 CAN 总线上产生 SDO 报文。它的价值是复用对象存在性、访问权限、数据类型和自定义 indication 逻辑。
5.1 dcfgen 的配置边界
Lely v2.3.0 发布说明明确写明:当时 dcfgen 尚不能自动创建 Scanner List 和 Dispatching List。当前公开的 dcf-tools 文档列出了普通 rpdo、tpdo 和 sdo 配置,但没有给出 MPDO 列表的 YAML 生成语法。
据此可以确认的是:
- v2.3.0 时需要手工提供这两类对象;
- 截至本文查阅的公开 dcf-tools 文档,没有找到“已新增 MPDO 列表自动生成”的明确说明。
这不等价于证明所有当前工具版本都绝对不支持。项目落地时应检查最终生成的 DCF 是否真实包含 0x1FA0~0x1FFF,而不是仅依据 YAML 配置名称推断。
6. Lely 的 MPDO 发送运行流程
6.1 DAM-MPDO:应用显式给出目标对象
DAM 没有 Scanner List 路由,因为目标地址由本次调用直接提供。应用通过 co_dam_mpdo_event(),或 C++ coapp 层的 DamMpdoEvent(),传入目标 Node-ID、Index、Sub-index 和数据。
核心流程可整理为:
1 | application |
DAM API 返回值能够说明本地参数检查、组帧或发送调用是否成功,但不能证明远端对象已经写入。协议没有对应确认帧;需要可靠闭环时,应用仍应使用 SDO 回读、远端状态 PDO 或应用级确认机制。
6.2 SAM-MPDO:对象事件驱动 Scanner List
SAM 的入口不是由应用每次手工指定目标节点。应用先报告“本地某个对象发生事件”:
1 | co_dev_sam_mpdo_event(dev, idx, subidx) |
Lely v2.3.0 发布说明说明,C++ SetEvent() 会同时触发普通 TPDO event tracking 和 SAM-MPDO event tracking,因此上层不需要预先判断对象究竟映射到哪一种 PDO。
SAM 的主要调用关系如下:
1 | 应用修改本地对象 |
这条路径的核心不是“周期扫描整个对象字典”,而是“对象事件先给出候选 Index/Sub-index,再由 Scanner List 判断该对象是否属于某个 SAM-MPDO”。这样避免了每次事件都遍历所有应用对象。
7. Lely 的 MPDO 接收运行流程
下图把发送和接收路径放在同一张图中。应重点观察:DAM 直接使用帧内目标对象;SAM 必须经过 Dispatching List 才得到本地目标对象。
1 | flowchart TD |
7.1 共同入口:co_rpdo_recv() 与 co_rpdo_read_frame()
co_rpdo_recv() 是 RPDO 服务注册到 CAN network 的接收回调。对于需要立即处理的事件型数据,它进入 co_rpdo_read_frame();同步普通 PDO 还可能先缓存到下一个 SYNC,但 MPDO 按协议是事件驱动,不应依赖同步触发。
co_rpdo_read_frame() 负责解析帧并更新对象字典,返回 0 或内部 SDO abort code。该返回值属于 Lely 内部错误传播,不是发送到总线的 SDO response。
7.2 接收 SAM-MPDO
SAM 接收流程为:
- 从 Byte 0 获取来源 Node-ID,解析来源 Index/Sub-index。
co_dev_map_sam_mpdo()查询0x1FD0~0x1FFF。- 找到匹配项后,返回本地 Index/Sub-index。
- 通过本地对象字典 download 路径写入数据。
- 上层
RpdoRead()、RpdoGet()和OnRpdoWrite()可像普通 RPDO 一样观察更新;Lely v2.3.0 发布说明明确说明了这一点。
如果 Dispatching List 没有匹配项,不能凭帧内 Index 直接写入同名本地对象,因为 SAM 的帧地址描述来源,不描述目的。
7.3 接收 DAM-MPDO
DAM 接收流程为:
- 解析并校验目标 Node-ID;非本节点且不是广播地址时忽略。
- 使用帧内 Index/Sub-index 直接定位本地对象。
- 检查对象存在性、写权限和数据长度。
- 通过本地对象字典 download 路径写入。
Lely 的 C++ 应用层对 DAM 有一个重要限制:OnWrite() 能看到本地对象被写入,但无法仅凭该回调区分来源是 DAM-MPDO 还是 SDO download。原因是两条路径最终复用了同一本地对象 download indication。
8. 为什么 CANopenNode 没有实现 MPDO
8.1 已确认的实现状态
在本文检查的 CANopenNode 基线中,PDO 公开对象和 API 仍围绕普通 RPDO/TPDO:
1 | CO_PDO_common_t |
没有发现对应的 CO_MPDO_t、DAM/SAM 初始化入口、Scanner/Dispatching List 运行时查找函数,或类似 CO_CONFIG_MPDO 的独立配置能力。仓库中保留 CO_EMC_DAM_MPDO 一类标准错误码,也不能证明存在 MPDO 收发路径;错误码表和协议服务实现是两个不同层次。
CANopenNode Issue #588 在本文查阅时仍为 open,且没有 assignee 或 milestone。维护者的回复可以归纳为三点:
- 自己没有实际使用 MPDO 的经验;
- 功能可以加入
CO_PDO; - 工作并非简单改动,而现有 PDO 代码已经较复杂。
Issue #72 中,维护者也明确说明当时未实现 MPDO,并建议通过增加普通 PDO、自定义 COB-ID,或使用 SDO Domain 传输大数据。
8.2 “为什么没实现”的工程解释
以下是基于协议和现有结构的工程分析,不是维护者原话。
MPDO 不是在普通 PDO 前面加 4 字节头即可完成。一个可互操作的实现至少需要:
- 识别 mapping 子索引 0 的
0xFE/0xFF特殊模式。 - 实现 DAM 和 SAM 两套不同的地址语义。
- 支持
0x1FA0~0x1FCFScanner List。 - 支持
0x1FD0~0x1FFFDispatching List。 - 处理目标 Node-ID、DAM 广播和非目标帧过滤。
- 对每帧执行动态 OD 查找、长度检查和访问权限检查。
- 把对象不可用等异常连接到
0x8230EMCY 与内部错误状态。 - 处理通信复位、动态 OD 修改、条件编译和并发访问。
- 为普通 PDO、SAM、DAM、不同数据宽度和错误路径增加一致性测试。
CANopenNode 面向资源受限 MCU,并强调可配置裁剪。加入 MPDO 会增加 ROM、RAM、条件分支和测试矩阵,而实际使用面远小于普通 PDO 和 SDO。结合维护者缺少实际使用经验,可以判断其优先级和长期维护收益目前不足;但不能把原因简单写成“MCU 性能不够”。
9. CANopenNode 中如何替代 MPDO
替代方案应按需求选择,而不是机械地用某一种服务覆盖所有场景。
| 实际需求 | 推荐方案 | 主要代价与限制 |
|---|---|---|
| 固定过程数据超过默认 4 个 PDO | 增加 TPDO/RPDO 数量,分配唯一 COB-ID | 需要管理 COB-ID 冲突、总线负载和更多映射对象 |
| 少量对象高频实时更新 | 普通 PDO | 映射固定,但实时性最好 |
| 低频参数读写且需要确认 | expedited/segmented SDO | 请求响应开销更高 |
| 大块缓冲区、文件或记录 | SDO Domain,必要时 block transfer | 不适合硬实时过程闭环 |
| 稀疏事件、只在自有系统内使用 | 自定义应用层 CAN 帧 | 不是标准 MPDO,第三方工具无法自动解释 |
| 设备 profile 明确要求 MPDO | 更换支持 MPDO 的协议栈,或为 CANopenNode 完整扩展 | 实现和一致性测试成本最高 |
CANopenNode 并不存在“只能有 4 个 TPDO”的硬性协议限制。4 个是预定义连接集中的默认数量;项目可以增加 PDO 对象并使用其他无冲突 COB-ID。需要注意的是,同一 CAN-ID 被两个生产者使用时,只有数据完全相同的位级仲裁才不会立即暴露冲突;正常设计必须保证生产 COB-ID 唯一。
对于大于 32 位、必须确认成功的数据,SDO 不是退而求其次,而是语义正确的选择。对于固定且高频的数据,增加普通 PDO 也通常比移植 MPDO 更容易验证。
10. CanFestival 是否实现了 MPDO
本文检查了 ljessendk/CanFestival 的 include/pdo.h 和 src/pdo.c,以及 RT-Thread 分支 wdfk-prog/canfestival-rtt 的对应 PDO 路径。
公开头文件提供的主要接口为:
1 | buildPDO() |
buildPDO() 的运行模型是:读取固定 TPDO mapping count,逐条读取 mapping parameter,再通过 getODentry() 把本地对象复制到 payload。
proceedPDO() 的运行模型是:按 COB-ID 查找 RPDO,读取固定 mapping parameter,再通过 setODentry() 写入对应本地对象。
在所检查的版本中,没有发现以下 MPDO 必需路径:
- mapping count 为
0xFE/0xFF时的专用分支; - Byte 0 Node-ID 和 DAM/SAM 标志解析;
0x1FA0~0x1FCFScanner List 查询;0x1FD0~0x1FFFDispatching List 查询;- DAM/SAM 专用 API 或服务状态;
0x8230DAM target error 的运行时连接。
因此可得出的限定结论是:
在
ljessendk/CanFestival@bebf332d和wdfk-prog/canfestival-rtt@0e73022b的检查范围内,没有发现完整的 CiA 301 SAM-MPDO 或 DAM-MPDO 实现;其 PDO 代码是固定映射的普通 RPDO/TPDO 路径。
这不是对所有历史私有分支或厂商派生版本的断言。某个产品若声称基于 CanFestival 支持 MPDO,仍需要检查其私有补丁,而不能根据“CanFestival”名称直接判断。
11. 当前 Lely 主站与 CANopenNode MCU 从机应如何选择
当前项目组合是:
1 | Linux 主站:Lely CANopen,支持 SAM/DAM MPDO |
CANopen 是双端协议能力的组合。主站支持 MPDO,并不会让普通 PDO 对端自动获得 MPDO 能力。若 Lely 发出 DAM-MPDO:
- CANopenNode 可能在 CAN filter 层接收到相同 COB-ID;
- 但它不会按 MPDO 头解析 Node-ID、Index 和 Sub-index;
- 手工在 EDS 中增加 Scanner/Dispatching List,也不会自动创建协议处理代码;
- 把 MPDO payload 当成普通 RPDO 映射,还会把前 4 字节寻址头误当成过程数据。
因此项目应采用以下边界:
- CANopenNode 默认和扩展功能测试不把 MPDO 列为通过项。
- 固定过程量继续使用普通 TPDO/RPDO。
- 配置、诊断和需要结果确认的访问使用 SDO。
- 对象数量增加时,优先扩展普通 PDO 数量并规划唯一 COB-ID。
- 只有设备 profile 或第三方互操作规范明确依赖 MPDO 时,才评估协议栈替换或完整实现 MPDO。
这也解释了一个容易混淆的事实:Lely 的 RPDO/TPDO 文档和源码中能看到 MPDO 错误路径,不代表 CANopenNode 或 CanFestival 对端能够互操作。功能验收必须以双方都实现同一通信服务为前提。
12. 结论
MPDO 的核心价值不是提高单帧数据量,而是用 4 字节寻址头换取动态对象选择:
- DAM 由生产者直接指定目标节点和目标对象;
- SAM 由生产者声明来源对象,消费者通过 Dispatching List 路由到本地对象;
- 两者都只剩最多 4 字节数据,且没有逐帧成功确认;
- 普通 PDO、MPDO 和 SDO 的选择,应由数据是否固定、是否需要确认、数据宽度和实时性共同决定。
Lely 已把 MPDO 集成进 co_tpdo_t、co_rpdo_t、co_dev_t 和 NMT 事件桥接,支持 DAM/SAM 的生产与消费。CANopenNode 当前没有完整实现,维护者也未给出明确版本计划;CanFestival 的所查版本同样只发现普通固定映射 PDO 路径。
对当前 Lely 主站 + CANopenNode MCU 从站项目,最小风险方案仍是普通 PDO + SDO。只有外部 profile 明确要求 MPDO 时,才值得承担协议栈替换或扩展实现的成本。
参考资料
- 用户提供:《CiA 301 V4.2.0(中文注释版)》,第 7.2.3、7.5.2.36、7.5.2.39、7.5.2.40 节及 EMCY error code 表。
- Lely CANopen - Library overview,查阅日期 2026-08-01。
- Lely CANopen - Standards support,查阅日期 2026-08-01。
- Lely CANopen - New release v2.3.0,MPDO API、C++ 回调和 dcfgen 边界。
- Lely CANopen - Build configuration,
LELY_NO_CO_MPDO。 - Lely Doxygen 2.4.0 - pdo.c,Scanner/Dispatching List 与本地 SDO 访问函数。
- Lely Doxygen 2.4.0 - rpdo.c,RPDO 接收和帧解析入口。
- Lely GitLab master commit 88848aa2。
- CANopenNode Issue #588 - Future Support for MPDOs?。
- CANopenNode Issue #72 - Multiple(xed) PDOs。
- CANopenNode commit 9b8beed8。
- CanFestival mirror commit bebf332d。
- CanFestival RT-Thread branch commit 0e73022b。










