4. 对象字典(OD)基础:对象字典的概念、索引与子索引、对象字典的数据类型

说实话,刚接触CANopen那会儿,我对着“对象字典”这三个字琢磨了半天。听着挺玄乎,其实说白了,它就是CANopen设备的一个“数据仓库”。

你想想看,一个电机驱动器,它需要知道自己的额定电流、当前转速、故障状态。这些数据放哪儿?总得有个规矩的地方存着。对象字典就是干这个的。它把设备里所有能访问的数据,都按一套标准格式组织起来。

我习惯把它理解成一本“说明书”。别人想读你的数据,或者想改你的参数,都得通过这本说明书来找位置。

4.1 对象字典到底是什么?

对象字典,英文叫Object Dictionary,简称OD。它是一个有序的数据结构集合。每个CANopen设备都必须有一个OD。

OD的核心作用就两个:

  • 标准化数据访问:不管你是哪家公司的驱动器,只要支持CANopen,读电流参数都走同一个索引。
  • 设备描述:OD里存着设备类型、制造商信息、通信参数、应用数据。别人一看OD,就知道你这设备能干什么。

关键认知:OD不是代码里的一个数组,它是一个逻辑概念。在STM32上,它可能是一段RAM区域,也可能是Flash里的常量表。CANopenNode的实现里,OD就是一个结构体数组。

4.2 索引与子索引——怎么找到数据?

OD里的每个数据项,都用两个编号定位:索引和子索引。

索引是一个16位的数字,范围0x0000到0xFFFF。它就像一本书的章号。比如0x1000是设备类型,0x1017是心跳生产者时间。

子索引是一个8位的数字,范围0x00到0xFF。它就像章下面的小节号。有些对象是数组或记录,就得靠子索引来区分具体元素。

举个例子,0x2000这个索引,子索引0x01存速度比例系数,子索引0x02存加速度时间。你读0x2000的0x01,拿到的是比例系数;读0x2000的0x02,拿到的是加速度。同一个索引,不同子索引,意义完全不同。

索引 子索引 参数名称 数据类型
0x1000 0x00 设备类型 UNSIGNED32
0x1017 0x00 心跳生产者时间 UNSIGNED16
0x2000 0x01 速度比例系数 UNSIGNED32
0x2000 0x02 加速度时间 UNSIGNED16

我的习惯:在项目初期,我会先画一张OD映射表。把每个索引、子索引、数据类型、默认值、读写属性都列清楚。这张表就是你和上层协议栈沟通的桥梁,省得后面改来改去。

4.3 对象字典的数据类型

OD里存的数据,不是随便什么格式都行。CANopen标准定义了一套基础数据类型。我挑几个常用的说说。

4.3.1 基本整数类型

  • UNSIGNED8/16/32:无符号整数,长度分别是1、2、4字节。存状态、标志位、计数器,用UNSIGNED8就够了。
  • INTEGER8/16/32:有符号整数。存温度、速度偏差这些可能为负的值。

4.3.2 浮点类型

  • REAL32:32位浮点数,符合IEEE 754标准。我做的伺服驱动器,位置环和速度环的PID系数都用REAL32存。精度够用,传输也快。

4.3.3 可见字符串

  • VISIBLE_STRING:ASCII字符串。设备名称、软件版本号,都用这个类型。注意长度要提前定好,别超了。

4.3.4 域类型

  • DOMAIN:不限定格式的原始数据块。适合存固件升级包、配置文件。我在做OTA升级时,就用DOMAIN类型来传输固件二进制流。
数据类型 长度 取值范围 典型用途
UNSIGNED8 1字节 0 ~ 255 状态码、子索引数量
UNSIGNED16 2字节 0 ~ 65535 心跳时间、PDO映射参数
UNSIGNED32 4字节 0 ~ 4294967295 设备类型、错误寄存器
INTEGER16 2字节 -32768 ~ 32767 温度值、速度偏差
REAL32 4字节 ±1.18e-38 ~ ±3.4e38 PID系数、位置值
VISIBLE_STRING 可变 ASCII字符 设备名称、版本号

避坑提醒:我曾经在一个项目里,把UNSIGNED32的数据用UNSIGNED16来定义。结果上位机读到的值全是截断的,排查了半天才发现是数据类型定义错了。数据类型一定要和实际存储长度严格对应,差一个字节都不行。

4.4 对象字典的结构图

下面这张图,是我画的对象字典结构关系。你可以看到,索引和子索引如何定位到具体的数据项,以及数据类型如何决定存储方式。

对象字典结构关系图 对象字典结构关系图 索引 (Index) 16位,0x0000~0xFFFF 子索引 (Subindex) 8位,0x00~0xFF 数据项 实际存储的值 数据类型决定存储方式 UNSIGNED8 → 1字节,存状态码、标志位 UNSIGNED16 → 2字节,存心跳时间、PDO映射参数 REAL32 → 4字节,存PID系数、位置值 VISIBLE_STRING → 可变长度,存设备名称、版本号 示例:读取设备类型 索引 0x1000,子索引 0x00 数据类型 UNSIGNED32,长度 4 字节 返回值:0x00000000(标准设备类型)

4.5 在CANopenNode中定义OD

CANopenNode里,OD是用一个结构体数组定义的。每个结构体描述一个对象条目。我贴一段实际代码,你看看就明白了。

// CANopenNode中对象字典条目的结构体定义
typedef struct {
    uint16_t            index;          // 索引
    uint8_t             subIndex;       // 子索引
    uint8_t             attribute;      // 读写属性
    uint8_t             dataType;       // 数据类型
    uint32_t            minValue;       // 最小值
    uint32_t            maxValue;       // 最大值
    void*               dataPtr;        // 指向实际存储的指针
    uint32_t            dataLength;     // 数据长度
} OD_entry_t;

实际定义OD时,会像下面这样。每个条目都指定了索引、子索引、数据类型和存储位置。

// 对象字典定义示例
const OD_entry_t OD_Entries[] = {
    // 设备类型
    {0x1000, 0x00, ODA_SDO_R, ODT_UNSIGNED32, 0, 0xFFFFFFFF, &DeviceType, 4},
    // 心跳生产者时间
    {0x1017, 0x00, ODA_SDO_RW, ODT_UNSIGNED16, 0, 0xFFFF, &HeartbeatTime, 2},
    // 自定义参数:速度比例系数
    {0x2000, 0x01, ODA_SDO_RW, ODT_UNSIGNED32, 0, 0xFFFFFFFF, &SpeedRatio, 4},
    // 自定义参数:加速度时间
    {0x2000, 0x02, ODA_SDO_RW, ODT_UNSIGNED16, 0, 0xFFFF, &AccelTime, 2},
};

我的经验:定义OD时,dataPtr一定要指向一个全局变量或者静态变量。我见过有人直接指向局部变量的地址,结果SDO一读,数据全乱套了。另外,dataLength必须和数据类型严格匹配,UNSIGNED32就是4字节,别写成2。

4.6 对象字典的通信访问方式

OD里的数据,主要通过两种方式访问:

  • SDO(服务数据对象):点对点访问,可以读写任意索引和子索引。适合配置参数、读取诊断信息。速度慢,但可靠。
  • PDO(过程数据对象):广播式访问,只能映射到OD里特定的条目。适合实时传输控制指令和状态数据。速度快,开销小。

这两种方式,后面章节会详细讲。你现在只需要记住:OD是数据仓库,SDO和PDO是搬运工。

注意:不是所有OD条目都能通过PDO访问。只有那些标记为“可映射”的条目才行。我在一个项目里,想把设备序列号映射到PDO里,结果发现它不支持映射。查了标准才知道,VISIBLE_STRING类型默认不能做PDO映射。所以设计OD时,提前想好哪些数据要走PDO,哪些走SDO。

4.7 小结

对象字典是CANopen设备的灵魂。没有OD,设备就是一堆散乱的数据。有了OD,数据就有了标准化的访问路径。

这一章,我们讲了:

  • OD的概念:标准化的数据仓库
  • 索引和子索引:16位索引 + 8位子索引,精确定位数据
  • 数据类型:UNSIGNED、INTEGER、REAL、VISIBLE_STRING、DOMAIN
  • 在CANopenNode中如何定义OD条目
  • SDO和PDO的访问方式