首页 / 驱动算法 / CANopen专题

CANopen 协议基础:对象字典与通信对象

CANopen 是工业伺服/运动控制领域最普及的高层 CAN 协议。本文从 CAN 报文讲起,拆解对象字典(OD)、NMT/SDO/PDO/心跳等通信对象,用 PDO 映射演示把"数据怎么打包进一个 CAN 帧"讲清楚。

1. CAN 总线快速回顾:报文是怎么传的

CAN(Controller Area Network)是差分串行总线,两根线 CANH/CANL,具有抗干扰强、错误检测完善、多主仲裁的特点。一个 CAN 数据帧的组成:

单帧位时间(标准帧,理想值)

tframe ≈ (44 + 8·DLC) / 波特率,实际还要加填充位与间隔

500kbps · 8 字节数据 ≈ 108 位 ≈ 216 μs

CAN 只解决了"报文怎么传",但报文里装什么、谁跟谁通信、错误了怎么办——这是 CANopen 要回答的问题。

2. CANopen 的核心思想:一切皆对象字典

CANopen(CiA 301)建立在 CAN 之上,定义了:

一句话:CANopen = CAN 传输 + 对象字典寻址 + 标准通信服务

3. 对象字典(OD):从站的大脑

对象字典把每个数据组织成 索引(Index, 16 位):子索引(Subindex, 8 位) 的两级表。索引按区域划分:

索引范围区域典型内容
0000h保留
1000h–1FFFh通信参数区设备类型、错误寄存器、PDO通信参数(1400h/1800h)、PDO映射(1600h/1A00h)、SDO参数、心跳(1017h)、节点ID(1017h附近)
2000h–5FFFh厂商特定区厂家自定义参数、内部变量
6000h–9FFFh标准设备行规区CiA 402:状态字6041h、控制字6040h、目标位置607Ah、操作模式6060h…
A000h–FFFFh保留

每个 OD 条目有固定属性:对象类型(VAR/数组/记录)、数据类型(U8/I8/U16/U32/REAL…)、访问属性(ro/rw/wo/const)、映射属性(能否被 PDO 映射)。

✅ 记住这个寻址习惯

读伺服当前状态:读 6041h 子索引0;写目标位置:写 607Ah 子索引0。掌握了"索引:子索引"两级寻址,就掌握了 CANopen 数据访问的钥匙。

4. 通信对象:各司其职的五类报文

通信对象方向用途典型 COB-ID
NMT(网络管理)主→从启动/停止/进入预操作/复位节点000h(+命令+节点ID)
SYNC(同步)主→广播同步所有从站开始采样/执行080h
SDO(服务数据)双向读写对象字典(配置),可任意访问请求 600h+ID / 响应 580h+ID
PDO(过程数据)双向实时数据(位置/速度/电流),周期或触发TPDO 180h+ID · RPDO 200h+ID
EMCY(紧急报文)从→主故障/错误上报080h+ID
心跳/节点守护从→主节点在线状态监控700h+ID

4.1 NMT:网络状态机

每个从站必须处于以下状态之一:初始化 → 预操作(Pre-operational)→ 运行(Operational)↔ 停止(Stopped)

NMT 报文 2 字节:命令字节 + 目标节点 ID(0=广播所有节点)。常用命令:01h 启动、02h 停止、80h 进入预操作、81h 复位节点。

4.2 SDO:配置的"挂号信"

SDO 用"请求-应答"可靠传数据,适合配置但不适合实时。单帧写 4 字节以内数据:

SDO 单帧写(加速传输)

请求:600h+ID · [2F/2B/27/23] [索引低] [索引高] [子索引] [数据0..3]

响应:580h+ID · [4F/4B/47/43] 同索引子索引,出错则 [80h]+错误码

超过 4 字节走分段传输(0x10/0x21 等),这里不展开。

4.3 PDO:实时的"快递"

PDO 是数据面通信:把对象字典里几个条目"映射"进一个 CAN 帧(最多 8 字节),触发方式可选周期、事件、同步、远程请求。发送 PDO 叫 TPDO,接收 PDO 叫 RPDO。

PDO 的映射关系由 OD 配置:

⚠️ 映射的坑

映射总位数不能超过 64 位(8 字节)。改映射时通常要先把 PDO 通信参数里的"有效"标志(1F00h 节点保护 / 对应 bit)置 0,改完再置 1 才生效——很多现场"改了映射没反应"就是这个原因。

5. 交互式演示:PDO 映射与数据打包

勾选左侧对象字典条目(总长不超过 8 字节),右侧实时生成 PDO 映射表(0x1A00 各子索引)和打包后的一个 CAN 帧 8 字节数据——这就是 TPDO 在从站里干的事。

PDO 映射与 CAN 帧打包演示
TPDO1 · COB-ID 0x180+NodeID · 传输类型 1(同步)· 映射 5 字节 · 打包帧 [00 00 00 00 00 00 00 00]

观察要点:每个映射条目都是 32 位的 索引|子索引|位长;勾选的数据按顺序紧凑拼进 8 字节;总位数不能超过 64 位,否则提示溢出。

6. 代码实现(C语言)

6.1 对象字典条目结构

/* CANopen 对象字典条目最小结构(简化版) */ typedef enum { OD_U8, OD_U16, OD_U32, OD_I32, OD_REAL } od_type_t; typedef enum { OD_RO, OD_RW, OD_WO, OD_CONST } od_attr_t; typedef struct { uint16_t index; /* 索引 */ uint8_t subindex; /* 子索引 */ od_type_t type; /* 数据类型 */ od_attr_t attr; /* 访问属性 */ void *data; /* 指向实际变量 */ } od_entry_t; uint8_t g_statusword_lo, g_statusword_hi; uint32_t g_actual_pos; uint16_t g_actual_vel; /* 对象字典表(这里只列几个,实际成百上千条) */ static const od_entry_t g_od[] = { { 0x1000, 0x00, OD_U32, OD_RO, &g_device_type }, { 0x6041, 0x00, OD_U16, OD_RO, &g_statusword_lo }, { 0x6063, 0x00, OD_U32, OD_RO, &g_actual_pos }, { 0x606C, 0x00, OD_U16, OD_RO, &g_actual_vel }, /* ... */ };

6.2 SDO 单帧读/写分发

/* 按 索引:子索引 查找 OD 条目 */ static const od_entry_t* od_find(uint16_t idx, uint8_t sub) { for (int i = 0; i < (int)(sizeof(g_od)/sizeof(g_od[0])); i++) if (g_od[i].index == idx && g_od[i].subindex == sub) return &g_od[i]; return NULL; } /* 处理一帧 SDO 请求(加速传输,4 字节内) */ void sdo_process(const uint8_t *rx, uint8_t *tx) { uint8_t cmd = rx[0]; uint16_t idx = (rx[2] << 8) | rx[1]; uint8_t sub = rx[3]; const od_entry_t *e = od_find(idx, sub); if (!e) { tx[0] = 0x80; tx[4] = 0x00; tx[5] = 0x06; return; } /* 0x06020000 无此对象 */ if ((cmd & 0x40) == 0) { /* 0x40 读请求 */ uint32_t v = 0; switch (e->type) { case OD_U8: v = *(uint8_t*)e->data; tx[0] = 0x4F; break; case OD_U16: v = *(uint16_t*)e->data; tx[0] = 0x4B; break; default: v = *(uint32_t*)e->data; tx[0] = 0x43; break; } tx[1] = idx & 0xFF; tx[2] = idx >> 8; tx[3] = sub; tx[4] = v & 0xFF; tx[5] = (v>>8)&0xFF; tx[6] = (v>>16)&0xFF; tx[7] = (v>>24)&0xFF; } else { /* 写请求 0x2F/0x2B/0x27/0x23 */ if (e->attr != OD_RW && e->attr != OD_WO) { tx[0] = 0x80; return; } uint32_t v = rx[4] | (rx[5]<<8) | (rx[6]<<16) | (rx[7]<<24); switch (e->type) { case OD_U8: *(uint8_t*)e->data = (uint8_t)v; tx[0] = 0x60; break; case OD_U16: *(uint16_t*)e->data = (uint16_t)v; tx[0] = 0x60; break; default: *(uint32_t*)e->data = v; tx[0] = 0x60; break; } tx[1] = idx & 0xFF; tx[2] = idx >> 8; tx[3] = sub; } }

6.3 TPDO 打包

/* 按映射表把 OD 条目打包进 CAN 帧(小端序) */ uint8_t tpdo_build(const uint32_t *map, uint8_t n_entries, uint8_t *buf) { uint8_t bit = 0, byte = 0; for (uint8_t i = 0; i < n_entries; i++) { uint16_t idx = (map[i] >> 16) & 0xFFFF; uint8_t sub = (map[i] >> 8) & 0xFF; uint8_t len = map[i] & 0xFF; const od_entry_t *e = od_find(idx, sub); if (!e) continue; uint32_t v = 0; switch (e->type) { case OD_U8: v = *(uint8_t*)e->data; break; case OD_U16: v = *(uint16_t*)e->data; break; default: v = *(uint32_t*)e->data; break; } /* 逐位写入(紧凑拼包) */ for (uint8_t b = 0; b < len; b++) { buf[byte] |= ((v >> b) & 1) << bit; if (++bit == 8) { bit = 0; byte++; } } } return byte; /* 返回 DLC */ }

7. 工程要点与常见误区

要点 1:COB-ID 冲突排查

多从站共用一个 CAN 网络,PDO/SDO/心跳 ID 都按节点 ID 偏移。节点 ID 重复或与主站配置不符是"通信不上"的第一原因,先用总线分析仪抓 ID 分布。

要点 2:PDO 触发方式按场景选

位置/速度环同步控制用同步触发(SYNC 对齐);普通状态上报用周期/事件触发。同步 PDO 配合 CiA 402 才有确定性。

要点 3:总线负载率要预算

所有周期 PDO + 心跳 + 周期 SDO 加起来别超过总线容量的 30%~50%(留裕量给 EMCY 突发)。1Mbps 下一个 8 字节 PDO 约 216μs,10 个从站各 1ms 周期就会到 20% 以上。

误区 1:SDO 当 PDO 用

SDO 是请求-应答、有协议开销,不适合周期实时传。实时数据必须走 PDO。

误区 2:改 PDO 映射不重入操作

映射要在从站进入预操作时改,改完确认,不能边运行边乱改。

误区 3:以为 CANopen 只传 8 字节

单帧是 8 字节,但 SDO 分段、PDO 多帧/多对象组合、同步联动可以覆盖任意规模和复杂控制需求。

8. 小结

  1. CAN 解决"报文怎么传",CANopen 解决"报文装什么、怎么协作"
  2. 对象字典用 索引:子索引 两级寻址,通信区 1xxxh / 厂商区 2xxx-5xxxh / 行规区 6xxx-9xxxh
  3. 五类通信对象各司其职:NMT 管状态、SDO 管配置、PDO 管实时、EMCY 管故障、心跳管在线
  4. PDO 映射 = 把 OD 条目按 索引|子索引|位长 打包进 8 字节帧,总长 ≤64 位
  5. 从站实现的核心就是"一张 OD 表 + SDO 分发 + PDO 打包"三件事

下一篇进入运动控制视角——CiA 402 伺服行规:状态机怎么迁、控制字/状态字怎么用、七种工作模式怎么选。