简介:面向CANopen运动控制开发者,CiA402SampleNode-main是一份CiA402从站协议示例工程,重点演示伺服驱动器对象字典的定义与配置方法。CIA402是CANopen体系中针对伺服和运动控制的关键子协议,而对象字典正是设备配置与通信的核心,因此该示例对理解协议落地非常实用。压缩包内共5个文件,以C源码、头文件、对象字典文件(.od)为主,附带README与LICENSE,便于快速搭建402子协议从站框架。已有543人学习下载。通过示例代码可掌握对象字典的组织方式,熟悉PDO与SDO的通信配置流程,了解位置控制、速度限制等CIA402特有对象的实现思路;.od字典文件则直观呈现对象索引、数据类型、访问权限及默认值等完整属性,既适合初学者对照学习,也可作为工程师在项目移植和二次开发时的参考模板,帮助提升设备的网络兼容性与功能表现。
1. 先别急着找 main 函数,CiA402 示例节点真正算数的是状态机
看到 “CiA402SampleNode” 这个名字,多数人第一反应是进src/main找入口函数。实际上,这个工程里真正承载信息量的不是main()本身,而是它把 CANopen 从站协议栈与 CiA402 设备行规拼装在一起的那一层。CiA402 是 CANopen 协议族里针对驱动器和运动控制设备的行规,它定义了状态机、对象字典、工作模式,以及主站与从站之间交换数据的方式。这个 SampleNode 正是“如何用最少的代码实现一个合规 CiA402 节点”的最佳参照物。
文章面向两类人:一类是做伺服、步进、无刷驱动固件的嵌入式工程师,需要快速在自己芯片上搭出协议栈骨架;另一类是写主站或测试工具的人,需要读懂从站内部状态流转,才能定位“指令发了没反应”的问题。理解了示例工程的组织方式,你能少踩一半的坑,剩下的坑集中在状态机转移条件和 PDO 映射上,本文会一一说明。
2. CiA402 状态机与对象字典:先把节点的骨架立起来
2.1 状态机 9 个状态和 13 个转移条件
CiA402 最核心的部分不是通信,而是状态机。主站通过写对象字典索引0x6040(控制字 Controlword)控制从站状态跳转,从站通过0x6041(状态字 Statusword)回报当前状态。很多新手把控制字直接写 0x000F,以为就能跑到 Operation Enabled,实际上协议要求状态转移必须一步一步来。
下面是完整的 9 个状态和关键转移条件:
| 状态 | 控制字位模式(关键位) | 状态字位反馈(关键位) |
|---|---|---|
| Not ready to switch on | 无需操作 | bit0=0 |
| Switch on disabled | 无 | bit0=0, bit1=1, bit2=1 |
| Ready to switch on | bit0=1, bit1=0, bit2=0, bit3=0 | bit0=1, bit1=1, bit2=0 |
| Switched on | bit0=1, bit1=1, bit2=0, bit3=0 | bit0=1, bit1=1, bit2=1 |
| Operation enabled | bit0=1, bit1=1, bit2=1, bit3=0 | bit0=1, bit1=1, bit2=1, bit3=1 |
| Quick stop active | bit0=1, bit1=1, bit2=0, bit3=1 | bit0=1, bit1=1, bit2=1, bit3=0 |
| Fault reaction active | 内部 | bit0=1, bit1=1, bit2=0, bit3=1 |
| Fault | 内部 | bit4=1 |
| Disabled (Switch on disabled 后置位) | bit0=0, bit1=0, bit2=0, bit3=0 | bit0=0, bit1=1, bit2=1 |
提示:控制字 bit3(Enable operation / Quick stop)是使用频率最高的误用点。直接置位 bit3 并不会让设备从 Switch on disabled 跳到 Operation enabled,必须先经过 Ready to switch on 和 Switched on 两个中间态。
状态转移还有一个容易疏忽的细节:Fault 状态下,控制字 bit7(Fault reset)必须产生一个上升沿,即先写 1 再写 0,从站才会退回到 Switch on disabled。示例节点里一般会用edge_detect或简单记录上一次写入值的方式实现,避免主站一直置位导致无法退出复位状态。
2.2 对象字典里绕不开的核心索引
对象字典(Object Dictionary,OD)是 CANopen 从站的“寄存器地图”。CiA402 规范的附件里定义了一组 0x6000 系列索引,示例节点的实现至少需要覆盖以下内容:
| 索引 | 名称 | 类型 | 作用 |
|---|---|---|---|
| 0x6040 | Controlword | U16 可写 | 状态机控制入口 |
| 0x6041 | Statusword | U16 只读 | 状态机反馈出口 |
| 0x605A | Quick stop option code | I16 可写 | 快速停止时的行为选择 |
| 0x6060 | Modes of operation | I8 可写 | 选择工作模式,例如 PP=1,PV=3,HM=6,CSP=8 |
| 0x6061 | Modes of operation display | I8 只读 | 当前生效模式 |
| 0x6080 | Motor rated current | U32 | 电机额定电流,mA 为单位 |
| 0x6081 | Motor rated torque | U32 | 额定扭矩,0.1 Nm 为单位 |
| 0x6083 | Motor rated speed | U32 | 额定转速,rpm 为单位 |
| 0x6091 | Gear ratio | U32 数组 | 减速比:分子分母 |
在代码里,最直观的组织方式是用一个结构体将状态机和 OD 绑定:
typedef struct { uint16_t controlword; /* 0x6040 */ uint16_t statusword; /* 0x6041 */ int8_t mode; /* 0x6060 期望模式 */ int8_t mode_active; /* 0x6061 实际模式 */ int32_t target_pos; /* 0x607A PP模式目标位置 */ uint32_t target_vel; /* 0x60FF PV模式目标速度 */ uint8_t fault_code; /* 本地错误码,用于映射 0x603F 错误码 */ } cia402_node_t;你会发现示例节点普遍把状态机逻辑与 OD 访问函数分开:OD 的read/write回调里只做数据合法性校验,不直接驱动电机;真正让电机动起来的是主循环周期任务调用cia402_process()时对mode的解析。这个分层能保证 SDO 异步访问与 PDO 同步过程互不干扰。
2.3 心跳报文与节点守护
CANopen 有一个基础机制叫心跳(Heartbeat),CiA402 示例节点几乎都会在初始化时开启。生产者周期发送0x700加节点 ID 作为 COB-ID,主站作为消费者据此判断从站是否掉线。常见配置是生产者时间 100ms,消费者超时设为 300ms 以容忍一个周期抖动。
初始化时需要配置两个 OD 条目:
0x1017:Producer heartbeat time,单位毫秒,默认 0 表示关闭。0x1016:Consumer heartbeat time,每条都是 32 位,高 16 位是节点 ID,低 16 位是超时时间。
注意:调试时把0x1017设为 0,会让主站立刻判断节点离线。大多数协议栈实现里,只要节点在线并周期性发送心跳,0x1016不配置也能跑,但主站日志会一直刷 “No Heartbeat received” 告警,这种日志噪音容易掩盖真正的通信故障。示例节点一般会默认开启心跳并在初始化日志里打印周期值,方便你用 CAN 分析仪直接验证。
3. 用 main 函数把 CiA402 节点立起来:最小工程怎么写
3.1 目录组织与协议栈选型
CiA402 示例节点通常不会选全功能商业协议栈,一般做法是使用开源 CANopen 协议栈(如 CanOpenNode、CANopenSocket 等)再叠加设备行规文件。目录结构大致是这样:
CiA402SampleNode-main/ ├── src/ │ ├── main.c /* 入口和主循环 */ │ ├── cia402_app.c /* 状态机与模式处理 */ │ ├── cia402_app.h │ ├── can_driver.c /* CAN 硬件收发适配层 */ │ └── od.c /* 对象字典定义 */ ├── include/ │ ├── cia402.h /* 状态枚举与宏定义 */ │ └── od.h └── build/选协议栈的原则是先看它是否支持你芯片的 CAN 驱动适配接口。很多工程师拿到示例工程后急于阅读协议栈源码,其实更应该先读can_driver.c的收发函数,它是协议栈与硬件之间的唯一桥梁。如果这块不对,后边 SDO 永远超时,和状态机写错的表现完全一样。
3.2 main 函数里的初始化顺序
main 函数不能省,它的核心不是处理状态机,而是保证硬件、协议栈、应用层三者按正确顺序就绪。下面是一个最小实现的初始化逻辑:
int main(void) { /* 1. 板级硬件初始化:时钟、GPIO、CAN外设 */ board_init(); /* 2. 协议栈初始化:传入节点ID和位时序参数 */ canopen_node_t node; canopen_config_t cfg = { .node_id = 0x05, /* 从站节点ID,由拨码开关或烧录配置决定 */ .bitrate = 1000000, /* 1Mbps 高速模式 */ .sync_period_ms = 1, /* SYNC 周期1ms,用于CSP/CSV模式 */ .heartbeat_time_ms = 100 /* 心跳生产周期100ms */ }; canopen_init(&node, &cfg); /* 3. 应用层CiA402注册,把OD表和状态机绑定到协议栈 */ cia402_register(&node, &app_node); /* 4. 主循环 */ while (1) { canopen_process(&node); /* 处理RX/TX队列、定时器事件 */ cia402_process(&app_node); /* 状态机推进,仅当state变化时执行 */ app_loop(); /* 应用逻辑:传感器采样、PWM输出等 */ } return 0; /* 嵌入式固件不会执行到这里,但保留入口完整性 */ }参数说明:这里的sync_period_ms需要和主站 SYNC 周期保持一致。如果主站发 SYNC 周期是 1ms 而这里写 2ms,PDO 数据更新时间会与期望的同步周期错位,表现为速度波动或位置误差偏大。bitrate必须与主站完全一致,CAN 总线在位速率不匹配时只能收到错误帧或总线关闭,这是新手最容易误判为“从站未上电”的情况。
3.3 主循环里状态机的推进方式
状态机推进不建议每个循环都无条件执行。合理做法是“事件驱动 + 周期扫描结合”:
void cia402_process(cia402_node_t *node) { uint16_t sw = node->statusword; uint16_t cw = node->controlword; /* 状态字 bit7 为故障位,仅当置位时启动恢复 */ if (sw & 0x0080) { node->state = CIA402_FAULT; } switch (node->state) { case CIA402_SWITCH_ON_DISABLED: /* 等待 0x6040 bit0=1 且 bit1=0, bit2=0 */ if ((cw & 0x0007) == 0x0001) { node->state = CIA402_READY_TO_SWITCH_ON; sync_statusword(node); } break; case CIA402_READY_TO_SWITCH_ON: if ((cw & 0x0007) == 0x0003) { node->state = CIA402_SWITCHED_ON; sync_statusword(node); } break; case CIA402_SWITCHED_ON: if ((cw & 0x000F) == 0x0007) { node->state = CIA402_OPERATION_ENABLED; motor_enable_output(1); /* 打开驱动输出级 */ sync_statusword(node); } break; case CIA402_OPERATION_ENABLED: /* 这里做模式判断并映射到电机控制 */ if (node->mode_active == CIA402_MODE_PV) { velocity_control(node->target_vel); } else if (node->mode_active == CIA402_MODE_PP) { position_control(node->target_pos); } break; default: motor_enable_output(0); break; } }这里实现了一个关键行为:只有进入 Operation enabled 才使能电机输出,而不是一上电就让电机处于可驱动状态。这种设计符合安全规范,也是 CiA402 示例节点会被当作模板的主要原因。注意sync_statusword()函数必须把0x6041的值实时同步回协议栈,否则主站读到的状态字永远是复位时的初值。
4. 模式切换与 PDO/SDO 数据流:故障多发区在哪
4.1 从 PV 切换到 PP 的时序陷阱
CiA402 允许在运行中切换模式,但切换不是瞬间生效。以速度模式(PV,3)切位控模式(PP,1)为例,必须满足:
- 先从
0x6060写入期望模式值 1。 - 等待
0x6061读到 1,表示模式切换完成。 - 再配置 PP 模式特有的对象,例如
0x607A目标位置、0x607C回零位置。 - 最后通过控制字 bit4(New set point)触发一次位置装载。
很多工程师在写主站时,刚写入0x6060就立刻写0x607A,导致目标位置落在模式切换过程中。协议栈对0x607A的写入是在每个 SYNC 周期采样的,如果模式还没切完,采样值会被忽略,而主站端并不感知。调试时务必在主站侧加一个“模式确认”步骤,不要相信写入返回成功就代表已生效。
4.2 PDO 映射与动态映射的取舍
示例节点通常预定义了 4 个 TPDO 和 4 个 RPDO,但 CiA402 真正需要的数据往往超过默认映射范围。例如 CSP 循环同步位置模式,需要至少两个 RPDO:一个传目标位置0x607A(4 字节),另一个传控制字0x6040(2 字节);TPDO 则需要传实际位置0x6064和状态字0x6041。
一个常见做法是在固化固件时直接把映射写进 OD:
static const od_entry_t od_entries[] = { { 0x1600, 0x01, OD_MAP, {0x6040, 0x10, 0} }, /* RPDO1 映射控制字 16位 */ { 0x1600, 0x02, OD_MAP, {0x607A, 0x20, 0} }, /* RPDO1 映射目标位置 32位 */ { 0x1A00, 0x01, OD_MAP, {0x6041, 0x10, 0} }, /* TPDO1 映射状态字 16位 */ { 0x1A00, 0x02, OD_MAP, {0x6064, 0x20, 0} }, /* TPDO1 映射实际位置 32位 */ };每个映射条目按 32 位存储:高 16 位是索引,中间 8 位是子索引,低 8 位是位长。示例代码里这三个字段往往被写成一个uint32_t,新手赋值时容易把位长写成字节数,导致 PDO 收不到数据或长度不匹配。
动态映射即通过0x1C12/0x1C13修改 PDO 参数,对主站来说灵活,但对从站协议栈的压力更大,因为每个 SYNC 周期都要重新解析映射表。示例节点默认关闭动态映射,固件里写死是最稳的做法。
4.3 排查数据流问题的三板斧
如果主站上报“状态字超时”或“PDO 无更新”,按下面顺序排查:
- CAN 总线抓包,看 TPDO 的 COB-ID 是否按
0x180 + NodeID在周期发送。如果 TPDO 存在但主站没收到,检查主站侧过滤规则。 - 读
0x1000设备类型和0x1018厂商信息,确认从站没有跑错固件。这在多个节点同网段时特别容易发生。 - 检查 RPDO 的 inhibit time(
0x1800子索引 2)和 event timer(0x1800子索引 5)。很多“数据更新慢”的问题不是通信故障,而是默认 event timer 设成了 100ms 或更大。调小到 10ms 能显著减少延迟。
注意:如果主站用 SDO 读
0x6041能读到正确值但 PDO 收不到,优先怀疑映射表长度错误,而不是 CAN 滤波器问题。CAN 滤波器只会滤掉整个报文,不会让 PDO 部分数据缺失。
5. 验证 CiA402 节点的三种手段:从总线抓包到 CoE 迁移复用
5.1 用主站工具模拟完整状态流转
拿到示例工程后,不要急着接真实电机,先用 CAN 主站工具验证状态机和对象字典。常见的做法是打开 busmaster 或 PCAN-View,手动发送 SDO 命令:
SDO 写 0x6040 = 0x0006 /* 先进入 Ready to switch on */ SDO 写 0x6040 = 0x0007 /* Switched on */ SDO 写 0x6040 = 0x000F /* Operation enabled */每发一条指令后,紧接着读0x6041状态字,确认 bit0 与 bit1 等位按前文表格变化。如果状态字卡在某个值不动,用 Wireshark 的 CAN 解析插件看 SDO 回复里的 abort code:0x06090030表示参数值超出范围,0x06010002表示子索引不存在。这两种错误码是最常见的,分别对应写错对象索引和写错子索引。
5.2 把 CiA402 状态机迁移到 CANopen over EtherCAT
CiA402 不限于 CANopen 物理层。EtherCAT 的 CoE(CANopen over EtherCAT)协议直接复用了这套对象字典和状态机,唯一的区别是数据通道从 CAN 报文换成了 EtherCAT 的邮箱邮箱和过程数据。如果你在示例节点上已经把状态机的逻辑从 CAN 驱动中剥离出来,迁移到 EtherCAT 从站时只需要替换驱动层,cia402_process()几乎不用改。这验证了一个设计准则:状态机不应该依赖任何总线 API,它只操作controlword和statusword两个变量。
5.3 一个实用的诊断技巧:用状态字位图定位故障根因
最后分享一个我常用的技巧,不用任何调试器也能判断从站为何不进入 Operation enabled。0x6041的 bit3 为 1 表示使能完成,bit7 为 1 表示故障。把状态字读回来之后,不要只看十进制数值,而是按二进制位逐位比对。例如读到0x0250,二进制是0010 0101 0000,其中 bit4 和 bit8 为 1,说明从站报告了一个故障且设置了保留位,此时直接去查0x603F错误码得到具体原因。这个习惯能在与第三方主站联调时快速区分“主站指令错”和“从站硬件故障”,把问题定位时间从小时级压到分钟级。
本文还有配套的精品资源,点击获取