CANopenNode协议栈之对象字典概念 📅 2026/7/10 2:22:54 对象字典OD是赋予 CANopenNode 身份与行为的核心数据结构。它由 CiA 301 标准定义是影响应用对象、通信对象以及设备状态机行为的所有数据项的集合。它作为通信栈与应用层之间的唯一接口——每一次 SDO 读取、每一次 PDO 传输以及每一次心跳交换最终都会触及某个 OD 变量。字典中的每个对象均通过16 位索引和8 位子索引进行寻址从而构成一张二维查找表将每个数据项映射至已知位置。理解了对象字典也就理解了所有 CANopen 设备的核心骨干。对象字典的结构种对象类型CANopenNode 在字典中定义了三种基本对象类型每种类型都有对应的 C 结构体表示对象类型CiA 301 代码C 结构体描述VAR7OD_obj_var_t任意数据类型的单一变量位于子索引 0ARRAY8OD_obj_array_t相同类型变量组成的数组子索引 0 保存元素数量RECORD9OD_obj_record_t[]由不同类型子元素组成的结构体每个元素拥有各自的属性OD_obj_var_t结构体最为简单——它包含一个指向实际数据的指针dataOrig、一个属性位掩码attribute以及以字节为单位的数据长度dataLength。OD_obj_array_t在此基础上进行了扩展为子索引 0 和数组元素分别提供了独立的指针和属性并增加了用于指针运算的dataElementSizeof。OD_obj_record_t是一个由类OD_obj_var_t条目组成的数组每个条目都标有各自的subIndex。OD 条目与 OD 根在更高一层的抽象中每个 OD 对象由一个OD_entry_t表示——这是一个目录条目包含 16 位索引、子条目数量、对象类型标识符、指向特定类型对象数据的指针odObject以及指向可选应用扩展的指针extension。这些条目被收集到一个有序数组中而OD_t结构体则使用一个大小字段对该数组进行了封装typedef struct { uint16_t index; // 例如 0x1000 uint8_t subEntriesCount; // 包含子索引 0 在内的子条目总数 uint8_t odObjectType; // ODT_VAR, ODT_ARR 或 ODT_REC CO_PROGMEM void* odObject; // 指向特定类型对象数据的指针 OD_extension_t* extension; // 可选的应用层扩展默认为 NULL } OD_entry_t; typedef struct { uint16_t size; // 条目数量不包含空白终止符 OD_entry_t* list; // OD 条目的有序数组 } OD_t;下图展示了这些结构体之间的关系OD 条目列表必须按索引升序排列。这是一项硬性要求——OD_find()使用复杂度为 O(log N) 的二分查找算法如果条目顺序混乱查找操作将发生静默失败。存储组按生命周期组织数据OD 变量并非以扁平数据块的形式存储而是被组织成多个存储组这些组是 C 结构体其名称体现了持久化语义。在示例 OD 中定义了两个组存储组用途持久化至 NVMPERSIST_COMM跨越电源周期的通信参数设备类型、COB-ID、心跳时间、PDO 参数是RAM易失性运行时数据错误寄存器、错误历史、SDO 服务器参数否生成的头文件将每个组声明为 typedef 结构体和 extern 全局变量。例如OD_PERSIST_COMM_t包含所有持久化通信参数而OD_RAM_t存放易失性运行时状态。这种分组机制使得存储子系统能够选择性地将特定组保存至非易失性存储器或从中恢复。访问 OD 变量接口层通用访问模式CANopenNode 通过 OD 接口提供了一种简洁且与类型无关的访问模式。无需直接访问底层 C 结构体。其操作流程始终如下使用OD_find()通过索引查找OD 条目——返回指向OD_entry_t的指针使用OD_getSub()通过子索引获取子对象——填充OD_IO_t结构体通过OD_IO_t中的函数指针进行读或写——基于流操作类似于 POSIX 的read()/write()OD_IO_t结构体包含一个OD_stream_t持有dataOrig、dataLength、dataOffset、attribute、index、subIndex以及函数指针read和write。当未配置扩展时这些指针默认指向OD_readOriginal()和OD_writeOriginal()——这是简单的memcpy操作通过dataOffset处理部分数据传输。辅助 Getter 与 Setter 函数对于简单的单次访问CANopenNode 在OD_get_value()和OD_set_value()的基础上提供了类型化的便捷封装。这些是static inline函数可自动处理类型大小函数C 类型CANopen 类型OD_get_i8/OD_set_i8int8_tINTEGER8OD_get_u16/OD_set_u16uint16_tUNSIGNED16OD_get_u32/OD_set_u32uint32_tUNSIGNED32OD_get_f32/OD_set_f32float32_tREAL32OD_get_u64/OD_set_u64uint64_tUNSIGNED64OD_getPtr()void*直接指针访问辅助函数方式更为简单但在频繁访问时效率较低因为每次调用都会重新执行完整的OD_find()→OD_getSub()链。对于热点路径变量建议在初始化后缓存OD_IO_t并直接调用io.read()。直接全局访问对于简单应用生成的头文件还会暴露直接的 C 全局变量和条目快捷方式#include OD.h /* 直接变量访问无锁无查找 */ uint32_t devType OD_PERSIST_COMM.x1000_deviceType; OD_PERSIST_COMM.x1018_identity.serialNumber 0x12345678; /* 直接条目指针访问 */ OD_entry_t *entry OD_ENTRY_H1001_errorRegister;如果已通过OD_extension_init()在某条目上注册了 OD 扩展直接变量访问将绕过该扩展从而读取到陈旧或无意义的数据。在扩展处于活动状态时请务必使用 IO 接口进行访问。属性定义访问权限每个 OD 子对象都带有一个 8 位属性位掩码OD_attr_t用于控制谁可以访问它以及如何访问。这些标志是 CANopen 访问模型的强制执行机制属性数值含义ODA_SDO_R0x01SDO 服务器可读ODA_SDO_W0x02SDO 服务器可写ODA_TPDO0x04可映射至 TPDO发送ODA_RPDO0x08可映射至 RPDO接收ODA_TSRDO0x10可映射至发送 SRDOODA_RSRDO0x20可映射至接收 SRDOODA_MB0x40多字节变量int16 及以上——需处理大小端ODA_STR0x80字符串类型——较短的写入操作将以零填充剩余部分属性通过按位或运算组合。例如地址 0x1001 处的错误寄存器定义为ODA_SDO_R | ODA_TPDO可通过 SDO 读取可映射至 TPDO而地址 0x1017 处的生产者心跳时间则使用ODA_SDO_RW | ODA_MB可通过 SDO 读写多字节用于大小端转换。内联函数OD_mappable()通过测试ODA_TRPDO | ODA_TRSRDO位来检查变量是否可以映射至任何 PDO 或 SRDO。OD 扩展自定义读/写行为OD_extension_t机制是 CANopenNode OD 接口最强大的功能。它允许应用使用自定义函数覆盖默认基于memcpy的读/写操作从而有效地将任何 OD 条目转换为按需计算其值的虚拟变量。当对某条目调用OD_extension_init()时extension指针即被设置。随后OD_getSub()会检查该指针如果非 NULL它会将io.read和io.write指向扩展的函数指针如果指针为 NULL则指向OD_readDisabled/OD_writeDisabled。该扩展还携带一个可选的object指针该指针会通过stream-object传递给自定义函数此外还有一个用于事件驱动 TPDO 触发的flagsPDO位域。OD_readOriginal()和OD_writeOriginal()函数可在自定义扩展函数内部使用。这允许扩展在增强行为例如验证取值范围的同时仍将实际的内存操作委托给默认实现处理。返回码与 SDO 终止映射每个 OD 访问函数都会返回一个ODR_t枚举值。这些代码与 CiA 301 SDO 终止码一一对应可通过OD_getSDOabCode()函数进行转换。这种设计意味着 SDO 服务器可以直接将 OD 返回码传递给网络而无需单独的错误映射层ODR_t 值SDO 终止码含义ODR_OK(0)0x00000000成功ODR_PARTIAL(-1)—部分传输需再次调用ODR_IDX_NOT_EXIST(5)0x06020000索引不在字典中ODR_SUB_NOT_EXIST(14)0x06090011未找到子索引ODR_READONLY(4)0x06010002尝试写入只读对象ODR_TYPE_MISMATCH(11)0x06070010数据类型/长度不匹配ODR_INVALID_VALUE(15)0x06090030值超出有效范围CANopen 数据类型CANopenNode 使用映射至 CiA 301 / IEC 61131-3 数据类型的标准 C 类型。下表展示了整个 OD 中使用的核心映射关系CANopenNode C 类型IEC 61131-3CANopen 名称dataType 代码bool_tBOOLBOOLEAN0x01int8_tSINTINTEGER80x02int16_tINTINTEGER160x03int32_tDINTINTEGER320x04uint8_tUSINTUNSIGNED80x05uint16_tUINTUNSIGNED160x06uint32_tUDINTUNSIGNED320x07uint64_tULINTUNSIGNED640x1Bfloat32_tREALREAL320x08char[]STRINGVISIBLE_STRING0x09uint8_t[]BITSTRINGOCTET_STRING0x0A完整的 OD 文件对一个真正的对象字典由生成的文件对OD.h和OD.c进行定义它们是由 CANopenEditor 工具根据 XML 设备描述生成的。其结构遵循一致的模式文件部分内容示例计数器用于对象计数的#define宏OD_CNT_RPDO 4,OD_CNT_TPDO 4存储组结构体按生命周期分组的 C typedefOD_PERSIST_COMM_t,OD_RAM_tExtern 声明全局变量和 OD 指针声明extern OD_PERSIST_COMM_t OD_PERSIST_COMM;条目快捷方式用于直接条目访问的#define宏#define OD_ENTRY_H1001 OD-list[1]数据初始化所有存储组的默认值.x1000_deviceType 0x00000000OD 对象常量类型描述符 (VAR/ARRAY/RECORD)OD_obj_var_t,OD_obj_record_t[]OD 条目列表带有终止符的OD_entry_t有序数组{0x1000, 0x01, ODT_VAR, ...}, ...OD 根封装条目列表的OD_t结构体OD_t *OD _OD;生成文件中的警告十分明确“请勿手动编辑此文件”。手动修改会破坏与 XML 源文件的同步且极易引发错误。整个 OD 定义应通过编辑器工具及其 XML 项目文件遵循 CiA 311 标准的.xpd/.xdd格式进行管理。线程安全考量OD 变量会在多个执行上下文中被访问。CANopenNode 通常至少运行在两个线程中一个快速实时线程处理 PDO、SYNC和一个慢速主线程处理 SDO 服务器、心跳。两者可能会并发读写同一个 OD 变量。协议栈提供了CO_LOCK_OD()和CO_UNLOCK_OD()宏用于临界区保护。在主线程中访问可映射至 PDO 的变量时必须使用这些宏。然而在实时线程中绝对不可使用加锁操作因为这会阻塞 ISR在自定义的read()/write()扩展函数内部也绝对不可使用加锁操作因为这会导致死锁。在初始化阶段即线程启动之前则无需加锁。