CANopen MPDO:动态对象寻址

摘要:解析 MPDO 的 DAM/SAM 寻址、对象列表与报文流程,并结合 Lely、CANopenNode 和 CanFestival 源码说明实现差异及替代方案。

在这里插入图片描述

@[toc]

1. MPDO 解决了普通 PDO 的什么问题

普通 PDO 的优势是协议开销小、实时性高,但它的对象映射通常在通信前确定:一个 PDO COB-ID 对应一组固定的对象字典条目。收到数据帧后,消费者按照 0x1600~0x17FF 中的映射,把第 1 段、第 2 段数据写到预先配置的本地对象。

当设备存在大量低频、稀疏变化的 8/16/32 位对象时,固定映射会遇到三个问题:

  1. 为每组对象分配 PDO,会增加通信参数、映射对象和 COB-ID 管理量。
  2. 反复动态重映射 PDO,需要先禁用 PDO、清空映射、修改条目、恢复映射,再重新启用,不能把它当成逐帧选择机制。
  3. 改用 SDO 虽然可以逐次携带 Index/Sub-index,但每次访问都有请求、响应和协议状态,语义更可靠,实时性和总线效率却不同于 PDO。

MPDO(Multiplexed Process Data Object)在 PDO 数据区中携带 Node-ID、Index 和 Sub-index,使同一个 PDO COB-ID 能够按帧选择不同对象。它可以近似理解为:

1
2
3
PDO 的单帧、无确认、事件驱动传输
+
SDO 风格的对象字典地址

但这只是帮助理解的类比。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
2
3
4
5
6
81 00 22 00 78 56 34 12
│ │─────│ │──────────│
│ │ │ └─ 数据 0x12345678
│ │ └───────── Sub-index 0x00
│ └──────────────── Index 0x2200,小端
└─────────────────── DAM 标志 0x80 + Node-ID 1

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart LR
appA["生产者应用"] --> damBuild["组装 DAM-MPDO<br/>目标 Node-ID + 本地 Index/Sub-index + Value"]
damBuild --> canBus["同一个 MPDO COB-ID"]
canBus --> node1["节点 1:地址匹配"]
canBus --> node2["节点 2:地址不匹配"]
node1 --> localOd["检查本地对象、长度和写权限"]
localOd --> writeOk["写入目标对象"]
localOd --> writeErr["目标不可用:错误处理/EMCY"]
node2 --> discard["忽略该帧"]

appB["SAM 生产者对象事件"] --> scanner["Object Scanner List<br/>0x1FA0~0x1FCF"]
scanner --> samBuild["组装 SAM-MPDO<br/>来源 Node-ID + 来源 Index/Sub-index + Value"]
samBuild --> canBus2["同一个 MPDO COB-ID"]
canBus2 --> dispatch["消费者 Object Dispatching List<br/>0x1FD0~0x1FFF"]
dispatch --> localMap["来源对象映射到本地 Index/Sub-index"]
localMap --> localOd2["写入本地对象"]

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
2
3
4
5
远端 Node-ID + 远端 Index + 远端 Sub-index

Object Dispatching List

本地 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

工程上可以使用以下判断:

  1. 字段固定、频率高、对时延敏感:普通 PDO。
  2. 数据超过 32 位,或调用方必须知道成功/失败:SDO。
  3. 对象很多、变化稀疏、每项不超过 32 位、允许无确认,并且双方协议栈均支持标准 MPDO:MPDO。

MPDO 不是“更多 PDO”的通用替代品。如果对象数量不多,扩展普通 PDO 往往更简单;如果数据必须可靠落盘,MPDO 又不如 SDO 合适。

5. Lely 如何表示和配置 MPDO

Lely 官方的 standards support 页面把 MPDO、0x1FA0~0x1FCF0x1FD0~0x1FFF 列为已实现能力;构建时可以通过 --disable-mpdoLELY_NO_CO_MPDO 去除相关代码。

Lely 没有另建一套完全独立于 PDO 的服务对象。MPDO 仍由:

1
2
3
4
co_tpdo_t:生产路径
co_rpdo_t:消费路径
co_dev_t:对象字典和列表查询
co_nmt_t:对象事件到 TPDO 服务的桥接

协同完成。关键公开符号包括:

1
2
3
4
5
6
7
8
CO_PDO_MAP_SAM_MPDO
CO_PDO_MAP_DAM_MPDO
co_dam_mpdo_event()
co_sam_mpdo_event()
co_dev_sam_mpdo_event()
co_dev_chk_sam_mpdo()
co_dev_map_sam_mpdo()
co_sam_mpdo_up()

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 文档列出了普通 rpdotpdosdo 配置,但没有给出 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
2
3
4
5
6
7
8
9
10
11
application
↓ co_dam_mpdo_event()
检查 TPDO 通信参数和 mapping mode = 0xFF

检查事件触发条件和 inhibit time

Byte0 = 0x80 | destination Node-ID
Byte1..3 = destination Index/Sub-index
Byte4..7 = data

co_tpdo_send_frame() / can_net_send()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
应用修改本地对象

SetEvent() / co_dev_sam_mpdo_event()
↓ 对象字典 SAM event indication
co_nmt_on_sam_mpdo_event()
↓ 将对象事件转交给相关 TPDO 服务
co_sam_mpdo_event()

co_dev_chk_sam_mpdo()
↓ 查询 0x1FA0~0x1FCF
co_sam_mpdo_up()
↓ 本地 SDO upload 读取对象,最多 4 字节
组装来源 Node-ID + 来源 Index/Sub-index + Value

CAN 发送

这条路径的核心不是“周期扫描整个对象字典”,而是“对象事件先给出候选 Index/Sub-index,再由 Scanner List 判断该对象是否属于某个 SAM-MPDO”。这样避免了每次事件都遍历所有应用对象。

7. Lely 的 MPDO 接收运行流程

下图把发送和接收路径放在同一张图中。应重点观察:DAM 直接使用帧内目标对象;SAM 必须经过 Dispatching List 才得到本地目标对象。

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
flowchart TD
damApi["DAM API<br/>co_dam_mpdo_event"] --> damCheck["检查 TPDO 有效、事件模式、0xFF mapping"]
damCheck --> damFrame["编码目标 Node-ID<br/>目标 Index/Sub-index + Value"]
damFrame --> tx["TPDO 发送到 CAN network"]

objEvent["本地对象事件"] --> devSam["co_dev_sam_mpdo_event"]
devSam --> nmtSam["co_nmt_on_sam_mpdo_event"]
nmtSam --> samEvent["co_sam_mpdo_event"]
samEvent --> scanner["co_dev_chk_sam_mpdo<br/>查询 Scanner List"]
scanner --> samUp["co_sam_mpdo_up<br/>本地 SDO upload"]
samUp --> samFrame["编码来源 Node-ID<br/>来源 Index/Sub-index + Value"]
samFrame --> tx

rx["CAN frame 到达 RPDO receiver"] --> recv["co_rpdo_recv"]
recv --> read["co_rpdo_read_frame"]
read --> mode{"mapping mode"}
mode -->|"0xFE SAM"| samParse["解析来源 Node-ID/Index/Sub-index"]
samParse --> dispatch["co_dev_map_sam_mpdo<br/>查询 Dispatching List"]
dispatch --> localSam["得到本地 Index/Sub-index"]
localSam --> odWrite["通过本地 SDO download 写 OD"]

mode -->|"0xFF DAM"| damParse["校验目标 Node-ID<br/>使用帧内目标 Index/Sub-index"]
damParse --> odWrite
odWrite --> callback["对象 write indication / 应用回调"]
odWrite --> error["失败:SDO abort code<br/>RPDO error/EMCY 路径"]

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 接收流程为:

  1. 从 Byte 0 获取来源 Node-ID,解析来源 Index/Sub-index。
  2. co_dev_map_sam_mpdo() 查询 0x1FD0~0x1FFF
  3. 找到匹配项后,返回本地 Index/Sub-index。
  4. 通过本地对象字典 download 路径写入数据。
  5. 上层 RpdoRead()RpdoGet()OnRpdoWrite() 可像普通 RPDO 一样观察更新;Lely v2.3.0 发布说明明确说明了这一点。

如果 Dispatching List 没有匹配项,不能凭帧内 Index 直接写入同名本地对象,因为 SAM 的帧地址描述来源,不描述目的。

7.3 接收 DAM-MPDO

DAM 接收流程为:

  1. 解析并校验目标 Node-ID;非本节点且不是广播地址时忽略。
  2. 使用帧内 Index/Sub-index 直接定位本地对象。
  3. 检查对象存在性、写权限和数据长度。
  4. 通过本地对象字典 download 路径写入。

Lely 的 C++ 应用层对 DAM 有一个重要限制:OnWrite() 能看到本地对象被写入,但无法仅凭该回调区分来源是 DAM-MPDO 还是 SDO download。原因是两条路径最终复用了同一本地对象 download indication。

8. 为什么 CANopenNode 没有实现 MPDO

8.1 已确认的实现状态

在本文检查的 CANopenNode 基线中,PDO 公开对象和 API 仍围绕普通 RPDO/TPDO:

1
2
3
4
5
CO_PDO_common_t
CO_RPDO_t
CO_TPDO_t
CO_RPDO_init() / CO_RPDO_process()
CO_TPDO_init() / CO_TPDO_process()

没有发现对应的 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 字节头即可完成。一个可互操作的实现至少需要:

  1. 识别 mapping 子索引 0 的 0xFE/0xFF 特殊模式。
  2. 实现 DAM 和 SAM 两套不同的地址语义。
  3. 支持 0x1FA0~0x1FCF Scanner List。
  4. 支持 0x1FD0~0x1FFF Dispatching List。
  5. 处理目标 Node-ID、DAM 广播和非目标帧过滤。
  6. 对每帧执行动态 OD 查找、长度检查和访问权限检查。
  7. 把对象不可用等异常连接到 0x8230 EMCY 与内部错误状态。
  8. 处理通信复位、动态 OD 修改、条件编译和并发访问。
  9. 为普通 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/CanFestivalinclude/pdo.hsrc/pdo.c,以及 RT-Thread 分支 wdfk-prog/canfestival-rtt 的对应 PDO 路径。

公开头文件提供的主要接口为:

1
2
3
4
5
6
7
buildPDO()
proceedPDO()
sendPDOrequest()
sendPDOevent()
sendOnePDOevent()
PDOEnable() / PDODisable()
PDOInit() / PDOStop()

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~0x1FCF Scanner List 查询;
  • 0x1FD0~0x1FFF Dispatching List 查询;
  • DAM/SAM 专用 API 或服务状态;
  • 0x8230 DAM target error 的运行时连接。

因此可得出的限定结论是:

ljessendk/CanFestival@bebf332dwdfk-prog/canfestival-rtt@0e73022b 的检查范围内,没有发现完整的 CiA 301 SAM-MPDO 或 DAM-MPDO 实现;其 PDO 代码是固定映射的普通 RPDO/TPDO 路径。

这不是对所有历史私有分支或厂商派生版本的断言。某个产品若声称基于 CanFestival 支持 MPDO,仍需要检查其私有补丁,而不能根据“CanFestival”名称直接判断。

11. 当前 Lely 主站与 CANopenNode MCU 从机应如何选择

当前项目组合是:

1
2
Linux 主站:Lely CANopen,支持 SAM/DAM MPDO
MCU 从站:CANopenNode,当前检查基线未实现 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 字节寻址头误当成过程数据。

因此项目应采用以下边界:

  1. CANopenNode 默认和扩展功能测试不把 MPDO 列为通过项。
  2. 固定过程量继续使用普通 TPDO/RPDO。
  3. 配置、诊断和需要结果确认的访问使用 SDO。
  4. 对象数量增加时,优先扩展普通 PDO 数量并规划唯一 COB-ID。
  5. 只有设备 profile 或第三方互操作规范明确依赖 MPDO 时,才评估协议栈替换或完整实现 MPDO。

这也解释了一个容易混淆的事实:Lely 的 RPDO/TPDO 文档和源码中能看到 MPDO 错误路径,不代表 CANopenNode 或 CanFestival 对端能够互操作。功能验收必须以双方都实现同一通信服务为前提。

12. 结论

MPDO 的核心价值不是提高单帧数据量,而是用 4 字节寻址头换取动态对象选择:

  • DAM 由生产者直接指定目标节点和目标对象;
  • SAM 由生产者声明来源对象,消费者通过 Dispatching List 路由到本地对象;
  • 两者都只剩最多 4 字节数据,且没有逐帧成功确认;
  • 普通 PDO、MPDO 和 SDO 的选择,应由数据是否固定、是否需要确认、数据宽度和实时性共同决定。

Lely 已把 MPDO 集成进 co_tpdo_tco_rpdo_tco_dev_t 和 NMT 事件桥接,支持 DAM/SAM 的生产与消费。CANopenNode 当前没有完整实现,维护者也未给出明确版本计划;CanFestival 的所查版本同样只发现普通固定映射 PDO 路径。

对当前 Lely 主站 + CANopenNode MCU 从站项目,最小风险方案仍是普通 PDO + SDO。只有外部 profile 明确要求 MPDO 时,才值得承担协议栈替换或扩展实现的成本。

参考资料

  1. 用户提供:《CiA 301 V4.2.0(中文注释版)》,第 7.2.3、7.5.2.36、7.5.2.39、7.5.2.40 节及 EMCY error code 表。
  2. Lely CANopen - Library overview,查阅日期 2026-08-01。
  3. Lely CANopen - Standards support,查阅日期 2026-08-01。
  4. Lely CANopen - New release v2.3.0,MPDO API、C++ 回调和 dcfgen 边界。
  5. Lely CANopen - Build configurationLELY_NO_CO_MPDO
  6. Lely Doxygen 2.4.0 - pdo.c,Scanner/Dispatching List 与本地 SDO 访问函数。
  7. Lely Doxygen 2.4.0 - rpdo.c,RPDO 接收和帧解析入口。
  8. Lely GitLab master commit 88848aa2
  9. CANopenNode Issue #588 - Future Support for MPDOs?
  10. CANopenNode Issue #72 - Multiple(xed) PDOs
  11. CANopenNode commit 9b8beed8
  12. CanFestival mirror commit bebf332d
  13. CanFestival RT-Thread branch commit 0e73022b