ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CANopen主站协议栈解析:CiA301与DS402从站控制实战

2026/9/16 13:16:18 拓冰建站 浏览量
CANopen主站协议栈解析:CiA301与DS402从站控制实战 简介CANopen 301/402 主站协议栈实现面向嵌入式与工业自动化开发人员用于构建支持 CiA301 网络管理及 DS402 运动控制的主站设备可协调 PDO/SDO 通信并连接伺服驱动器、步进电机、传感器等从站适用于工业现场总线控制与运动控制场景是学习 CANopen 协议与开发主站设备的重要参考。资源共 96 个文件、约 87KB以 91 个 Python 源码文件为主涵盖协议栈核心实现、SDO/PDO 处理、DS402 电机控制与测试脚本同时包含 XML 配置、Markdown/TXT 说明文档及许可证文件。目录划分为 src、scripts、tests 等模块便于按功能阅读和二次开发。目前已有 795 人学习下载。通过这份源码开发者可以直接复用主站通信框架理解 CANopen 对象字典、同步周期、错误处理和状态机的实现思路也可参考其中的异步测试代码进行协议验证与功能扩展快速用于实际从站设备调试。1. 拆开 canopen_301_402-master主站不是“会发 SDO”就够了拿到一台带 CiA301 的 CANopen 从站设备很多人第一反应是“连上 CAN 卡发个 SDO 把目标位置写进去”。结果要么设备不进使能要么进了使能一动就飞车。问题几乎都出在把协议栈当成“发报工具”而不是先按 301 建立网络状态再按 402 把电机状态机一步一推。canopen_301_402-master 就是把主站骨架搭好的源码包底层有 SDO/PDO 读写、对象字典镜像上层有 DS402 状态机处理和命令行调试接口。适合自己集成控制器、调试第三方驱动器、或者在 ROS 里塞一个 CANopen 主站的工程师。拆它之前把 CiA301 和 DS402 的层级结构摆清楚比直接抓包有用得多。2. CiA301 与 DS402主站视角下的协议层级结构2.1 CiA301 的网络管理、对象字典与 SDO 报文格式CANopen 的“层级结构”可以拆成三层看最底下是 CAN 数据链路层中间是 CiA301 定义的通讯对象最上面是 DS402 这种设备行规。主站协议栈做的事就是把这三层粘在一起让用户不用每次手动去拼 CAN 帧。CiA301 里最关键的两个东西是对象字典和 SDO对象字典是所有数据交换的中转站SDO 则提供读写对象字典的可靠通道。例如读 DS402 从站的状态字 0x6041底层就是在 0x600nodeID 上发一条 SDO 请求从站从 0x580nodeID 回一条响应。import can bus can.Bus(interfacesocketcan, channelcan0, bitrate500000) node 1 def sdo_read(bus, node, index, subindex): msg can.Message( arbitration_id0x600 node, data[0x40, index 0xFF, (index 8) 0xFF, subindex, 0, 0, 0, 0], is_extended_idFalse, ) bus.send(msg) resp bus.recv(timeout1.0) if resp is None: raise TimeoutError(fSDO read 0x{index:04X} timeout) return resp.data # 读 DS402 状态字 0x6041 子索引 0 r sdo_read(bus, node, 0x6041, 0) print(r.hex())这段代码里0x40是 initiate upload request表示“我要读对象字典”后面两个字节是小端索引低字节在前所以 0x6041 写成了0x41, 0x60再后面是子索引。响应数据的前四个字节不是业务数据第一个字节如果等于0x4F表示读取成功如果高于0x80则后面跟着的 4 字节是 abort code。tests/test_sdo.py 里的用例基本就是在覆盖这类请求在不同数据长度下的行为比如 1 字节、2 字节、4 字节返回值时的 SDO 命令字差异。2.2 DS402 状态机为什么使能必须按顺序推DS402 给驱动器定义了一套有限状态机从Switch on disabled到Operation enabled不是一条 SDO 就能跳过去的中间必须经过Ready to switch on和Switched on。控制字 0x6040 和状态字 0x6041 是这套状态机的门把手主站通过写控制字驱动转换从站把当前状态放在状态字里回报两者要对得上后续位置、速度指令才会被执行。控制字操作值十六进制预期迁移Shutdown0x06Switch on disabled → Ready to switch onSwitch on0x07Ready to switch on → Switched onEnable operation0x0FSwitched on → Operation enabledDisable voltage0x00任意使能状态 → Switch on disabledQuick stop0x02Operation enabled → Quick stop active很多厂商的驱动器允许从任意状态直接写 0x0F 进使能但协议栈如果照着“偷懒”写法实现碰到带安全回路的驱动器就会失败。正确做法是每写一步控制字就读一次状态字 0x6041确认状态位已经切到位再发下一步。状态字里 bit0 表示 ready to switch onbit1 表示 switched onbit2 表示 operation enabledbit3 表示 fault。test_402.py 里大量断言都在检查这些位def wait_state(bus, node, mask, value, timeout2.0): deadline time.time() timeout while time.time() deadline: data sdo_read(bus, node, 0x6041, 0) status int.from_bytes(data[4:6], little) if (status mask) value: return status time.sleep(0.01) raise TimeoutError(fstate not reached, status0x{status:04X})这段代码把读到的状态字小端拼成 int再按位比较。使用时不建议直接比较全字因为厂商会在其他 bit 上塞自定义标志比如 bit7 是 warningbit10 是 target reached只匹配关心的 mask 才不会误判。2.3 心跳、节点守护与 SYNC 的职责分配CiA301 的通信对象里SDO 和 PDO 负责“数据”NMT、心跳和 SYNC 负责“秩序”。主站要在启动时给每个从站发 NMT 命令让节点进入 operational 态之后从站按 0x1017 配置的心跳时间周期发心跳帧主站在 0x1016 里配置超时时间超过时间没收到心跳就认为从站掉线或进入 fault。SYNC 则是给 PDO 一个统一的节奏主站定期发 0x80所有映射了 SYNC 的 PDO 在这个时机刷新避免总线上的电机各自乱跑。理解这层职责分配后再看 canopen_301_402-master 的源码结构就不容易迷路SDO 负责参数配置PDO 负责周期控制NMT 和心跳负责生命周期管理DS402 在它们之上只做电机状态控制。主站代码里如果只封装了 SDO 而没有心跳监控那它只能算一个调试工具不能算完整的主站协议栈。3. 源码包结构从 src、scripts 到 tests 的主站骨架3.1 目录里到底放了什么解压 canopen_301_402-master 后目录结构并不复杂但信息量不小。src/canopen_301_402_old是旧版实现适合做对比参考src下应该是当前协议栈主体scripts/clt.py是命令行入口方便在不上位机的情况下直接读对象字典、发 NMT、切 DS402 状态tests/下面有 test_402.py、test_sdo.py、test_obj.py还有 async 子目录说明这套主站既支持同步调用也考虑了 asyncio 并发场景。顶层同时存在 CMakeLists.txt、setup.py 和 package.xml说明它既能被 pip 安装也能被 CMake 项目引用甚至可以作为 ROS 包直接依赖。pip install -e . python -m pytest tests/ -v先pip install -e .把主站库装进当前 Python 环境再用 pytest 跑测试是最快的熟悉路径。需要注意的是很多测试用例会实例化can.Bus如果当前机器没接真实 CAN 卡可以用can.interfaces.virtual创建一个虚拟总线或者直接把can.Busmock 掉。tests/async 里的用例对这种并发读写的覆盖通常比同步测试更接近真实轮询场景比如两个协程同时读写同一个从站对象字典主站需要加锁或排队。3.2 主站初始化NMT 复位和 Boot-up 等待CANopen 主站上电后第一件正事不是读参数而是发送 NMT 命令复位节点。NMT 帧使用 CAN-ID0x000数据域第一个字节是命令码第二个字节是目标节点地址0表示所有节点。常用命令码包括0x01进入 operational、0x02进入 stopped、0x80进入 pre-operational、0x81复位节点、0x82复位通信。主站做完复位后从站会发一帧 boot-up 报文标识符是0x700 nodeID数据域为0x00这帧报文是主站判断“从站已经活着”的重要依据。def nmt_command(bus, cmd, node_id0): msg can.Message(arbitration_id0x000, data[cmd, node_id], is_extended_idFalse) bus.send(msg) nmt_command(bus, 0x81, 1) # 复位节点 1 boot bus.recv(timeout1.0) # 等 boot-up if boot.arbitration_id 0x700 1 and boot.data[0] 0x00: print(node 1 bootup ok)注意NMT 复位后从站会回到 pre-operational 状态此时 SDO 可用但 PDO 不传输。在这个阶段先读一遍 0x6041 和 0x6061确认从站固件支持 DS402再决定是否切到 operational。有些从站上电后就直接发 boot-up主站如果没有在启动时设置好接收过滤器可能把 boot-up 当成普通报文丢掉所以我一般会在初始化时用bus.set_filters把 0x700、0x580、0x180 这类标识符范围都接收进来。3.3 对象字典镜像与 SDO 客户端设计主站协议栈的另一个核心是“对象字典镜像”。设备端自己有一份对象字典主站也必须知道每个对象在哪个索引、子索引上能读还是能写。SDO 客户端就是在镜像上做 read/write 操作传索引、子索引、数据字节最后组帧发送。canopen_301_402_old 和当前 src 之间的差别往往就在 SDO 分帧处理上读 32 位数据用0x40请求后从站响应可能用快速传输也可能用分段传输写数据时根据长度要选0x2F、0x27、0x23等不同命令字。索引子索引对象含义访问0x10000设备类型只读0x10170心跳生产者时间ms读/写0x14001RPDO1 COB-ID读/写0x16001-8RPDO1 映射条目读/写0x60400控制字读/写0x60410状态字只读0x60600运行模式读/写0x60610模式显示只读实际操作时主轴使能通常优先写 0x6040状态确认读 0x6041。test_obj.py 检查的则是这种镜像是否和实际设备一致比如向 0x1017 写入 100ms再读回来如果设备返回的不是 100可能是单位不对或者该对象对某些子索引是只读的。4. 驱动 DS402 从站从使能到位置模式的完整流程4.1 使能序列控制字逐步推进让一个 DS402 驱动器进入可运行的Operation enabled状态完整顺序是先复位节点再设为位置模式 0x60601然后写控制字 0x60400x06确认 ready 后写 0x07确认 switched on 后写 0x0F最后状态字 bit2 置位表示使能完成。下面是一段可以直接套用的核心逻辑import time import can bus can.Bus(interfacesocketcan, channelcan0, bitrate500000) node 1 def sdo_write(bus, node, index, subindex, data_bytes): data [0x2F if len(data_bytes) 1 else 0x23, index 0xFF, (index 8) 0xFF, subindex, data_bytes[0] if len(data_bytes) 0 else 0, data_bytes[1] if len(data_bytes) 1 else 0, data_bytes[2] if len(data_bytes) 2 else 0, data_bytes[3] if len(data_bytes) 3 else 0] bus.send(can.Message(arbitration_id0x600 node, datadata, is_extended_idFalse)) rsp bus.recv(timeout1.0) if rsp is None or rsp.data[0] ! 0x60: raise RuntimeError(fSDO write to 0x{index:04X} failed) # 1. 位置模式 sdo_write(bus, node, 0x6060, 0, [0x01]) # 2. Shutdown sdo_write(bus, node, 0x6040, 0, [0x06, 0x00]) wait_state(bus, node, 0x0001, 0x0001) # 3. Switch on sdo_write(bus, node, 0x6040, 0, [0x07, 0x00]) wait_state(bus, node, 0x0003, 0x0003) # 4. Enable operation sdo_write(bus, node, 0x6040, 0, [0x0F, 0x00]) wait_state(bus, node, 0x0007, 0x0007)sdo_write里对 1 字节数据用0x2F对 4 字节数据用0x23这是快速 SDO 写协议的两种命令字。控制字 0x6040 在对象字典里被定义为 16 位对象所以传[0x06, 0x00]时按两字节写入命令字仍要视作 4 字节处理实际多数设备会忽略高位但保险起见我用0x23而不是0x2F。如果设备在写 0x06 后一直不置位 ready 位就要先检查 0x6061 确认当前模式是不是真在位置模式或者该驱动器的使能条件还包括急停端子状态。4.2 PDO 映射把控制字和目标位置塞进一帧SDO 的响应式读写适合配置参数但位置控制如果每个周期都走 SDO总线响应时间和从站处理时间都吃不消。DS402 场景下更常见的做法是用 RPDO 从主站往从站发控制字和目标位置用 TPDO 把状态字和实际位置传回主站。PDO 不需要应答一帧 8 字节足够装 16 位控制字和 32 位目标位置。# 清除 RPDO1 映射 sdo_write(bus, node, 0x1600, 0, [0x00, 0x00, 0x00, 0x00]) # 映射条目0x6040 子索引 0长度 16 位 sdo_write(bus, node, 0x1600, 1, [0x10, 0x00, 0x40, 0x60]) # 映射条目0x607A 子索引 0长度 32 位 sdo_write(bus, node, 0x1600, 2, [0x20, 0x00, 0x7A, 0x60]) # 启用映射条目数为 2 sdo_write(bus, node, 0x1600, 0, [0x02, 0x00, 0x00, 0x00])映射对象 0x1600 的子索引 0 是映射条目计数子索引 1 开始是映射条目。每个映射条目的 4 字节结构是高两字节为对象索引第三字节为子索引最低字节为数据位长度。所以0x6040的 16 位映射写成小端就是[0x10, 0x00, 0x40, 0x60]0x607A 的 32 位映射写成[0x20, 0x00, 0x7A, 0x60]。映射写完后通常需要复位节点或从新进入 pre-operational 才能生效这一步最容易踩坑。配置完映射后直接发 RPDO从站可能毫无反应因为它还在用旧映射。我一般会在 PDO 配置完成后按这种顺序重建链路复位节点 → 等待 boot-up → 切 operational → 读 0x1A00 验证 TPDO 映射。RPDO 的默认 CAN-ID 是0x200 nodeID数据域前两字节控制字后四字节目标位置。4.3 位置模式参数单位、速度与加减速DS402 的设备侧参数不像步进电机那样直接给脉冲数很多驱动器把位置单位映射到编码器分辨率和机械传动比上。0x608F 是位置编码器分辨率0x6091 是电机轴转数与负载轴转数的齿轮比0x607A 是目标位置0x6081 是轮廓速度0x6083 和 0x6084 分别是轮廓加速度和减速度。索引对象说明0x607A目标位置位置模式下给定目标值0x6081轮廓速度位置模式下最大速度0x6083轮廓加速度加速斜率0x6084轮廓减速度减速斜率0x60FF目标速度速度模式给定0x6046速度比例因子数字量到物理量换算实际调试时我会先把 0x6081 设到较小值比如编码器每秒转 1000 个脉冲再写 0x607A观察速度。如果电机只抖不动回读 0x6061大多数情况是模式被从站切换回了默认的速度模式。还可以通过 0x6041 的 bit10 target reached 判断是否到位这个 bit 比单纯看位置误差更可靠因为某些驱动器在使能时就会把 target reached 置位。5. 测试与排错这套主站怎么在总线上站稳5.1 从 tests 看协议栈验证边界test_402.py 的用例通常会把 6040/6041 的状态位迁移一分不差地跑一遍test_sdo.py 则关注 abort code 和字节序test_obj.py 检查对象字典在“写入后读回”是否一致。本地跑测试时建议先跑python -m pytest tests/test_obj.py确认基础对象字典没有问题再跑 test_402因为它会大量依赖 SDO 读写函数。如果 tests/async 目录里的用例失败先检查是不是并发发送 SDO 导致节点没来得及响应CANopen 主站实现里必须用一个发送队列把所有 SDO 请求串行化。5.2 SDO abort code 与心跳超时怎么看从站返回的 SDO 响应中如果第一个字节是0x80后面四字节就是 abort code。最常见的几个abort code含义0x06010000访问方式不支持对象只读却尝试写0x06020000对象字典中不存在该索引0x06090011子索引不存在0x08000020数据无法传输或存储0x08000021本地控制导致不允许传输心跳超时是另一类高频故障。主站配置了 0x1016 消费者时间从站 0x1017 设 100ms如果总线上超过 150ms 没看到心跳帧主站应立即进入故障处理。注意 0x1017 的单位是毫秒且最小值为 00 表示禁止发送心跳。测试时可以用一个 CAN 报文工具周期发送 0x700nodeID 数据域为状态字节模拟从站心跳来判断主站监控逻辑是否真的在看帧。5.3 用 clt.py 逐条验证使能链路最后回到 scripts/clt.py这个命令行脚本最大的价值是“单步调试”。使能失败时不需要写完整 Python 程序一条一条命令就能定位卡点python scripts/clt.py -i can0 -n 1 read 0x6041 python scripts/clt.py -i can0 -n 1 write 0x6040 0x06 python scripts/clt.py -i can0 -n 1 read 0x6041 python scripts/clt.py -i can0 -n 1 write 0x6040 0x07 python scripts/clt.py -i can0 -n 1 read 0x6041 python scripts/clt.py -i can0 -n 1 write 0x6040 0x0F具体参数名以仓库里 clt.py 解析的参数为准但思路一致每写一个控制字就回读一次状态字看 bit 是否按预期翻转。如果写在 0x06 后状态字一直是0x0040switch on disabled说明从站没有进入 ready问题一般不在协议栈而在驱动器的使能引脚或模式配置如果状态字已经显示 operation enabled但一给目标位置就报警那就去看 0x603F 错误码先清掉 fault 再重新走使能序列。主站代码再复杂调试时也得回到这种最原始的读状态字、写控制字循环里来。本文还有配套的精品资源点击获取