canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
摘要:canopennode-rtt 将 CANopenNode 接入 RT-Thread,可同时承载设备与控制器角色;本文还对比 RTT-CanFestival 的架构、CiA 402、维护与选型差异。
仓库地址:https://github.com/wdfk-prog/canopennode-rtt
在线文档:https://wdfk-prog.space/canopennode-rtt/
上游协议栈:https://github.com/CANopenNode/CANopenNode
本文基于canopennode-rttmaster分支 2026-09-01 的公开状态整理,参考提交:b5a9dc1a242d784358ebc8a5bf81d5afcdc8f4ba。
@[toc]
如果你正在 RT-Thread 上做伺服驱动、运动控制器、工业 I/O、传感器节点、执行器、网关或者任何需要 CANopen 的 MCU 项目,那么我比较推荐关注一下这个仓库:wdfk-prog/canopennode-rtt。
它不是重新写了一套 CANopen 协议栈,而是把成熟的 CANopenNode V4 作为上游核心,通过 RT-Thread 的 CAN 设备框架、线程、信号量、互斥锁、Kconfig、SCons、DFS/EEPROM 等基础设施,把协议栈真正包装成一个适合 RT-Thread 工程使用的软件包。
这里我想先强调一个很容易被名字或者传统项目结构限制住的点:
这个软件包并不应该被理解成“CANopen 从站软件包”。它当然可以很方便地做从站,但它同样可以利用 CANopenNode 已有的 NMT Master、SDO Client、LSS Master、SYNC Producer、Heartbeat Consumer、PDO 等能力,构建 MCU 侧的 CANopen 主站、控制器或者 commissioning 工具。
更准确地说,canopennode-rtt 是一个“CANopenNode 的 RT-Thread 集成与运行时层”。它本身保持角色中立,最终设备在 CANopen 网络里承担什么角色,由你打开哪些协议能力、怎样设计 Object Dictionary,以及应用层如何组织网络决定。
这也是我认为这个仓库比较有价值的地方:它没有为了“做一个从站 Demo”把能力封死,而是尽量保留 CANopenNode 原本的组合能力,让同一个 MCU 既可以作为一个正常 CANopen 节点运行,也可以同时去管理、配置和控制总线上的其他节点。
下面我从工程角度把这个软件包为什么值得用、它现在已经具备什么能力、为什么可以同时做从站和主站、RT-Thread 侧是怎样组织运行时的,以及后续 CiA 402(历史上也常称 DS402)规划到了什么边界,完整梳理一遍。
1. 为什么不直接把 CANopenNode 源码扔进 RT-Thread 工程?
CANopenNode 本身已经是一套成熟的开源 CANopen 协议栈,它提供了 CiA 301 核心通信对象,并包含 LSS、Gateway、Safety 等扩展能力。问题是,上游协议栈故意把硬件相关部分抽象成 target driver,因此“协议代码能编译”与“在 RT-Thread 项目里好用”其实是两回事。
如果直接把 CANopenNode 源码复制进一个 BSP,通常很快就会遇到几类工程问题:
- CAN 控制器怎样与
rt_device设备框架对接? - CAN RX 回调到底在中断里做多少工作,怎样把协议处理安全地移到线程上下文?
CO_process()、SYNC、RPDO、TPDO 应该跑在同一个线程还是拆开?- realtime 周期从哪里来,微秒级
timeDifference_us怎样得到? - CANopenNode 的各种
CO_CONFIG_*宏怎样与 RT-Threadmenuconfig对接? - 开 SDO Client、SRDO、Gateway 后,SCons 怎样只编译真正需要的源文件?
- Object Dictionary 是用 Demo 还是产品自己的 XDD 生成文件?
- 0x1010/0x1011 参数保存怎样接 DFS、EEPROM 或用户自己的非易失存储?
- 一个工程里如果既要做 SDO Server,又要做 SDO Client,锁、生命周期和线程上下文怎样约束?
- Communication Reset 以后对象被重新创建,回调、线程、存储、过滤器、测试夹具怎样重新绑定?
这些问题都不是 CANopen 规范本身的问题,而是“协议栈如何产品化接入 RTOS”的问题。
canopennode-rtt 的主要工作正是在这里。仓库 README 对自己的定位写得很清楚:CANopen 协议核心来自 CANopenNode git 子模块,本仓库负责 RT-Thread target driver、package wrapper、Kconfig/SCons、运行线程、storage backend、Demo OD 和相关验证支持。
换句话说,它把最容易在每个项目里重复造一遍的胶水层集中维护了。
1.1 整体分层
先看整个软件包的位置。重点观察:CANopenNode core 没有被 RT-Thread 私有逻辑侵入,RT-Thread 相关代码集中在 wrapper 与 target driver 层。
1 | flowchart TB |
这张图其实就是仓库最核心的价值:你仍然使用上游 CANopenNode 的协议实现,而不是 fork 出一套越来越难升级的私有协议栈;RT-Thread 特有的线程、设备、存储和构建逻辑则由软件包统一处理。
1.2 为什么使用 git submodule
仓库把 CANopenNode/ 作为 git 子模块固定在明确 revision 上。这个做法对于协议栈集成很重要,因为“CANopenNode V4”并不是一个永远静止的 API。上游的配置宏、Object Dictionary API、SDO Client、Gateway、driver contract 都会随着版本演进。
通过子模块固定 revision,可以让以下几件事变得可追踪:
- 当前 RT-Thread port 到底对应哪一个 CANopenNode 版本;
- 升级上游时能明确看到协议核心版本变化;
- CI 和目标板验证可以绑定到同一个上游 commit;
- 产品工程不会因为某次无意中的
git pull直接换掉协议核心。
因此首次拉取仓库时要使用:
1 | git clone --recursive https://github.com/wdfk-prog/canopennode-rtt.git |
已经克隆但缺少子模块时则执行:
1 | git submodule update --init --recursive |
如果编译时出现 CANopenNode/301 缺失,优先检查子模块,而不是手工从别的版本复制一份源码进来。
2. 先把“从站”和“主站”说清楚:CANopen 不是只有一个 Master/Slave 开关
很多人第一次接触 CANopen 时会把它类比 Modbus RTU:一个 Master 对多个 Slave,然后自然会问“这个协议栈到底是主站还是从站?”
CANopen 实际上更适合从“协议对象的角色组合”来理解。
一个节点可以同时具有:
- NMT Slave;
- NMT Master;
- SDO Server;
- SDO Client;
- Heartbeat Producer;
- Heartbeat Consumer;
- RPDO 接收;
- TPDO 发送;
- SYNC Consumer;
- SYNC Producer;
- TIME Consumer;
- TIME Producer;
- LSS Slave;
- LSS Master。
也就是说,“设备自己是一个 CANopen 节点”和“设备控制其他 CANopen 节点”并不冲突。
例如一个运动控制 MCU 可以有自己的 Node-ID 1,对外提供自己的 SDO Server、Heartbeat 和状态 PDO;与此同时,它还可以作为 NMT Master 向 Node 2、3、4 发送 NMT 命令,通过 SDO Client 给远端驱动写参数,通过 Heartbeat Consumer 监控远端节点,通过 SYNC Producer 驱动同步 PDO 周期。
这时你把它叫“主站”“控制器节点”或者“带本地 CANopen 节点能力的控制器”都可以,但从实现上看,它仍然是多个 CANopen 角色的组合。
2.1 角色矩阵
下面这张图可以直观看到:同一个 CANopenNodeRTT 实例并不是只能选左边或者右边。
1 | flowchart LR |
这也是为什么我特别不建议把 canopennode-rtt 宣传成“CANopen 从站包”。这样会把它已经暴露出来的很多能力直接忽略掉。
2.2 上游 CANopenNode 本身就支持这些角色
CANopenNode 官方 README 明确列出了:
- NMT slave,同时提供 simple NMT master;
- SDO server;
- SDO client,可访问网络中其他 CANopen 设备的 Object Dictionary;
- Heartbeat producer/consumer;
- SYNC producer/consumer;
- TIME producer/consumer;
- LSS master/slave;
- CiA 309-3 ASCII gateway,映射 NMT master、LSS master 和 SDO client。
canopennode-rtt 做的事情,是把这些能力通过 Kconfig、SCons 和 RT-Thread runtime 暴露出来,而不是在 port 层把它们删掉。
当前配置中已经可以看到很明确的主站侧选项:
| 能力 | 关键 Kconfig | 当前含义 |
|---|---|---|
| NMT Master | PKG_CANOPENNODE_NMT_MASTER |
允许本节点发送简单 NMT master 命令 |
| SDO Client | PKG_CANOPENNODE_USING_SDO_CLIENT |
访问远端节点 OD |
| Heartbeat Consumer | PKG_CANOPENNODE_USING_HB_CONS |
监控远端节点在线与 NMT 状态 |
| SYNC Producer | PKG_CANOPENNODE_SYNC_PRODUCER |
本节点产生 SYNC |
| LSS Master | PKG_CANOPENNODE_USING_LSS_MASTER |
commissioning / gateway 场景 |
| ASCII Gateway | PKG_CANOPENNODE_USING_GATEWAY_ASCII |
编译 CiA 309-3 gateway helper |
| EMCY Consumer | PKG_CANOPENNODE_EM_CONSUMER |
接收远端节点 EMCY |
因此从能力基础上说,“可以作为主站使用”是有明确代码和配置支撑的,不是一个宣传口号。
但这里也要把边界说严谨:
这些能力可以组成主站或控制器应用,不等于仓库现在已经提供了一套面向所有场景的完整 CANopen Network Manager。
例如远端节点发现策略、设备类型识别、上电编排、参数下发顺序、故障恢复、设备替换、CiA 402 控制状态机等,仍然属于应用层或 profile 层需要设计的内容。
这反而是合理的边界:基础通信能力在软件包里,产品策略留给产品。
3. RT-Thread 侧不是简单“起一个线程跑 CO_process”
这个软件包另一个值得推荐的点,是它没有把所有协议处理粗暴地塞进一个 while(1) 里。
当前运行模型把 CAN 接收、实时 CANopen 路径和 mainline 异步处理分开:
co_rx:CAN RX 辅助线程;co_rt:realtime CANopen 处理线程;co_main:mainline 处理线程;co_tmr:周期 timer,用于唤醒 realtime 路径。
README 和集成文档给出的职责非常清楚:
- RX 线程从 RT-Thread CAN 设备读取帧并分发到 CANopenNode RX callback;
- realtime 线程处理 SYNC、SRDO、RPDO、TPDO;
- mainline 线程处理 NMT、Heartbeat、SDO、Emergency、Storage、LSS、Gateway 等异步逻辑。
3.1 运行时数据流
观察下面这张图时,重点看 RX 与 realtime/mainline 的解耦。CAN 驱动回调只负责唤醒,不把复杂协议逻辑堆在 ISR 里。
1 | flowchart TB |
这种拆法对 MCU 很实用,因为 PDO/SYNC 路径与 SDO、Storage 之类的异步工作对时延的要求明显不同。
3.2 默认优先级关系
当前配置指南给出的默认线程参数为:
| 线程 | 默认优先级 | 默认栈 |
|---|---|---|
| RX helper | 2 | 2048 B |
| realtime | 3 | 2048 B |
| mainline | 10 | 2048 B |
RT-Thread 的优先级数值越小,优先级越高,因此默认关系满足:
1 | RX helper priority <= realtime priority < mainline priority |
这不是说所有项目都必须照抄这三个数字,而是说明这个软件包已经把不同路径的实时性等级考虑进运行模型了。
如果你的 BSP 里还有 EtherCAT、中断密集 ADC、运动控制 fast loop、USB、文件系统或者高优先级安全任务,那么仍然应该基于整个系统重新做优先级预算;但至少 CANopen 这一侧已经有清晰边界,而不是隐藏在应用主线程里。
3.3 为什么不在 CAN ISR 直接跑 CANopenNode API
CO_driver_target.h 中已经明确说明,本 RT-Thread port 运行在 RT-Thread 线程上下文:co_rx 分发接收 callback,co_rt 处理实时路径,co_main 处理异步路径。可能获取 port 锁的 CANopenNode API 不应直接从 ISR 调用,需要把工作延后到线程。
这条约束很重要。
很多嵌入式协议栈第一次能跑起来,靠的是“中断一来直接处理”;等项目复杂后就开始遇到锁、优先级反转、不可重入 API、长 ISR、文件系统调用等问题。现在把线程所有权写清楚,后续应用层调用 SDO Client、Gateway、OD extension 时就有明确依据。
4. 这个软件包真正解决的是“RT-Thread 工程化集成”
如果只看 CANopenNode 上游源码,很多功能本来就有,所以有人可能会问:既然上游已经有 NMT、SDO、PDO、LSS,那么这个 RT-Thread 软件包到底增加了什么?
我认为主要价值可以概括为五层:设备接入、运行时、配置裁剪、持久化,以及可验证性。
4.1 RT-Thread CAN 设备绑定
软件包通过 RT-Thread CAN 设备框架绑定实际 CAN 控制器。默认配置项是:
1 | PKG_CANOPENNODE_CAN_DEV_NAME="can1" |
实际项目里可以改成 can0、can1 或 BSP 导出的其他 CAN 设备名。初始化阶段会通过 RT-Thread 设备框架查找、打开并配置 CAN 设备,而不是在协议层直接依赖 STM32 HAL、FDCAN HAL 或某一个具体芯片寄存器。
这带来的好处是:只要 BSP 的 CAN 驱动已经按 RT-Thread 设备模型工作,协议栈移植层通常不需要跟着 MCU 型号重写。
4.2 CAN 接收与硬件过滤器
CANopenNode 本身需要多个 RX buffer 来匹配不同 COB-ID。canopennode-rtt 可以选择使用 RT-Thread CAN HDR 硬件过滤器:
1 | PKG_CANOPENNODE_USING_RTT_CAN_FILTER |
如果 BSP 支持硬件过滤器并且 filter bank 足够,软件包会尽量使用硬件过滤;如果资源不足或配置失败,则可以回退到软件 RX 分发。
这点对 STM32 之类硬件过滤 bank 数量有限的平台很实用,因为“开了硬件过滤器”不应该变成“一旦资源不够整个协议栈启动失败”的单点风险。
4.3 Kconfig 与 SCons 不只是提供几个开关
软件包不是把整个 CANopenNode 目录无脑加入编译,而是根据 Kconfig 选择真正需要的源文件。
例如:
- 开启 SDO Server,构建
CO_SDOserver.c; - 开启 SDO Client,构建
CO_SDOclient.c; - 开启 TIME,加入
CO_TIME.c; - 开启 LSS Master/Slave,加入对应 LSS 源文件;
- 开启 Gateway,加入 CiA 309-3 相关实现;
- 开启 SRDO/GFC,加入 CiA 304 相关实现;
- 开启 Demo NMT Master Test,才把对应测试夹具编进固件。
这对于 MCU 很关键。CANopenNode 是一个可裁剪协议栈,如果 RT-Thread 软件包最后把所有功能都硬编进去,就失去了原本的设计优势。
4.4 CO_CONFIG_* 与 RT-Thread 配置统一
CANopenNode V4 的很多能力通过 CO_CONFIG_* 宏决定,而 RT-Thread 用户通常希望所有组件都能在 menuconfig 中统一配置。
port/rtthread/CO_config_rtt.h 就承担这层映射:上层选 PKG_CANOPENNODE_USING_SDO_CLIENT,最终转换为上游 CANopenNode 所需的 SDO Client 配置;选择 Gateway、FIFO、CRC、LSS 等功能时,也按照依赖关系组合对应宏。
因此使用者不需要一边改 rtconfig.h,一边再手工维护一份完全独立的 CO_config.h。
4.5 Communication Reset 生命周期
CANopen 的 Communication Reset 不是简单把一个状态变量清零。协议通信对象可能需要重新初始化,callback、OD binding、Heartbeat Consumer、Storage、CAN mode 等相关运行状态都要重新建立。
canopennode-rtt 把这些动作放进统一的 CANopenNodeRTT 生命周期中,让应用层不必自己拼一个“reset 后重新造 CO_t”的私有流程。
这也是为什么仓库后续做主站能力、Gateway、Demo fixture 时,可以在同一个 wrapper 生命周期里重绑定,而不需要每个功能自己维护一套对象所有权。
5. 从站怎么用:默认路径已经足够适合多数 CANopen 设备
先看大家最熟悉的设备/从站场景。
一个典型 CANopen MCU 设备至少会有:
- 自己的 Node-ID;
- NMT Slave;
- Heartbeat Producer;
- Object Dictionary;
- SDO Server;
- 根据业务需要配置 RPDO/TPDO;
- 可能还会有 EMCY、SYNC、TIME、LSS Slave 和参数持久化。
canopennode-rtt 默认配置就偏向这类 bring-up 场景:SDO Server、PDO、SYNC、LSS Slave 等基础能力可以直接通过 Kconfig 启用。
5.1 最短 bring-up 链路
从工程角度,第一次把节点跑起来建议只追求一条最短链路:
1 | flowchart LR |
第一次联调不要一上来就把 LSS、SRDO、Gateway、Storage、几十个 PDO 全部打开。先确认 CAN 物理层和基础 CiA 301 链路正常,然后再加能力,定位问题会简单很多。
5.2 最基本的配置
在 RT-Thread 工程里至少需要:
1 | RT_USING_HEAP |
首次 demo 可以继续保留:
1 | PKG_CANOPENNODE_APP_AUTO_INIT=y |
然后确认:
1 | PKG_CANOPENNODE_CAN_DEV_NAME="can1" |
这里 bitrate 单位是 kbit/s,因此 1000 对应 1 Mbit/s。
5.3 自动初始化和手动初始化
单节点 Demo 最方便的是 PKG_CANOPENNODE_APP_AUTO_INIT=y,软件包会在 RT-Thread 应用初始化阶段创建默认实例。
如果产品希望自己控制对象生命周期,可以关闭 auto init,然后显式初始化:
1 |
|
有两个细节不要忽略:
第一,CANopenNodeRTT 首次使用前要零初始化。上面使用静态对象天然满足这一点。
第二,传入的 CAN 设备名需要在实例生命周期内保持有效,因为 wrapper 保存的是字符串指针,而不是复制一份名字。
5.4 怎样确认从站真的起来了
如果 Node-ID 是 1,节点完成启动后应该能看到 CANopen Boot-up:
1 | COB-ID: 0x701 |
接下来再使用主站工具或者测试程序访问 SDO Server。
只要 Boot-up 和 SDO 这两步正常,说明至少下面这条链已经成立:
1 | RT-Thread CAN driver |
再往后才是产品自己的 PDO mapping、EMCY、参数持久化和应用状态机。
6. Object Dictionary:Demo 能让你跑起来,但产品一定要换成自己的 OD
CANopen 设备开发里,Object Dictionary 不是“一个用于调试的表”,而是设备通信模型的中心。
设备的通信参数、应用变量、PDO mapping、Identity、Heartbeat、Storage 参数,以及后续设备 profile 对象,都围绕 OD 组织。
仓库提供:
1 | examples/demo_device/ |
这套 Demo 的意义是帮助首次 bring-up 和自动测试,不应该原样变成产品 OD。
6.1 推荐的产品工作流
比较推荐的做法是把 XDD/EDS 作为设备描述的 source of truth,然后用 CANopenEditor 生成:
1 | XDD / 项目定义 |
应用只去绑定生成后的对象,不在运行时凭空发明一套“隐藏 OD”。
这种方式尤其适合后续做 CiA 402、复杂 PDO、多轴对象或者产品参数持久化,因为 EDS/XDD 本身就是主站工具、调试工具和设备集成的重要交付物。
6.2 Demo OD 里为什么有 0x2300~0x2303
当前仓库为了协议自动验证,在 Demo OD 里增加了一些 test-only 诊断对象,例如:
0x2300:TIME consumer 诊断;0x2301:远端 EMCY consumer 诊断;0x2302:GFC 测试记录;0x2303:MCU SDO Client 自动测试状态。
这些对象的定位是“测试可观察性”,而不是新的 CANopen 标准对象,更不是产品 API。
这一点在写产品 EDS/XDD 时要分清。Demo 测试对象可以帮助开发阶段自动收集证据,但产品对象字典应该按自己的设备模型重新定义。
7. 主站怎么做:不是换一套协议栈,而是在同一实例上打开控制器能力
现在进入这篇文章最想强调的部分。
如果你要让 RT-Thread MCU 控制其他 CANopen 节点,不需要把 canopennode-rtt 换成另一套“Master Stack”。CANopenNode 本身已经有需要的基础对象,当前 RT-Thread wrapper 也已经把相应编译与运行入口接出来。
一个最小的 CANopen 控制器通常至少需要:
- NMT Master:让远端节点进入 Operational、Stopped、Pre-operational,或者做 reset;
- Heartbeat Consumer:不能只发命令,还要确认远端节点真的在线、状态真的变了;
- SDO Client:读取 Identity、配置 PDO、写设备参数;
- PDO:运行阶段收发过程数据;
- 如果使用同步运动或者同步采样,还需要 SYNC Producer;
- 如果要在现场分配 Node-ID/bitrate,可以加 LSS Master;
- 如果希望通过串口/Console 暴露标准控制接口,可以加 CiA 309-3 Gateway。
7.1 一个典型 MCU 主站结构
下面这张图不是说所有项目必须照这个层次实现,而是展示 canopennode-rtt 已经能提供哪些“积木”。
1 | flowchart TB |
这里真正需要你在产品层补充的是“编排策略”:什么时候发现设备、什么时候配置、哪些 SDO 失败允许重试、启动前检查哪些 Identity、心跳掉线后怎样处理、PDO 映射是谁负责、设备恢复后是否允许自动重新进入 Operational。
基础协议对象不需要从零写。
7.2 NMT Master 已经不是纸面能力
仓库当前已经有专门的 NMT Master 自动测试模块:
1 | port/rtthread/demo/CO_demo_nmt_master.c |
对应配置:
1 | PKG_CANOPENNODE_DEMO_NMT_MASTER_TEST=y |
它会自动选择:
1 | PKG_CANOPENNODE_NMT_MASTER |
测试中,RT-Thread MCU 自己仍然保留本节点的 NMT Slave、Heartbeat、SDO、PDO 等功能,同时调用:
1 | CO_NMT_sendCommand(...) |
去控制远端节点。
这正好证明了前面那句话:本地“从站侧能力”与对远端的 NMT Master 能力并不互斥。
7.3 NMT Master 自动序列
当前测试不是“发一帧看总线上有没有数据”,而是把 Heartbeat Consumer 也纳入状态确认:
1 | sequenceDiagram |
正式测试序列包含 START、STOP、PREOP、RESET_COMM、RESET_NODE、再次 START,并对远端状态迁移进行确认。
所以这里的价值并不只是“CANopenNode 有一个 CO_NMT_sendCommand() 函数”,而是 RT-Thread wrapper 已经开始围绕这个能力建立 MCU 侧验证路径。
7.4 但它不等于完整 Network Manager
当前 NMT Master 测试文档自己也明确说明:它验证的是 simple NMT Master command producer 与远端节点控制,不等价于完整 NMT network manager。
这是一个很重要的工程边界。
一个真正的产品级主站往往还需要:
- 设备清单和拓扑配置;
- Expected Node-ID;
- Identity 校验;
- 启动参数下载;
- PDO mapping 下发;
- 设备 Ready 条件;
- 故障恢复策略;
- 网络状态机;
- 产品自己的诊断与日志;
- 对不同设备 profile 的控制器逻辑。
这些逻辑没有必要塞进通用 RT-Thread port,它应该属于更上层的 controller application 或 profile module。
8. SDO Client:主站真正做参数配置时最常用的能力之一
NMT 只能改变节点网络管理状态,真正做设备配置通常离不开 SDO Client。
从上游 CANopenNode 的定义看,SDO Server 用来暴露“本节点”的 Object Dictionary,SDO Client 则用来访问“其他节点”的 Object Dictionary。对于主站/控制器来说,后者通常负责:
- 读取
0x1000Device Type; - 读取
0x1018Identity Object; - 修改 Heartbeat 参数;
- 配置远端 PDO Communication Parameter;
- 配置远端 PDO Mapping Parameter;
- 写入厂商自定义参数;
- 读取错误记录或诊断对象;
- 在设备 profile 中下发模式、限制值或初始化参数。
当前软件包直接提供:
1 | PKG_CANOPENNODE_USING_SDO_CLIENT |
配置指南对 PKG_CANOPENNODE_USING_SDO_CLIENT 的描述也很直接:用于 master、gateway 或本地工具访问远端节点。
8.1 为什么 SDO Client 不能简单写成一个阻塞函数
在 PC 上写测试工具时,我们很容易写出这种伪代码:
1 | write_sdo(node, index, subindex, value); |
但 MCU/RTOS 环境里,如果一个 SDO transfer 因远端节点掉线卡住几百毫秒,主线程不能因此把整个 CANopen runtime 阻塞掉。
CANopenNode 的 SDO Client 本身是非阻塞状态机,这意味着更适合在 co_main 中周期推进。产品层只需要在它外面维护自己的 request/result 生命周期,而不是把线程 sleep 当协议状态机。
当前 Demo 已经有 PKG_CANOPENNODE_DEMO_SDO_CLIENT_TEST,并通过 test-only OD 0x2303 发布 request、active、completion sequence 和原生 SDO 结果,用来验证 MCU 侧非阻塞 SDO Client。
这类测试设计很有意义,因为它把“API 能编译”提升到了“运行时状态机能够被 Host 驱动和观察”的层次。
8.2 主站应用层应该怎样组织 SDO
推荐把 SDO Client 当成一个异步资源,而不是到处直接调用。
例如一个控制器可以设计成:
1 | Device Manager |
是否允许并发多个 Client、是否给不同远端节点配置多个 SDO Client channel,取决于 Object Dictionary 与资源设计,但总原则是不要把网络管理策略塞进底层 driver。
9. Heartbeat Consumer:主站不能只会发命令,还必须知道对方到底活没活
如果一个所谓“主站”只有 NMT 发送和 SDO 写入,却不知道远端节点是不是在线,那它只能算一个 CAN 发包器。
Heartbeat Consumer 是控制器应用非常重要的基础能力。
当前软件包默认就支持:
1 | PKG_CANOPENNODE_USING_HB_CONS=y |
并且 NMT Master 自动测试把 Heartbeat Consumer 作为远端状态确认的核心证据。
这个设计很值得借鉴:
1 | 发出 NMT START |
对于产品主站也是一样。CAN 控制器返回“帧发送成功”只能证明本地驱动接受了发送请求,不能证明远端节点执行了命令。
9.1 Heartbeat Consumer 还能承担什么
在真实项目中,它通常还能作为:
- 节点上线发现;
- 节点掉线检测;
- 远端 NMT 状态观测;
- reset 后重新上线判定;
- 自动重配置触发条件;
- 系统故障降级条件;
- HMI/上位机节点健康状态来源。
如果后续做 CiA 402 Controller,Heartbeat Consumer 仍然会是“这个驱动节点是否存在、当前是否允许继续控制”的基础网络证据之一。
10. SYNC Producer + PDO:做实时控制时,主站能力就不只是 SDO 配参数
CANopen 运行阶段真正高频的数据交换主要依靠 PDO,而不是 SDO。
在一个控制器场景中,可能存在:
- 主站向多个驱动发送目标位置、目标速度、Controlword;
- 各驱动反馈 Statusword、实际位置、实际速度;
- 所有驱动按照同一个 SYNC 周期更新同步 PDO;
- 控制器在每个 SYNC 周期形成新的 process image。
当前配置中:
1 | PKG_CANOPENNODE_USING_SYNC=y |
这些并不是“只为从站准备”的能力。
对于 controller application 来说,SYNC Producer 尤其关键。它可以让 MCU 成为同步网络的节拍来源,而 RPDO/TPDO 则承担周期过程数据交换。
10.1 realtime 线程为什么要单独存在
前面说到 co_rt 专门处理:
1 | SYNC -> RPDO -> TPDO -> SRDO |
这就是为了让周期过程数据路径与 SDO、Storage、Gateway 等异步逻辑分开。
对于普通 I/O 节点,这能降低同步 PDO 抖动;对于未来运动控制 profile,这个架构边界更重要。
当前 CiA 402 规划 Issue 中已经明确提出:同步模式未来需要在 CO_process_RPDO() 与 CO_process_TPDO() 之间插入有界、非阻塞的 profile fast path,而不去修改上游 CANopenNode core。
这说明现有 realtime 架构不是为了 Demo 临时搭出来的,它已经为后续同步控制场景留出了合理插入点。
11. LSS Master 和 CiA 309-3 Gateway:让 MCU 不只是“设备”,还可以做现场配置入口
CANopen 项目在批量设备、现场调试和售后阶段,经常会遇到 Node-ID、bitrate 和远端参数配置问题。
这时 LSS 和 Gateway 的价值就体现出来了。
11.1 LSS Slave 与 LSS Master 都可以选
当前软件包默认有 LSS Slave:
1 | PKG_CANOPENNODE_USING_LSS_SLAVE=y |
用于让外部 LSS Master 配置本节点。
同时也提供:
1 | PKG_CANOPENNODE_USING_LSS_MASTER |
用于本 MCU 去配置其他节点。
这意味着同一套软件包既能做“被配置设备”,也能做 commissioning controller。
11.2 CiA 309-3 ASCII Gateway
CANopenNode 上游 Gateway ASCII 将 NMT Master、SDO Client 和 LSS Master 映射成 CiA 309-3 ASCII 命令接口。
canopennode-rtt 当前也提供:
1 | PKG_CANOPENNODE_USING_GATEWAY_ASCII |
以及 RT-Thread 侧 CO_gateway_RTT.c/.h helper。
这里的边界是:软件包负责编译和运行 gateway helper,实际命令从哪里来由应用决定。你可以把它接到:
- RT-Thread FinSH/MSH;
- UART;
- USB CDC;
- TCP/串口网关;
- 产品自己的 CLI。
于是 MCU 本身就可以成为一个现场 CANopen 工具入口。
例如:
1 | flowchart LR |
对于没有 Linux 主机、只有 MCU + CAN + 一个维护串口的工业设备,这种模式非常实用。
12. event-driven mainline:不必永久用固定 1 ms 轮询烧 CPU
当前 master 已经加入 event-driven mainline scheduling。
配置项:
1 | PKG_CANOPENNODE_GLOBAL_TIMERNEXT |
开启后,wrapper 会利用 CANopenNode 的 timerNext_us 和 callback-pre 机制,通过 RT-Thread event 唤醒 mainline,而不是无条件固定 1 ms 轮询所有异步对象。
如果关闭,则仍保留原有固定轮询路径。
12.1 这个功能为什么有价值
在一个资源不算紧张的 MCU 上,1 ms 轮询听起来没什么。但当系统同时有:
- 多个通讯协议;
- 文件系统;
- 显示;
- 低功耗需求;
- 电机控制;
- 大量中断;
- 多个 CANopen 实例;
固定高频唤醒就会逐渐成为不必要的 CPU 占用来源。
timerNext_us 的思路是让协议对象告诉调度器“下一次最早什么时候需要再处理”,再配合 callback-pre 在新事件到达时提前唤醒。
这类改动看起来不像“多支持了一个协议”,但它对一个 RTOS 软件包的成熟度很关键:协议栈开始从“能跑”向“能与其他实时任务长期共存”演进。
12.2 realtime 路径仍保持独立
event-driven mainline 并没有把 realtime path 也改成事件聚合。
当前 realtime 仍然由周期 timer -> semaphore -> co_rt 驱动,用于 SYNC/RPDO/TPDO/SRDO。
也就是说,异步路径可以尽量少唤醒,而同步实时路径继续保持明确周期,两者边界没有混在一起。
13. 高分辨率时间:CANopenNode 的微秒时间基准终于不必只靠 RTOS Tick 猜
CANopenNode V4 的处理函数普遍使用微秒时间差参数。RT-Thread 系统 tick 如果是 1 ms 甚至 10 ms,直接把 tick 换算成微秒在很多场景下只能得到比较粗的时间基准。
当前仓库已经加入高分辨率时间抽象:
- 默认可以使用 RT-Thread tick fallback;
- 也可以配置专用 1 MHz hardware timer backend;
- mainline 和 realtime 都使用测得的微秒 delta;
- Communication Reset 时会重新建立时间基线。
这对于 SYNC、周期调度、SDO timeout、Heartbeat 等所有依赖 timeDifference_us 的对象都有意义。
当然,硬件 timer backend 仍然有明确约束,例如当前文档要求关注 1 MHz、32-bit 和单实例所有权等问题,具体使用前应按 BSP 的 timer 实现确认。
这个功能再次说明软件包的方向不是“只做一个协议 Demo”,而是在补齐 RT-Thread 上长期运行需要的时间、调度和生命周期基础设施。
14. Storage:把 0x1010/0x1011 接到 RT-Thread 实际存储资源
CANopen 产品经常需要保存:
- 通信参数;
- 应用参数;
- 厂商参数;
- LSS 配置;
- PDO mapping;
- 校准值。
CANopenNode 提供 storage 基础对象,但最终落到什么非易失介质,仍然需要 target integration。
canopennode-rtt 当前支持三类 backend:
| Backend | 场景 |
|---|---|
| DFS | 有文件系统的设备,可以落文件 |
| AT24CXX EEPROM | 外挂 EEPROM 场景 |
| User backend | 产品自己接 NOR/NAND/FRAM/参数系统 |
对应总开关:
1 | PKG_CANOPENNODE_USING_STORAGE |
并且可以选择持久化:
1 | PKG_CANOPENNODE_STORAGE_PERSIST_COMM |
14.1 为什么我更看重“User backend”
DFS 和 AT24CXX 能让 Demo 很快跑起来,但真正产品里往往已经有自己的参数存储模块,例如:
- 双备份参数区;
- CRC + version;
- wear leveling;
- 参数迁移;
- 掉电保护;
- Flash 分区;
- 安全写入事务。
因此通用软件包最重要的不是强迫所有人使用一种存储,而是允许产品把 CANopen storage 接到已有参数系统。
PKG_CANOPENNODE_USING_STORAGE_USER 就是为这种场景保留的边界。
15. 测试不是另起炉灶:MCU 侧与 Linux Host 分工很清晰
这个仓库还有一个我比较喜欢的设计:没有试图把所有测试都塞进 MCU。
当前配套仓库:
https://github.com/wdfk-prog/canopen-slave-tester
它基于 Linux/Lely,主要承担 Host/主站自动协议测试;canopennode-rtt 则负责 MCU/RT-Thread 侧的协议运行、测试夹具和诊断对象。
两边通过标准 CANopen 对象和 Demo/Test OD 配合,不共享一套私有进程内接口。
15.1 普通从站测试拓扑
1 | Linux / Lely Master or Tester |
Host 可以验证:
- Boot-up;
- SDO Server;
- PDO;
- TIME;
- EMCY;
- GFC/SRDO 等按配置启用的能力。
15.2 验证 MCU NMT Master 时角色反过来
NMT Master 验证时,Linux 侧可以切成 Lely BasicSlave Node 2:
1 | RT-Thread MCU / CANopenNode Node 1 |
MCU 负责发送 NMT command,并用 Heartbeat Consumer 验证远端真实状态;Linux 侧则验证自己收到的命令 callback 与 reset 行为。
两边证据互补:
1 | MCU:远端在线 + NMT state + reset Boot-up |
这是一个很好的测试思想:不要让同一个实现既当被测对象,又自己宣布自己通过。
16. 一块 MCU 同时“做从站”和“做主站”到底是什么样子
前面把能力拆开讲了,这里把它们重新拼起来。
假设我们有一个设备:
- MCU 自己是 CANopen Node 1;
- 对上位机暴露自己的状态和参数;
- 同时管理两个远端执行器 Node 2、Node 3;
- 上电后检查两个执行器;
- 下发必要参数;
- 运行阶段同步发送控制目标;
- 任何一个远端掉线时进入系统降级状态。
这时候 Node 1 可以同时打开:
1 | 本地设备侧: |
系统关系可以画成:
1 | flowchart TB |
这时说 Node 1 是“从站”不完整,说它是“纯主站”也不完整。
它更像一个有自己 Object Dictionary 的 CANopen 控制器节点。
这正是 canopennode-rtt 保持 role-neutral 的意义。底层 wrapper 不需要根据“主站模式/从站模式”切换一套完全不同的 runtime;角色由协议对象和应用组合得到。
16.1 这对产品架构有什么好处
最大的好处是减少重复基础设施。
如果一个团队分别找一套 CANopen Slave Stack 和一套 CANopen Master Stack,常见结果是:
- 两套 CAN driver adapter;
- 两套 timer;
- 两套 mutex/critical section 规则;
- 两套日志;
- 两套配置系统;
- 两套对象字典概念;
- 两套升级维护路径;
- 两套协议行为差异。
而 CANopenNode 本身已经允许这些角色组合,同一个 RT-Thread wrapper 继续复用,会让系统边界更统一。
17. 三种推荐配置思路:设备节点、控制器节点、现场网关
为了更直观,下面给三个常见配置组合。这里不是完整 .config,而是帮助理解“哪些能力通常一起出现”。最终依赖关系仍以仓库 Kconfig 为准。
17.1 方案 A:普通 CANopen 设备 / 从站
适合:传感器、执行器、I/O 模块、自研驱动器、通用 MCU 设备。
建议重点开启:
1 | PKG_USING_CANOPENNODE=y |
产品 OD 根据设备实际对象模型生成。
如果设备只被上位机控制,不需要主动访问其他节点,则 SDO Client、NMT Master、LSS Master 可以不启用。
17.2 方案 B:CANopen 控制器 / 简单主站
适合:小型运动控制器、设备协调控制器、测试工装、机器人子系统控制器。
在设备侧基础能力之外重点增加:
1 | PKG_CANOPENNODE_NMT_MASTER=y |
应用层再实现:
1 | Node discovery / expected node list |
这套结构已经是真正的主站/控制器框架基础。
17.3 方案 C:Commissioning / Gateway 工具节点
适合:产线工装、售后工具、现场配置器、网关设备。
重点增加:
1 | PKG_CANOPENNODE_USING_SDO_CLIENT=y |
上层把 Gateway 接到 Console、UART、USB 或网络接口,就可以做一个 MCU 侧 CANopen 维护入口。
这种方式特别适合现场设备本身没有 Linux 主机,但又希望保留标准化诊断/配置通道的产品。
18. 哪些项目比较适合直接用 canopennode-rtt
我认为下面几类项目尤其适合。
18.1 已经使用 RT-Thread,并且 BSP CAN 驱动成熟
这是最直接的场景。
如果你的 CAN 已经通过 RT-Thread device framework 导出为 can0/can1,那这个软件包可以最大程度复用已有 BSP,而不需要给每一个 STM32、NXP、GD32 或其他 MCU 单独写一套 CANopen driver。
18.2 想用成熟 CANopenNode,但不想自己维护 RTOS 胶水层
CANopenNode 上游值得用,但 target integration 的生命周期、线程、锁、配置和 Storage 仍然需要做。
如果项目团队的真正业务是运动控制、机器人、工业控制,而不是“研究怎样把 CANopenNode 移到 RT-Thread”,那么直接复用一个持续维护的 wrapper 更划算。
18.3 设备未来可能从单纯从站升级成控制器
很多项目第一版只要求:
1 | 上位机 <-> MCU CANopen Device |
第二版就变成:
1 | 上位机 <-> 主控 MCU <-> 多个 CANopen 子节点 |
如果一开始就使用 role-neutral 的 CANopenNode 集成层,未来加 SDO Client、NMT Master、Heartbeat Consumer 时不用整体替换协议栈。
18.4 需要可裁剪,而不是“所有功能一次性打包”
Kconfig + SCons 组合比较适合资源受限 MCU。
你可以根据产品需要只开:
- CiA 301 基础;
- SDO Server;
- PDO;
- Storage;
也可以扩展到:
- SDO Client;
- NMT Master;
- LSS;
- Gateway;
- GFC/SRDO;
- Event-driven mainline。
资源和功能之间的关系比较透明。
18.5 希望 Host 自动测试与 MCU 代码分开
有 Linux + SocketCAN/Lely 测试条件的团队,很适合用 canopen-slave-tester 做 Host 自动协议验证,再让 canopennode-rtt 只承担 MCU 侧被测实现。
这种结构比把所有测试命令都写成 MSH 手工操作更容易持续集成。
19. 和“自己移植 CANopenNode”相比,实际省掉的工作不只是一个 CAN driver
如果从零做一套 RT-Thread port,第一阶段可能只需要几天:写 driver,起线程,看到 Boot-up。
真正耗时间的是第二阶段以后。
例如:
19.1 第一次 Communication Reset
你会发现:
- 旧
CO_t怎么释放; - CAN normal mode 怎么退出;
- RX 线程是不是还在读旧对象;
- callback 需要不要重绑;
- Storage 与 LSS 参数在哪个时机加载;
- realtime timer 是否需要停;
- 应用持有的指针是不是已经失效。
19.2 第一次加 SDO Client
你会发现:
- FIFO/segment/block 配置要一起跟上;
- mainline 里怎样 non-blocking process;
- 同节点 local transfer 是否打开;
- timeout 与 application state 怎么连接;
- 回调线程上下文怎么约束。
19.3 第一次加 Gateway
你会发现:
- 命令输入与响应输出怎样接;
- Console 是否与 Gateway 抢 UART;
- callback 能不能直接打印;
- Gateway 与 mainline 谁驱动谁;
- 多线程怎样避免并发访问状态机。
19.4 第一次做参数持久化
你会发现:
- 0x1010/0x1011 与产品自己的参数系统怎样对接;
- 什么时候写 Flash;
- reset 后怎样加载;
- LSS Node-ID/bitrate 什么时候生效;
- 掉电中断怎么办。
这些才是一个协议栈 wrapper 真正长期维护的成本。
canopennode-rtt 当前已经把其中相当一部分问题显式化到代码、Kconfig 和文档里,所以它的价值不能只用“有没有 CANopen 协议代码”来衡量。
20. 和 RTT-CanFestival 相比怎么选:两者都能跑 CANopen,但技术路线和适用阶段已经明显不同
提到 RT-Thread 上的 CANopen,很多老用户第一时间想到的其实不是 canopennode-rtt,而是 RT-Thread 软件包生态里已经存在多年的 CanFestival 移植。
这里的对比对象特指:
- RT-Thread 软件包
CanFestival; - 对应仓库:https://github.com/gbcwbz/canfestival-rtt;
- 该仓库 README 明确说明,它是从 CanFestival-3 项目移植到 RT-Thread 的实现。
所以在推荐 canopennode-rtt 时,有一个问题很难绕开:RT-Thread 已经有 CanFestival 了,为什么还要再做一个 CANopenNode 的 RT-Thread 集成?
我的结论是:两者并不是“同一个协议栈换了一个名字”,也不应该简单理解成“新的就一定比旧的好”。它们代表的是两套不同的 CANopen 协议栈、不同的 RT-Thread 集成方式,以及不同的工程取舍。
如果是一个已经稳定量产、基于 CanFestival 跑了很多年的项目,没有必要为了追新而强制迁移;但如果现在从零开始做新的 RT-Thread CANopen 产品,尤其希望后续继续扩展主站、Gateway、Storage、自动测试、现代 Object Dictionary 工作流或者持续跟进上游协议栈,那么我会更倾向从 canopennode-rtt 开始。
20.1 最根本的区别:底层不是同一套 CANopen 协议栈
canopennode-rtt 的协议核心来自:
https://github.com/CANopenNode/CANopenNode
仓库本身主要维护 RT-Thread integration/runtime,协议核心通过 git submodule 固定到明确 revision。也就是说,RT-Thread wrapper 与 CANopenNode upstream 是明显分层的。
RTT-CanFestival 则来自另一条技术路线。gbcwbz/canfestival-rtt README 明确写着:
1 | Forked from the CanFestival-3 project |
它把 CanFestival 的协议实现和 RT-Thread 适配一起保存在自己的 src/、inc/ 中。当前源码目录可以直接看到 nmtMaster.c、nmtSlave.c、sdo.c、pdo.c、emcy.c、sync.c、lifegrd.c、lss.c、can_rtthread.c、timer_rtthread.c 等文件。
因此两者最本质的关系其实是:
1 | flowchart LR |
canopennode-rtt 更像“现代 CANopenNode 上游 + RT-Thread 适配层”;RTT-CanFestival 更像“把一套 CanFestival 协议栈整体移植并固化到 RT-Thread 软件包中”。这决定了后面很多差异。
20.2 先用一张表把主要差异摆出来
| 对比维度 | canopennode-rtt |
RTT-CanFestival / canfestival-rtt |
|---|---|---|
| CANopen 核心 | CANopenNode V4,上游通过 git submodule 固定 revision | CanFestival-3 派生代码直接位于仓库 src/ / inc/ |
| RT-Thread 定位 | integration/runtime wrapper,尽量不修改 upstream core | 协议栈与 RT-Thread port 一体化维护 |
| 从站能力 | NMT Slave、SDO Server、PDO、Heartbeat、LSS Slave 等按 Kconfig 裁剪 | 有 NMT Slave、SDO、PDO、Heartbeat/Node Guarding 等传统 CanFestival 能力 |
| 主站能力 | NMT Master、SDO Client、Heartbeat Consumer、SYNC Producer、LSS Master、Gateway 等可组合 | 源码包含 NMT Master/SDO 等主站基础,并提供可运行的 Master402 示例 |
| CiA 402 | Device/Controller 正在规划和分阶段实现,当前不能宣称完整支持 | 当前仓库已经自带 CiA 402 Master 示例,可控制 Node-ID 2 伺服的 Profile Position 场景 |
| 定时模型 | realtime 使用 RT-Thread software timer 唤醒;时间测量默认 tick fallback,可选专用 1 MHz High-Res Timer | timer_rtthread.c 直接依赖 RT-Thread hwtimer,配置 1 MHz one-shot,并由 cf_timer 线程执行 TimeDispatch() |
| CAN RX | 独立 co_rx helper,支持可选 RT-Thread CAN HDR hardware filter,失败可回退软件分发 |
cf_recv 线程 + semaphore,当前 port 中收到帧后在全局 mutex 下调用 canDispatch() |
| Mainline / realtime 分工 | co_main 与 co_rt 明确分离,SYNC/RPDO/TPDO/SRDO 走 realtime path |
主要由 CAN dispatch 与 CanFestival timer dispatch 驱动,没有 CANopenNode 式 mainline/realtime wrapper 分层 |
| 多实例设计 | CANopenNodeRTT 为显式实例;CAN binding count 可配置;tick 时间源支持多实例,高精度 Timer backend 当前限制单实例 |
当前 port 使用全局 candev、OD_Data、canfstvl_mutex 等状态,结构上更接近单实例实现 |
| 构建裁剪 | Kconfig 映射 CO_CONFIG_*,SCons 按功能选择 CANopenNode 源文件 |
当前 src/SConscript 在 CAN + HWTIMER 条件下编译固定核心文件集合,示例通过单独开关加入 |
| Object Dictionary | 明确使用 XDD/生成 OD 工作流,Demo OD 与产品 OD 边界清晰 | Master402 自带 .od、生成后的 _od.c/.h,更偏 CanFestival 原有 OD 工作流和固定示例 |
| Storage | 已提供 DFS、AT24CXX EEPROM、User backend,并接 0x1010/0x1011 | 当前 RT-Thread port 没有同层级的通用 RT-Thread Storage backend abstraction,需要项目自行扩展 |
| 扩展能力 | Kconfig 已覆盖 LSS、CiA 309-3 Gateway、GFC、SRDO、TIME 等多个能力组 | 仓库保留传统 CanFestival 功能;当前构建脚本和 RT-Thread wrapper 暴露的高级扩展相对少 |
| 测试体系 | MCU demo/test OD + 独立 Linux/Lely Host 测试仓库 + CI/文档 | 优势主要是已有长期使用历史和 Master402 示例,当前仓库自动化验证体系相对简单 |
| 维护状态(2026-09-01 基线) | 本仓库当天仍有提交,上游 CANopenNode 2026 年仍在更新 | GitHub repository metadata 显示最近一次 push 为 2022-11-02,但历史用户量和社区积累更大 |
| 协议核心许可证 | CANopenNode upstream 为 Apache-2.0;wrapper 自身仍应按仓库实际许可文件确认 | 仓库明确声明 LGPL-2.1 |
这张表里最容易误解的一项是“主站能力”:RTT-CanFestival 不是只能做从站,canopennode-rtt 也不是因为有 NMT Master 就天然比它更像主站。 两边都有主站基础能力,真正的差异在于主站能力怎样被组织、维护和继续扩展。
20.3 RTT-CanFestival 的优势:老、直接,而且现在就有一个能参考的 CiA 402 主站例子
先说 RTT-CanFestival 的优势,因为这些优势是真实存在的。
第一,它进入 RT-Thread 生态更早,历史使用时间明显更长。gbcwbz/canfestival-rtt 仓库创建于 2018 年,长期以来就是很多 RT-Thread 用户搜索 CANopen 时最容易遇到的现成方案。对于已经基于它量产的项目,这种“代码已经在自己的硬件上跑了很多年”的工程价值,远比 GitHub 上哪个仓库更新更重要。
第二,它的接入方式很直接。README 给出的依赖就是:
1 | RT-Thread 3.0+ |
配置 CAN device name、timer device name 和两个线程优先级,就可以进入 CanFestival 的运行模型。对于功能单一、硬件固定、团队已经熟悉 CanFestival API 的产品,这种简单直接反而是一种优势。
第三,也是当前它相对 canopennode-rtt 最明显的现实优势:它现在就带有 examples/master402/。
该示例 README 明确说明:
- 它是一个 CiA 402 主站示例;
- 可以控制伺服驱动器;
- 默认伺服 Node-ID 为 2;
- 默认使用 Profile Position 模式;
- 可以通过 FinSH 测试 servo on 和相对移动。
因此,如果你的目标非常具体:
“我今天就是想在 RT-Thread MCU 上通过 CANopen 控制一台 CiA 402 伺服,先把 Profile Position 跑起来。”
那么 RTT-CanFestival 的 Master402 确实比当前 canopennode-rtt 更接近一个现成起点。canopennode-rtt 现在已有 NMT Master、SDO Client、SYNC、PDO 等 Controller 所需通信能力,但 CiA 402 Controller profile 本身仍在 Roadmap 中,这一点不能回避。
第四,RTT-CanFestival 没有 git submodule 这一层管理成本。协议栈源码就在包里,对于一些封闭、长期冻结、不希望处理 upstream revision 的项目,这种“拿到什么就编什么”的方式也更容易管理。
20.4 canopennode-rtt 的优势:更适合把 CANopen 当成长期维护的产品基础设施
如果项目不是“一次把某个 Demo 跑起来”就结束,而是希望 CANopen 在未来几年持续增加功能,我认为 canopennode-rtt 的优势会更明显。
首先是上游关系更清晰。CANopenNode core 保持为独立 git submodule,RT-Thread port 不需要把整个协议核心 fork 进来修改。升级协议栈时,可以明确看到 submodule revision 从哪里变到哪里,也更容易把 wrapper 的兼容性问题和 upstream 行为变化分开定位。
其次是配置粒度更细。canopennode-rtt 并不是简单地在 RT_USING_CAN 打开后把所有协议文件一起编进去,而是把 NMT、SDO Server、SDO Client、Heartbeat Consumer、TIME、SYNC、PDO、Storage、LSS、Gateway、GFC、SRDO 等能力拆成 Kconfig 选项,再由 SCons 根据实际配置决定源文件集合。
对于资源受限 MCU,这种方式更容易回答两个问题:
- 我到底编进去了什么?
- 我为了某个主站功能又引入了哪些依赖?
第三是运行时边界更适合复杂 RTOS 项目。CAN RX、realtime、mainline 分开之后,哪个上下文可以调用什么 API、SYNC/RPDO/TPDO 在哪里执行、Communication Reset 如何重建对象、Gateway/Storage callback 在哪里重新绑定,都会比“协议栈函数能跑起来”更加显式。
第四是多实例方向。当前 RTT-CanFestival port 的 can_rtthread.c 使用:
1 | static rt_device_t candev; |
这意味着当前 RT-Thread port 本身是明显围绕单个全局 CANopen 上下文组织的。canopennode-rtt 则把运行时放在 CANopenNodeRTT 实例中,并提供 CAN binding count。需要注意:开启专用 High-Res Timer 时目前仍有 package-wide 单 Timer、单实例限制;但关闭 High-Res、使用 tick fallback 时,wrapper 本身保留多实例设计空间。
第五是 RT-Thread 周边集成更完整。Storage、ULOG、PIN LED、CAN HDR filter、event-driven mainline、High-Resolution Time、Host 测试仓库等都已经被当成软件包的一部分来处理,而不是留给每个产品再次造胶水层。
20.5 定时架构的差异尤其值得看:两边都需要“时间”,但用法完全不同
RTT-CanFestival 对 hwtimer 是直接依赖。当前 src/SConscript 的 DefineGroup() 同时依赖:
1 | RT_USING_CAN |
timer_rtthread.c 中会:
- 找到配置的 RT-Thread hwtimer;
- 把频率设置为 1 MHz;
- 设置为 one-shot;
- timer timeout 后释放 semaphore;
cf_timer线程被唤醒;- 在 CanFestival mutex 下调用
TimeDispatch()。
这是 CanFestival 原有 timer queue 与 RT-Thread hwtimer 的直接适配,逻辑比较清楚,但代价是目标 BSP 必须提供可用 hwtimer,而且这个硬件 Timer 是协议栈运行的基础依赖。
canopennode-rtt 则把“realtime worker 如何被唤醒”和“CANopenNode 每次 process 实际经过多少微秒”拆成两个问题:
1 | RT-Thread software timer |
也就是说,不开 High-Res Timer 时,CANopenNode 仍然可以运行,只是 timeDifference_us 的精度受 RT-Thread tick 限制;真正需要更精确微秒时间时,再额外占用一个符合要求的硬件 Timer。
这两种设计不存在绝对对错。
- RTT-CanFestival 的模型更直接,CanFestival timer 本身就由 hwtimer 驱动;
canopennode-rtt的模型更强调“基本可运行能力”和“高精度时间能力”解耦,对 BSP Timer 资源紧张的项目更灵活。
20.6 CiA 402 是目前两者最容易被拿来比较、也最容易说过头的地方
如果只看目录,RTT-CanFestival 有 Master402,而 canopennode-rtt 的 CiA 402 还在开发,很容易直接得出:
1 | CanFestival 支持 402 |
这个判断太粗。
更准确的说法是:
RTT-CanFestival 当前已经交付了一个具体的 CiA 402 Master 应用示例;canopennode-rtt 当前已经具备构建 Controller 所需的大量 CANopen 通信能力,但尚未交付规划中的通用 CiA 402 Device/Controller profile。
CanFestival 的 Master402 目前公开说明的目标很具体:Node-ID 2、Profile Position、servo on、相对移动。它很有参考价值,但不能仅凭这个示例就把它扩大解释成“已经提供完整、通用、经过一致性验证的 CiA 402 Controller framework”。
反过来,canopennode-rtt 也不能因为 NMT Master、SDO Client、PDO、SYNC 都已经具备,就提前宣称 CiA 402 Controller 已经完成。通信能力是 profile 的地基,不等于 profile 状态机本身。
所以当前选型可以非常实际:
- 现在马上要做一个简单 402 主站控制伺服:RTT-CanFestival 的
Master402参考价值更直接; - 现在主要做 CANopen Device,后续准备演进多轴 402 Device/Controller:
canopennode-rtt的长期架构更值得关注; - 要求严格 CiA 402 conformance、复杂模式、多厂商互操作:两边都不能只看 Demo 名字,仍需要按目标规范版本建立对象、状态机和互操作验证矩阵。
20.7 RTT-CanFestival 的几个现实短板
第一,维护活跃度需要纳入新项目选型。以本文 2026-09-01 的 GitHub 元数据为基线,gbcwbz/canfestival-rtt 最近一次代码 push 是 2022-11-02。仓库并没有被 archived,而且已有用户和 issue 仍然具有参考价值,但对一个预计维护五年甚至更久的新产品来说,协议栈和 port 后续由谁维护必须提前考虑。
第二,当前 port 的全局状态比较重。candev、OD_Data、mutex 都是 package-level static,全局资源模型在单 CANopen 实例上简单,但当产品需要多个 CAN 控制器、多个逻辑实例或者更复杂生命周期管理时,扩展成本会明显增加。
第三,构建粒度较粗。当前 src/SConscript 在 CAN + HWTIMER 条件满足后固定加入 NMT Master、NMT Slave、SDO、PDO、EMCY、SYNC、Timer 等核心文件;虽然代码中还有 lss.c、dcf.c 等文件,但它们并不在当前这组固定 source list 中。相比之下,canopennode-rtt 把更多能力直接做成 Kconfig + SCons 的显式依赖关系。
第四,RT-Thread 平台能力集成相对少。它解决了 CAN 与 hwtimer 这两个最核心的 port 点,但没有像 canopennode-rtt 一样继续向 Storage backend、CAN hardware filter fallback、event-driven mainline、独立 Host tester 等方向扩展。
这不代表旧实现“不能做”这些功能,而是说需要产品自己继续维护更多适配代码。
20.8 canopennode-rtt 也有自己的短板,不应该把推荐文章写成单方面碾压
canopennode-rtt 最大的短板首先就是它很新。仓库 2026 年才建立,虽然目前开发活跃、文档和测试补得比较快,但现场项目数量、长期运行历史、第三方使用反馈显然不可能和一个 2018 年就进入 RT-Thread 社区的方案相比。
第二,它现在没有 RTT-CanFestival 那样可以直接拿来控制伺服的成熟 Master402 示例。CiA 402 Device/Controller 仍然是 Roadmap,当前阶段如果项目唯一目标就是快速控制已有 402 驱动器,这会直接影响选型。
第三,git submodule 虽然有利于锁定上游 revision,也会增加仓库拉取、离线镜像、版本升级和 CI 缓存管理的复杂度。团队如果对子模块使用不熟,第一次构建最常见的问题就是只 clone 了 wrapper,没有拉 CANopenNode/。
第四,High-Res Timer 当前仍有单实例限制,而且依赖用户/BSP 自己确认物理 Timer 真的是 32-bit、1 MHz、up-counting 并且未被其他模块占用。这个能力比“强制要求 hwtimer”更灵活,但也意味着高精度配置的责任被显式交给了产品集成者。
第五,许可证信息也需要产品团队认真处理。CANopenNode upstream 当前是 Apache-2.0;RTT-CanFestival 仓库则明确声明 LGPL-2.1。canopennode-rtt wrapper 自身的仓库级许可证应以实际仓库文件为准,商业产品在发布前不应该仅根据 upstream CANopenNode 的 Apache-2.0 就推断整个 wrapper 的许可义务。
20.9 两套 API 和 OD 模型不兼容,迁移不是“替换一个目录”
如果你现在已经在用 RTT-CanFestival,又考虑迁移到 canopennode-rtt,需要先接受一个事实:这不是 HAL 层兼容替换,而是协议栈迁移。
CanFestival 常见对象和接口围绕:
1 | CO_Data |
CANopenNode 则围绕:
1 | CO_t |
同时还会涉及:
- Object Dictionary 生成方式变化;
- 应用 callback 重新绑定;
- SDO Client 状态机重写;
- NMT Master API 重写;
- PDO mapping 与 OD access 重新检查;
- timer/lifecycle 模型变化;
- reset 和 storage 行为重新验证。
所以对于已经稳定的 CanFestival 产品,我不建议因为“CANopenNode 更新”就直接重写。只有当现有项目真正受到维护、扩展、协议能力、资源组织或者测试体系限制时,迁移才有足够收益。
20.10 最后给一个实际选型建议
如果现在让我按项目类型选择,我会这样判断:
| 项目情况 | 更建议的起点 | 原因 |
|---|---|---|
| 新建 RT-Thread CANopen Device / I/O / Sensor / Actuator | canopennode-rtt |
上游活跃、配置裁剪清晰、OD/Storage/测试链路更适合继续产品化 |
| 新建 MCU CANopen Controller,但不依赖现成 402 profile | canopennode-rtt |
NMT Master、SDO Client、Heartbeat Consumer、SYNC Producer、LSS Master 等基础已经齐全 |
| 今天就要快速参考一个 RT-Thread CiA 402 PP 主站 | RTT-CanFestival | Master402 已经存在,目标场景明确,能直接参考 servo on/relative move |
| 已经量产并稳定运行的 CanFestival 项目 | 优先继续 CanFestival | 不为技术栈“新旧”制造无业务收益的迁移风险 |
| 未来需要 Gateway、Storage、多协议能力裁剪、自动 Host 测试 | canopennode-rtt |
wrapper 已经把这些能力当成长期基础设施设计 |
| 需要多个 CANopen 实例 | 优先评估 canopennode-rtt |
显式实例模型更合适;但开启 High-Res Timer 时仍需注意当前单实例限制 |
| 对 LGPL 有明确合规流程、且已有 CanFestival 资产 | CanFestival 仍可合理使用 | 许可证义务已知,既有资产与验证证据可能比重新选栈更有价值 |
所以我推荐 canopennode-rtt,并不是因为 RTT-CanFestival “不能用”。恰恰相反,CanFestival 的 RT-Thread port 对很多早期项目起到了非常重要的作用,而且今天仍然保留一个对 CiA 402 主站很有价值的例子。
真正的区别在于:如果把 CANopen 看成一个未来还会继续扩展、维护、测试和升级的产品基础设施,CANopenNode + role-neutral RT-Thread wrapper 这条路线更符合我对新项目的长期预期;如果面对的是一个已经稳定的 CanFestival 存量项目,或者当前唯一目标就是快速参考现成的 402 主站控制流程,那么 RTT-CanFestival 仍然有它非常明确的优势。
21. 关于 CiA 402 / DS402:这是后续重点方向,但当前文章不把规划写成已完成功能
如果这个仓库未来面向伺服、步进驱动、机器人关节或者运动控制,CiA 402 几乎一定是大家最关注的扩展之一。
仓库目前已经有公开规划 Issue:
https://github.com/wdfk-prog/canopennode-rtt/issues/9
Issue 标题为:
1 | [Plan] CiA402 Multi-axis Device Implementation |
这里需要特别强调状态:
截至本文基线,CiA 402/DS402 属于正在规划和分阶段实施的额外 profile 能力,不应该把它宣传成当前 master 已经提供完整 CiA 402 Device/Controller。
21.1 当前规划最重要的架构决策
Issue 中已经把长期角色边界写得比较清楚:
1 | CANopenNode / RT-Thread communication layer |
也就是说,后续不会把 canopennode-rtt 强行定义成“只做 CiA 402 从站”。
规划方向是:
- communication layer 保持角色中立;
- CiA 402 拆成
common + device + controller; - 当前阶段优先实现
common + device; - Controller 后续独立实施;
- 不修改 CANopenNode upstream/core;
- Device 与 Controller 不共用一个混乱的 runtime 状态机。
这和本文前面强调的软件包定位是一致的。
21.2 Device 与 Controller 是两个不同问题
CiA 402 Device 的含义是:
1 | 远端 Controller |
也就是本 MCU 自己就是驱动器。
而 CiA 402 Controller 是:
1 | 远端 Drive Statusword |
也就是本 MCU 控制其他 CiA 402 驱动器。
这两者可以共享 state enum、mode enum、Controlword/Statusword 定义等无角色公共逻辑,但不能把本地电机 runtime 与远端 drive control runtime 混成一个对象。
21.3 多轴 Device 规划
当前 Issue 还规划了单 Node-ID 多轴 Device:
1 | CAN Bus |
也就是说,一个 MCU/一个 CANopen Node-ID 可以管理多个逻辑轴,而不是为了每个电机都创建一个 CO_t 和一个 Node-ID。
多轴对象通过 XDD 显式定义并生成 OD,不计划运行时偷偷创建隐藏对象。
21.4 realtime 架构已经为同步模式留出了位置
未来 CSP/CSV/CST 等同步模式对 SYNC 周期与 PDO 时序要求更高。
当前规划希望复用已有 realtime path:
1 | CO_process_SYNC() |
也就是说,先让目标值通过 RPDO 进入本地 OD,再执行 profile 同步逻辑,然后由 TPDO 发布新的状态/实际值。
这条路径不需要为了 CiA 402 去给 CANopenNode core 打一个私有 callback hook。
21.5 当前不要宣称 CiA 402 Conformance
公开规划也明确要求:在规范矩阵、Host、目标板和互操作验证形成完整证据以前,不宣称 CiA 402 conformance。
这是正确的做法。
CiA 402 不是“定义几个 0x6040/0x6041 对象就算支持”。真正实现还涉及:
- PDS FSA;
- Controlword / Statusword transition;
- operation mode;
- PP/PV/HM;
- CSP/CSV/CST;
- Quick Stop;
- Fault Reaction;
- Following Error;
- PDO 时序;
- SYNC;
- 多轴对象分区;
- EMCY 与诊断;
- 与 NMT 状态的交互。
所以这部分现在最合适的表述就是:已经有明确架构与实施计划,正在逐步补齐,但仍属于 Roadmap。
21.6 Roadmap 关系图
1 | flowchart LR |
图中的虚线表示规划中的后续 Controller 路径,不代表当前 master 已交付。
22. GFC、SRDO、TIME 等能力怎么看:有实现入口不等于你的产品天然满足对应系统要求
当前软件包还能裁剪 CiA 304 的 GFC、SRDO,以及 TIME、EMCY Consumer 等能力。
这里同样要区分“协议对象可编译运行”和“产品系统已经满足应用要求”。
例如:
1 | PKG_CANOPENNODE_USING_GFC |
可以启用对应协议能力,但这不代表设备因此自动满足功能安全认证、SIL/PL、双通道硬件、故障覆盖率或者系统级安全生命周期要求。
仓库文档也明确说明,Demo GFC 诊断用于协议验证,不驱动真实执行器,也不代表功能安全认证。
这种边界应该保留。通用协议栈负责报文和状态对象,产品安全设计仍然需要独立的 hazard analysis、hardware architecture、diagnostic coverage 和验证证据。
23. 当前已知限制:推荐使用不等于回避边界
任何软件包推荐如果只写优点、不写限制,实际参考价值会很低。
当前仓库 README 已经列出几项已知限制。
23.1 Trace recorder 当前不可用
CANopenNode 的 trace 模块还没有适配本包当前使用的 SDO Server 和 Object Dictionary API,因此该选项刻意保持不可用。
如果你的项目强依赖 CANopenNode trace,需要先补适配,而不是在 Kconfig 里强行打开。
23.2 内置 AT24CXX EEPROM backend 当前限制单实例
如果一个系统需要多个 CANopenNode 实例共享或分别使用 EEPROM,当前内置 adapter 不是直接开箱即用的多实例方案。
可以考虑产品自定义 User Storage backend。
23.3 硬件 CAN HDR filter 是优化路径,不是强依赖
过滤 bank 不够或者配置失败时会回退软件 RX 分发。
如果系统总线负载很高,应实际测量 CPU 占用和 RX latency,而不是默认认为“有硬件过滤选项就一定全走硬件”。
23.4 Demo OD 只用于 bring-up
产品必须替换自己的 OD,并重新检查:
- PDO mapping;
- Identity;
- Storage group;
- LSS;
- 参数权限;
- profile 对象;
- EDS/XDD 交付。
23.5 “可做主站”不等于当前提供完整产品级主站应用框架
这是本文反复强调的边界。
协议能力已经具备并且部分能力有自动测试,但设备发现、网络编排、远端 profile controller、故障恢复等产品策略还需要应用层实现。
把这个边界说清楚,反而更容易正确选型。
24. 我为什么推荐这个仓库
总结下来,我推荐它不是因为“又多了一个 CANopen 实现”,而是因为它选择了一个更合理的维护边界。
24.1 不重复实现 CANopen 核心
协议核心直接来自 CANopenNode,上游继续负责 CiA 301 等核心协议对象。
RT-Thread 软件包只维护 target integration。
这比复制一份 CANopenNode 源码后长期私有修改更健康。
24.2 RT-Thread 习惯比较统一
Kconfig、SCons、rt_device、thread、semaphore、mutex、ULOG、DFS、PIN 等都走 RT-Thread 自己的基础设施。
对 RT-Thread 开发者来说学习成本更低。
24.3 能力是可裁剪的
从最小设备节点到 SDO Client/NMT Master/Gateway,都可以按项目需要组合,不必一次把全部协议对象塞进 MCU。
24.4 主站与从站不是两套互斥产品
这一点是我最看重的。
设备今天只做 SDO Server/PDO,明天可以加 SDO Client/NMT Master;一个 Node-ID 可以同时有本地设备职责和远端控制职责。
这让产品架构有更大的演进空间。
24.5 测试方向明确
MCU 侧提供可观察性和 fixture,Linux/Lely Host 提供独立测试角色;NMT Master 验证还能反转角色,让 Linux 充当远端 Slave。
这种测试边界比只靠串口日志“看起来正常”可靠得多。
24.6 文档已经开始覆盖真正的集成问题
当前文档不仅有 README,还拆出了:
- Quick Start;
- RT-Thread Integration;
- Configuration;
- High-Resolution Time;
- LSS Persistence;
- Testing;
- NMT Master Test;
- Object Dictionary;
- Submodule Update;
- Troubleshooting。
对于开源嵌入式软件包来说,这些文档的重要性不低于代码本身。
25. 一个建议的首次上手顺序
如果你第一次使用这个仓库,我建议不要从所有高级功能开始。
第一步:确认 RT-Thread CAN 驱动
先在 BSP 层确认:
1 | rt_device_find("can1") |
对应设备存在,CAN 收发器、电气连接、120 Ω 终端、电源、bitrate 都正确。
第二步:完整拉取子模块
1 | git clone --recursive https://github.com/wdfk-prog/canopennode-rtt.git |
或者:
1 | git submodule update --init --recursive |
第三步:只开基础设备能力
先保持:
1 | PKG_USING_CANOPENNODE=y |
确认 CAN 设备名、Node-ID 和 bitrate。
第四步:看 Boot-up
Node-ID 1 应看到:
1 | 701#00 |
如果没有,先排查:
1 | CAN device open |
不要先怀疑 PDO。
第五步:验证 SDO
用 CANopen Master、Lely、CANopen 工具或者配套测试仓库访问 Demo OD。
确认 SDO Server 之后,再继续 PDO。
第六步:替换产品 OD
将 Demo XDD/OD 换成产品自己的对象模型。
这一步之后才真正开始“设备产品化”。
第七步:需要主站时再打开 Client/Master 能力
例如:
1 | PKG_CANOPENNODE_NMT_MASTER=y |
先用一个可控远端节点验证:
1 | remote online |
然后再实现产品自己的 network manager。
第八步:同步控制再评估 SYNC Producer 与 realtime 周期
涉及多轴控制或者同步 I/O 时,再根据系统周期规划:
1 | PKG_CANOPENNODE_TIMER_PERIOD_US |
同时检查整个 RT-Thread 的线程优先级和 CPU budget。
26. 结语:把它当成 RT-Thread 上的 CANopen“基础设施层”,会比把它叫从站包更准确
如果只想做一个最简单的 CANopen 从站,canopennode-rtt 可以完成这个任务:RT-Thread CAN 设备绑定、Boot-up、NMT Slave、Heartbeat、SDO Server、PDO、LSS Slave、Object Dictionary、Storage 都有明确接入路径。
但如果只把它看成“从站包”,其实低估了这套集成的上限。
CANopenNode 上游已经提供 simple NMT Master、SDO Client、LSS Master、SYNC Producer、Gateway 等能力;canopennode-rtt 当前的 Kconfig、SCons 和 runtime 也已经把这些能力接入 RT-Thread,并且 NMT Master、SDO Client 等方向已经有对应 MCU 侧自动验证支持。
因此更准确的定位应该是:
canopennode-rtt 是 CANopenNode 在 RT-Thread 上的集成与运行时基础层。它既可以承载普通 CANopen Device,也可以作为 MCU CANopen Controller/Master 的基础,并允许同一个节点同时保留本地设备能力和远端控制能力。
对现在只做单节点设备的项目,这意味着可以少维护一套 target port;对未来要扩展多节点控制的项目,这意味着不需要因为“要做主站”而整体换栈;对计划进入伺服和运动控制的项目,现有 realtime、PDO、SYNC、OD/XDD 和 role-neutral 架构也为后续 CiA 402 留出了比较合理的演进空间。
CiA 402/DS402 等 profile 目前仍处于规划与分阶段实施状态,尤其 Controller 端不能提前当成已交付能力宣传;但从公开 Roadmap 看,方向已经明确:通信层保持角色中立,Device 与 Controller 分开,优先把基础能力做扎实,再逐步补 profile。
如果你正在使用 RT-Thread,又刚好需要 CANopen,我认为这个仓库值得直接拉下来阅读代码和文档,而不是只看 README 里“CANopenNode integration package”这一句话。
先把一个标准 Node 跑起来,再根据项目需要打开 NMT Master、SDO Client、Heartbeat Consumer、SYNC Producer、LSS Master 和 Gateway。你会发现,这套软件包真正提供的不是“主站版”和“从站版”两个产品,而是一组可以在 RT-Thread MCU 上按需求组合的 CANopen 基础能力。
相关链接
- canopennode-rtt:https://github.com/wdfk-prog/canopennode-rtt
- 中文 README:https://github.com/wdfk-prog/canopennode-rtt/blob/master/README.zh-CN.md
- 在线文档:https://wdfk-prog.space/canopennode-rtt/
- CANopenNode 上游:https://github.com/CANopenNode/CANopenNode
- CANopenNode 官方文档:https://canopennode.github.io/CANopenNode/
- 配套 Linux/Lely 测试仓库:https://github.com/wdfk-prog/canopen-slave-tester
- CiA 402 多轴 Device/Controller 规划:https://github.com/wdfk-prog/canopennode-rtt/issues/9
- RTT-CanFestival / canfestival-rtt:https://github.com/gbcwbz/canfestival-rtt
- RT-Thread CanFestival 软件包页面:https://packages.rt-thread.org/en/detail.html?package=CanFestival









