Lely CANopen:co_dev 设备对象字典与 EDS/DCF 解析、concise DCF 和 PDO 事件机制
@[toc]
一句话结论
Lely 的 co_dev_t 不是协议状态机,也不是简单的 EDS 文件句柄,而是 CANopen 设备描述在运行期的根对象:
- 它保存 Node-ID、网络号、设备名称、身份信息、支持的位速率、LSS 和 dummy PDO 类型等设备级元数据;
- 它用一棵以 16 位索引为 key 的红黑树管理全部
co_obj_t,每个co_obj_t再管理自己的co_sub_t; dcf.c先把 EDS/DCF 的 INI 文本解析为通用配置树,再逐层创建co_dev_t → co_obj_t → co_sub_t;$NODEID + n不只是文本替换,解析器会记录相应 flag,co_dev_set_id()在 Node-ID 改变时遍历整棵对象字典并按差值修正相关值;- 文本 EDS/DCF 与 concise DCF 是两种完全不同的格式:前者描述设备结构和元数据,后者是用于参数配置的一组紧凑二进制对象写入记录;
co_dev_tpdo_event()和 SAM-MPDO 事件接口不会直接发送 CAN 帧,而是根据 PDO 通信参数与映射参数反查受影响的 PDO,并通知上层 PDO 服务;- 在资源受限目标上,运行期解析 EDS/DCF 依赖标准 I/O 和动态内存,更适合改用
dcf2c生成静态co_sdev描述。
最重要的心智模型是:
1 | EDS/DCF text |
1. co_dev_t 在 CANopen 设备模型中的位置
CiA 301 把 CANopen 设备概括为通信功能、对象字典和应用程序三部分,对象字典位于通信对象与应用对象之间。对象通过 16 位索引寻址,复合对象中的数据字段再通过 8 位子索引寻址。
Lely 将其直接映射为三级对象:
1 | co_dev_t |
前一篇文章解决的是:
1 | 一个 index 内部有哪些 sub-object |
本篇解决的是:
1 | 整个设备有哪些 index |
1.1 co_dev_t 不负责什么
co_dev_t 本身不执行:
1 | CAN 控制器收发 |
它提供的是设备级描述、对象查找、值访问和事件解析基础。NMT、SDO、PDO、EMCY、LSS 等协议对象在这棵对象字典上读取配置并访问数据。
2. 五个源文件分别负责什么
2.1 include/lely/co/dev.h
这是 co_dev_t 的公开 C API,主要提供:
- 设备创建、销毁和 Node-ID 管理;
- 对象索引枚举、插入、移除和查找;
- 设备名称、供应商、产品、版本等元数据接口;
- 支持位速率、LSS、dummy PDO 类型等能力描述;
- 设备级 current value 读写接口和 typed getter/setter;
- concise DCF 的内存与文件读写接口;
- TPDO 与 SAM-MPDO 事件 indication 注册和触发接口。
应用通常只持有不透明指针:
1 | co_dev_t *dev; |
2.2 include/lely/co/detail/dev.h
这是内部头文件,公开 struct __co_dev 的真实布局。
理解以下问题时必须阅读它:
co_dev_t实际保存哪些字段;- 哪些字段会被条件编译移除;
- 对象字典树和设备元数据的所有权;
- TPDO、MPDO 回调是否存在于当前 ABI。
2.3 src/co/dev.c
该文件实现六类机制:
1 | 1. co_dev_t 生命周期 |
2.4 include/lely/co/dcf.h
公开接口非常小,核心职责是:
1 | EDS/DCF file or text |
即从文件或内存文本创建、初始化一个运行时设备描述。
2.5 src/co/dcf.c
这是运行期 EDS/DCF 构造器,负责:
- 调用通用 INI parser 读取 section/key;
- 解析
[DeviceInfo]、对象列表和对象 section; - 创建
co_obj_t与co_sub_t; - 解析数据类型、访问权限、limits、default、ParameterValue 和 PDO mapping;
- 处理
$NODEID表达式; - 处理 CompactSubObj、CompactPDO、UploadFile 和 DownloadFile;
- 解析
[DeviceComissioning]并设置实际 Node-ID、网络号和波特率; - 在失败时回收已经构造的对象。
3. 总体架构和两条不同的数据入口
Lely 中有两条容易混淆的 DCF 路径。
3.1 文本 EDS/DCF:构建设备结构
1 | flowchart TD |
它回答的是:
1 | 这个设备有哪些对象 |
3.2 concise DCF:向已有对象字典回放参数
1 | flowchart LR |
它不创建对象结构,只向已经存在的对象字典写入 current value。
因此:
文本 EDS/DCF 是设备描述;concise DCF 是参数写入清单。二者都叫 DCF,但用途、格式和调用链完全不同。
4. struct __co_dev:设备根对象的真实布局
内部字段可以按职责分为五组。
| 分类 | 典型字段 | 作用 |
|---|---|---|
| 设备地址 | netid、id |
网络号和 Node-ID |
| 对象字典 | tree |
以 16 位 index 为 key 的对象红黑树 |
| 描述字符串 | name、vendor_name、product_name、order_code |
EDS/DCF 中的设备描述信息 |
| 身份与能力 | vendor_id、product_code、revision、baud、rate、lss、dummy |
设备身份、支持速率、LSS 和 dummy PDO 类型 |
| 事件接口 | TPDO、SAM-MPDO indication 与用户数据 | 把对象变化通知给上层 PDO 服务 |
4.1 tree 是设备对象字典的核心
设备初始化时,tree 使用 16 位无符号整数比较函数:
1 | key = co_obj_t::idx |
因此对象遍历天然按索引升序,以下操作不需要线性扫描数组:
1 | find index |
4.2 元数据与对象字典值不是同一份信息
例如:
1 | dev->vendor_id |
是设备描述级元数据,而对象 0x1018:01 也是 Vendor-ID 的网络可访问表示。
两者在概念上有关,但 co_dev_t 不会自动保证所有元数据字段与任意对象值永远双向同步。解析器在构建设备时会分别填充它们;运行期修改某一侧时,应用仍应明确维护一致性。
4.3 条件编译会改变结构体 ABI
名称、TPDO、MPDO 等功能字段可能被宏裁剪。因此:
1 | library build macros |
否则 sizeof(struct __co_dev) 和字段偏移可能不一致,不能把这些宏仅看成链接时可选功能。
5. 设备生命周期与对象所有权
5.1 创建和初始化
动态模式下:
1 | co_dev_create(id) |
合法 Node-ID 为:
1 | 1 ... 127 |
255 不会作为正常 CANopen 节点地址上总线,它表示设备描述当前尚未绑定实际 Node-ID。
5.2 插入对象
co_dev_insert_obj() 的主要判断顺序为:
1 | flowchart TD |
它具有两个重要性质:
- 同一对象重复插入同一设备是幂等的;
- 同一索引不能同时存在两个不同对象。
5.3 移除对象
co_dev_remove_obj():
1 | verify obj belongs to dev |
移除只解除所有权,不一定销毁对象。动态模式下若随后调用 co_obj_destroy(),对象和子对象才被释放。
5.4 销毁设备
动态模式下,co_dev_destroy() 最终会:
1 | destroy every co_obj_t in tree |
所有权关系为:
1 | co_dev_t owns co_obj_t |
这也是 DCF 解析失败时只需清理根设备即可回收整棵对象字典的原因。
6. 对象查找和设备级值访问
6.1 查找路径
1 | co_dev_find_sub(dev, idx, subidx) |
时间复杂度可概括为:
1 | O(log number_of_objects) |
6.2 co_dev_get_val() 只是便捷封装
设备级 current value API 本质上是:
1 | find sub-object |
例如设备级 setter 不会因为入口在 co_dev_t 就增加协议语义。
它仍然:
- 不检查 SDO 网络访问权限;
- 不调用 download indication;
- 不生成 SDO abort code;
- 不自动触发 TPDO;
- 不执行应用状态约束。
所以应区分:
| 入口 | 语义 |
|---|---|
co_dev_set_val() / typed setter |
本地直接修改对象字典缓存 |
co_sub_dn_ind() |
模拟或处理协议级写入,执行权限和回调 |
co_dev_tpdo_event() |
通知映射到该条目的 TPDO 发生事件 |
典型应用更新流程可能是:
1 | application changes value |
直接 setter 与 PDO event 是两个独立动作。
7. Node-ID 与 $NODEID + n 的重定位机制
这是 co_dev.c 与 dcf.c 之间最关键的协作之一。
7.1 为什么解析器先使用 0xFF
从 EDS/DCF 构建设备时,解析器先初始化:
1 | dev->id = 0xFF |
此时对象中的某些字段可能写成:
1 | $NODEID + 0x180 |
解析器在读取这些值时,不只计算一个数值,还会为对应值设置 Node-ID-relative flag。
可能涉及:
1 | minimum value |
7.2 co_dev_set_id() 不是简单赋值
设置实际 Node-ID 时,函数遍历整个对象树:
1 | flowchart TD |
其中:
1 | delta = new_node_id - old_node_id |
因此 $NODEID + n 的运行时语义是可重定位值,而不是只在文件加载时展开一次。
7.3 工程影响
该机制使同一份 EDS 可以复用于多个节点:
1 | EDS describes base COB-ID expressions |
但也带来约束:
- 只有被正确设置 Node-ID flag 的值才会调整;
- 应用直接写入一个绝对数值不会自动获得 relative 属性;
- 清除或误设 flag 会造成 Node-ID 更改后的 COB-ID 错误;
- 运行期改变 Node-ID 时,应确保协议服务未同时使用旧配置。
8. EDS/DCF 文本解析入口
dcf.h 暴露两类入口:
1 | file path |
两者的内部流程相同:
1 | flowchart TD |
8.1 两阶段解析的意义
dcf.c 没有一边逐字符读取文件一边直接操作对象树,而是先得到通用配置结构,再构建设备。
优点:
- 文件和字符串入口共享后半段逻辑;
- section/key 查找与 CANopen 对象构建分离;
- 错误信息能关联到配置位置;
- CompactSubObj、CompactPDO 等跨 section 机制更容易实现。
限制:
- 需要保存配置树和动态对象树;
- 峰值内存高于直接流式解析;
- 不适合极小 MCU 在运行期加载大型 EDS。
9. co_dev_parse_cfg() 的总解析顺序
核心顺序可以概括为:
1 | [DeviceInfo] |
这种顺序并非任意安排。
9.1 先构造显式对象,再处理 CompactPDO
CompactPDO 可以自动补建缺失的 PDO 通信参数和映射参数对象。
若先创建 CompactPDO,再读取显式对象:
1 | synthesized object |
所以源码先构建文件中明确列出的对象,再对缺失项进行补建,避免自动对象覆盖用户定义。
9.2 最后读取 DeviceComissioning
对象解析阶段可以先在 Node-ID 255 下展开 $NODEID 表达式并记录 relative flags。最后读取实际 Node-ID 时,co_dev_set_id() 统一重定位相关值。
这把:
1 | syntax parsing |
和:
1 | actual network address binding |
分成两个阶段。
10. [DeviceInfo] 如何映射到 co_dev_t
设备级 section 主要填充描述和能力信息。
| EDS/DCF 信息 | co_dev_t 中的用途 |
|---|---|
| VendorName | 供应商名称 |
| ProductName | 产品名称 |
| OrderCode | 订货号 |
| VendorNumber | Vendor-ID |
| ProductNumber | Product code |
| RevisionNumber | revision number |
| BaudRate_x | 支持的位速率位图 |
| LSS_Supported | LSS 能力标志 |
| DynamicChannelsSupported 等能力项 | 对应设备能力或诊断信息 |
对象字典中可能同时存在:
1 | 0x1008 manufacturer device name |
解析器不会把所有 DeviceInfo 字段简单视为对象 0x1000 区域的替代品。设备级元数据与网络对象各有用途:
- 元数据便于工具在不逐项读取对象时识别设备;
- 对象字典条目供 SDO、PDO 和应用访问;
- 一致性由输入 DCF、生成工具和应用共同保证。
10.1 [DummyUsage]
DummyUsage 描述某些基本数据类型是否允许用作 PDO mapping 的填充项。
co_dev_t::dummy 以位图保存这些能力,供 PDO 映射验证逻辑查询。它不是实际对象值,也不会自动生成一个制造商对象。
11. 三类对象列表如何驱动对象构建
EDS/DCF 通常通过以下 section 列出对象索引:
1 | [MandatoryObjects] |
解析流程为:
1 | flowchart TD |
11.1 列表类别不改变红黑树存储方式
Mandatory、Optional 和 Manufacturer 主要是 DCF 描述类别。进入 co_dev_t 后,所有对象都按 index 进入同一棵红黑树。
也就是说运行期查找不会先问:
1 | is this mandatory or optional |
而是直接按索引查找。
11.2 重复索引的结果
若不同列表或自动生成流程试图插入相同索引的另一个对象:
1 | co_dev_insert_obj() |
因此对象列表不是可以无条件合并的普通数组。
11.3 强制对象缺失的处理边界
解析器会对典型强制对象缺失发出诊断,例如:
1 | 0x1000 Device type |
但 EDS/DCF 解析器的职责并不是完整实现所有设备 profile 的一致性证明。某些问题是 warning,某些结构错误才会导致创建失败。
12. 单个对象 section 的构建流程
12.1 先创建 co_obj_t
co_obj_build():
1 | parse object index |
若后续步骤失败,构造器会将对象从设备移除并销毁,避免留下半初始化对象。
12.2 ObjectType 决定后续结构分支
主要分为两类:
1 | simple object |
简单对象
通常只有 sub-index 0:
1 | object section |
DOMAIN 在部分场景下可省略显式 DataType,因为对象代码已经确定其数据语义。
复合对象
必须描述成员结构,并在以下两种形式中选择一种:
1 | SubNumber |
源码要求二者不能同时作为同一对象的结构来源。
13. 显式子对象与 CompactSubObj
13.1 显式子对象
复合对象可通过独立 section 描述成员,例如概念上:
1 | [1018] |
每个子 section 都经过独立 co_sub_build() 和 co_sub_parse_cfg()。
这种方式适合 RECORD,因为每个字段可以具有不同:
1 | data type |
13.2 CompactSubObj
CompactSubObj 用一个公共模板描述一组同类型成员。
解析器会:
1 | create sub-index 0 as UNSIGNED8 |
它适合 ARRAY 或大量同构条目,避免重复书写 section。
13.3 CompactName 与 CompactValue
若存在对应的 Name/Value section,解析器可以再为各成员填充:
1 | individual parameter names |
因此 CompactSubObj 并不等于所有子对象必须具有完全相同的名字和值,它主要压缩共同结构描述。
13.4 子索引 0 的结构检查
对 ARRAY/RECORD,解析完成后会检查:
1 | sub-index 0 exists |
因为按照 CiA 301,复合对象的子索引 0 通常保存支持的最高子索引或条目数量。
需要注意:解析器能检查基础结构,但不会替代设备 profile 对每个字段含义的全部验证。
14. co_sub_parse_cfg():一条 EDS/DCF 条目如何落到运行时字段
这是文本描述变成运行时对象语义的核心函数。
| EDS/DCF key | 运行时结果 |
|---|---|
ParameterName |
子对象名称 |
Denotation |
可选显示描述 |
DataType |
co_sub_t::type |
LowLimit |
minimum value |
HighLimit |
maximum value |
AccessType |
RO、WO、RW、RWR、RWW 或 CONST |
DefaultValue |
default value |
PDOMapping |
PDO mappable flag |
ObjFlags |
对象行为 flags |
ParameterValue |
current value,并标记显式参数值 |
UploadFile |
DOMAIN 上传文件模式和文件名 |
DownloadFile |
DOMAIN 下载文件模式和文件名 |
14.1 AccessType 映射
解析器接受的主要访问类型为:
1 | ro |
它们被转换为 Lely 的 CO_ACCESS_* 位组合。
需要继续区分:
1 | AccessType |
三者不是同一个概念。
14.2 Limits 的处理
若启用对象 limits,LowLimit 和 HighLimit 会解析到 co_sub_t 的 min/max。
以下问题在 dcf.c 中可能只产生 warning:
1 | LowLimit > HighLimit |
这意味着:
成功加载 DCF 不等于所有值都已经通过运行期 SDO 范围检查。
后续默认 SDO download indication 仍会依据 min/max 返回相应 abort code。
15. DefaultValue、ParameterValue 与 current value 的优先级
这一点直接决定设备启动后的对象值。
15.1 存在 ParameterValue
1 | DefaultValue |
15.2 不存在 ParameterValue
解析器会把 default 复制到 current value:
1 | default value |
因此运行期对象通常仍有初始 current value。
但概念上必须保留区别:
1 | default |
运行期调用 co_sub_set_def() 不会自动再次覆盖 current value。
15.3 $NODEID flag 的传播
如果 DefaultValue 是 Node-ID-relative,而 ParameterValue 缺失,则从 default 复制 current value 时,相关 relative 属性也需要传播到 current value。
否则后续 co_dev_set_id() 只会调整 default,不会调整实际正在使用的 current value。
16. DOMAIN、UploadFile 与 DownloadFile
DOMAIN 用于表示任意长度数据。Lely 允许 DCF 把它连接到文件。
16.1 UploadFile
1 | DOMAIN current value |
16.2 DownloadFile
1 | SDO download data |
16.3 为什么存的是文件名而不是文件内容
对象字典只保存一个外部数据源引用:
1 | small dictionary entry |
这样可避免在对象字典中常驻固件、配置包或大块二进制数据。
16.4 访问权限修正
解析器会根据 UploadFile/DownloadFile 的方向检查或修正 access,使文件模式与网络访问方向一致。
但文件 I/O 的错误仍可能在真正执行 SDO upload/download 时才暴露,例如:
1 | file does not exist |
17. co_val_lex_dcf() 的类型解析策略
DCF 文本中的 value 最终必须转换为 union co_val。
17.1 基本数值类型
整数、布尔、浮点、时间等大部分类型委托给通用 value lexer。
1 | text |
17.2 OCTET_STRING 与 DOMAIN
这两类值按十六进制字节串解释,而不是普通 C 字符串。
例如概念上:
1 | 010203A0 |
17.3 UNICODE_STRING
输入文本按 UTF-8 读取,再转换为 CANopen Unicode string 所使用的 UTF-16 code units。
因此:
1 | file encoding |
是两个不同层次。
17.4 解析成功不等于业务值有效
lexer 负责:
1 | syntax and type conversion |
而以下约束可能位于其他层:
1 | min/max range |
18. CompactPDO:自动补建 PDO 参数对象
[CompactPDO] 用于压缩 PDO 参数描述。解析器会根据配置为缺失对象创建通信参数和映射参数。
18.1 RPDO 对象对
1 | 0x1400 ... 0x15FF |
18.2 TPDO 对象对
1 | 0x1800 ... 0x19FF |
18.3 构建原则
1 | flowchart TD |
18.4 它解决什么问题
CompactPDO 解决的是描述重复:
1 | many PDOs |
它不会自动证明映射对象一定适合业务应用。映射目标的数据类型、长度、access 和 PDO mapping 属性仍需要 PDO 模块验证。
19. [DeviceComissioning]:把通用描述绑定到具体网络
源码使用的 section 名为:
1 | [DeviceComissioning] |
需要按该拼写理解现有实现和文件兼容性。
主要字段包括:
| 字段 | 作用 |
|---|---|
NodeID |
设置设备 Node-ID,并触发 relative value 重定位 |
NetNumber |
设置网络号 |
NodeName |
设置节点名称 |
Baudrate |
设置实际使用的位速率 |
LSS_SerialNumber |
设置 LSS identity 中使用的序列号信息 |
19.1 DeviceInfo 与 DeviceComissioning 的区别
1 | DeviceInfo |
例如:
1 | supported baud rates |
20. concise DCF 的二进制布局
Lely 的设备 API还支持 concise DCF 的内存和文件读写。
20.1 总体格式
1 | +-------------------------+ |
每条 entry:
1 | +-------------------------+ |
固定头长度为:
1 | 2 + 1 + 4 = 7 bytes per entry |
多字节整数和 CANopen 基本数据按 little-endian 编码。
20.2 读取流程
1 | flowchart TD |
未知对象记录被消费但不必导致整份 concise DCF 立即失败,这允许配置数据包含当前设备版本不识别的条目。
20.3 写出流程
co_dev_write_dcf() 的实现分两遍:
1 | first pass |
索引范围可用于只导出对象字典的一部分。
20.4 与 SDO download 的区别
concise DCF restore 是本地对象字典操作,不等价于通过 CAN 网络逐条执行 SDO download。
它通常不会自动获得:
1 | network access check |
因此若某个参数必须通过自定义 indication 才能真正写入硬件,仅恢复对象字典缓存可能不够,应用还需要执行相应同步步骤。
21. TPDO event:从一个对象变化反查受影响的 PDO
co_dev_tpdo_event(dev, idx, subidx) 的核心问题是:
1 | which TPDO maps this object entry |
21.1 扫描范围
函数扫描 TPDO 通信参数对象:
1 | 0x1800 ... 0x19FF |
并读取对应映射对象:
1 | 0x1A00 ... 0x1BFF |
21.2 过滤条件
候选 TPDO 需要满足相关条件,例如:
- 通信对象和映射对象存在;
- COB-ID 有效;
- 不是不适用的 MPDO 形式;
- 传输类型允许事件触发;
- 某个映射条目确实引用目标 index/sub-index。
21.3 匹配后做什么
1 | find matching TPDO number |
它不在 dev.c 中直接:
1 | pack PDO bytes |
这些动作由上层 TPDO 服务对象处理。
21.4 为什么不在每个 co_sub_t 保存反向映射表
当前实现选择在事件触发时扫描 PDO 配置:
优点:
- 不需要维护额外反向索引;
- PDO 重映射后无需同步多份缓存;
- 对象字典始终是配置事实来源。
代价:
- 每次 event 需要遍历候选 TPDO 和映射项;
- 高频变量、超多 PDO 场景的成本更高;
- 上层应避免对未映射变量无意义地反复触发 event。
22. SAM-MPDO event
SAM-MPDO 使用源地址模式,把生产者对象字典中的 index/sub-index 携带在 MPDO 中。
co_dev 的 SAM-MPDO event 逻辑会:
1 | locate configured SAM-MPDO producer |
CiA 301 的模型限制一个设备只有一个此类 SAM-MPDO producer,因此它与普通 TPDO 的多实例扫描语义不同。
同样,设备层只做配置解析和事件 indication,不承担 CAN 帧传输状态机。
23. 文本 DCF 解析的错误与回滚边界
23.1 根级失败
若文件解析或设备构建失败:
1 | __co_dev_fini() |
因此返回给调用方的不是一棵需要手工逐项清理的半成品树。
23.2 对象级失败
co_obj_build() 创建对象后,若名称、对象代码或子对象解析失败,会移除并销毁当前对象。
23.3 子对象级失败
co_sub_build() 创建并插入子对象后,后续属性解析失败时会销毁该子对象;动态模式下对象的连续值存储也会随拓扑变化更新。
23.4 warning 与 fatal error 必须区分
典型 fatal error:
1 | missing mandatory structural key |
典型 warning:
1 | recommended mandatory object missing |
因此工程上应同时检查:
1 | return value |
只看函数成功返回可能遗漏配置质量问题。
24. 条件编译和嵌入式部署策略
24.1 与本篇直接相关的宏
| 宏 | 影响 |
|---|---|
LELY_NO_CO_DCF |
移除 EDS/DCF 运行期解析支持 |
LELY_NO_CO_DCF_RESTORE |
移除 application parameter concise DCF 保存/恢复相关功能 |
LELY_NO_CO_OBJ_NAME |
不保存对象和子对象名称 |
LELY_NO_CO_OBJ_LIMITS |
不保存 min/max,也不执行对应范围检查 |
LELY_NO_CO_OBJ_DEFAULT |
不保存 default metadata |
LELY_NO_CO_OBJ_FILE |
移除 UploadFile/DownloadFile |
LELY_NO_CO_TPDO |
移除 TPDO 相关能力和设备事件字段 |
LELY_NO_CO_MPDO |
移除 MPDO 支持 |
LELY_NO_CO_SDEV |
移除静态设备描述支持 |
LELY_NO_MALLOC |
移除大部分动态创建路径,且会连带关闭标准 I/O |
24.2 为什么 LELY_NO_MALLOC 下不适合运行期 DCF
官方构建关系为:
1 | no malloc |
这与 dcf.c 的实现需求一致:
- 配置树需要动态节点;
- 对象、子对象和值需要动态存储;
- 文件入口需要 stdio;
- 字符串与 DOMAIN 可能继续分配内存。
24.3 dcf2c 的定位
资源受限目标更适合:
1 | build host |
这将复杂文本解析从目标机移动到构建阶段,换取:
- 固定 RAM/Flash 布局;
- 更确定的启动时间;
- 无文件系统依赖;
- 更小的运行期错误面。
代价是修改 DCF 后需要重新生成并编译固件。
25. 线程安全和运行阶段边界
dev.c 与 dcf.c 内部没有为对象树提供通用 mutex。
以下操作不应与 SDO/PDO/NMT 访问无保护并发:
1 | parse and insert objects |
推荐生命周期:
1 | initialization phase |
若必须运行期加载新 DCF,应在更高层提供全局设备配置锁,并重新评估所有保存的 co_obj_t *、co_sub_t * 和 value pointer 生命周期。
26. 完整运行流程整理
26.1 从 EDS/DCF 到运行期设备
1 | flowchart TD |
26.2 应用修改值并触发 TPDO
1 | application computes new process value |
26.3 concise DCF 恢复
1 | existing co_dev_t |
26.4 Node-ID 更改
1 | old Node-ID |
27. 关键风险与工程注意事项
27.1 把文本 DCF 与 concise DCF 混为一谈
风险:错误地认为 co_dev_read_dcf() 能解析 INI 文本。
处理:
1 | co_dev_create_from_dcf_file/text |
27.2 直接 setter 绕过协议和硬件副作用
风险:对象字典值更新了,但硬件寄存器、Flash 或应用状态没有更新。
处理:需要协议语义时走 indication;恢复 concise DCF 后执行应用同步流程。
27.3 current value 更新后没有触发 TPDO
风险:应用认为变量变化就会自动发 PDO。
处理:根据应用模型显式调用 co_dev_tpdo_event(),或由更高层封装 setter + event。
27.4 $NODEID flag 不正确
风险:Node-ID 改变后 COB-ID 没有变化,或绝对值被错误重定位。
处理:只对真正的 $NODEID + n 表达式设置相应 flag,修改配置后验证关键通信对象。
27.5 成功解析被误认为完整合规
风险:忽略 warning,带着越界默认值或 profile 不一致对象运行。
处理:将诊断 warning 纳入 CI;配合 dcfchk 和设备 profile 专项校验。
27.6 运行期热加载对象字典
风险:协议服务仍保存旧对象或值指针,造成悬空引用。
处理:优先在启动前一次性构建;热更新时停止服务并建立严格所有权边界。
27.7 元数据与标准对象不一致
风险:工具读取 DeviceInfo 得到一组身份,SDO 读取 0x1018 得到另一组身份。
处理:生成 DCF 时从单一数据源产生两部分信息,并在测试中交叉验证。
27.8 CompactPDO 被误认为完整 PDO 配置器
风险:自动建出了参数对象,但映射目标无效或不允许映射。
处理:继续执行 PDO mapping 验证,并在 NMT Pre-operational 阶段测试配置。
28. 推荐源码阅读顺序
1 | include/lely/co/dev.h |
29. 推荐调试断点和观察变量
29.1 文本入口
1 | co_dev_create_from_dcf_file |
观察:
1 | dev->id |
29.2 对象构建
1 | co_obj_build |
观察:
1 | index |
29.3 Node-ID 重定位
1 | co_dev_set_id |
观察:
1 | old id |
29.4 concise DCF
1 | co_dev_read_sub |
观察:
1 | entry count |
29.5 PDO event
1 | co_dev_tpdo_event |
观察:
1 | changed index and sub-index |
30. 常见误区
30.1 误区:co_dev_t 就是一个 EDS 文件
错误。EDS/DCF 只是构造来源之一,co_dev_t 是运行期设备对象和对象字典根节点。
30.2 误区:DCF 只有一种格式
错误。文本 EDS/DCF 用于描述设备,concise DCF 是二进制参数记录。
30.3 误区:修改 dev->id 只影响一个字段
错误。公开 co_dev_set_id() 会重定位所有标记为 $NODEID 相对表达式的值。
30.4 误区:读取 DCF 成功就没有配置问题
错误。部分范围和一致性问题只产生 warning。
30.5 误区:ParameterValue 与 DefaultValue 相同
错误。前者是配置后的 current value,后者是默认元数据;缺少 ParameterValue 时解析器才会用 default 初始化 current。
30.6 误区:直接修改对象值会自动发送 TPDO
错误。值修改和 TPDO event indication 是两个步骤。
30.7 误区:co_dev_tpdo_event() 直接发送 CAN 帧
错误。它解析映射并通知上层 TPDO 服务。
30.8 误区:CompactPDO 会验证整个应用映射
错误。它主要补建标准参数对象,最终合法性仍由 PDO 配置和映射检查决定。
30.9 误区:co_dev_read_dcf() 等价于远程 SDO 写
错误。它本地恢复 current values,不自动执行全部网络权限、indication 和硬件副作用。
30.10 误区:MCU 上运行期解析 EDS 一定更灵活、更好
不一定。运行期解析增加动态内存、文件系统、启动时间和错误处理成本。静态 co_sdev 往往更适合资源受限固件。
31. 最终心智模型
可以把这部分实现记成六层:
1 | 第 1 层:文件描述 |
再加两条旁路:
1 | concise DCF |
最终完整图:
1 | flowchart TD |
最简洁的结论是:
co_dev_t负责“一个设备是谁、有哪些对象,以及如何从 index 定位对象”;dcf.c负责“把 EDS/DCF 的描述语言翻译为这棵运行时对象树”;concise DCF 负责“向已有对象树回放参数值”;PDO event 负责“从一个对象值变化反查需要被通知的 PDO”。这四部分共同把静态设备描述、运行时数据和 CANopen 协议对象连接起来。
参考源码与资料
本文核心源码
关联源码
include/lely/co/obj.hinclude/lely/co/detail/obj.hsrc/co/obj.cinclude/lely/co/val.hsrc/co/val.cinclude/lely/co/type.hsrc/co/type.cinclude/lely/util/config.hsrc/util/config.cinclude/lely/co/sdev.hsrc/co/sdev.csrc/co/pdo.c
Lely 官方资料
协议参考
- CiA 301 V4.2.0:CANopen 设备模型、对象字典、对象代码、索引/子索引、PDO 参数对象和 SDO 访问模型。
前置学习笔记
Lely_CANopen_co_obj_co_sub_对象字典节点_机制原理与运行流程.md








