3、对象字典(OD)基础:对象字典结构、索引与子索引、数据类型定义、OD文件生成
对象字典,说白了就是CANopen设备的“户口本”。
你想想看,一个I/O从站上电后,主站怎么知道它有几个输入口?输出口是PNP还是NPN?心跳周期设多少?这些信息全得有个地方存着,而且格式得统一。对象字典就是干这个的。
我刚开始接触CANopen时,觉得对象字典就是个配置文件,后来踩了坑才明白——它其实是设备模型的核心。主站和从站之间所有的通信,最终都要落到对象字典的某个条目上。
3.1 对象字典的结构
对象字典本质上是一个线性数组,每个元素叫一个“对象”。每个对象通过索引(Index)来寻址,索引范围从0x0000到0xFFFF,总共65536个位置。
但这么多位置不是随便用的。CANopen标准把整个索引空间划分成了几个功能区:
| 索引范围 | 用途 | 说明 |
|---|---|---|
| 0x0000 | 保留 | 未使用 |
| 0x0001 – 0x001F | 静态数据类型 | 预定义的基本数据类型描述 |
| 0x0020 – 0x003F | 复杂数据类型 | 结构体、数组等复合类型 |
| 0x0040 – 0x005F | 设备制造商特定类型 | 厂商自定义的数据类型 |
| 0x0060 – 0x025F | 设备配置文件区域 | 符合CiA 401/402等规范的对象 |
| 0x1000 – 0x1FFF | 通信对象 | COB-ID、心跳、同步等通信参数 |
| 0x2000 – 0x5FFF | 制造商特定对象 | 厂商自定义功能 |
| 0x6000 – 0x9FFF | 标准化设备对象 | 数字量I/O、模拟量等标准对象 |
| 0xA000 – 0xFFFF | 保留/补充 | 未来扩展或网络管理 |
嗯,这里要注意:0x1000-0x1FFF是通信对象区,每个CANopen设备都必须实现。比如0x1000是设备类型,0x1017是心跳生产者时间。这些是协议栈能跑起来的基础。
核心要点:对象字典不是你想怎么排就怎么排的。标准区必须按规范来,制造商区可以自由发挥,但也要遵循基本的数据类型规则。
3.2 索引与子索引
一个索引下面还能挂多个子索引(SubIndex)。为什么需要子索引?
举个例子:一个8通道的数字量输出模块,每个通道的配置参数(极性、滤波时间、故障安全值)如果都单独占一个索引,那索引空间很快就用完了。更合理的做法是:用一个索引代表“通道配置数组”,子索引0表示通道数量,子索引1-8分别对应每个通道的配置。
子索引的规则很简单:
- 子索引0:通常表示该对象支持的子索引数量(即数组长度)
- 子索引1-N:实际的数据条目
- 子索引范围:0x00 – 0xFF(最多256个)
我记得有一次调试一个模拟量采集模块,主站一直读不到正确的通道数。查了半天,发现是子索引0的值写死了0xFF,主站以为有255个通道,实际只有8个。从那以后,我每次写OD都会先检查子索引0的值。
个人习惯:我一般把子索引0定义为UINT8类型,值就是实际支持的条目数。这样主站通过SDO读子索引0,就知道该索引下有多少数据可读。
3.3 数据类型定义
对象字典里的每个条目都得有明确的数据类型。CANopen标准预定义了一套基础类型,放在索引0x0001-0x001F里。常用的有:
| 数据类型名 | 索引 | 长度 | 说明 |
|---|---|---|---|
| BOOLEAN | 0x0001 | 1字节 | 布尔值,0或1 |
| INTEGER8 | 0x0002 | 1字节 | 有符号8位整数 |
| INTEGER16 | 0x0003 | 2字节 | 有符号16位整数 |
| INTEGER32 | 0x0004 | 4字节 | 有符号32位整数 |
| UNSIGNED8 | 0x0005 | 1字节 | 无符号8位整数 |
| UNSIGNED16 | 0x0006 | 2字节 | 无符号16位整数 |
| UNSIGNED32 | 0x0007 | 4字节 | 无符号32位整数 |
| REAL32 | 0x0008 | 4字节 | IEEE 754单精度浮点 |
| VISIBLE_STRING | 0x0009 | 可变 | ASCII字符串 |
| OCTET_STRING | 0x000A | 可变 | 字节数组 |
除了基础类型,你还可以定义复合类型。比如一个“通道配置”结构体,包含极性(BOOLEAN)、滤波时间(UNSIGNED16)、故障安全值(INTEGER16)。复合类型定义在0x0020-0x003F区域,然后在具体对象里引用。
避坑指南:我曾经在定义复合类型时,把结构体成员的对齐方式搞错了。CANopen标准要求成员按1字节对齐,但编译器默认可能是4字节对齐。结果主站读到的数据全是错位的。解决方案是在结构体定义前加#pragma pack(1)。
3.4 OD文件生成
对象字典不能手写二进制,太容易出错了。业界通用的做法是用.eds文件(Electronic Data Sheet)来描述OD。EDS是纯文本格式,主站工具通过解析EDS文件就知道从站支持哪些对象。
一个典型的EDS文件结构如下:
[FileInfo]
FileName=MyIOStation.eds
FileVersion=1
FileRevision=1
Description=8-Channel Digital Input Slave
[DeviceInfo]
VendorName=MyCompany
ProductName=IO-8DI
ProductNumber=0x0001
RevisionNumber=0x0001
BaudRate_10=1
BaudRate_20=1
BaudRate_50=1
BaudRate_125=1
BaudRate_250=1
BaudRate_500=1
BaudRate_800=1
BaudRate_1000=1
SimpleBootUpMaster=0
SimpleBootUpSlave=0
[1000]
ParameterName=Device Type
ObjectType=0x07
DataType=0x0007
AccessType=ro
DefaultValue=0x00000191
PDOMapping=0
[1017]
ParameterName=Heartbeat Time
ObjectType=0x07
DataType=0x0006
AccessType=rw
DefaultValue=1000
PDOMapping=0
[6000]
ParameterName=Read Input 8-Bit
ObjectType=0x07
DataType=0x0005
AccessType=ro
DefaultValue=0
PDOMapping=1
每个对象条目用[索引号]开头,下面跟着属性:
- ObjectType:0x07表示普通变量,0x08表示数组,0x09表示记录(结构体)
- DataType:引用前面定义的数据类型索引
- AccessType:ro(只读)、rw(读写)、wo(只写)
- DefaultValue:上电默认值
- PDOMapping:是否允许映射到PDO,1表示允许
对于带子索引的对象,写法稍有不同:
[6000sub0]
ParameterName=Number of Entries
ObjectType=0x07
DataType=0x0005
AccessType=ro
DefaultValue=8
PDOMapping=0
[6000sub1]
ParameterName=Input Channel 1
ObjectType=0x07
DataType=0x0005
AccessType=ro
DefaultValue=0
PDOMapping=1
[6000sub2]
ParameterName=Input Channel 2
ObjectType=0x07
DataType=0x0005
AccessType=ro
DefaultValue=0
PDOMapping=1
我建议你直接用CANopenNode自带的OD编辑器,或者用CANeds(Vector的工具)来生成EDS。手写容易漏字段,尤其是ObjectType和DataType的对应关系。
生成OD的推荐流程:
- 先列出设备需要的所有功能对象(通信对象+应用对象)
- 确定每个对象的数据类型和访问权限
- 用EDS编辑器创建文件,逐条填写
- 用OD检查工具验证格式(比如CANopenNode的
od_verify脚本) - 将EDS文件转换为C代码中的OD数组(CANopenNode提供
od_generate工具)
最后一步很关键。CANopenNode的OD是用C语言结构体数组实现的。生成工具会把EDS文件解析成类似这样的代码:
/* 自动生成的对象字典条目示例 */
const OD_entry_t OD_entries[] = {
{0x1000, 0x00, &OD_1000_00__device_type, sizeof(OD_1000_00__device_type)},
{0x1017, 0x00, &OD_1017_00__heartbeat_time, sizeof(OD_1017_00__heartbeat_time)},
{0x6000, 0x00, &OD_6000_00__num_entries, sizeof(OD_6000_00__num_entries)},
{0x6000, 0x01, &OD_6000_01__input_1, sizeof(OD_6000_01__input_1)},
/* ... 更多条目 */
};
嗯,到这里你应该明白了:对象字典不是玄学,它就是一张结构清晰的表格。索引是行号,子索引是列号,数据类型是单元格的格式。EDS文件就是这张表格的Excel版本,而C代码数组是它在单片机里的实际存在形式。
这张图把对象字典的层次关系画清楚了。从上到下:整个索引空间分成几个功能区,每个索引下面挂着一组子索引,子索引0是计数器,子索引1-N是实际数据。你写OD时,脑子里要有这个结构。