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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EDS/DCF text
↓ parse INI sections and keys
config_t
↓ build device description
co_dev_t
↓ 16-bit index red-black tree
co_obj_t
↓ 8-bit sub-index red-black tree
co_sub_t

metadata + current value + access + mapping + callbacks

concise DCF binary
↓ replay object-value records
co_dev_t object dictionary

local value changed
↓ resolve PDO mappings
TPDO or SAM-MPDO event indication

1. co_dev_t 在 CANopen 设备模型中的位置

CiA 301 把 CANopen 设备概括为通信功能、对象字典和应用程序三部分,对象字典位于通信对象与应用对象之间。对象通过 16 位索引寻址,复合对象中的数据字段再通过 8 位子索引寻址。

Lely 将其直接映射为三级对象:

1
2
3
4
5
6
7
8
co_dev_t
一个 CANopen 设备及其完整对象字典

co_obj_t
一个 16-bit index 对象

co_sub_t
一个 8-bit sub-index 条目

前一篇文章解决的是:

1
2
3
一个 index 内部有哪些 sub-object
每个 sub-object 的值放在哪里
SDO/PDO 如何读写一个 sub-object

本篇解决的是:

1
2
3
4
5
整个设备有哪些 index
设备描述从哪里来
Node-ID 改变时哪些值需要同步变化
文本 DCF 和 concise DCF 如何进入对象字典
一个本地值变化如何定位相关 TPDO

1.1 co_dev_t 不负责什么

co_dev_t 本身不执行:

1
2
3
4
5
6
7
CAN 控制器收发
SocketCAN I/O
NMT 状态机调度
SDO 分段或块传输
PDO 定时器和 SYNC 调度
EDS 编辑与生成
线程同步

它提供的是设备级描述、对象查找、值访问和事件解析基础。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
2
3
4
5
6
1. co_dev_t 生命周期
2. 对象红黑树管理
3. Node-ID 相对值重定位
4. 设备级元数据和 current value 访问
5. concise DCF 编解码
6. TPDO 与 SAM-MPDO 映射事件解析

2.4 include/lely/co/dcf.h

公开接口非常小,核心职责是:

1
2
EDS/DCF file or text
→ dynamically created co_dev_t

即从文件或内存文本创建、初始化一个运行时设备描述。

2.5 src/co/dcf.c

这是运行期 EDS/DCF 构造器,负责:

  • 调用通用 INI parser 读取 section/key;
  • 解析 [DeviceInfo]、对象列表和对象 section;
  • 创建 co_obj_tco_sub_t
  • 解析数据类型、访问权限、limits、default、ParameterValue 和 PDO mapping;
  • 处理 $NODEID 表达式;
  • 处理 CompactSubObj、CompactPDO、UploadFile 和 DownloadFile;
  • 解析 [DeviceComissioning] 并设置实际 Node-ID、网络号和波特率;
  • 在失败时回收已经构造的对象。

3. 总体架构和两条不同的数据入口

Lely 中有两条容易混淆的 DCF 路径。

3.1 文本 EDS/DCF:构建设备结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart TD
A["EDS or DCF file"] --> B["config_parse_ini_file"]
C["EDS or DCF text"] --> D["config_parse_ini_text"]
B --> E["config_t section and key tree"]
D --> E
E --> F["co_dev_parse_cfg"]
F --> G["parse DeviceInfo"]
F --> H["parse object lists"]
H --> I["build co_obj_t"]
I --> J["build co_sub_t"]
F --> K["parse CompactPDO"]
F --> L["parse DeviceComissioning"]
G --> M["runtime co_dev_t"]
J --> M
K --> M
L --> M

它回答的是:

1
2
3
4
5
这个设备有哪些对象
对象是什么类型
子对象如何组织
访问权限和映射属性是什么
默认值和初始值是什么

3.2 concise DCF:向已有对象字典回放参数

1
2
3
4
5
flowchart LR
A["concise DCF binary"] --> B["co_dev_read_dcf"]
B --> C["decode index sub-index size data"]
C --> D["find existing co_sub_t"]
D --> E["replace current value"]

它不创建对象结构,只向已经存在的对象字典写入 current value。

因此:

文本 EDS/DCF 是设备描述;concise DCF 是参数写入清单。二者都叫 DCF,但用途、格式和调用链完全不同。


4. struct __co_dev:设备根对象的真实布局

内部字段可以按职责分为五组。

分类 典型字段 作用
设备地址 netidid 网络号和 Node-ID
对象字典 tree 以 16 位 index 为 key 的对象红黑树
描述字符串 namevendor_nameproduct_nameorder_code EDS/DCF 中的设备描述信息
身份与能力 vendor_idproduct_coderevisionbaudratelssdummy 设备身份、支持速率、LSS 和 dummy PDO 类型
事件接口 TPDO、SAM-MPDO indication 与用户数据 把对象变化通知给上层 PDO 服务

4.1 tree 是设备对象字典的核心

设备初始化时,tree 使用 16 位无符号整数比较函数:

1
2
key = co_obj_t::idx
order = ascending unsigned 16-bit index

因此对象遍历天然按索引升序,以下操作不需要线性扫描数组:

1
2
3
4
5
6
find index
find next index
find previous index
get first object
get last object
list indices in a range

4.2 元数据与对象字典值不是同一份信息

例如:

1
dev->vendor_id

是设备描述级元数据,而对象 0x1018:01 也是 Vendor-ID 的网络可访问表示。

两者在概念上有关,但 co_dev_t 不会自动保证所有元数据字段与任意对象值永远双向同步。解析器在构建设备时会分别填充它们;运行期修改某一侧时,应用仍应明确维护一致性。

4.3 条件编译会改变结构体 ABI

名称、TPDO、MPDO 等功能字段可能被宏裁剪。因此:

1
2
3
library build macros
must equal
application build macros

否则 sizeof(struct __co_dev) 和字段偏移可能不一致,不能把这些宏仅看成链接时可选功能。


5. 设备生命周期与对象所有权

5.1 创建和初始化

动态模式下:

1
2
3
4
5
6
7
co_dev_create(id)

allocate struct __co_dev

__co_dev_init(dev, id)

initialize metadata and object tree

合法 Node-ID 为:

1
2
1 ... 127
255 for unconfigured device

255 不会作为正常 CANopen 节点地址上总线,它表示设备描述当前尚未绑定实际 Node-ID。

5.2 插入对象

co_dev_insert_obj() 的主要判断顺序为:

1
2
3
4
5
6
7
8
9
10
flowchart TD
A["co_dev_insert_obj"] --> B{"object belongs to another device"}
B -->|yes| X["return failure"]
B -->|no| C{"object already belongs to this device"}
C -->|yes| Y["return success"]
C -->|no| D{"same index already exists"}
D -->|yes| X
D -->|no| E["set obj device pointer"]
E --> F["insert rbnode into device tree"]
F --> Y

它具有两个重要性质:

  1. 同一对象重复插入同一设备是幂等的;
  2. 同一索引不能同时存在两个不同对象。

5.3 移除对象

co_dev_remove_obj()

1
2
3
4
5
6
7
verify obj belongs to dev

remove rbnode

reinitialize node key

clear obj->dev

移除只解除所有权,不一定销毁对象。动态模式下若随后调用 co_obj_destroy(),对象和子对象才被释放。

5.4 销毁设备

动态模式下,co_dev_destroy() 最终会:

1
2
3
4
5
6
7
destroy every co_obj_t in tree

each co_obj_t destroys its co_sub_t members

free device strings

free co_dev_t

所有权关系为:

1
2
co_dev_t owns co_obj_t
co_obj_t owns co_sub_t

这也是 DCF 解析失败时只需清理根设备即可回收整棵对象字典的原因。


6. 对象查找和设备级值访问

6.1 查找路径

1
2
3
4
5
6
7
co_dev_find_sub(dev, idx, subidx)

co_dev_find_obj(dev, idx)
↓ red-black tree
co_obj_find_sub(obj, subidx)
↓ red-black tree
co_sub_t

时间复杂度可概括为:

1
2
3
O(log number_of_objects)
+
O(log number_of_subobjects_in_object)

6.2 co_dev_get_val() 只是便捷封装

设备级 current value API 本质上是:

1
2
3
find sub-object

call corresponding co_sub API

例如设备级 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
2
3
4
5
application changes value

co_dev_set_val_*()

co_dev_tpdo_event(idx, subidx)

直接 setter 与 PDO event 是两个独立动作。


7. Node-ID 与 $NODEID + n 的重定位机制

这是 co_dev.cdcf.c 之间最关键的协作之一。

7.1 为什么解析器先使用 0xFF

从 EDS/DCF 构建设备时,解析器先初始化:

1
dev->id = 0xFF

此时对象中的某些字段可能写成:

1
2
3
$NODEID + 0x180
$NODEID + 0x600
$NODEID + 0x700

解析器在读取这些值时,不只计算一个数值,还会为对应值设置 Node-ID-relative flag。

可能涉及:

1
2
3
4
minimum value
maximum value
default value
current value

7.2 co_dev_set_id() 不是简单赋值

设置实际 Node-ID 时,函数遍历整个对象树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flowchart TD
A["co_dev_set_id old to new"] --> B["iterate every co_obj_t"]
B --> C["iterate every co_sub_t"]
C --> D{"MIN_NODEID flag"}
D -->|yes| E["adjust minimum by delta"]
C --> F{"MAX_NODEID flag"}
F -->|yes| G["adjust maximum by delta"]
C --> H{"DEF_NODEID flag"}
H -->|yes| I["adjust default by delta"]
C --> J{"VAL_NODEID flag"}
J -->|yes| K["adjust current value by delta"]
E --> L["store new Node-ID"]
G --> L
I --> L
K --> L

其中:

1
delta = new_node_id - old_node_id

因此 $NODEID + n 的运行时语义是可重定位值,而不是只在文件加载时展开一次。

7.3 工程影响

该机制使同一份 EDS 可以复用于多个节点:

1
2
3
EDS describes base COB-ID expressions
actual Node-ID selected later
all marked values are rebased automatically

但也带来约束:

  • 只有被正确设置 Node-ID flag 的值才会调整;
  • 应用直接写入一个绝对数值不会自动获得 relative 属性;
  • 清除或误设 flag 会造成 Node-ID 更改后的 COB-ID 错误;
  • 运行期改变 Node-ID 时,应确保协议服务未同时使用旧配置。

8. EDS/DCF 文本解析入口

dcf.h 暴露两类入口:

1
2
file path
in-memory text

两者的内部流程相同:

1
2
3
4
5
6
7
8
9
flowchart TD
A["create device from file or text"] --> B["allocate co_dev_t"]
B --> C["initialize with Node-ID 255"]
C --> D["parse INI into config_t"]
D --> E["co_dev_parse_cfg"]
E --> F{"success"}
F -->|yes| G["return populated device"]
F -->|no| H["finalize partial device"]
H --> I["return failure"]

8.1 两阶段解析的意义

dcf.c 没有一边逐字符读取文件一边直接操作对象树,而是先得到通用配置结构,再构建设备。

优点:

  • 文件和字符串入口共享后半段逻辑;
  • section/key 查找与 CANopen 对象构建分离;
  • 错误信息能关联到配置位置;
  • CompactSubObj、CompactPDO 等跨 section 机制更容易实现。

限制:

  • 需要保存配置树和动态对象树;
  • 峰值内存高于直接流式解析;
  • 不适合极小 MCU 在运行期加载大型 EDS。

9. co_dev_parse_cfg() 的总解析顺序

核心顺序可以概括为:

1
2
3
4
5
6
7
8
9
10
11
12
13
[DeviceInfo]

[DummyUsage]

[MandatoryObjects]
[OptionalObjects]
[ManufacturerObjects]

construct every explicit object

[CompactPDO]

[DeviceComissioning]

这种顺序并非任意安排。

9.1 先构造显式对象,再处理 CompactPDO

CompactPDO 可以自动补建缺失的 PDO 通信参数和映射参数对象。

若先创建 CompactPDO,再读取显式对象:

1
2
3
synthesized object
may collide with
explicit 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
2
3
4
0x1008 manufacturer device name
0x1009 hardware version
0x100A software version
0x1018 identity object

解析器不会把所有 DeviceInfo 字段简单视为对象 0x1000 区域的替代品。设备级元数据与网络对象各有用途:

  • 元数据便于工具在不逐项读取对象时识别设备;
  • 对象字典条目供 SDO、PDO 和应用访问;
  • 一致性由输入 DCF、生成工具和应用共同保证。

10.1 [DummyUsage]

DummyUsage 描述某些基本数据类型是否允许用作 PDO mapping 的填充项。

co_dev_t::dummy 以位图保存这些能力,供 PDO 映射验证逻辑查询。它不是实际对象值,也不会自动生成一个制造商对象。


11. 三类对象列表如何驱动对象构建

EDS/DCF 通常通过以下 section 列出对象索引:

1
2
3
[MandatoryObjects]
[OptionalObjects]
[ManufacturerObjects]

解析流程为:

1
2
3
4
5
6
7
flowchart TD
A["read SupportedObjects count"] --> B["allocate index list"]
B --> C["parse entries 1 through N"]
C --> D["format object section name from index"]
D --> E["co_obj_build"]
E --> F["co_obj_parse_cfg"]
F --> G["insert object into device tree"]

11.1 列表类别不改变红黑树存储方式

Mandatory、Optional 和 Manufacturer 主要是 DCF 描述类别。进入 co_dev_t 后,所有对象都按 index 进入同一棵红黑树。

也就是说运行期查找不会先问:

1
is this mandatory or optional

而是直接按索引查找。

11.2 重复索引的结果

若不同列表或自动生成流程试图插入相同索引的另一个对象:

1
2
3
co_dev_insert_obj()
→ duplicate key
→ failure

因此对象列表不是可以无条件合并的普通数组。

11.3 强制对象缺失的处理边界

解析器会对典型强制对象缺失发出诊断,例如:

1
2
3
0x1000 Device type
0x1001 Error register
0x1018 Identity object

但 EDS/DCF 解析器的职责并不是完整实现所有设备 profile 的一致性证明。某些问题是 warning,某些结构错误才会导致创建失败。


12. 单个对象 section 的构建流程

12.1 先创建 co_obj_t

co_obj_build()

1
2
3
4
5
6
7
8
9
parse object index

create co_obj_t

insert into co_dev_t

set name and object code

parse sub-object structure

若后续步骤失败,构造器会将对象从设备移除并销毁,避免留下半初始化对象。

12.2 ObjectType 决定后续结构分支

主要分为两类:

1
2
3
4
5
simple object
VAR or DOMAIN

structured object
ARRAY or RECORD or DEFSTRUCT

简单对象

通常只有 sub-index 0

1
2
3
object section
acts as
sub-object 0 description

DOMAIN 在部分场景下可省略显式 DataType,因为对象代码已经确定其数据语义。

复合对象

必须描述成员结构,并在以下两种形式中选择一种:

1
2
3
SubNumber
or
CompactSubObj

源码要求二者不能同时作为同一对象的结构来源。


13. 显式子对象与 CompactSubObj

13.1 显式子对象

复合对象可通过独立 section 描述成员,例如概念上:

1
2
3
4
5
6
7
8
9
10
11
12
13
[1018]
ObjectType=0x09
SubNumber=5

[1018sub0]
ParameterName=Number of entries
DataType=0x0005
AccessType=ro

[1018sub1]
ParameterName=Vendor-ID
DataType=0x0007
AccessType=ro

每个子 section 都经过独立 co_sub_build()co_sub_parse_cfg()

这种方式适合 RECORD,因为每个字段可以具有不同:

1
2
3
4
5
6
data type
access type
limits
default value
PDO mapping
flags

13.2 CompactSubObj

CompactSubObj 用一个公共模板描述一组同类型成员。

解析器会:

1
2
3
4
5
6
7
create sub-index 0 as UNSIGNED8
name = NrOfObjects
value = member count

create sub-index 1 ... N
same data type
shared access and mapping properties

它适合 ARRAY 或大量同构条目,避免重复书写 section。

13.3 CompactName 与 CompactValue

若存在对应的 Name/Value section,解析器可以再为各成员填充:

1
2
individual parameter names
individual current values

因此 CompactSubObj 并不等于所有子对象必须具有完全相同的名字和值,它主要压缩共同结构描述。

13.4 子索引 0 的结构检查

对 ARRAY/RECORD,解析完成后会检查:

1
2
sub-index 0 exists
sub-index 0 type is UNSIGNED8

因为按照 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
2
3
4
5
6
ro
wo
rw
rwr
rww
const

它们被转换为 Lely 的 CO_ACCESS_* 位组合。

需要继续区分:

1
2
3
4
5
6
7
8
AccessType
controls network read/write and PDO direction capabilities

PDOMapping
controls whether entry may appear in a PDO map

ObjFlags
changes default storage or file behavior

三者不是同一个概念。

14.2 Limits 的处理

若启用对象 limits,LowLimit 和 HighLimit 会解析到 co_sub_t 的 min/max。

以下问题在 dcf.c 中可能只产生 warning:

1
2
3
LowLimit > HighLimit
DefaultValue outside limits
ParameterValue outside limits

这意味着:

成功加载 DCF 不等于所有值都已经通过运行期 SDO 范围检查。

后续默认 SDO download indication 仍会依据 min/max 返回相应 abort code。


15. DefaultValue、ParameterValue 与 current value 的优先级

这一点直接决定设备启动后的对象值。

15.1 存在 ParameterValue

1
2
3
4
5
6
DefaultValue
→ stored as default metadata

ParameterValue
→ stored as current value
→ CO_OBJ_FLAGS_PARAMETER_VALUE set

15.2 不存在 ParameterValue

解析器会把 default 复制到 current value:

1
2
3
default value
↓ copy
current value

因此运行期对象通常仍有初始 current value。

但概念上必须保留区别:

1
2
3
4
5
default
factory or profile default metadata

current
value currently used by protocol and application

运行期调用 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
2
3
4
5
DOMAIN current value
stores filename

SDO upload
reads file contents

16.2 DownloadFile

1
2
3
4
5
SDO download data
written to file

DOMAIN current value
stores filename

16.3 为什么存的是文件名而不是文件内容

对象字典只保存一个外部数据源引用:

1
2
small dictionary entry
→ potentially large file

这样可避免在对象字典中常驻固件、配置包或大块二进制数据。

16.4 访问权限修正

解析器会根据 UploadFile/DownloadFile 的方向检查或修正 access,使文件模式与网络访问方向一致。

但文件 I/O 的错误仍可能在真正执行 SDO upload/download 时才暴露,例如:

1
2
3
4
file does not exist
permission denied
storage full
path invalid

17. co_val_lex_dcf() 的类型解析策略

DCF 文本中的 value 最终必须转换为 union co_val

17.1 基本数值类型

整数、布尔、浮点、时间等大部分类型委托给通用 value lexer。

1
2
3
text
↓ lexical conversion by CANopen type
union co_val

17.2 OCTET_STRING 与 DOMAIN

这两类值按十六进制字节串解释,而不是普通 C 字符串。

例如概念上:

1
2
3
010203A0

01 02 03 A0

17.3 UNICODE_STRING

输入文本按 UTF-8 读取,再转换为 CANopen Unicode string 所使用的 UTF-16 code units。

因此:

1
2
3
file encoding
and
CANopen value encoding

是两个不同层次。

17.4 解析成功不等于业务值有效

lexer 负责:

1
syntax and type conversion

而以下约束可能位于其他层:

1
2
3
4
min/max range
profile-specific enumeration
state-dependent write rules
hardware capability

18. CompactPDO:自动补建 PDO 参数对象

[CompactPDO] 用于压缩 PDO 参数描述。解析器会根据配置为缺失对象创建通信参数和映射参数。

18.1 RPDO 对象对

1
2
3
4
5
0x1400 ... 0x15FF
RPDO communication parameters

0x1600 ... 0x17FF
RPDO mapping parameters

18.2 TPDO 对象对

1
2
3
4
5
0x1800 ... 0x19FF
TPDO communication parameters

0x1A00 ... 0x1BFF
TPDO mapping parameters

18.3 构建原则

1
2
3
4
5
6
7
8
flowchart TD
A["parse CompactPDO entry"] --> B["compute communication index"]
B --> C["compute mapping index"]
C --> D{"explicit object already exists"}
D -->|yes| E["keep explicit object"]
D -->|no| F["create standard parameter object"]
F --> G["create required sub-objects"]
G --> H["insert into device tree"]

18.4 它解决什么问题

CompactPDO 解决的是描述重复:

1
2
3
4
5
many PDOs
×
standard communication fields
×
up to 64 mapping entries

它不会自动证明映射对象一定适合业务应用。映射目标的数据类型、长度、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
2
3
4
5
DeviceInfo
what the device supports and what it is

DeviceComissioning
how this device instance is assigned in a network

例如:

1
2
3
4
5
supported baud rates
belongs to DeviceInfo

selected baud rate
belongs to DeviceComissioning

20. concise DCF 的二进制布局

Lely 的设备 API还支持 concise DCF 的内存和文件读写。

20.1 总体格式

1
2
3
4
5
6
7
8
9
+-------------------------+
| number of entries u32 |
+-------------------------+
| entry 1 |
+-------------------------+
| entry 2 |
+-------------------------+
| ... |
+-------------------------+

每条 entry:

1
2
3
4
5
6
7
8
9
+-------------------------+
| index u16 | 2 bytes
+-------------------------+
| sub-index u8 | 1 byte
+-------------------------+
| data size u32 | 4 bytes
+-------------------------+
| data | size bytes
+-------------------------+

固定头长度为:

1
2 + 1 + 4 = 7 bytes per entry

多字节整数和 CANopen 基本数据按 little-endian 编码。

20.2 读取流程

1
2
3
4
5
6
7
8
9
10
flowchart TD
A["read u32 entry count"] --> B["read index"]
B --> C["read sub-index"]
C --> D["read byte size"]
D --> E{"sub-object exists"}
E -->|yes| F["decode by target CANopen type"]
F --> G["replace current value"]
E -->|no| H["consume and discard payload"]
G --> I["next entry"]
H --> I

未知对象记录被消费但不必导致整份 concise DCF 立即失败,这允许配置数据包含当前设备版本不识别的条目。

20.3 写出流程

co_dev_write_dcf() 的实现分两遍:

1
2
3
4
5
6
7
first pass
calculate count and total size

allocate DOMAIN buffer

second pass
serialize every selected sub-object

索引范围可用于只导出对象字典的一部分。

20.4 与 SDO download 的区别

concise DCF restore 是本地对象字典操作,不等价于通过 CAN 网络逐条执行 SDO download。

它通常不会自动获得:

1
2
3
4
5
network access check
custom download indication
SDO abort semantics
application state validation
hardware side effect

因此若某个参数必须通过自定义 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
2
3
find matching TPDO number

invoke registered TPDO event indication

它不在 dev.c 中直接:

1
2
3
4
pack PDO bytes
apply inhibit timer
schedule event timer
send CAN frame

这些动作由上层 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
2
3
4
5
locate configured SAM-MPDO producer

verify event-driven communication parameters

notify upper layer with source index and sub-index

CiA 301 的模型限制一个设备只有一个此类 SAM-MPDO producer,因此它与普通 TPDO 的多实例扫描语义不同。

同样,设备层只做配置解析和事件 indication,不承担 CAN 帧传输状态机。


23. 文本 DCF 解析的错误与回滚边界

23.1 根级失败

若文件解析或设备构建失败:

1
2
__co_dev_fini()
destroys all objects already attached to device

因此返回给调用方的不是一棵需要手工逐项清理的半成品树。

23.2 对象级失败

co_obj_build() 创建对象后,若名称、对象代码或子对象解析失败,会移除并销毁当前对象。

23.3 子对象级失败

co_sub_build() 创建并插入子对象后,后续属性解析失败时会销毁该子对象;动态模式下对象的连续值存储也会随拓扑变化更新。

23.4 warning 与 fatal error 必须区分

典型 fatal error:

1
2
3
4
5
6
missing mandatory structural key
duplicate index or sub-index
invalid data type syntax
invalid ObjectType
both SubNumber and CompactSubObj selected
allocation failure

典型 warning:

1
2
3
4
recommended mandatory object missing
limit order suspicious
default or current value outside limits
some profile consistency issue

因此工程上应同时检查:

1
2
3
return value
and
diagnostic output

只看函数成功返回可能遗漏配置质量问题。


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
2
3
4
5
no malloc
→ standard I/O automatically disabled

no standard I/O
→ EDS/DCF support automatically disabled

这与 dcf.c 的实现需求一致:

  • 配置树需要动态节点;
  • 对象、子对象和值需要动态存储;
  • 文件入口需要 stdio;
  • 字符串与 DOMAIN 可能继续分配内存。

24.3 dcf2c 的定位

资源受限目标更适合:

1
2
3
4
5
6
7
8
build host
read EDS/DCF

dcf2c

generate co_sdev C structure

compile static device description into firmware

这将复杂文本解析从目标机移动到构建阶段,换取:

  • 固定 RAM/Flash 布局;
  • 更确定的启动时间;
  • 无文件系统依赖;
  • 更小的运行期错误面。

代价是修改 DCF 后需要重新生成并编译固件。


25. 线程安全和运行阶段边界

dev.cdcf.c 内部没有为对象树提供通用 mutex。

以下操作不应与 SDO/PDO/NMT 访问无保护并发:

1
2
3
4
5
6
parse and insert objects
remove or destroy objects
change Node-ID and rebase relative values
restore concise DCF
replace strings or DOMAIN values
change TPDO or MPDO indication pointers

推荐生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
initialization phase
load or initialize device description
validate diagnostics
register indications
create protocol services

runtime phase
keep object topology stable
perform controlled value access
trigger PDO events explicitly

shutdown phase
stop protocol services
ensure callbacks are no longer running
destroy device description

若必须运行期加载新 DCF,应在更高层提供全局设备配置锁,并重新评估所有保存的 co_obj_t *co_sub_t * 和 value pointer 生命周期。


26. 完整运行流程整理

26.1 从 EDS/DCF 到运行期设备

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart TD
A["read EDS or DCF"] --> B["parse INI configuration"]
B --> C["initialize co_dev_t with Node-ID 255"]
C --> D["parse device metadata"]
D --> E["parse object index lists"]
E --> F["create co_obj_t for each index"]
F --> G["create co_sub_t entries"]
G --> H["parse type access limits default and value"]
H --> I["insert all nodes into red-black trees"]
I --> J["synthesize CompactPDO objects"]
J --> K["apply commissioning Node-ID and bitrate"]
K --> L["rebase NODEID-relative values"]
L --> M["runtime co_dev_t ready"]

26.2 应用修改值并触发 TPDO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
application computes new process value

co_dev_set_val_*()

current value updated

co_dev_tpdo_event(index, sub-index)

scan 0x1800/0x1A00 PDO configuration

invoke TPDO event indication

TPDO service applies transmission and timing rules

CAN frame is sent by lower layers

26.3 concise DCF 恢复

1
2
3
4
5
6
7
8
9
10
11
12
existing co_dev_t

read entry count

for every record
index + sub-index + size + payload

find existing sub-object

decode according to its CANopen type

replace current value

26.4 Node-ID 更改

1
2
3
4
5
6
7
8
9
old Node-ID

co_dev_set_id(new)

walk every object and sub-object

adjust marked min max default current values by delta

store new Node-ID

27. 关键风险与工程注意事项

27.1 把文本 DCF 与 concise DCF 混为一谈

风险:错误地认为 co_dev_read_dcf() 能解析 INI 文本。

处理:

1
2
3
4
5
co_dev_create_from_dcf_file/text
for EDS/DCF text

co_dev_read_dcf
for concise binary records

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
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
include/lely/co/dev.h
↓ understand public device API

include/lely/co/detail/dev.h
↓ inspect real struct and feature macros

src/co/dev.c: __co_dev_init and co_dev_create
↓ understand default state

src/co/dev.c: insert remove find object
↓ understand object red-black tree

src/co/dev.c: co_dev_set_id
↓ understand NODEID-relative relocation

include/lely/co/dcf.h
↓ understand text DCF entry points

src/co/dcf.c: create/init from file or text
↓ understand config parser boundary

src/co/dcf.c: co_dev_parse_cfg
↓ understand global section order

src/co/dcf.c: co_obj_parse_cfg
↓ understand object structures

src/co/dcf.c: co_sub_parse_cfg
↓ understand value, access, flags and files

src/co/dev.c: co_dev_read_dcf and write_dcf
↓ understand concise DCF

src/co/dev.c: co_dev_tpdo_event and SAM-MPDO event
↓ understand reverse mapping events

29. 推荐调试断点和观察变量

29.1 文本入口

1
2
3
4
co_dev_create_from_dcf_file
co_dev_create_from_dcf_text
__co_dev_init_from_dcf_cfg
co_dev_parse_cfg

观察:

1
2
3
4
dev->id
config section name
current parser error position
number of object indices

29.2 对象构建

1
2
3
4
5
6
co_obj_build
co_obj_parse_cfg
co_sub_build
co_sub_parse_cfg
co_dev_insert_obj
co_obj_insert_sub

观察:

1
2
3
4
5
6
7
8
9
index
sub-index
ObjectType
DataType
AccessType
PDOMapping
flags
default value
current value

29.3 Node-ID 重定位

1
2
3
4
co_dev_set_id
co_obj_set_id
co_sub_set_id related helper
co_val_set_id

观察:

1
2
3
4
5
old id
new id
delta
NODEID flags
0x1200 through 0x1BFF communication values

29.4 concise DCF

1
2
3
4
co_dev_read_sub
co_dev_read_dcf
co_dev_write_sub
co_dev_write_dcf

观察:

1
2
3
4
5
6
entry count
index
sub-index
encoded size
target type
unknown-entry discard path

29.5 PDO event

1
2
3
co_dev_tpdo_event
co_dev_sam_mpdo_event
registered event indication

观察:

1
2
3
4
5
6
7
changed index and sub-index
TPDO communication index
mapping object index
transmission type
COB-ID
mapping entries
callback argument

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 误区:ParameterValueDefaultValue 相同

错误。前者是配置后的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
第 1 层:文件描述
EDS / DCF INI text

第 2 层:通用配置树
config_t sections and keys

第 3 层:设备根对象
co_dev_t metadata + object red-black tree

第 4 层:对象节点
co_obj_t index + sub-object red-black tree

第 5 层:数据入口
co_sub_t type + access + values + callbacks

第 6 层:协议消费
SDO / PDO / NMT / EMCY / LSS / application

再加两条旁路:

1
2
3
4
5
concise DCF
directly restores current values into an existing tree

TPDO event
scans PDO configuration and reports which producer is affected

最终完整图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
flowchart TD
A["EDS or DCF text"] --> B["INI config parser"]
B --> C["co_dev_t"]
C --> D["object rbtree by 16-bit index"]
D --> E["co_obj_t"]
E --> F["sub-object rbtree by 8-bit sub-index"]
F --> G["co_sub_t"]

H["DeviceComissioning"] --> I["Node-ID and network assignment"]
I --> C
I --> J["rebase NODEID-relative values"]
J --> G

K["concise DCF binary"] --> L["restore current values"]
L --> G

G --> M["SDO and RPDO write"]
G --> N["SDO and TPDO read"]
G --> O["local application access"]

O --> P["co_dev_tpdo_event"]
P --> Q["scan communication and mapping objects"]
Q --> R["TPDO service indication"]

最简洁的结论是:

co_dev_t 负责“一个设备是谁、有哪些对象,以及如何从 index 定位对象”;dcf.c 负责“把 EDS/DCF 的描述语言翻译为这棵运行时对象树”;concise DCF 负责“向已有对象树回放参数值”;PDO event 负责“从一个对象值变化反查需要被通知的 PDO”。这四部分共同把静态设备描述、运行时数据和 CANopen 协议对象连接起来。


参考源码与资料

本文核心源码

关联源码

  • include/lely/co/obj.h
  • include/lely/co/detail/obj.h
  • src/co/obj.c
  • include/lely/co/val.h
  • src/co/val.c
  • include/lely/co/type.h
  • src/co/type.c
  • include/lely/util/config.h
  • src/util/config.c
  • include/lely/co/sdev.h
  • src/co/sdev.c
  • src/co/pdo.c

Lely 官方资料

协议参考

  • CiA 301 V4.2.0:CANopen 设备模型、对象字典、对象代码、索引/子索引、PDO 参数对象和 SDO 访问模型。

前置学习笔记

  • Lely_CANopen_co_obj_co_sub_对象字典节点_机制原理与运行流程.md