简介:这是一份面向伺服驱动与运动控制开发者的CiA402子协议从站字典示例包,围绕CANopen协议中关键的对象字典概念,演示如何构建符合CIA402规范的CANopen节点,解决设备配置、状态管理与参数读写等实际开发问题,适合正在学习CANopen协议或需要移植伺服应用的工程师。包体非常精简,仅5个文件、约15KB,包含C源码与头文件用于实现从站逻辑,OD文件用于定义对象字典条目,另有Markdown说明和License文件,结构清晰,便于快速通读。目前已有543人学习下载。通过这个示例,读者可以直观理解字典对象的组织方式、PDO与SDO的配置方法,以及位置控制、速度限制、故障诊断与状态报告等伺服功能的实现思路;也可将其作为工程模板,进一步扩展符合CIA402标准的从站节点,对实际项目中的协议适配和设备联调有直接帮助。
1. 一个从站示例,看清CiA402的对象字典骨架
CiA402是CANopen的伺服行规,很多人拿到设备第一件事是找CiA402标准规范中文版,但标准里全是对象索引和状态转换表,啃完还是不知道代码如何组织。这个CiA402SampleNode-main压缩包给出了一个非常朴素的答案:testSlave.c、testSlave.h、testSlave.od,三个文件把一个CiA402从站的最小骨架摆在你面前。它解决的问题很具体:对象字典怎么定义、控制字怎么进状态机、SDO/PDO怎么被字典驱动。适合两类人,一类是做伺服或运动控制卡驱动的工程师,想快速搭一个能响应主站命令的从站;另一类是现场调试的老手,需要理解第三方伺服为什么在某个索引上行为异常。它不是完整产品,但把CiA402最核心的“字典驱动”机制讲清楚了。
2. testSlave.od 里的对象字典与CiA402行规定义
2.1 为什么先看OD而不是先看源码
在CANopen从站里,对象字典是唯一的对外数据契约。主站不关心你的电机用什么控制算法,只通过索引和子索引访问对象。CiA402行规做的事情,就是把这些对象里的0x6040控制字、0x6041状态字、0x6060运行模式等位置和含义固定下来。testSlave.od 文件用文本描述对象表,编译时会转成C语言结构体。拿到示例第一件事应该是打开这个od文件,而不是去逐行读testSlave.c。因为节点能对外提供什么服务,全部体现在这张表里。
od是OpenCANopen和CANopenNode常见的对象字典描述格式,每个中括号段对应一个对象或一个子索引。数据项包括子索引数量、数据类型、访问权限、默认值和参数名。testSlave.od里还会出现注释行,用分号开头,这些注释往往比正文更重要,记录了作者当时为什么保留这个对象。
2.2 通信对象区:0x1000到0x1FFF必须先配好
一个CiA402从站无论多简单,通信层对象是少不了的。testSlave.od里会看到0x1000设备类型、0x1008设备名称、0x1014紧急报文COB-ID、0x1017心跳周期。下面是这类od文件最常见的段结构:
[0x1000] SubNumber = 0 DataType = 7 AccessType = const ParameterName = "Device Type" DefaultValue = 0x00020192 [0x1017] SubNumber = 0 DataType = 6 AccessType = rw ParameterName = "Producer Heartbeat Time" DefaultValue = 100逻辑说明:0x1000的低16位是设备行规号,CiA402对应0x0192,也就是十进制402。如果这个值被改掉,主站可能不按伺服行规处理,状态机不会正常激活。0x1017是心跳生产者周期,单位毫秒,0表示停止发心跳。主站配置节点保护时,会根据这个周期判断从站是否离线。这里把默认值写成100,是通用的推荐值,现场应用如果心跳太快会给CAN总线增加不必要的负载。
通信对象区里有一个容易踩坑的是0x1005 SYNC COB-ID。通常默认0x80,但有些网关会把同步报文ID改到其它值。如果从站没有收到SYNC,采用同步传输的PDO就一直不发。排查时先看一下0x1005的实际值是否和主站配置一致。
| 索引 | 对象名称 | 类型 | 访问 | 说明 |
|---|---|---|---|---|
| 0x1000 | Device Type | UNSIGNED32 | const | 低16位应等于0x0192 |
| 0x1008 | Manufacturer Device Name | VISIBLE_STRING | const | 设备名,主站日志里常用 |
| 0x1009 | Manufacturer Hardware Version | VISIBLE_STRING | const | 硬件版本 |
| 0x100A | Manufacturer Software Version | VISIBLE_STRING | const | 软件版本 |
| 0x1014 | Emergency COB-ID | UNSIGNED32 | rw | 默认0x80+nodeID |
| 0x1017 | Heartbeat Time | UNSIGNED16 | rw | 0表示禁止心跳 |
| 0x1018 | Identity Object | 复合 | ro | 包含厂商ID、产品码等 |
2.3 行规核心:0x6040/0x6041/0x6060/0x6061
从0x6040到0x6061是CiA402状态机的入口。testSlave.od里这几项决定了主站能不能把从站拉到Operation Enabled。下面是一组标准的定义,和示例文件的行事风格一致:
[0x6040] SubNumber = 1 DataType = 0x0006 AccessType = rw ParameterName = "Controlword" [0x6041] SubNumber = 1 DataType = 0x0006 AccessType = ro ParameterName = "Statusword" [0x6060] SubNumber = 1 DataType = 0x0005 AccessType = rw ParameterName = "Modes Of Operation" [0x6061] SubNumber = 1 DataType = 0x0005 AccessType = ro ParameterName = "Modes Of Operation Display"逻辑说明:0x6040和0x6041都是16位字,分别对应控制字输入和状态字输出。0x6060和0x6061使用8位有符号整数,写3进入位置模式、4是速度模式、6是回零模式。这里有个容易被忽略的点,0x6060是rw,但并不是所有模式都支持,从站要在0x6502里声明支持的模式掩码。主站写0x6060时,如果对应位没有置位,从站会回复SDO中止码0x06040041。
testSlave.c里处理状态机的方式并不复杂,它把0x6040的低4位作为状态机的输入,把计算出的0x6041写到字典。你不需要在0x6041上设置默认值,因为上电后协议栈会根据当前状态刷新它。如果od文件里给0x6041写了默认值,反而会误导调试者。
2.4 厂商特定对象怎么放
除了标准对象,示例里通常会留一段0x2000之后的区域给厂商特定参数,比如电机额定电流、编码器分辨率。放这里的好处是标准主站扫描时不会干扰行规解析。我一般会这样组织:0x2000到0x23FF放厂商参数,0x2500以后放诊断信息。每个参数仍然要有数据类型和访问权限,不能用裸变量替身。
有一点要提醒:新增对象时不要随意改变已有对象的索引和子索引。因为主站侧已经按照厂商提供的EDS文件访问设备,你一旦把0x6040从16位改成32位,或者把子索引从0改到1,整个行规兼容性就没了。这个示例的价值就在这里,它把标准对象的位置固定下来,扩展时只需要往厂商区加条目,不需要动标准区。
2.5 od文件不是设备运行时读取的配置
有些第一次接触CANopen的人以为testSlave.od是设备启动时读取的文件,其实不是。它是在编译阶段被一个生成器脚本转换为C数组的。也就是说,你在od里加了一个对象之后,不重新编译、不烧录就不会生效。这在开发阶段很常见:改了od,也烧录了,但发现没有新对象,十有八九是编译脚本没有把od重新生成代码,或者生成的头文件路径不对。检查一下编译输出里是否更新了CO_OD.h或类似文件,再查设备里的实际对象表。
3. testSlave.c 的启动流程与SDO/PDO配置
3.1 从主循环看从站的工作节奏
testSlave.c的写法很直白,典型的单线程循环:初始化协议栈对象,然后不断调用CANopen处理函数,在循环里做自己的应用逻辑。代码量不大,但骨架清晰。下面是抽取出来的核心逻辑,和原文件在结构上保持一致:
static void MainInit(void) { CO_OD_INITIALIZE(&od, CO_RESET_NODE); /* 用od默认值初始化字典表 */ CANopenInit(&co, &od); /* 协议栈绑定字典 */ NMTsetNodeID(&co.NMT, 0x0A); /* 节点ID设为10 */ Heartbeat_setTime(&co.Heartbeat, 100); /* 心跳周期100ms */ } static void MainLoop(void) { while (1) { CANopen_Process(&co); /* 收发CAN报文,更新对象字典 */ uint16_t ctrl = OD_get_u16(&od, 0x6040, 0); uint16_t status = StateMachine(ctrl); /* 根据控制字推状态字 */ OD_set_u16(&od, 0x6041, 0, status); /* 写回字典 */ } }逻辑说明:CO_OD_INITIALIZE在系统复位或协议栈复位时把所有对象恢复到od文件里的默认值。这个调用很关键,如果漏掉,设备重新上电后字典内容可能是随机的。CANopenInit负责注册SDO、PDO、心跳、紧急对象。NMTsetNodeID把节点ID固定为0x0A,这里用固定值只是为了示例,真实设备应该支持设置。主函数里CANopen_Process每调用一次就处理一轮CAN接收队列和发送队列,所以循环周期决定了CAN报文的响应延迟。如果这个循环里有耗时的电机控制算法,最好把时间片控制在1ms以内,否则SDO会超时。
3.2 SDO为什么几乎不用写代码
testSlave.c里没有单独的SDO处理函数,这是因为SDO服务完全由协议栈根据对象字典自动完成。主站发过来的请求里带着索引、子索引和数据类型,协议栈查表后直接读写对应内存。你真正要保证的是od文件里的访问权限和数据类型与协议栈期望一致。0x6040定义为rw,主站就能下载;0x6041定义为ro,主站写入就会收到SDO中止协议。
现场调试时经常有人问,为什么读0x6041能读到值,但写不进去?原因就在这里。拿到SDO中止码0x06010000时,意思就是访问权限不允许写。不是说协议栈坏了,而是对象本身是只读的。反过来,如果主站能写但读返回值一直是同一个数,多半是协议栈没有把应用值刷回字典,比如状态字只被应用主动更新,而你没有在od里把它标成ro。
3.3 PDO映射的两种表达方式
在CANopenNode这类协议栈里,PDO参数是通过0x1600/0x1A00系列对象动态描述的。testSlave.c里你看到的可能是数组,也可能是od生成的定义,但逻辑上等价于下面这个映射表。我习惯用数组把它拆开看,便于对照主站EDS文件:
const CO_PDO_MAP rpdo_map[] = { {0x6040, 0, 16}, /* Controlword,16位 */ {0x6060, 0, 8}, /* Modes of Operation,8位 */ }; const CO_PDO_MAP tpdo_map[] = { {0x6041, 0, 16}, /* Statusword,16位 */ {0x6061, 0, 8}, /* Modes Display,8位 */ };逻辑说明:每一行由对象索引、子索引、位长度构成。RPDO1收到的报文前16位被写入0x6040,后8位写入0x6060,总长度3字节。TPDO1则把状态字和模式显示打包发送,报文长度同样是3字节。这里最容易犯的错误是位长度与对象定义不一致。0x6040在字典里明明是16位,映射时写了8位,主站发来的控制字高字节就丢了,于是使能状态始终卡在Switch On Disabled。
从站侧的PDO映射参数可以由主站通过SDO重写,也可以在od文件里预置。量产设备的做法是预置一组默认映射,上电就能用。testSlave.od里的默认映射最好保持和主站EDS一致,否则调试工具读回0x1A00对象时发现映射内容对不上,会把所有PDO禁用。
3.4 传输类型影响数据实时性
PDO发送/接收的触发条件由0x1800/0x1400对象的传输类型决定。常见取值整理成表:
| 传输类型 | 触发方式 | 适用场景 |
|---|---|---|
| 0x00 | 同步非周期,收到SYNC后按映射发送 | 需要主站控制发送时机的非周期通信 |
| 0x01-0xF0 | 同步周期,每N个SYNC发送一次 | 周期插补,和总线同步节拍一致 |
| 0xFC | 仅事件触发,不使用SYNC | 变化量较少但实时性要求高的信号 |
| 0xFE | 事件触发,按应用逻辑发送 | 状态字等事件型数据 |
| 0xFF | 异步,由设备制造商指定 | 默认值,很多从站用它表示事件触发 |
在伺服控制里,速度模式和位置模式一般用同步周期传输,这样速度/位置设定值和SYNC对齐,避免主站和从站时间基准漂移。testSlave.c示例里如果只是演示功能,默认用异步传输也能跑,但你在真实龙门同步或多轴插补时,必须把它改成同步周期,并确保SYNC报文始终处于调度状态。
4. 把CiA402从站跑起来:编译、连接与排错
4.1 没有工程文件时怎么编译
压缩包里没有Makefile,这是源码包最常被问到的点。压缩包里的README会比代码更新得快,建议先扫一眼它提到的依赖项。常见做法是把testSlave.c编进你自己已有的协议栈工程,或者新建一个空目录,把依赖的协议栈源文件一起编。以CANopenNode风格为例,编译命令大致长这样:
gcc -c testSlave.c \ -I../canopennode/stack \ -I../canopennode/stack/CANopen \ -DCO_OD_ENTRY_MODE=1 \ -DCO_OD_LIST_MODE=0逻辑说明:-I把协议栈头文件目录加进来,testSlave.h和testSlave.od生成的OD头文件需要能被找到。CO_OD_ENTRY_MODE告诉协议栈使用单入口对象字典,CO_OD_LIST_MODE控制是否生成运行时可加入/删除对象的接口。这两个宏在不同版本里名字可能略有差异,如果编译报错找不到CO_OD,就去打开协议栈的CO_OD.h看它定义了什么开关,按实际版本调整。
编译通过不等于能跑。还需要提供CAN控制器驱动、定时器回调、发送接收中断。testSlave.c只负责应用和协议栈胶水层,底层收发你仍然要自己补。如果你手头没有现成驱动,最快的办法是先用canopennode的上层测试工程,把testSlave.c替换进去。
4.2 连接主站后的第一条命令
烧录完以后,先用最简单的手段验证设备是否在线。我喜欢用canopenctl或python-can,第一步都相同:读设备类型。
canopenctl sdo read 0x0A 0x1000 0逻辑说明:0x0A是从站节点ID,0x1000是设备类型索引,最后的0是子索引。返回的数据低16位如果等于0x0192,说明设备身份正确。如果超时,优先检查波特率。CANopen标准本身没有强制默认波特率,testSlave如果用的是250k,主站设成500k当然不通。其次是终端电阻,两个末端节点必须有120欧姆终端,否则SDO响应帧时有时无。
设备在线后,启动NMT并查看节点状态:
canopenctl nmt start 0x0A canopenctl status 0x0A如果status命令能看到心跳周期和启动状态,说明NMT和心跳工作正常。之后可以写控制字测试状态切换:先写0x0006进入Ready to Switch On,再写0x0007进入Switched On,最后写0x000F进入Operation Enabled。每一步都读回0x6041,确认状态字低四位是按CiA402标准走的。
4.3 SDO中止码与PDO不刷新
实际调试中最大的干扰来自SDO中止码和PDO行为。常见问题整理成下面这张表:
| 现象 | 可能原因 | 检查顺序 |
|---|---|---|
| SDO读写超时 | 节点ID/波特率不一致 | CAN总线终端,节点ID |
| SDO报0x06010000 | 对象访问权限为只读 | 看od里AccessType |
| SDO报0x06040041 | 对象不存在或子索引不对 | 看索引是否在od里 |
| 写0x6060被中止 | 模式不支持 | 读0x6502支持的模式掩码 |
| RPDO写了没反应 | 映射长度或数据类型不匹配 | 读0x1600映射 |
| TPDO一直不发 | 传输类型是同步但SYNC没到 | 抓SYNC报文 |
举个例子:主站向0x6060写8(回零模式),从站回0x06040041。很多人的第一反应是协议栈兼容性问题,但实际上0x6502对象里会声明支持哪些模式。读一下0x6502,如果对应bit没有被置位,从站就按标准拒绝。此时要么改0x6060为3/4切换速度位置模式,要么在od里把0x6502的位扩出来。testSlave.c示例里如果只实现了位置和速度模式,你就不要拿回零模式去测它。
PDO不刷新也是高频问题。比如TPDO1配置的是同步周期传输,但主站完全没有发SYNC,从站自然不往外发数据。抓总线波形能看到SYNC周期是否稳定。还有一种情况是映射表里放了不存在的索引,主站在协商PDO时把整个PDO对象置为无效。解决方法是先用SDO读0x1A00,看子索引0和子索引1/2是否和你预期一致。
4.4 用错误帧快速定位硬件问题
当CAN总线上出现连续错误帧时,软件排查已经意义不大。先用示波器或逻辑分析仪看CAN_H/CAN_L差分电压,正常显性位约2V,隐性位约0V。如果波形幅度不够,检查收发器芯片供电和终端电阻。testSlave.c本身不涉及这些,但协议栈是跑在硬件之上的,很多人在这个环节浪费了大量时间。
5. 把它改成可用的CiA402伺服节点:三个关键改法
5.1 在状态切换处挂上电机使能
testSlave.c的状态机只维护控制字和状态字的位关系,并不会真正给功率级上电。实际伺服要在这里插入自己的使能逻辑。常见做法是加一个钩子:
if ((ctrl & 0x000F) == 0x000F) { motor_power_enable(1); } else { motor_power_enable(0); }逻辑说明:控制字低四位为0x000F时对应Operation Enabled。注意这里有个顺序问题:不要在使能瞬间立刻下发目标速度,因为此时速度环可能还没完成内部初始化,容易造成飞车。经验值是先使能功率级,延时100ms,等待主站通过PDO下发0x60FF速度目标值,再解除内部的输出封锁。
5.2 补全位置模式对象
要支持位置控制,至少需要0x607A目标位置、0x6080最大速度、0x6093位置因子。在testSlave.od基础上加对象即可:
[0x607A] SubNumber = 1 DataType = 0x0007 AccessType = rw ParameterName = "Target Position" [0x6080] SubNumber = 1 DataType = 0x0003 AccessType = rw ParameterName = "Max Motor Speed"逻辑说明:0x607A是32位整数,单位由位置因子决定,很多伺服直接把它当脉冲数用。0x6080是无符号32位,限制模式下的最大转速。新增对象后必须重新生成OD表和头文件,否则主站读到0x607A时会返回对象不存在。如果这些对象也会通过PDO传输,还要在0x1600映射里登记,不能只改0x607A的定义。
5.3 用python-can按CiA402状态机做回归验证
调试从站时,我不太建议一直抱着调试工具点按钮。用脚本把状态切换和位置轮询写死,能更快暴露问题。python-can配合主站模式下的一小段脚本:
import canopen net = canopen.Network() net.connect(channel='PCAN_USBBUS1', bustype='pcan', bitrate=250000) node = canopen.RemoteNode(10, 'testSlave.od') net.add_node(node) node.sdo['Controlword'].raw = 0x0006 # Shutdown node.sdo['Controlword'].raw = 0x0007 # Switched On node.sdo['Controlword'].raw = 0x000F # Operation Enabled sw = node.sdo['Statusword'].raw if sw & 0x0400: # bit10 Target Reached tp = node.sdo['Target Position'].raw print(f'position reached: {tp}')逻辑说明:脚本先建立CAN网络连接,再用testSlave.od作为从站字典,之后按CiA402状态机顺序写控制字。0x0400对应状态字的Target Reached位,只有位置模式到位后才会置1。实际项目里要把这段放在循环里并加超时,因为伺服堵转时状态字可能一直停留在运行状态。最后打印目标位置只是示意,真实场景应该在这里对比实际编码器位置和0x6064需求位置,差值超过阈值就报错。
本文还有配套的精品资源,点击获取