对象字典内存模型

什么是对象字典

对象字典(Object Dictionary,简称 OD)是 CANOpen 设备的"数据中心"。它是一个结构化的数据集合,包含了设备的所有参数、配置、状态和通信参数。

关键特征:

  • 每个条目由 16 位索引(Index)+ 8 位子索引(Sub-index) 唯一标识
  • 每个条目有明确的数据类型、访问权限、默认值
  • 对象字典是 SDO 和 PDO 通信的基础
┌─────────────────────────────────────────────────┐
│               对象字典(Object Dictionary)        │
├──────────┬──────────┬──────────┬────────────────┤
│  索引    │ 子索引   │  数据类型 │  访问类型       │
├──────────┼──────────┼──────────┼────────────────┤
│ 0x1000   │ 0x00     │ UNSIGNED32│ ro (只读)      │ 设备类型
│ 0x1001   │ 0x00     │ UNSIGNED8 │ ro             │ 错误寄存器
│ 0x1003   │ 0x00     │ UNSIGNED8 │ ro             │ 错误数量
│ 0x1003   │ 0x01     │ UNSIGNED32│ ro             │ 错误码 1
│ 0x1003   │ 0x02     │ UNSIGNED32│ ro             │ 错误码 2
│ 0x2000   │ 0x00     │ INTEGER16 │ rw             │ 关节目标位置
└──────────┴──────────┴──────────┴────────────────┘

索引区域分配

CANOpen 规范定义了对象字典的索引区域:

索引范围区域名称说明示例
0x0000未使用保留–
0x1000-0x1FFF通信区通信参数、设备信息0x1000 设备类型
0x2000-0x5FFF制造商区制造商特定参数0x2000 关节位置
0x6000-0x6FFF输入区数字输入映射0x6000 数字输入
0x7000-0x7FFF输出区数字输出映射0x7000 数字输出
0x8000-0x8FFF配置区配置参数0x8000 节点 ID
0x9000-0x9FFF信息区状态信息0x9000 运行时间
0xA000-0xFFFF设备行规区特定行规参数CiA 402: 0x6040 控制字

通信区详解(0x1000-0x1FFF)

索引名称类型访问说明
0x1000Device typeUNSIGNED32ro设备类型标识
0x1001Error registerUNSIGNED8ro错误状态(可 PDO 映射)
0x1002Manufacturer statusUNSIGNED32ro制造商状态
0x1003Pre-defined error fieldARRAYro标准错误记录
0x1005COB-ID SYNCUNSIGNED32rwSYNC 消息 COB-ID
0x1006Communication cycle periodUNSIGNED32rw通信周期(微秒)
0x1007Synchronous window lengthUNSIGNED32rw同步窗口长度
0x1008Manufacturer device nameVISIBLE_STRINGconst设备名称
0x1009Manufacturer hardware versionVISIBLE_STRINGconst硬件版本
0x100AManufacturer software versionVISIBLE_STRINGconst软件版本
0x1010Store parametersARRAYrw保存参数到非易失存储
0x1011Restore default parametersARRAYrw恢复默认参数
0x1012COB-ID TimeUNSIGNED32rwTIME 消息 COB-ID
0x1014COB-ID EMCYUNSIGNED32rwEMCY 消息 COB-ID
0x1015Inhibit time EMCYUNSIGNED16rwEMCY 抑制时间
0x1016Consumer heartbeatARRAYrw心跳消费者配置
0x1017Producer heartbeatUNSIGNED16rw心跳生产者周期
0x1018Identity objectRECORDro制造商信息(厂商 ID、产品码等)

三种对象类型

1. VAR(变量)

单一值,最简单的对象类型。

[1000]
ParameterName=Device type
ObjectType=0x07    ; VAR
DataType=0x0007    ; UNSIGNED32
AccessType=ro
DefaultValue=0x00000000

C 语言内存表示:

typedef struct {
    uint16_t index;
    uint8_t  sub_index;
    uint16_t data_type;
    uint8_t  access_type;
    void    *data;      /* 指向实际数据的指针 */
    size_t   data_size; /* 数据大小(字节) */
} od_var_t;

/* 实例:设备类型 */
uint32_t device_type = 0x00010192;
od_var_t od_1000 = {
    .index = 0x1000,
    .sub_index = 0x00,
    .data_type = 0x0007,  /* UNSIGNED32 */
    .access_type = OD_ACCESS_RO,
    .data = &device_type,
    .data_size = sizeof(uint32_t)
};

2. ARRAY(数组)

多个相同类型的值,sub-index 0 表示元素个数。

[1003]
ParameterName=Pre-defined error field
ObjectType=0x08    ; ARRAY
DataType=0x0007    ; UNSIGNED32
AccessType=ro
SubNumber=9        ; sub0 ~ sub8

[1003sub0]
ParameterName=Number of errors
ObjectType=0x07
DataType=0x0005    ; UNSIGNED8
AccessType=ro
DefaultValue=0

[1003sub1]
ParameterName=Standard error field 1
ObjectType=0x07
DataType=0x0007    ; UNSIGNED32
AccessType=ro
DefaultValue=0

C 语言内存表示:

typedef struct {
    uint16_t index;
    uint16_t data_type;   /* 所有元素的类型相同 */
    uint8_t  access_type;
    uint8_t  max_sub;     /* 最大子索引 */
    void    *data;        /* 数组指针(data[0] = count, data[1..N] = 元素) */
    size_t   element_size;
} od_array_t;

/* 实例:错误记录 */
uint32_t error_field[9] = {0};  /* [0]=count, [1..8]=errors */
od_array_t od_1003 = {
    .index = 0x1003,
    .data_type = 0x0007,  /* UNSIGNED32 */
    .access_type = OD_ACCESS_RO,
    .max_sub = 8,
    .data = error_field,
    .element_size = sizeof(uint32_t)
};

/* 访问:读取错误数量 */
uint8_t error_count = ((uint32_t*)od_1003.data)[0];

3. RECORD(记录)

多个不同类型的字段,sub-index 0 表示字段个数。

[1018]
ParameterName=Identity object
ObjectType=0x09    ; RECORD
AccessType=ro
SubNumber=5

[1018sub0]
ParameterName=Highest sub-index supported
ObjectType=0x07
DataType=0x0005    ; UNSIGNED8
AccessType=ro
DefaultValue=0x04

[1018sub1]
ParameterName=Vendor-ID
ObjectType=0x07
DataType=0x0007    ; UNSIGNED32
AccessType=ro
DefaultValue=0x00000001

[1018sub2]
ParameterName=Product code
ObjectType=0x07
DataType=0x0007
AccessType=ro
DefaultValue=0x00000001

[1018sub3]
ParameterName=Revision number
ObjectType=0x07
DataType=0x0007
AccessType=ro
DefaultValue=0x00000001

[1018sub4]
ParameterName=Serial number
ObjectType=0x07
DataType=0x0007
AccessType=ro
DefaultValue=0x00000001

C 语言内存表示:

typedef struct {
    uint16_t index;
    uint8_t  access_type;
    uint8_t  max_sub;
    struct {
        uint16_t data_type;
        void    *data;
        size_t   data_size;
    } fields[];  /* 每个字段类型不同 */
} od_record_t;

/* 实例:制造商信息 */
uint32_t vendor_id = 0x00000001;
uint32_t product_code = 0x00000001;
uint32_t revision_number = 0x00000001;
uint32_t serial_number = 0x00000001;

od_record_t od_1018 = {
    .index = 0x1018,
    .access_type = OD_ACCESS_RO,
    .max_sub = 4,
    .fields = {
        {0x0005, &vendor_id, sizeof(uint32_t)},
        {0x0007, &product_code, sizeof(uint32_t)},
        {0x0007, &revision_number, sizeof(uint32_t)},
        {0x0007, &serial_number, sizeof(uint32_t)}
    }
};

统一对象字典抽象

在实际实现中,我们通常用一个统一的结构体来表示所有三种类型:

typedef enum {
    OD_TYPE_VAR,
    OD_TYPE_ARRAY,
    OD_TYPE_RECORD
} od_type_t;

typedef struct {
    uint16_t index;
    uint8_t  max_sub;
    od_type_t type;
    union {
        /* VAR */
        struct {
            uint16_t data_type;
            uint8_t  access;
            void    *data;
            size_t   size;
        } var;
        /* ARRAY */
        struct {
            uint16_t data_type;
            uint8_t  access;
            void    *data;
            size_t   element_size;
        } array;
        /* RECORD */
        struct {
            uint8_t  access;
            struct {
                uint16_t data_type;
                void    *data;
                size_t   size;
            } *fields;
        } record;
    };
} od_entry_t;

优势:

  • 统一的查找接口:od_find(index, sub_index)
  • 统一的访问接口:od_read() / od_write()
  • 内部根据 type 分支处理不同的内存布局

SDO 访问对象字典的流程

SDO Upload Request          Object Dictionary         SDO Upload Response
     │                           │                          │
     │───[0x600+N]──────────────>│                          │
     │  CCS=10, Index, SubIdx   │                          │
     │                           │── find(0x1000, 0) ──>   │
     │                           │  读取 data 指针          │
     │                           │  拷贝 4 字节到响应帧      │
     │                           │                          │
     │<──[0x580+N]───────────────│                          │
     │  SCS=01, Data(4 bytes)   │                          │

关键步骤:

  1. 服务端收到 SDO 请求后,根据索引和子索引查找对象字典条目
  2. 根据条目类型(VAR/ARRAY/RECORD)获取数据指针
  3. 检查访问权限(ro/rw/wo)
  4. 将数据拷贝到 SDO 响应帧(Expedited 最多 4 字节,Segmented 更多)
  5. 返回响应

内存对齐与大小端

大小端问题:

  • CAN 帧使用 小端模式(Little-Endian) 传输多字节数据
  • 如果 MCU 是大端模式(如某些 ARM 配置),需要在 SDO 传输时进行字节序转换
  • Linux x86/ARM 通常是小端模式,可以直接拷贝
/* 小端模式:可以直接内存拷贝 */
/* 大端模式:需要字节序转换 */
uint32_t to_le32(uint32_t value) {
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
    return __builtin_bswap32(value);
#else
    return value;
#endif
}

内存对齐:

  • 对象字典中的数据应按其自然大小对齐(32 位值按 4 字节对齐)
  • 避免非对齐访问导致的性能下降或硬件异常

动态对象字典

某些设备需要在运行时添加或删除对象字典条目。实现策略:

策略说明适用场景
静态数组编译时固定大小,运行时查找资源受限的 MCU
哈希表O(1) 查找,支持动态添加Linux 上位机
二叉搜索树O(log N) 查找,内存紧凑中等规模设备
链表简单但 O(N) 查找小型设备(条目 < 50)

总结

维度说明
对象类型VAR(单值)、ARRAY(同类型数组)、RECORD(不同类型结构体)
索引区域0x1000-0x1FFF 通信区,0x2000-0x5FFF 制造商区
C 表示void* 指针 + data_type + access_type
访问流程查找条目 -> 检查权限 -> 拷贝数据 -> 返回
大小端CAN 使用小端模式,大端 MCU 需转换
如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
Cygnus Yang 支付宝支付宝
Cygnus Yang 微信微信