FEATURED · 精选文章

CANopen主站从CiA 301到DS 402:协议实现与运动控制实战

发布时间 / 2026/9/16 12:15:09
来源 / 创域科博编辑部
栏目 / 资讯中心
CANopen主站从CiA 301到DS 402:协议实现与运动控制实战 简介面向工业自动化与嵌入式通信开发者的CANopen主站协议栈源码包基于Python实现CiA301基础规范与DS402驱动配置文件覆盖SDO/PDO数据交换、网络管理、步进与伺服电机控制等核心功能适用于需要快速搭建CANopen主站设备或深入学习协议实现的场景。压缩包共96个文件以91个Python源文件为主体包含主站核心逻辑、scripts辅助脚本、old_tests与tests两套测试模块其中test_402.py、test_sdo.py分别针对DS402运动控制和SDO读写流程另有README.md、License、package.xml等工程化配置整体仅87KB结构紧凑、便于逐模块阅读。目前已有795人学习下载。借助该资源可梳理301对象字典与402运动控制子协议的交互机制直接复用测试样例来验证主站功能并通过工具脚本加速总线通信调试对自研CANopen主站模块或排查驱动控制问题有较高参考价值。1. CANopen 主站不是把协议栈拉起来就完事一个 CANopen 主站真正的工作量往往不在收发那几条 CAN 帧上而在它同时要守两份规范CiA 301 规定了 NMT、SDO、PDO 这些传输手段DS 402 又给伺服和变频器定义了状态机和运动模式。很多工程师把主站做成了“能发 SDO 的工具”到联调时才发现驱动不走、看门狗乱跳、状态进不到 Operation Enabled根因其实是把 301 和 402 的边界搞混了。这篇文章按 CANopen 的层级结构把主站侧的协议处理和运动控制逻辑拆开讲最后给一套从配置、使能到状态机排错的可落地方案适合正在写或维护运动控制主站的人。2. 从 CiA 301 划清主站边界NMT/SDO/PDO 谁说了算2.1 主站先做 NMT 的三张表命令、心跳、节点控制CANopen 的层级结构里CiA 301 位于最底层它规定网络管理和服务数据。主站在这个层面上的身份是 NMT Master从站是 NMT Slave。从站上电后处于 Pre-operational不会主动发应用数据必须等主站发 NMT 命令把它切到 Operational。这个“切”的动作不是一次性的联调时最容易出的问题就是主站程序复位后忘了重发驱动器还停留在上一次的 Operational 状态里却已经没有 SYNC 在跑。主站控制 NMT 状态只用一条 CAN 帧COB-ID 固定是 0x000数据区两个字节第一字节是命令符第二字节是目标节点号。命令符的值决定让节点进入哪个状态广播时目标地址写 0。下表是运动控制里最常用的一组命令。命令符 CS功能目标状态0x01启动节点Operational0x02停止节点Stopped0x80进入预运行Pre-operational0x81复位节点节点初始化后自动进入 Pre-operational0x82复位通讯只复位通讯参数保留其余对象字典数据在 Linux 下用 SocketCAN 发 NMT 帧代码很短我一般会封装成这样一个函数void send_nmt(int fd, uint8_t cs, uint8_t node_id) { struct can_frame frame {0}; frame.can_id 0x000; /* NMT 固定使用 COB-ID 0 */ frame.can_dlc 2; frame.data[0] cs; /* 命令符例如 0x01 启动 */ frame.data[1] node_id; /* 0 表示广播到所有从站 */ write(fd, frame, sizeof(frame)); }命令符 0x01 启动时如果 node_id 填 0就是让总线上所有从站一起进 Operational。实际项目中我很少用广播因为多轴系统里某些轴可能还没完成配置单独控制更安全。需要注意的是Stopped 状态比较特殊它只响应 NMT 复位命令SDO 和 PDO 都不响应所以调试时误发 0x02 会让整条轴“消失”这时候先查主站有没有把节点从 Stopped 拉回来。心跳机制同样属于主站职责。主站作为心跳消费者要周期检查从站发来的 0x700node 心跳超时未到就判定节点丢失。心跳周期写从站 0x1017 对象主站侧的超时时间通常是心跳周期的三倍这个倍率不是拍脑袋定的是为了容忍调度抖动。2.2 SDO 是主站的配置口错在哪里要看 abort codeSDO 用于读写从站对象字典是非周期、确认型的服务。主站发请求到 0x600node从站回响应到 0x580node。写入一个 4 字节数据时请求帧第一个字节是 0x2B后面跟着索引、子索引和数据。这是主站代码里最常见的操作例如写 DS 402 的控制字 0x6040:0。void sdo_write_u32(int fd, uint8_t node, uint16_t index, uint8_t subindex, uint32_t value) { struct can_frame frame {0}; frame.can_id 0x600 node; /* SDO 请求 COB-ID */ frame.can_dlc 8; frame.data[0] 0x2B; /* 加速写数据长度 4 字节效率最高 */ frame.data[1] index 0xFF; frame.data[2] index 8; frame.data[3] subindex; frame.data[4] value 0xFF; frame.data[5] (value 8) 0xFF; frame.data[6] (value 16) 0xFF; frame.data[7] (value 24) 0xFF; write(fd, frame, sizeof(frame)); }这里有一个很多人栽过的细节SDO 请求发完不能立刻认为写成功必须等从站的响应帧回来。响应帧的第一个字节是 0x60 表示成功如果是 0x80 表示错误后面跟一个 4 字节的 abort code。abort code 是定位配置问题的最直接线索常见几个值得背下来ABORT CODE含义排查方向0x06010000不支持该访问方式对象只读却用了写命令0x06040043数据长度不匹配写 1 字节却发了 4 字节0x06090011子索引不存在子索引越界0x08000020数据超出范围数值超对象范围0x08000024PDO 长度不匹配映射的长度对不上我要求主站程序里每个 SDO 事务都要有超时和 abort code 日志这样联调时能直接定位是主站发错还是从站拒绝。SDO 虽然可靠但速度慢所以只用于配置阶段或非周期读取。控制字、状态字这类周期性数据必须走 PDO。2.3 PDO 管周期数据映射参数必须和 402 对象配合PDO 是生产/消费型数据没有应答适合周期性下载控制字或上传实际位置。主站侧要做的不是只发数据而是先通过 SDO 配置从站的 RPDO/TPDO 映射再在运行时按周期或按 SYNC 收发。RPDO 通信参数区在 0x1400 到 0x15FFTPDO 在 0x1800 到 0x19FF映射参数分别在 0x1600 系列和 0x1A00 系列。一个典型单轴运动控制映射如下通道对象属性RPDO10x6040 控制字、0x607A 目标位置、0x6060 模式TX 由主站周期发送TPDO10x6041 状态字、0x6064 实际位置RX 由从站周期上报映射配置完成并且节点进入 Operational 后主站通常在 0x80 同步帧触发后统一收发。此时对象字典里的值会和 PDO 数据同步更新不再需要逐条 SDO 查询。操作中要避免在运行中修改映射否则从站可能拒绝切换状态我一般把映射配置放在 Pre-operational 阶段全部确认后再切 Operational。1. 等一下DS 402 才是运动主站的核心标题切换等等第 1 章已存在。我需要修正章节号。修正按照规划一级章节应为 5 章。上一章是## 2.。下一章应该是## 3.。让我在继续时重新编号。3. DS 402 状态机才是运动主站的主逻辑3.1 Controlword 和 Statusword 的位定义不能只看表格DS 402 的核心不是运动模式而是驱动状态机。它定义驱动从无电状态到可运行状态之间要经过哪些中转态以及每一步主站该写什么。这个状态机的两端就是对象字典里的两个对象控制字 0x6040 由主站写入状态字 0x6041 由从站返回。控制字最常用的是低 4 位和 bit7。bit0 是 switch onbit1 是 enable voltagebit2 是 quick stopbit3 是 enable operationbit7 是 fault reset。状态字里bit0 表示 ready to switch onbit1 表示 switched onbit2 表示 operation enabledbit3 表示 faultbit6 表示 switch on disabled。只看单个 bit 往往不够要按掩码判断当前状态。DS 402 规范里从站状态与状态字值存在固定对应关系我记忆时用掩码 0x4F因为这个掩码能覆盖低 4 位和 bit6。驱动状态状态字参考值对应的 0x6040 控制字动作Switch on disabled0x40写 0x06 进入 ShutdownReady to switch on0x21写 0x07 进入 Switched onSwitched on0x31写 0x0F 进入 Operation enabledOperation enabled0x33写 0x00 可回到 Switched onFault0x08写 0x80 复位故障Quick stop active0x07写 0x06 回到 Ready这里要特别说明参考值不是唯一判断标准严谨做法是从状态字里解析每一位。比如 Operation enabled 的典型值是 0x33但某些驱动会在 bit7 上置 warning 位值变成 0x0133这时候不少初学者就以为状态不对。实际应该用掩码把 bit7 等干扰位滤掉再比较。3.2 主站写控制字的顺序shutdown、switch on、enable真正让驱动转起来主站要按顺序推进三条命令。从 Switch on disabled 开始先发 0x06 进入 Ready to switch on再发 0x07 进入 Switched on最后发 0x0F 进入 Operation enabled。三条命令之间必须等待状态字确认不能一次全发。有人为了省事直接发 0x0F这在部分驱动上会从 Switch on disabled 直接跳到 Operation enabled但遇到严格实现的从站就会拒绝。更危险的是从 Fault 状态复位的场景必须先写 0x80 并等待当前状态离开 Fault再重新走 shutdown 流程。如果 0x80 写一次就急着发 0x06Fault 复位还没完成命令会被吃掉。我通常把序列封装成带超时的状态转换函数int drive_enable(int fd, uint8_t node) { /* 等待状态字稳定目标状态值使用掩码 0x4F 比较 */ if (!wait_status(fd, node, 0x60, 0x4F, 3000)) return -1; sdo_write_u16(fd, node, 0x6040, 0, 0x0006); /* shutdown */ if (!wait_status(fd, node, 0x21, 0x4F, 3000)) return -1; sdo_write_u16(fd, node, 0x6040, 0, 0x0007); /* switch on */ if (!wait_status(fd, node, 0x31, 0x4F, 3000)) return -1; sdo_write_u16(fd, node, 0x6040, 0, 0x000F); /* enable operation */ if (!wait_status(fd, node, 0x33, 0x4F, 3000)) return -1; return 0; }注意 0x6040 在 DS 402 对象字典里是 u16所以这里用 sdo_write_u16。控制字的 bit4 到 bit6 留给了模式相关的动作例如 Profile Position 模式下 bit4 是 new setpointbit5 是 change immediately这些位会和使能位同时出现在一个控制字里但使能序列时只动低 4 位即可。3.3 运动模式的切换顺序先停再换模式DS 402 的模式选择对象是 0x6060主站写入模式编号从站进入相应模式。常用模式如下模式编号模式名称适用场景1Profile Position点到点定位最常用2Profile Velocity定速运行3Profile Torque扭矩控制5Homing回机械原点6Interpolated Position插补位置7Cyclic Sync Position周期同步位置8Cyclic Sync Velocity周期同步速度9Cyclic Sync Torque周期同步扭矩切换模式前我习惯先把运行停下来把 0x6040 的使能位去掉避免模式切换时驱动还带着速度或力矩跑。写 0x6060 之前还要确认驱动支持该模式大多数伺服只实现 Profile 模式加一两种周期同步模式写一个不存在的模式SDO 会返回 0x06020000 之类的错误。从站会通过模式显示对象 0x6061 回传当前实际模式写入 0x6060 后应先读 0x6061 确认模式已切换再开始使能。这一步在单轴调试时没什么感觉多轴联动时如果模式没就位就发运动命令轴会拒动或者直接 Fault。4. 把 CiA 301 传输与 DS 402 状态机合成一个完整主站流程4.1 启动时序模式、参数、同步和使能各就各位实际项目中主站不能只干“把驱动使能起来”这一件事。它需要先配置模式参数再做映射然后切状态最后才启动周期 PDO。这套顺序错一个环节联调时就会多花半天。我通常把这个流程拆成六个阶段写在一个启动函数里阶段动作对应对象1复位通讯参数0x1011:01 写入 0x64616F6C2配置模式和运动参数0x6060、0x607A、0x6083、0x60843配置 PDO 映射和通信周期0x1600、0x1400、0x10064切换到 OperationalNMT CS0x015使能驱动状态机0x6040 按 0x06、0x07、0x0F 推进6开始周期 PDO 和 SYNC0x80 同步帧第一阶段复位通讯参数是最容易被忽略的一步。0x1011 的 restore 机制要求写入字符串“load”对应的小端编码 0x64616F6C从站会恢复出厂预设。这里要小心复位通讯参数会把已经配置好的映射一起清掉所以只能放在所有 SDO 配置之前不能放在最后。配置运动参数时不同模式的参数不同。Profile Position 模式必须设置 0x6083加速度、0x6084减速度、0x6086紧急停止减速度还要给 0x607A 写入目标位置。不要在主站配置里写死速度因为现场调速时频繁改主站代码非常痛苦我一般把速度、加减速度都做成参数表下发。等所有 SDO 配置完成再发 NMT 0x01 进入 Operational。之后 PDO 开始流动接着走 3.2 节的使能序列。这里有个容易踩的坑如果使能命令用 SDO 发那配置阶段没问题但运动开始后 SDO 和控制字混在一路 CAN 总线上可能会被周期 PDO 挤占导致使能响应变慢。所以我通常会从使能阶段开始就把 0x6040 放进 RPDO后续直接写 PDO 数据。4.2 周期 PDO 同步SYNC 间隔和数据映射的参数匹配周期同步模式CSP、CSV对主站的要求更高。主站需要周期发送 SYNC 同步帧同时把目标位置或速度通过 RPDO 下发给各轴。SYNC 周期写 0x1006 通讯周期参数单位是微秒例如 4000 等于 4ms。PDO 的实际占用时间要算清楚。一个 RPDO 如果映射 8 字节数据CAN 数据帧长度约 8 字节数据加仲裁、控制和 CRC 字段一帧总线时间在 250 kbit/s 速率下约 0.6ms。一个带 8 个伺服轴的主站如果所有轴都挂在一路 CAN 上只算 PDO 就要接近 5ms还不能留出 SDO 余量。这个约束比很多人想得更严格。我见过一个现场案例4 个轴周期同步模式SYNC 周期 4ms每轴一个 TPDO 上报实际位置总线负载看起来只有百分之四十但偶尔出现堵转最终定位是 SYNC 帧发送被上层调度延迟了几个毫秒。主站侧要保证 SYNC 的发送抖动控制在 0.5ms 以内这就不能靠普通 Linux 用户态定时器要用高优先级线程或者实时接口。如果主站是主循环扫描的框架SYNC 发送要放在中断级别否则轴抖动是必然结果。映射参数配合上还有个细节同步帧发送后所有从站同时采样 RPDO同时上传 TPDO。主站要注意 RPDO 发送顺序避免在 SYNC 之后一窝蜂地把 8 个轴的控制字挤在同一个时间片否则会引发现场总线碰撞和仲裁延迟。我一般按轴序号依次发送 RPDO而不是集中在 SYNC 之后立刻发完。4.3 出错时的快速停止和故障复位也是一段状态机运动控制里运行状态远没有故障处理重要。DS 402 状态机里有一条分支运行中如果发生急停控制字 bit2quick stop会被清零驱动进入 Quick stop active然后按 0x6086 的减速度执行急停。主站在这个状态下不能急着发 0x00 控制字复位而是等驱动真正停下状态字变成 Fault 或回到 Ready再决定是否恢复。从站故障会通过 EMCY 紧急报文上报COB-ID 是 0x0800node数据区包含错误码和错误寄存器。主站最好维护一张从站 fault 表把 EMCY 的 0xFF 供应商错误码也记录下来因为很多伺服一段代码表示一个具体故障比如过流、过压、编码器异常单靠 DS 402 标准错误码不够用。复位故障时要写 0x6040 的 bit7。有位同事遇到过一个问题故障发生后反复写 0x80 都没反应后来从抓包看到驱动状态字一直是 0x08主站发的 0x80 只有一帧而某些从站要求 fault reset 信号保持至少一个通讯周期也就是说要连续写两帧相同控制字。这属于从站实现差异主站侧最稳的写法是检测到 Fault 后先连续发两次 0x80然后等待状态字离开 0x08再走正常 shutdown 序列。5. 主站联调的最后一百米从状态字和 abort code 反推 402 时序5.1 抓总线不看“报文内容”而看“状态跳变”联调时最好打开抓包工具看总线但看什么有讲究。不要逐帧看每个数据而是过滤出 0x6041 状态字和 0x6040 控制字的读写记录把状态字的低字节按掩码解析成状态名称再来回看命令顺序。用 Python 的 canopen 库可以很快做验证主站代码跑在真实设备上时也可以单独开一个调试脚本挂到同一路 VPATH 上。我常用的一段验证脚本是在总线侧监听状态字变化import canopen network canopen.Network() network.connect(channelcan0, bustypesocketcan) node network.add_node(1, drive.eds) def status_name(statusword): mask statusword 0x4F mapping { 0x40: Switch on disabled, 0x21: Ready to switch on, 0x31: Switched on, 0x33: Operation enabled, 0x07: Quick stop active, 0x08: Fault, } return mapping.get(mask, hex(statusword)) while True: network.send_message(0x700, [1]) # 请求心跳保留也可用监听器 statusword node.sdo[0x6041].raw print(status_name(statusword)) time.sleep(0.1)这段脚本的价值不在于完整驱动逻辑而在于验证主站每条控制字命令有没有让状态字按预期跳变。如果发了 0x06 后状态字停在 0x40 不动说明要么从站不接受 SDO要么 0x6040 的写入对象不对。此时别急着改时序先回读 0x6040确定写进去的数据是不是自己以为的那个值。5.2 一个自查顺序模式、控制字、映射、急停状态字停滞不跳变时我一般按下面顺序排查而不是反复调时序读 0x6061确认实际模式和 0x6060 写入的一致。读 0x6040确认对象实际值等于主站发送值排除 SDO 写入落在错误索引。用 SDO 回读 0x6064 实际位置确认 TPDO 映射没有占错位置。查 EMCY 错误码如果从站报了 Fault先复位不要继续推控制字。确认当前状态是否在 Quick stop active急停回路是否被外部 PLC 拉掉。这套顺序对单轴和多轴都适用。多轴场景下还要额外核对每个轴的 NMT 状态因为一个轴从站掉线重启后会自动回 Pre-operational就你不会再收 PDO。此时主站光看 SDO 读得到数据但周期控制字已经失效表现就是轴使能不上。最直观的办法是看心跳监视器哪一路心跳超时就先从 NMT 状态拉起来再重新走一遍该轴自己的使能流程。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻