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的推荐流程:

  1. 先列出设备需要的所有功能对象(通信对象+应用对象)
  2. 确定每个对象的数据类型和访问权限
  3. 用EDS编辑器创建文件,逐条填写
  4. 用OD检查工具验证格式(比如CANopenNode的od_verify脚本)
  5. 将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代码数组是它在单片机里的实际存在形式。

对象字典结构图 索引空间 0x0000 – 0xFFFF 每个索引对应一个对象,可包含多个子索引 0x0000-0x025F 数据类型区 0x1000-0x1FFF 通信对象区 0x2000-0x5FFF 制造商区 0x6000-0x9FFF 标准设备区 0xA000+ 保留区 索引 0x6000:8位数字量输入 子索引0:通道数量(=8) 子索引1:通道1输入值 子索引2:通道2输入值 ... 子索引3-7:通道3-7输入值 子索引8:通道8输入值 每个子索引都有独立的数据类型、访问权限和默认值 子索引0固定表示“条目数量”,由标准规定

这张图把对象字典的层次关系画清楚了。从上到下:整个索引空间分成几个功能区,每个索引下面挂着一组子索引,子索引0是计数器,子索引1-N是实际数据。你写OD时,脑子里要有这个结构。