lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站

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

在这里插入图片描述

@[toc]

推荐判断

如果项目的目标是在 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 节点之外,它还需要知道:

  1. 网络里应该有哪些节点;
  2. 哪些节点必须存在,哪些节点允许缺席;
  3. 节点启动后是否需要 Reset Communication;
  4. 是否需要检查 Vendor-ID、Product Code、Revision、Serial Number;
  5. 哪些参数需要在 boot 阶段通过 SDO 下载;
  6. Heartbeat producer/consumer 如何配置;
  7. 主站什么时候启动从站进入 Operational;
  8. 从站 TPDO 应映射到主站哪个 RPDO;
  9. 某个节点掉线、重启或重新 Boot-up 后,应用侧状态如何恢复;
  10. 运行阶段的 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 作为可裁剪功能。上游构建配置中可以分别关闭 masternmt-bootnmt-cfg,这说明这些能力不是应用示例里临时拼出来的辅助代码,而是协议栈内部正式维护的功能路径。

Lely 对 NMT Master boot 还定义了专门的 SDO timeout、boot wait timeout、SDO retry、reset timeout 等参数。换句话说,它并不是只负责发送一个 NMT Start,而是已经考虑“一个主站如何等待、检查和配置远端节点”这类网络管理问题。

这对多节点主站非常重要。项目不再需要自己从零设计:

1
2
3
4
5
6
7
8
收到 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 数据。

这意味着产品可以把很多本来散落在源码中的配置:

1
2
3
4
5
6
7
8
Node-ID
Identity check
Heartbeat
NMT startup policy
mandatory/optional node
SDO boot configuration
RPDO/TPDO mapping
SYNC period

集中到网络描述文件中。

对主站项目来说,这种模式比在 main.c 里维护几十个 0x14000x16000x18000x1A00 写操作更容易审查,也更容易随从站 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=1LELY_NO_STDIO=1LELY_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 的方案,产品集成时许可证边界更直接。当然,最终发布仍然应该保留仓库 LICENSENOTICE 和适用的 upstream 版权信息。

lely-canopen-rtt 解决的不是 CANopen 协议,而是 RT-Thread 集成问题

直接把 Lely 源码加入 RT-Thread 工程并不等于完成了移植。真正困难的是 I/O、时间、线程和生命周期。

这个软件包最核心的设计是 single-owner:RT-Thread 可以有很多应用线程、中断和驱动 callback,但所有 Lely EV/IO2/CANopen 对象只允许一个 owner thread 访问。

target 中固定:

1
2
3
#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_tco_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 有几个更吸引我的点:

  1. dcfgen 的输入天然是整个网络,而不是只围绕某个节点对象字典;
  2. Lely C core 明确采用 passive/asynchronous 思路,适合由 RTOS 接管 I/O 和调度;
  3. lely-canopen-rtt 把线程所有权收口为 single-owner,而不是让业务代码直接穿过多个 timer/CAN callback;
  4. Apache-2.0 的集成边界更直接;
  5. 本软件包把上游源文件选择、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 两部分。

1
2
3
4
5
6
7
8
flowchart LR
EDS["从站 EDS / DCF"] --> GEN["dcfgen + master.yml"]
GEN --> DCF["Master DCF"]
DCF --> COMPACT["compact_master_dcf.py"]
COMPACT --> DCF2C["dcf2c --no-strings"]
DCF2C --> SDEV["master_sdev.c / const co_sdev"]
SDEV --> BUILD["RT-Thread firmware build"]
BUILD --> MCU["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 内推进

目标端的主路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart LR
APP["Application / MSH"] -->|"post command"| MQ["RT-Thread message queue"]
APP -->|"read"| SNAP["owner-published snapshot"]

RX["CAN RX callback"] -->|"pin + event"| EVT["RT-Thread event"]
TIMER["timer callback"] -->|"pin + event"| EVT
STATUS["CAN status callback"] -->|"pin + event"| EVT

MQ --> OWNER["single Lely owner thread"]
EVT --> OWNER
OWNER --> LOOP["Lely ev_loop"]
LOOP --> CO["NMT / SDO / PDO / SYNC / EMCY / TIME"]
CO --> NET["io_can_net"]
NET --> UCAN["io_user_can"]
NET --> UTIMER["io_user_timer"]
UCAN --> DRIVER["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 时,启动过程大致可以理解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sequenceDiagram
participant App as RT-Thread application
participant Runtime as lely_rtt_runtime
participant Owner as Lely owner thread
participant Lely as Lely CANopen
participant CAN as RT-Thread CAN
participant Slave as Remote node

App->>Runtime: configure_master(master_sdev)
App->>Runtime: start()
Runtime->>Owner: create owner thread
Owner->>CAN: open / configure CAN
Owner->>Lely: create io_ctx / ev_loop / io_user_* / io_can_net
Owner->>Lely: create co_dev from static co_sdev
Owner->>Lely: create NMT Master and RESET_NODE
Lely->>CAN: NMT / SDO / Heartbeat related traffic
CAN->>Slave: CANopen frames
Slave-->>CAN: Boot-up / Heartbeat / SDO / PDO / EMCY
CAN-->>Owner: RX event
Owner->>Lely: drain frame and event loop
Lely-->>Runtime: publish boot/state result
Runtime-->>App: 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_tco_csdo_t 到底在哪个线程创建,也不会拿到这些对象后在错误线程中直接调用。

SDO 也不是一个简单的同步 read()。软件包用 request object 表达它的生命周期:

1
2
3
4
5
6
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-rttSConscript 不是扫描整个 upstream src/,而是从 metadata/RTTHREAD_SOURCE_ALLOWLIST.txt 读取允许进入 target 的源文件,并明确拒绝 Linux/POSIX/Win32 I/O backend、thread loop、fiber executor 等不属于当前 RT-Thread 架构的源文件。

Kconfig 再对 CANopen feature 做第二层裁剪。例如:

1
2
3
4
5
6
7
8
9
10
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 也是按需开启:

1
2
3
4
5
6
7
8
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 需要让本仓库的 KconfigSConscript 进入构建。

基础开关:

1
PKG_USING_LELY=y

它会选择 heap、device、CAN 和 event 等 runtime 需要的 RT-Thread 组件。

如果使用自动初始化:

1
2
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 描述网络策略

仓库示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 统一入口如下:

1
2
3
4
5
6
7
8
9
10
11
12
.\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

这条命令背后的核心链路是:

1
2
3
4
5
remote DCF + master.yml
-> dcfgen
-> compact Master DCF
-> dcf2c
-> master_sdev.c

产品真正需要版本管理的是:生成输入、生成工具版本和最终生成产物之间的关系。

第五步:开启需要的主站控制面

最小 Master 示例会选择基础 NMT Master/CSDO/NMT boot。需要应用主动控制时,再逐项打开:

1
2
3
4
5
6
7
8
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 根命令。

常用联调命令例如:

1
2
3
4
5
6
7
8
9
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:

1
lely_rtt_runtime_t *runtime = lely_rtt_runtime_get_default();

查看远端状态:

1
2
3
4
5
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:

1
2
lely_rtt_runtime_post_nmt(runtime,
LELY_RTT_NMT_COMMAND_START, 1);

做一次远端 SDO upload 时,使用 request object:

1
2
3
4
5
6
7
8
9
10
11
12
13
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.statusabort_code

用在伺服、I/O 和编码器上时应该怎么理解

这个软件包是 CANopen 通信与 Master runtime,不是某个设备 profile 的高级业务库。

例如连接一个 CiA 402 伺服驱动时,Lely 可以完成:

  • NMT 管理;
  • SDO 访问 0x60400x60410x60600x607A 等对象;
  • 根据 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 的场景:

  1. 本机是 CANopen 主站/Manager,而不是简单从站;
  2. 网络里有多个远端节点;
  3. 需要比较完整的 NMT boot/configuration、Heartbeat、SDO、PDO/SYNC 管理;
  4. 希望根据 EDS/DCF 自动生成 Master 网络配置;
  5. 希望 MCU 运行时不解析 DCF;
  6. 项目是 RT-Thread,希望明确控制 Lely 与 ISR/driver/application thread 的并发关系;
  7. 可以接受对网络拓扑采用 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,而是从网络配置到运行时的完整链条:

1
2
3
4
5
6
7
8
设备 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 主站最容易散落到业务代码里的网络拓扑、启动配置、对象字典、异步协议状态和线程边界重新组织到各自应该在的位置。

作为软件包分享时,可以重点强调的价值

  1. 不是简单移植 Lely,而是为 RT-Thread 建立完整的 CANopen Master runtime。
  2. Lely 的 NMT Master/boot/configuration 能力比“简单 NMT master”更贴近多节点网络管理。
  3. dcfgen 让主站配置从手写 C 逻辑转为可审查的网络 YAML + EDS/DCF。
  4. dcf2c 把 Master OD 静态化,MCU 不需要文件系统和运行时 DCF parser。
  5. single-owner 把 Lely 对象与 RT-Thread ISR、driver callback、业务线程之间的并发边界说清楚。
  6. 应用通过 command queue、request object 和 snapshot 使用主站,不直接跨线程操作 Lely service。
  7. Kconfig + source allowlist 控制 target 实际编译的协议能力和上游源码边界。
  8. 对 CAN FD length、CAN status、hardware filter 等 BSP 差异采用显式契约,不在通用层猜测。
  9. 与 CANopenNode、CanFestival 相比,优势集中在“复杂主站 + 网络生成 + RT-Thread 并发模型”,而不是宣称所有维度都更强。
  10. 与商业栈相比,优势是 Apache-2.0、源码可控和可定制,代价是产品团队自己承担 target/HIL 验证和功能扩展。

如果一个团队已经开始为主站自己维护远端节点表、Heartbeat 表、boot state、SDO 配置数组、PDO mapping 数组、多个线程之间的 CANopen 锁,并且这些逻辑越来越难维护,那么这个软件包的价值就已经不只是“换一个协议栈”,而是把主站重新整理成一套可生成、可裁剪、可审查的工程架构。

参考资料

  1. wdfk-prog/lely-canopen-rtthttps://github.com/wdfk-prog/lely-canopen-rtt
  2. Lely CANopen - Library overview:https://opensource.lely.com/canopen/docs/overview/
  3. Lely CANopen - Build configuration:https://opensource.lely.com/canopen/docs/configuration/
  4. Lely CANopen - EDS/DCF tools:https://opensource.lely.com/canopen/docs/dcf-tools/
  5. Lely CANopen - C++ tutorial / Master DCF:https://opensource.lely.com/canopen/docs/cpp-tutorial/
  6. Lely CANopen - Standards support:https://opensource.lely.com/canopen/docs/standards/
  7. CANopenNode repository:https://github.com/CANopenNode/CANopenNode
  8. CANopenNode documentation:https://canopennode.github.io/CANopenNode/
  9. CanFestival:https://canfestival.org/
  10. RT-Thread CanFestival port:https://github.com/gbcwbz/canfestival-rtt
  11. MicroControl CANopen / CANopen FD Master:https://www.microcontrol.net/en/portfolio/protocol-stacks/canopen/canopen-master-stack/
  12. CiA 301 CANopen application layer and communication profile。本文协议术语同时参考项目提供的 CiA 301 V4.2.0 中文注释资料。

说明:本文按 lely-canopen-rtt 2026-09-11 可见 main(head 09fd73e980cb2dc2f832a09ea6d41b3bbb00061d)及对应公开文档整理。Host/源码级证据不等同于具体 BSP、目标板和多节点 HIL 验证结果。