lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站
摘要:面向 RT-Thread MCU 的 CANopen 主站选型,比较 Lely、CANopenNode、CanFestival 与商业栈,并说明本软件包的架构、静态对象字典生成和使用流程。

文章目录
- [lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站](#lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站)
-
- 推荐判断
- [为什么 CANopen 主站不能只看"支持 NMT、SDO、PDO"](#为什么 CANopen 主站不能只看“支持 NMT、SDO、PDO”)
- [为什么 Lely CANopen 比较适合作为主站核心](#为什么 Lely CANopen 比较适合作为主站核心)
-
- [NMT Master、Boot 和 Configuration 是协议栈的一等能力](#NMT Master、Boot 和 Configuration 是协议栈的一等能力)
- [`dcfgen` 把"主站代码"提升成"网络配置"](#
dcfgen把“主站代码”提升成“网络配置”) - [Lely 本身是 passive + asynchronous,适合被 RTOS 接管调度](#Lely 本身是 passive + asynchronous,适合被 RTOS 接管调度)
- [`dcf2c` 让对象字典在 Host 生成、在 MCU 静态使用](#
dcf2c让对象字典在 Host 生成、在 MCU 静态使用) - [Apache-2.0 对嵌入式产品集成比较友好](#Apache-2.0 对嵌入式产品集成比较友好)
- [`lely-canopen-rtt` 解决的不是 CANopen 协议,而是 RT-Thread 集成问题](#
lely-canopen-rtt解决的不是 CANopen 协议,而是 RT-Thread 集成问题) - [和其他 CANopen 协议栈怎么比较](#和其他 CANopen 协议栈怎么比较)
-
- [与 CANopenNode:不是谁"更强",而是主站抽象不同](#与 CANopenNode:不是谁“更强”,而是主站抽象不同)
- [与 CanFestival:Lely 更适合新项目建立现代化生成与并发边界](#与 CanFestival:Lely 更适合新项目建立现代化生成与并发边界)
- [与商业 Master Stack:开源灵活性不等于商业交付能力](#与商业 Master Stack:开源灵活性不等于商业交付能力)
- [整体框架:Host 负责"生成网络",MCU 负责"运行网络"](#整体框架:Host 负责“生成网络”,MCU 负责“运行网络”)
- [Target Runtime:所有 CANopen 状态只在一个 owner thread 内推进](#Target Runtime:所有 CANopen 状态只在一个 owner thread 内推进)
- 主站从启动到运行的流程
- [Master 控制面已经覆盖哪些常用能力](#Master 控制面已经覆盖哪些常用能力)
- [Kconfig 和源码选择为什么也是这个软件包的优势](#Kconfig 和源码选择为什么也是这个软件包的优势)
- [怎么使用:从一个远端 Node1 到真正的产品主站](#怎么使用:从一个远端 Node1 到真正的产品主站)
-
- [第一步:把软件包接入 RT-Thread 工程](#第一步:把软件包接入 RT-Thread 工程)
- [第二步:准备每个远端设备的 EDS/DCF](#第二步:准备每个远端设备的 EDS/DCF)
- [第三步:用 `master.yml` 描述网络策略](#第三步:用
master.yml描述网络策略) - [第四步:生成静态 Master Object Dictionary](#第四步:生成静态 Master Object Dictionary)
- 第五步:开启需要的主站控制面
- [第六步:先用 MSH 把网络跑通](#第六步:先用 MSH 把网络跑通)
- [第七步:应用通过 owner-safe API 使用主站](#第七步:应用通过 owner-safe API 使用主站)
- [用在伺服、I/O 和编码器上时应该怎么理解](#用在伺服、I/O 和编码器上时应该怎么理解)
- 当前必须说明的能力边界
-
- [1. CAN FD transport 不等于 CANopen FD](#1. CAN FD transport 不等于 CANopen FD)
- [2. 当前 public Master bridge 不提供通用 dynamic PDO remapping](#2. 当前 public Master bridge 不提供通用 dynamic PDO remapping)
- [3. LSS core 可选,但当前没有对应的 owner-safe Master application bridge](#3. LSS core 可选,但当前没有对应的 owner-safe Master application bridge)
- [4. 静态网络配置更适合拓扑相对稳定的设备](#4. 静态网络配置更适合拓扑相对稳定的设备)
- [5. Host CI 不等于目标板验证](#5. Host CI 不等于目标板验证)
- 最终应该怎么选
- 作为软件包分享时,可以重点强调的价值
- 参考资料
推荐判断
如果项目的目标是在 RT-Thread MCU 上实现一个真正承担网络管理职责的 CANopen 主站,而不是只做几条 NMT 命令和简单 SDO 读写,我更推荐以 Lely CANopen 为协议栈核心,再通过 lely-canopen-rtt 这一层完成 RT-Thread 适配。
推荐它的原因并不是"Lely 支持 CANopen,所以可以用",而是它的设计方式和主站需求比较匹配:Lely 本身把 NMT Master、远端节点 boot、配置请求、SDO Client、PDO、SYNC、EMCY、TIME 等能力组织成完整的 CANopen 服务对象;dcfgen 又能够从网络 YAML 和各个从站的 EDS/DCF 生成 Master DCF,把节点身份、Heartbeat、启动策略、PDO 映射和配置数据放到网络配置模型中;dcf2c 进一步把对象字典转换成 C 静态描述,使 MCU 不需要在运行时解析文本 DCF。
lely-canopen-rtt 在这个基础上解决了另一个实际问题:Lely 上游的 EV/IO2/CANopen 模型如何安全地运行在 RT-Thread 的 CAN 驱动、定时器、线程和 ISR/callback 环境中。软件包没有把上游协议栈改造成一套新的 CANopen,而是保留 Lely 的异步模型,并通过 single-owner runtime 把所有 Lely 对象收口到一个专用线程中执行。
因此,它比较适合下面这类项目:
- MCU 是 CANopen 网络中的控制器、Manager 或传统意义上的主站;
- 需要管理多个伺服、I/O 模块、编码器或其他 CANopen 从站;
- 启动阶段需要检查节点、配置参数、监控 Heartbeat,并决定何时进入 Operational;
- 运行阶段既有周期 PDO,又有 SDO 参数访问、EMCY 故障处理和 NMT 控制;
- 希望把网络拓扑和对象字典尽量放在 Host 侧生成,而不是在 MCU 代码里手写大量索引、COB-ID 和启动状态机;
- 项目使用 RT-Thread,希望协议栈和 CAN 驱动、定时器、应用线程之间有明确的并发边界。
如果只是做一个小型 CANopen 从站,或者主站仅需要"发 NMT + 偶尔做一次 SDO",CANopenNode 往往更直接;如果项目已经使用 CanFestival 并且现有设备、对象字典和 DS402 示例都围绕它建立,也没有必要为了换协议栈而重写;如果项目要求商业技术支持、CANopen FD、Safety 或明确的认证交付,则商业 CANopen Master Stack 可能比开源方案更合适。
所以这篇文章的结论不是"Lely 在任何场景都比其他协议栈好",而是:当 RT-Thread 设备要承担较完整的 CANopen 主站职责时,Lely 的网络管理能力、Host 配置生成能力和异步架构,与 lely-canopen-rtt 的 single-owner RTOS 适配组合在一起,形成了一条比较完整的工程路径。
为什么 CANopen 主站不能只看"支持 NMT、SDO、PDO"
很多 CANopen 协议栈的功能列表看起来都差不多:NMT、SDO、PDO、Heartbeat、EMCY、SYNC 基本都有。仅按功能名称做选型,很容易得到"几个协议栈都差不多"的结论。
但主站和从站真正的复杂度并不在同一个位置。
对于一个普通 CANopen 从站,核心任务通常是维护自己的对象字典,响应 SDO,收发 PDO,处理 NMT 状态和 Heartbeat。对象字典主要描述"我这个设备有什么数据和通信参数"。
主站面对的是整个网络。除了自己也是一个 CANopen 节点之外,它还需要知道:
- 网络里应该有哪些节点;
- 哪些节点必须存在,哪些节点允许缺席;
- 节点启动后是否需要 Reset Communication;
- 是否需要检查 Vendor-ID、Product Code、Revision、Serial Number;
- 哪些参数需要在 boot 阶段通过 SDO 下载;
- Heartbeat producer/consumer 如何配置;
- 主站什么时候启动从站进入 Operational;
- 从站 TPDO 应映射到主站哪个 RPDO;
- 某个节点掉线、重启或重新 Boot-up 后,应用侧状态如何恢复;
- 运行阶段的 SDO 请求如何与自动 NMT boot/configuration 共用默认 SDO 通道。
这就是为什么主站选型时,我更关注"有没有网络管理模型"和"网络配置如何进入固件",而不是只看有没有 sendNMT()、SDO_Read() 之类的 API。
CiA 301 本身也不是一个单一的传统主从协议。NMT 使用 Master/Slave 模型,SDO 使用 Client/Server 模型,PDO 使用 Producer/Consumer 模型。一个主站程序实际上需要同时组织这些不同通信模型,并把它们统一到节点生命周期和应用控制流程中。
对主站来说,一个好用的协议栈应该尽量减少应用层重复实现这些网络管理状态,而不是只提供报文级 API。
为什么 Lely CANopen 比较适合作为主站核心
NMT Master、Boot 和 Configuration 是协议栈的一等能力
Lely 的 C CANopen library 本身提供 NMT Master 能力,并且把 NMT boot slave 和 NMT configuration request 作为可裁剪功能。上游构建配置中可以分别关闭 master、nmt-boot 和 nmt-cfg,这说明这些能力不是应用示例里临时拼出来的辅助代码,而是协议栈内部正式维护的功能路径。
Lely 对 NMT Master boot 还定义了专门的 SDO timeout、boot wait timeout、SDO retry、reset timeout 等参数。换句话说,它并不是只负责发送一个 NMT Start,而是已经考虑"一个主站如何等待、检查和配置远端节点"这类网络管理问题。
这对多节点主站非常重要。项目不再需要自己从零设计:
text
收到 Boot-up
-> 查询 Identity
-> 判断是否允许该节点
-> 写 Heartbeat
-> 写通信参数
-> 写 PDO mapping
-> 等待配置结果
-> 决定是否进入 Operational
Lely 的 NMT Master/boot/configuration 已经承担了其中相当一部分标准协议状态机。应用层更应该关注产品策略,而不是重新实现通用 CANopen boot 过程。
需要注意,Lely 并不是完整实现 CiA 302 的所有扩展。官方 standards 页面明确列出了 flying master、network redundancy 等未实现或 application-specific 的部分。因此这里推荐的是它当前已实现的 CANopen Manager/NMT boot/configuration 路径,而不是把它描述成"完整 CiA 302 实现"。
dcfgen 把"主站代码"提升成"网络配置"
我认为 Lely 对主站最有价值的部分之一并不只在 runtime,而是 dcfgen。
普通从站的对象字典主要描述本机。主站对象字典却天然与整个网络有关:远端节点是谁、Heartbeat 怎么监控、哪些节点需要 boot、PDO 怎么对接、启动阶段要写哪些参数,这些本来就更像网络配置,而不是业务 C 代码。
Lely 的 dcfgen 使用 YAML 描述 Master 和各个 slave,并读取 slave EDS/DCF。它可以生成 Master DCF,还可以在启用 remote PDO mapping 时,根据从站 PDO 自动构造主站对应 PDO 映射。涉及启动配置时,它还能生成用于远端配置的 concise DCF 数据。
这意味着产品可以把很多本来散落在源码中的配置:
text
Node-ID
Identity check
Heartbeat
NMT startup policy
mandatory/optional node
SDO boot configuration
RPDO/TPDO mapping
SYNC period
集中到网络描述文件中。
对主站项目来说,这种模式比在 main.c 里维护几十个 0x1400、0x1600、0x1800、0x1A00 写操作更容易审查,也更容易随从站 EDS/DCF 变化重新生成。
Lely 本身是 passive + asynchronous,适合被 RTOS 接管调度
Lely 官方对底层 C CANopen library 的一个重要描述是 passive:协议库本身不强制创建线程、不直接接管系统时钟,也不绑定某个具体 CAN 驱动。CAN 帧输入、CAN 帧输出和时间推进由外部环境提供。
同时,它的请求模型是异步的。发起请求本身不要求阻塞整个协议栈,完成结果通过 callback/future/executor 路径继续处理。
这对 Linux 很方便,对 RTOS 其实更重要。因为 MCU 项目最不希望发生的事情之一,就是第三方协议栈内部偷偷创建线程、使用 pthread/TLS,或者把某个 SDO transaction 做成阻塞式等待,最终导致 BSP、libc 和调度模型被协议栈反向绑定。
lely-canopen-rtt 利用的正是这一点:RT-Thread 负责"什么时候运行、CAN 从哪里来、timer 怎么触发",Lely 负责"收到这个帧和这个时间后 CANopen 状态机应该怎么推进"。
dcf2c 让对象字典在 Host 生成、在 MCU 静态使用
Lely 常见的桌面用法可以直接加载 EDS/DCF,但 MCU 没必要保留这套运行时文本解析路径。
lely-canopen-rtt 的 target policy 固定 LELY_NO_CO_DCF=1、LELY_NO_STDIO=1、LELY_NO_CO_OBJ_FILE=1,目标端不会运行时读取 DCF 文件。Host 使用 dcf2c 把 DCF 生成 const struct co_sdev 静态描述,再编译进固件。
这个设计有几个直接好处:
- MCU 不依赖文件系统;
- 不需要在启动时解析 INI/DCF 文本;
- 对象字典结构在构建阶段就确定;
- 生成产物可以进入代码审查和版本管理;
- 网络配置变化可以表现为可审查的生成 diff;
- target 只保留实际运行所需的数据结构和协议路径。
它并不等于"完全不用 heap"。当前 runtime 仍选择 RT-Thread heap,并由 Lely 根据静态 co_sdev 创建运行时对象。这里的"静态"指的是对象字典描述和生成输入不需要 target 解析文本,而不是宣称整个协议栈全部静态分配。
Apache-2.0 对嵌入式产品集成比较友好
Lely core 使用 Apache License 2.0,lely-canopen-rtt 仓库本身也使用 Apache-2.0。相较于 LGPL runtime 的方案,产品集成时许可证边界更直接。当然,最终发布仍然应该保留仓库 LICENSE、NOTICE 和适用的 upstream 版权信息。
lely-canopen-rtt 解决的不是 CANopen 协议,而是 RT-Thread 集成问题
直接把 Lely 源码加入 RT-Thread 工程并不等于完成了移植。真正困难的是 I/O、时间、线程和生命周期。
这个软件包最核心的设计是 single-owner:RT-Thread 可以有很多应用线程、中断和驱动 callback,但所有 Lely EV/IO2/CANopen 对象只允许一个 owner thread 访问。
target 中固定:
c
#define LELY_NO_THREADS 1
#define LELY_NO_ATOMICS 1
#define LELY_NO_TIMEOUT 1
这些宏的含义不是"RT-Thread 不能多线程",而是"Lely 内部不承担线程同步"。跨线程工作必须先进入 RT-Thread 的 event/message queue,再由 owner 串行执行。
这条边界非常适合 MCU:
- CAN RX callback 不直接执行复杂 CANopen 状态机;
- timer callback 不直接进入 Lely;
- MSH 或业务线程不直接持有
co_nmt_t、co_csdo_t、PDO/SYNC/EMCY/TIME service pointer; - 所有异步协议状态都在 owner 中推进;
- 应用通过 queue 提交命令,通过 snapshot 读取状态;
- shutdown 先关闭 callback/command admission,再等待已进入 callback 退出,最后释放 CANopen 对象。
这比"给所有 Lely API 外面加 mutex"更容易建立明确的 ownership 规则。
和其他 CANopen 协议栈怎么比较
下面的比较重点放在"嵌入式主站"而不是单纯比较 CANopen 协议覆盖率。
| 维度 | Lely + lely-canopen-rtt |
CANopenNode | CanFestival / canfestival-rtt |
MicroControl CANopen Master |
|---|---|---|---|---|
| 许可证 | Apache-2.0 | Apache-2.0 | runtime LGPL-2.1,工具 GPL | 商业授权 |
| 主要语言 | 上游 C/C++,本软件包 target 使用 C core | ANSI C | ANSI C | C99 源码交付 |
| 主站定位 | NMT Master、boot、configuration、SDO Client 等是正式协议栈能力 | 官方功能表称为 Simple NMT master,并提供 SDO Client/LSS Master/Gateway |
可构建 Master/Slave,具备 NMT、SDO Client、PDO、EMCY、LSS 等 | 面向复杂控制器的完整商业 Master 产品 |
| 网络配置方式 | dcfgen 从 Master YAML + slave EDS/DCF 生成 Master DCF/配置数据 |
以 OD 和应用初始化为核心,CANopenEditor 可生成 OD C 文件 | Objdict editor/generator,节点对象字典模式较传统 | 商业工具/API,支持运行时参数化 |
| 主站 boot/config | Lely 提供 NMT boot/configuration state machine;软件包可发布 boot snapshot 和 manual cfg | 能做 NMT/SDO/LSS Commander,但复杂网络启动策略通常更多落在应用组织层 | 支持主站和 concise DCF,已有传统 Master 示例 | 官方宣称支持 CiA 301/302/305,适合复杂网络管理 |
| PDO/SYNC | 支持;软件包提供静态 PDO event 和 transmission type 控制 | 支持,动态 PDO mapping 能力成熟 | 支持 | 支持 |
| RT-Thread 集成 | 本软件包直接使用 RT-Thread CAN/event/timer/message queue | 栈本身跨 MCU/RTOS,需要目标 port | 已有 canfestival-rtt,使用 RT-Thread CAN + hwtimer,并带 Master402 示例 |
需要按商业栈 driver/API 接入 |
| 并发模型 | passive async + single-owner,非 owner 通过 queue/snapshot | 非阻塞 process 模型,可单线程或多线程组织 | 传统 timer/CAN callback + port 模型,RTT 移植中有 CAN RX/timer 线程 | 由商业实现和接口约束决定 |
| 主站配置生成 | 强,dcfgen -r 很适合把网络拓扑、Heartbeat、boot 和 PDO 放到生成流程 |
更偏设备 OD 与应用组合 | 有 OD generator,但工具链和工程模式较传统 | 通常有配套商业工具 |
| 目标端 DCF | 本软件包使用 dcf2c 静态化,不运行时解析 DCF |
常用生成 OD.c/OD.h |
常用 generator 生成节点对象字典 C | 取决于产品方案 |
| 工程透明度 | 源码、生成链、Kconfig、source allowlist 都可审查 | 源码成熟、社区大、MISRA C:2012 有说明 | 源码开放,但历史分支较多 | 商业支持强,但内部实现受供应商产品约束 |
| 更推荐的场景 | RT-Thread MCU 多节点主站、网络 boot/config/PDO 管理 | 从站、通用节点、轻量主站、需要成熟 MCU 社区时 | 已有 CanFestival 项目、需要沿用现有 DS402/OD 资产 | Safety、CANopen FD、商业支持、明确交付责任 |
与 CANopenNode:不是谁"更强",而是主站抽象不同
CANopenNode 是非常成熟的开源 CANopen 协议栈,而且对 MCU 很友好。它支持 SDO Client、NMT Master、LSS Master、Heartbeat consumer、PDO、SYNC、TIME、EMCY,并且官方还提供 CANopenDemo、CANopenLinux/CANopenSocket 和 CANopenEditor。它的社区和设备移植数量也明显更大。
如果开发的是一个 CANopen 从站,我通常不会因为 Lely 有 dcfgen 就否定 CANopenNode。从站的主要问题是本地 OD、PDO、SDO Server 和硬件驱动,CANopenNode 在这类场景非常直接。
但它自己的官方 README 对 NMT Master 的描述就是 Simple NMT master。它的 commander 能力更多通过 NMT Master + SDO Client + LSS Master/Gateway 组合出来。对于一个需要"按网络配置自动 boot 多个节点、检查 identity、生成远端 PDO mapping、形成配置数据"的主站,Lely 的 dcfgen + NMT boot/configuration 更像是围绕网络 Manager 设计的一套完整路径。
所以选择差异可以概括为:
- CANopenNode 更像一个非常成熟、可自由拼装的 CANopen node toolbox;
- Lely 在主站场景下更强调异步服务、Network Management 和 DCF 驱动的网络配置。
如果主站逻辑很简单,CANopenNode 的简洁反而是优势;当网络启动和配置变复杂时,Lely 的生成式网络配置价值会逐渐体现出来。
与 CanFestival:Lely 更适合新项目建立现代化生成与并发边界
CanFestival 并不是不能做主站。官方资料明确支持 NMT Master/Slave、Heartbeat、SDO clients/servers、PDO、EMCY、SYNC 和 LSS,也支持 concise DCF。RT-Thread 社区还有现成的 canfestival-rtt 移植,甚至提供了 CiA 402 Master 示例。
因此,如果一个项目已经基于 CanFestival 工作多年,或者目标就是快速复用现有 Master402 示例,继续使用 CanFestival 是合理选择。
但对新项目来说,Lely 有几个更吸引我的点:
dcfgen的输入天然是整个网络,而不是只围绕某个节点对象字典;- Lely C core 明确采用 passive/asynchronous 思路,适合由 RTOS 接管 I/O 和调度;
lely-canopen-rtt把线程所有权收口为 single-owner,而不是让业务代码直接穿过多个 timer/CAN callback;- Apache-2.0 的集成边界更直接;
- 本软件包把上游源文件选择、Host 生成和 target 运行时拆成了可审查的层。
CanFestival 官方站点也明确显示当前源码存在多个分支/镜像,主 repo 更新比较松散。这并不代表它不能稳定工作,而是新项目在长期维护时需要先确定自己要跟踪哪个 fork 和哪套工具链。
与商业 Master Stack:开源灵活性不等于商业交付能力
以 MicroControl CANopen Master 为例,官方产品直接面向复杂控制网络,明确覆盖 CiA 301、CiA 302、CiA 305,并提供 CANopen FD、可选 Safety、商业技术支持和 C99 源码交付。
如果项目的关键要求是:
- CANopen FD/CiA 1301;
- Safety;
- 厂商 SLA 和技术支持;
- 审核时需要明确的软件供应商责任;
- 希望购买现成的标准覆盖而不是自己维护开源集成;
那么商业栈可能是更稳妥的选择。
lely-canopen-rtt 的优势是源码完全可控、RT-Thread 集成透明、网络生成链可修改,而且没有商业 runtime 费用;它的代价是产品团队自己承担 target 验证、上游冻结版本维护以及尚未覆盖功能的扩展责任。
整体框架:Host 负责"生成网络",MCU 负责"运行网络"
理解这个软件包最重要的一点,是不要把 master.yml、remote DCF、master_sdev.c 和运行时 CANopen 对象混在一起。
它把工作明确分成 Host 和 Target 两部分。
#mermaid-svg-kKGYhb43ldEzqK0X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kKGYhb43ldEzqK0X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kKGYhb43ldEzqK0X .error-icon{fill:#552222;}#mermaid-svg-kKGYhb43ldEzqK0X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kKGYhb43ldEzqK0X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kKGYhb43ldEzqK0X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kKGYhb43ldEzqK0X .marker.cross{stroke:#333333;}#mermaid-svg-kKGYhb43ldEzqK0X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kKGYhb43ldEzqK0X p{margin:0;}#mermaid-svg-kKGYhb43ldEzqK0X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kKGYhb43ldEzqK0X .cluster-label text{fill:#333;}#mermaid-svg-kKGYhb43ldEzqK0X .cluster-label span{color:#333;}#mermaid-svg-kKGYhb43ldEzqK0X .cluster-label span p{background-color:transparent;}#mermaid-svg-kKGYhb43ldEzqK0X .label text,#mermaid-svg-kKGYhb43ldEzqK0X span{fill:#333;color:#333;}#mermaid-svg-kKGYhb43ldEzqK0X .node rect,#mermaid-svg-kKGYhb43ldEzqK0X .node circle,#mermaid-svg-kKGYhb43ldEzqK0X .node ellipse,#mermaid-svg-kKGYhb43ldEzqK0X .node polygon,#mermaid-svg-kKGYhb43ldEzqK0X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kKGYhb43ldEzqK0X .rough-node .label text,#mermaid-svg-kKGYhb43ldEzqK0X .node .label text,#mermaid-svg-kKGYhb43ldEzqK0X .image-shape .label,#mermaid-svg-kKGYhb43ldEzqK0X .icon-shape .label{text-anchor:middle;}#mermaid-svg-kKGYhb43ldEzqK0X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kKGYhb43ldEzqK0X .rough-node .label,#mermaid-svg-kKGYhb43ldEzqK0X .node .label,#mermaid-svg-kKGYhb43ldEzqK0X .image-shape .label,#mermaid-svg-kKGYhb43ldEzqK0X .icon-shape .label{text-align:center;}#mermaid-svg-kKGYhb43ldEzqK0X .node.clickable{cursor:pointer;}#mermaid-svg-kKGYhb43ldEzqK0X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kKGYhb43ldEzqK0X .arrowheadPath{fill:#333333;}#mermaid-svg-kKGYhb43ldEzqK0X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kKGYhb43ldEzqK0X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kKGYhb43ldEzqK0X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kKGYhb43ldEzqK0X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kKGYhb43ldEzqK0X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kKGYhb43ldEzqK0X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kKGYhb43ldEzqK0X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kKGYhb43ldEzqK0X .cluster text{fill:#333;}#mermaid-svg-kKGYhb43ldEzqK0X .cluster span{color:#333;}#mermaid-svg-kKGYhb43ldEzqK0X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kKGYhb43ldEzqK0X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kKGYhb43ldEzqK0X rect.text{fill:none;stroke-width:0;}#mermaid-svg-kKGYhb43ldEzqK0X .icon-shape,#mermaid-svg-kKGYhb43ldEzqK0X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kKGYhb43ldEzqK0X .icon-shape p,#mermaid-svg-kKGYhb43ldEzqK0X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kKGYhb43ldEzqK0X .icon-shape .label rect,#mermaid-svg-kKGYhb43ldEzqK0X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kKGYhb43ldEzqK0X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kKGYhb43ldEzqK0X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kKGYhb43ldEzqK0X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 从站 EDS / DCF
dcfgen + master.yml
Master DCF
compact_master_dcf.py
dcf2c --no-strings
master_sdev.c / const co_sdev
RT-Thread firmware build
MCU local CANopen Master
远端设备的 EDS/DCF 是"网络里这个从站长什么样"的 Host 输入;master.yml 是"主站准备怎么管理这个网络"的策略;生成后的 master_sdev.c 才是 MCU 本地 Master 对象字典描述。
仓库里的 examples/node1/node1.dcf 绝不能被理解成"MCU 本地 Node1"。它描述的是远端 Node1。真正被 MCU 绑定的是 examples/master_node1/master_sdev.c。
软件包还增加了 compact_master_dcf.py。原因是 Lely dcfgen 的 Manager 模板面向通用网络,某些 node-indexed array 会有较大的 CompactSubObj 范围。对于只有少数节点的 MCU,如果直接全部展开,会生成大量不使用的 sub-object。compactor 根据当前网络缩小这些范围,再交给 dcf2c,更符合 MCU 的资源模型。
这就是这个软件包与"直接把 Lely 编译进 RT-Thread"的差别:它不仅解决 runtime port,还把 Host 生成路径也纳入工程。
Target Runtime:所有 CANopen 状态只在一个 owner thread 内推进
目标端的主路径如下:
#mermaid-svg-g1VPNDNGlU3a19DZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g1VPNDNGlU3a19DZ .error-icon{fill:#552222;}#mermaid-svg-g1VPNDNGlU3a19DZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g1VPNDNGlU3a19DZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g1VPNDNGlU3a19DZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g1VPNDNGlU3a19DZ .marker.cross{stroke:#333333;}#mermaid-svg-g1VPNDNGlU3a19DZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g1VPNDNGlU3a19DZ p{margin:0;}#mermaid-svg-g1VPNDNGlU3a19DZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ .cluster-label text{fill:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ .cluster-label span{color:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ .cluster-label span p{background-color:transparent;}#mermaid-svg-g1VPNDNGlU3a19DZ .label text,#mermaid-svg-g1VPNDNGlU3a19DZ span{fill:#333;color:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ .node rect,#mermaid-svg-g1VPNDNGlU3a19DZ .node circle,#mermaid-svg-g1VPNDNGlU3a19DZ .node ellipse,#mermaid-svg-g1VPNDNGlU3a19DZ .node polygon,#mermaid-svg-g1VPNDNGlU3a19DZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-g1VPNDNGlU3a19DZ .rough-node .label text,#mermaid-svg-g1VPNDNGlU3a19DZ .node .label text,#mermaid-svg-g1VPNDNGlU3a19DZ .image-shape .label,#mermaid-svg-g1VPNDNGlU3a19DZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-g1VPNDNGlU3a19DZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-g1VPNDNGlU3a19DZ .rough-node .label,#mermaid-svg-g1VPNDNGlU3a19DZ .node .label,#mermaid-svg-g1VPNDNGlU3a19DZ .image-shape .label,#mermaid-svg-g1VPNDNGlU3a19DZ .icon-shape .label{text-align:center;}#mermaid-svg-g1VPNDNGlU3a19DZ .node.clickable{cursor:pointer;}#mermaid-svg-g1VPNDNGlU3a19DZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-g1VPNDNGlU3a19DZ .arrowheadPath{fill:#333333;}#mermaid-svg-g1VPNDNGlU3a19DZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-g1VPNDNGlU3a19DZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-g1VPNDNGlU3a19DZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g1VPNDNGlU3a19DZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-g1VPNDNGlU3a19DZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g1VPNDNGlU3a19DZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-g1VPNDNGlU3a19DZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-g1VPNDNGlU3a19DZ .cluster text{fill:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ .cluster span{color:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-g1VPNDNGlU3a19DZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-g1VPNDNGlU3a19DZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-g1VPNDNGlU3a19DZ .icon-shape,#mermaid-svg-g1VPNDNGlU3a19DZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g1VPNDNGlU3a19DZ .icon-shape p,#mermaid-svg-g1VPNDNGlU3a19DZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-g1VPNDNGlU3a19DZ .icon-shape .label rect,#mermaid-svg-g1VPNDNGlU3a19DZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g1VPNDNGlU3a19DZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-g1VPNDNGlU3a19DZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-g1VPNDNGlU3a19DZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} post command
read
pin + event
pin + event
pin + event
Application / MSH
RT-Thread message queue
owner-published snapshot
CAN RX callback
RT-Thread event
timer callback
CAN status callback
single Lely owner thread
Lely ev_loop
NMT / SDO / PDO / SYNC / EMCY / TIME
io_can_net
io_user_can
io_user_timer
RT-Thread CAN device
这张图里最重要的不是 ev_loop,而是 owner 边界。
CAN RX callback 收到帧时不会直接调用 co_nmt_on_*() 或 SDO/PDO 处理函数。它只取得一个短生命周期 pin、设置 event,然后退出。owner 被唤醒后再批量读取 RT-Thread CAN software FIFO,把帧送入 Lely。
应用线程也不直接调用底层 Lely service。比如应用想发送 NMT command,会把命令复制到 runtime message queue;想看远端 NMT 状态,则读取 owner 已发布的 atomic snapshot。
这样可以避免一个常见问题:CAN RX 在驱动 callback 中更新协议状态、业务线程同时访问对象字典、timer 又在另一个上下文触发超时,最后不得不在 Lely 对象外面铺很多锁。
在这里,规则非常简单:只有 owner 可以碰 Lely。
主站从启动到运行的流程
当使用静态 Master 时,启动过程大致可以理解为:
Remote node RT-Thread CAN Lely CANopen Lely owner thread lely_rtt_runtime RT-Thread application Remote node RT-Thread CAN Lely CANopen Lely owner thread lely_rtt_runtime RT-Thread application #mermaid-svg-JfqUCCaCsk0bWeod{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JfqUCCaCsk0bWeod .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JfqUCCaCsk0bWeod .error-icon{fill:#552222;}#mermaid-svg-JfqUCCaCsk0bWeod .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JfqUCCaCsk0bWeod .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JfqUCCaCsk0bWeod .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JfqUCCaCsk0bWeod .marker.cross{stroke:#333333;}#mermaid-svg-JfqUCCaCsk0bWeod svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JfqUCCaCsk0bWeod p{margin:0;}#mermaid-svg-JfqUCCaCsk0bWeod .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JfqUCCaCsk0bWeod text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JfqUCCaCsk0bWeod .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JfqUCCaCsk0bWeod .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JfqUCCaCsk0bWeod .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JfqUCCaCsk0bWeod .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JfqUCCaCsk0bWeod #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JfqUCCaCsk0bWeod .sequenceNumber{fill:white;}#mermaid-svg-JfqUCCaCsk0bWeod #sequencenumber{fill:#333;}#mermaid-svg-JfqUCCaCsk0bWeod #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JfqUCCaCsk0bWeod .messageText{fill:#333;stroke:none;}#mermaid-svg-JfqUCCaCsk0bWeod .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JfqUCCaCsk0bWeod .labelText,#mermaid-svg-JfqUCCaCsk0bWeod .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JfqUCCaCsk0bWeod .loopText,#mermaid-svg-JfqUCCaCsk0bWeod .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JfqUCCaCsk0bWeod .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JfqUCCaCsk0bWeod .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JfqUCCaCsk0bWeod .noteText,#mermaid-svg-JfqUCCaCsk0bWeod .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JfqUCCaCsk0bWeod .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JfqUCCaCsk0bWeod .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JfqUCCaCsk0bWeod .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JfqUCCaCsk0bWeod .actorPopupMenu{position:absolute;}#mermaid-svg-JfqUCCaCsk0bWeod .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JfqUCCaCsk0bWeod .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JfqUCCaCsk0bWeod .actor-man circle,#mermaid-svg-JfqUCCaCsk0bWeod line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JfqUCCaCsk0bWeod :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} configure_master(master_sdev) start() create owner thread open / configure CAN create io_ctx / ev_loop / io_user_* / io_can_net create co_dev from static co_sdev create NMT Master and RESET_NODE NMT / SDO / Heartbeat related traffic CANopen frames Boot-up / Heartbeat / SDO / PDO / EMCY RX event drain frame and event loop publish boot/state result snapshot / request completion
这里有两个很重要的工程语义。
第一,lely_rtt_runtime_start() 返回成功之前,owner 会完成基础 CAN/IO2/runtime 初始化,并创建本地 Master。启动失败不会把一个半初始化 runtime 当成可用对象发布。
第二,应用看到的 post 成功和远端节点真正完成动作是两回事。例如 lely_rtt_runtime_post_nmt() 返回 RT_EOK 只表示命令已经进入 owner queue;节点是否真的进入 Operational,要继续看 remote NMT snapshot。
这类语义看似麻烦,但对工业通信更安全,因为 API 不会把"本地已排队"伪装成"远端已经完成"。
Master 控制面已经覆盖哪些常用能力
当前 runtime.h 提供的 owner-safe 主站能力主要包括:
| 能力 | 软件包提供的接口语义 |
|---|---|
| Local NMT | 读取本地主站 NMT state snapshot |
| Remote NMT | 读取远端 state、Heartbeat timeout 后暂时标记 unavailable |
| Boot | 读取远端 NMT boot completion result |
| NMT command | 异步 post start/stop/pre-op/reset-node/reset-comm |
| Client-SDO | request object 形式的 upload/download |
| Block SDO | block upload/download |
| SDO cancel | 显式取消 queued/active application SDO |
| Manual NMT config | 通过 owner 调用 NMT configuration request,并返回 terminal result/diagnostic |
| Local OD | 访问本地 0x2000..0x5FFF manufacturer range |
| TPDO | 更新本地 OD 后触发已配置好的 static TPDO event |
| SYNC/PDO | 控制 0x1006 period、PDO transmission type,读取 processed-SYNC snapshot |
| EMCY | 保留远端 EMCY history,并支持本地主站 EMCY producer 操作 |
| TIME | 收取 TIME snapshot,或显式发送 absolute TIME |
| MSH | 使用 co 根命令进行示例调试和联调 |
这里最有价值的是"owner-safe"。应用不需要知道 co_nmt_t、co_csdo_t 到底在哪个线程创建,也不会拿到这些对象后在错误线程中直接调用。
SDO 也不是一个简单的同步 read()。软件包用 request object 表达它的生命周期:
text
create request
-> post upload/download
-> owner 启动 Client-SDO
-> wait / cancel
-> terminal result
-> destroy request
当前实现每个远端 Node-ID 同时最多一个 application SDO transaction,并且 application-owned Client-SDO 不借用 NMT boot 使用的 Client-SDO。这样做的目的就是避免应用 SDO 和自动 boot/configuration 同时抢 CiA 301 预定义 SDO 通道。
Kconfig 和源码选择为什么也是这个软件包的优势
协议栈移植到 MCU 后,最怕"为了一个 SDO Client 把桌面平台后端、pthread、filesystem、gateway 全部编进来"。
lely-canopen-rtt 的 SConscript 不是扫描整个 upstream src/,而是从 metadata/RTTHREAD_SOURCE_ALLOWLIST.txt 读取允许进入 target 的源文件,并明确拒绝 Linux/POSIX/Win32 I/O backend、thread loop、fiber executor 等不属于当前 RT-Thread 架构的源文件。
Kconfig 再对 CANopen feature 做第二层裁剪。例如:
text
PKG_LELY_USING_CO_CSDO
PKG_LELY_USING_CO_EMCY
PKG_LELY_USING_CO_LSS
PKG_LELY_USING_CO_MASTER
PKG_LELY_USING_CO_NMT_BOOT
PKG_LELY_USING_CO_NMT_CFG
PKG_LELY_USING_CO_RPDO
PKG_LELY_USING_CO_TPDO
PKG_LELY_USING_CO_SYNC
PKG_LELY_USING_CO_TIME
Master application bridge 也是按需开启:
text
PKG_LELY_USING_MASTER_COMMAND
PKG_LELY_USING_MASTER_SDO
PKG_LELY_USING_MASTER_NMT_CFG
PKG_LELY_USING_LOCAL_OD
PKG_LELY_USING_MASTER_PDO_TX
PKG_LELY_USING_MASTER_SYNC_PDO
PKG_LELY_USING_MASTER_EMCY
PKG_LELY_USING_MASTER_TIME
这使得"协议栈支持什么"和"产品这次实际启用了什么"是两个独立层次。对于资源受限 MCU,这比默认把所有功能打开更容易控制 ROM/RAM 和依赖关系。
怎么使用:从一个远端 Node1 到真正的产品主站
第一步:把软件包接入 RT-Thread 工程
上层 BSP 需要让本仓库的 Kconfig 和 SConscript 进入构建。
基础开关:
text
PKG_USING_LELY=y
它会选择 heap、device、CAN 和 event 等 runtime 需要的 RT-Thread 组件。
如果使用自动初始化:
text
PKG_LELY_APP_AUTO_INIT=y
PKG_LELY_CAN_DEV_NAME="can1"
然后根据实际 CAN 网络选择 bitrate、owner thread stack/priority/timeslice、RX batch 和 start/stop timeout。
这些值都是产品/BSP 参数,仓库默认值不是所有平台的推荐值。
第二步:准备每个远端设备的 EDS/DCF
CANopen 主站最好不要先手写远端对象表。正常路径是从设备厂商获取 EDS,或者在 CANopenEditor 中建立并导出具体 DCF。
例如伺服驱动可能提供 CiA 402 对象,I/O 模块可能按 CiA 401,编码器可能按 CiA 406。协议栈并不需要把这些 profile 全部硬编码成 C 结构;它首先把设备对象字典当作标准 CANopen OD 处理。
第三步:用 master.yml 描述网络策略
仓库示例:
yaml
master:
node_id: 127
baudrate: 1000
heartbeat_consumer: true
start: true
start_nodes: false
start_all_nodes: false
reset_all_nodes: false
stop_all_nodes: false
options:
heartbeat_multiplier: 3.0
retry_factor: 0
node1:
dcf: ../node1/node1.dcf
node_id: 1
revision_number: 0x00000001
serial_number: 0x00000001
boot: true
mandatory: false
reset_communication: true
这里最值得注意的不是 YAML 语法,而是职责位置。
Node-ID、Heartbeat multiplier、mandatory、automatic NMT Start、Reset Communication 这些应该是网络/产品策略,所以留在生成输入里。runtime.c 不应该根据某个项目随便猜出一套默认主站策略。
仓库中的 node_id: 127、revision/serial、Node1 是否 mandatory 等都只是 fixture,需要在产品使用前替换。
第四步:生成静态 Master Object Dictionary
仓库的 Windows Host 统一入口如下:
powershell
.\tools\gen_sdev.ps1 `
-Yml .\examples\master_node1\master.yml `
-Name master_sdev `
-OutDir .\examples\master_node1 `
-DcfFileName master.dcf `
-RemotePdo `
-CompactMaster `
-ErrorHistoryDepth 8 `
-MaxMasterSubObjects 256 `
-NoStrings `
-NoHeader `
-MetaFile master_sdev.meta
这条命令背后的核心链路是:
text
remote DCF + master.yml
-> dcfgen
-> compact Master DCF
-> dcf2c
-> master_sdev.c
产品真正需要版本管理的是:生成输入、生成工具版本和最终生成产物之间的关系。
第五步:开启需要的主站控制面
最小 Master 示例会选择基础 NMT Master/CSDO/NMT boot。需要应用主动控制时,再逐项打开:
text
PKG_LELY_USING_MASTER_COMMAND=y
PKG_LELY_USING_MASTER_SDO=y
PKG_LELY_USING_MASTER_NMT_CFG=y
PKG_LELY_USING_LOCAL_OD=y
PKG_LELY_USING_MASTER_PDO_TX=y
PKG_LELY_USING_MASTER_SYNC_PDO=y
PKG_LELY_USING_MASTER_EMCY=y
PKG_LELY_USING_MASTER_TIME=y
不要因为"以后可能会用"就全部启用。CANopen feature 和 application bridge 都会增加代码、状态和测试面。
第六步:先用 MSH 把网络跑通
仓库内 Master + Node1 示例启用 PKG_LELY_USING_MSH 后,会导出 co 根命令。
常用联调命令例如:
text
co status
co node 1
co boot 1
co nmt start 1
co nmt preop 1
co sdo read 1 0x1018 1 u32 1000
co sdo write 1 0x1017 0 u16 1000 1000
co sync status
co emcy 1
这一步适合验证:CAN 驱动是否正常、Node1 是否 Boot-up、Heartbeat 是否稳定、SDO 是否可访问、NMT command 是否真正改变远端状态。
需要注意,当前 MSH Kconfig 依赖仓库内 Master + Node1 示例。真实产品不应该把这个 fixture 当成最终应用接口,产品代码应使用公开 runtime API。
第七步:应用通过 owner-safe API 使用主站
自动初始化成功后,应用可以取得默认 runtime:
c
lely_rtt_runtime_t *runtime = lely_rtt_runtime_get_default();
查看远端状态:
c
rt_uint8_t state;
if (lely_rtt_runtime_get_remote_nmt_state(runtime, 1, &state) == RT_EOK) {
/* state contains the latest owner-published remote NMT state. */
}
发送 NMT Start:
c
lely_rtt_runtime_post_nmt(runtime,
LELY_RTT_NMT_COMMAND_START, 1);
做一次远端 SDO upload 时,使用 request object:
c
lely_rtt_sdo_request_t *req = lely_rtt_sdo_request_create();
struct lely_rtt_sdo_result result;
if (req
&& lely_rtt_runtime_post_sdo_upload(runtime, req,
1, 0x1018, 1, 1000) == RT_EOK
&& lely_rtt_sdo_request_wait(req, 1500) == RT_EOK
&& lely_rtt_sdo_request_get_result(req, &result) == RT_EOK) {
/* Check result.status, abort_code and result.data/result.size. */
}
if (req)
lely_rtt_sdo_request_destroy(req);
重点仍然是结果语义。post 成功只代表请求被接受;真正的 remote/protocol completion 要看 result.status 和 abort_code。
用在伺服、I/O 和编码器上时应该怎么理解
这个软件包是 CANopen 通信与 Master runtime,不是某个设备 profile 的高级业务库。
例如连接一个 CiA 402 伺服驱动时,Lely 可以完成:
- NMT 管理;
- SDO 访问
0x6040、0x6041、0x6060、0x607A等对象; - 根据 DCF 配置 PDO;
- 通过 RPDO/TPDO 交换实时控制和状态数据;
- 处理 Heartbeat、EMCY、SYNC。
但"CiA 402 Power Drive System 状态机应该何时写 0x0006、0x0007、0x000F""Homing/Profile Position/CSP 的产品控制策略"仍属于应用或单独的 device-profile driver。当前软件包没有把这些业务语义伪装成通用 CANopen Master API。
同样,连接 CiA 401 I/O 模块或 CiA 406 编码器时,软件包提供的是 OD/SDO/PDO/NMT 等通信基础;具体 I/O channel、position scaling、preset 等 profile 语义仍由上层理解。
这样的边界反而比较健康:CANopen 栈负责标准通信和网络生命周期,设备驱动负责 profile,产品逻辑负责机器行为。
当前必须说明的能力边界
推荐一个软件包时,把边界说清楚比只列优点更重要。
1. CAN FD transport 不等于 CANopen FD
软件包可以选择 CAN FD frame support,并明确适配 RT-Thread BSP 中 rt_can_msg.len 是 payload bytes 还是 raw DLC;但这不能自动推导成"已经实现 CiA 1301 CANopen FD Master"。当前文章讨论的核心仍然是经典 CANopen/CiA 301 路径。
如果产品明确需要 CANopen FD,应单独做标准和协议栈能力确认。
2. 当前 public Master bridge 不提供通用 dynamic PDO remapping
软件包可以触发已经配置好的 static TPDO,并可以调整支持的 PDO transmission type;但通用 dynamic PDO remapping 不在当前 owner-safe bridge 范围。
这与它的整体思路一致:优先在 Host 生成网络映射,而不是在运行时任意重构 PDO。
3. LSS core 可选,但当前没有对应的 owner-safe Master application bridge
Kconfig 可以启用 Lely LSS core,但当前公开 Master control plane 主要覆盖 NMT、SDO、OD、PDO/SYNC、EMCY、TIME,没有提供与这些模块同级的 RT-Thread LSS Master API。
如果产品需要在现场通过 LSS 动态分配 Node-ID/bitrate,需要继续扩展 owner-safe bridge,不能因为 upstream 有 LSS 就直接宣称当前软件包已经提供完整 LSS 产品接口。
这也是 CANopenNode 当前的一个实际优势:它已有明确的 LSS Master/Gateway 能力。
4. 静态网络配置更适合拓扑相对稳定的设备
dcfgen -> dcf2c 很适合工业设备、车辆控制器、机器人控制器这类"网络拓扑在固件构建时基本已知"的产品。
如果产品要求运行时发现任意第三方设备、动态创建大量对象和 PDO 映射,这套 Host-first 方法会显得更严格,需要额外设计动态配置层。
5. Host CI 不等于目标板验证
仓库已经把 Host regression tests 放入 GitHub Actions workflow,能够检查不少源码级和生成器契约;但这不等于真实 BSP 的 CAN driver、ISR 时序、bus-off recovery、queue pressure、CAN FD length convention 和多节点 HIL 已经在所有硬件上验证。
最终产品仍然需要用自己的 MCU、CAN controller、transceiver 和真实从站完成 target build 与总线测试。
最终应该怎么选
如果让我针对一个新的 RT-Thread CANopen 项目做选型,我会按下面的顺序判断。
优先选择 lely-canopen-rtt 的场景:
- 本机是 CANopen 主站/Manager,而不是简单从站;
- 网络里有多个远端节点;
- 需要比较完整的 NMT boot/configuration、Heartbeat、SDO、PDO/SYNC 管理;
- 希望根据 EDS/DCF 自动生成 Master 网络配置;
- 希望 MCU 运行时不解析 DCF;
- 项目是 RT-Thread,希望明确控制 Lely 与 ISR/driver/application thread 的并发关系;
- 可以接受对网络拓扑采用 Host-first、静态生成的工程模式。
更适合 CANopenNode 的场景:
- 主要开发 CANopen 从站;
- 主站只需要简单 NMT/SDO/LSS commander;
- 更看重成熟的 MCU port 社区、CANopenDemo、CANopenEditor 和现有 CANopenNode 生态;
- 需要其已经提供的 LSS Master 或其他特定功能。
更适合 CanFestival 的场景:
- 已有大量 CanFestival 代码和对象字典;
- 现有 RT-Thread 产品已经使用
canfestival-rtt; - 希望直接复用其现有 Master402 示例,不准备更换主站框架。
更适合商业协议栈的场景:
- 明确要求 CANopen FD、Safety、商业技术支持或标准覆盖承诺;
- 项目更看重供应商交付责任,而不是开源代码的完全可控性;
- 有对应的软件预算。
对于"RT-Thread + MCU + 多节点经典 CANopen 主站"这个具体组合,我更倾向于 Lely。真正让我推荐它的不是某一个 API,而是从网络配置到运行时的完整链条:
text
设备 EDS/DCF
-> 主站 YAML 网络策略
-> dcfgen 生成 Master DCF / 配置数据
-> dcf2c 生成静态 Master OD
-> RT-Thread single-owner runtime
-> NMT boot/configuration
-> SDO / PDO / SYNC / EMCY / TIME
-> 应用通过 owner-safe API 控制和观测
这条链把 CANopen 主站最容易散落到业务代码里的网络拓扑、启动配置、对象字典、异步协议状态和线程边界重新组织到各自应该在的位置。
作为软件包分享时,可以重点强调的价值
- 不是简单移植 Lely,而是为 RT-Thread 建立完整的 CANopen Master runtime。
- Lely 的 NMT Master/boot/configuration 能力比"简单 NMT master"更贴近多节点网络管理。
dcfgen让主站配置从手写 C 逻辑转为可审查的网络 YAML + EDS/DCF。dcf2c把 Master OD 静态化,MCU 不需要文件系统和运行时 DCF parser。- single-owner 把 Lely 对象与 RT-Thread ISR、driver callback、业务线程之间的并发边界说清楚。
- 应用通过 command queue、request object 和 snapshot 使用主站,不直接跨线程操作 Lely service。
- Kconfig + source allowlist 控制 target 实际编译的协议能力和上游源码边界。
- 对 CAN FD length、CAN status、hardware filter 等 BSP 差异采用显式契约,不在通用层猜测。
- 与 CANopenNode、CanFestival 相比,优势集中在"复杂主站 + 网络生成 + RT-Thread 并发模型",而不是宣称所有维度都更强。
- 与商业栈相比,优势是 Apache-2.0、源码可控和可定制,代价是产品团队自己承担 target/HIL 验证和功能扩展。
如果一个团队已经开始为主站自己维护远端节点表、Heartbeat 表、boot state、SDO 配置数组、PDO mapping 数组、多个线程之间的 CANopen 锁,并且这些逻辑越来越难维护,那么这个软件包的价值就已经不只是"换一个协议栈",而是把主站重新整理成一套可生成、可裁剪、可审查的工程架构。
参考资料
wdfk-prog/lely-canopen-rtt:https://github.com/wdfk-prog/lely-canopen-rtt- Lely CANopen - Library overview:https://opensource.lely.com/canopen/docs/overview/
- Lely CANopen - Build configuration:https://opensource.lely.com/canopen/docs/configuration/
- Lely CANopen - EDS/DCF tools:https://opensource.lely.com/canopen/docs/dcf-tools/
- Lely CANopen - C++ tutorial / Master DCF:https://opensource.lely.com/canopen/docs/cpp-tutorial/
- Lely CANopen - Standards support:https://opensource.lely.com/canopen/docs/standards/
- CANopenNode repository:https://github.com/CANopenNode/CANopenNode
- CANopenNode documentation:https://canopennode.github.io/CANopenNode/
- CanFestival:https://canfestival.org/
- RT-Thread CanFestival port:https://github.com/gbcwbz/canfestival-rtt
- MicroControl CANopen / CANopen FD Master:https://www.microcontrol.net/en/portfolio/protocol-stacks/canopen/canopen-master-stack/
- CiA 301 CANopen application layer and communication profile。本文协议术语同时参考项目提供的 CiA 301 V4.2.0 中文注释资料。
说明:本文按
lely-canopen-rtt2026-09-11 可见main(head09fd73e980cb2dc2f832a09ea6d41b3bbb00061d)及对应公开文档整理。Host/源码级证据不等同于具体 BSP、目标板和多节点 HIL 验证结果。