2026/9/13 6:13:05

CiA402从站示例解析:对象字典、状态机与SDO/PDO实现

CiA402从站示例解析:对象字典、状态机与SDO/PDO实现 简介这是一份面向伺服驱动与运动控制开发者的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的实际值是否和主站配置一致。索引对象名称类型访问说明0x1000Device TypeUNSIGNED32const低16位应等于0x01920x1008Manufacturer Device NameVISIBLE_STRINGconst设备名主站日志里常用0x1009Manufacturer Hardware VersionVISIBLE_STRINGconst硬件版本0x100AManufacturer Software VersionVISIBLE_STRINGconst软件版本0x1014Emergency COB-IDUNSIGNED32rw默认0x80nodeID0x1017Heartbeat TimeUNSIGNED16rw0表示禁止心跳0x1018Identity 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}, /* Controlword16位 */ {0x6060, 0, 8}, /* Modes of Operation8位 */ }; const CO_PDO_MAP tpdo_map[] { {0x6041, 0, 16}, /* Statusword16位 */ {0x6061, 0, 8}, /* Modes Display8位 */ };逻辑说明每一行由对象索引、子索引、位长度构成。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_MODE1 \ -DCO_OD_LIST_MODE0逻辑说明-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是从站节点ID0x1000是设备类型索引最后的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总线终端节点IDSDO报0x06010000对象访问权限为只读看od里AccessTypeSDO报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(channelPCAN_USBBUS1, bustypepcan, bitrate250000) 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(fposition reached: {tp})逻辑说明脚本先建立CAN网络连接再用testSlave.od作为从站字典之后按CiA402状态机顺序写控制字。0x0400对应状态字的Target Reached位只有位置模式到位后才会置1。实际项目里要把这段放在循环里并加超时因为伺服堵转时状态字可能一直停留在运行状态。最后打印目标位置只是示意真实场景应该在这里对比实际编码器位置和0x6064需求位置差值超过阈值就报错。本文还有配套的精品资源点击获取