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

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 节点之外,它还需要知道:

  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,而是已经考虑"一个主站如何等待、检查和配置远端节点"这类网络管理问题。

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

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 里维护几十个 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 中固定:

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_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 两部分。
#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_tco_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-rttSConscript 不是扫描整个 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 需要让本仓库的 KconfigSConscript 进入构建。

基础开关:

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.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,而是从网络配置到运行时的完整链条:

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

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

  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 验证结果。

相关推荐
Token掘金室1 小时前
JSON模式结构化输出报错
java·服务器·json
元岳数字人小元1 小时前
数字人交互的用户体验设计与场景交互感受
运维·人工智能·开源·人机交互·交互
一技安身1 小时前
【信创】外网打包ragflow镜像并导入内网银河麒麟V10arm64服务器
运维·服务器
秋饼1 小时前
Spring AI 2.0 接入 DeepSeek V4.1 Flash 生产级实战
java·ai·技术分享·后端开发
IT枫斗者枫哥1 小时前
Java 分批导出仍然 OOM?用 32 MiB 堆复现三种 CSV 写法
java
T01156181 小时前
全栈项目实战手记|艺培场馆课时预约小程序全项目开发历程完整复盘总结
运维·服务器·小程序
shehuiyuelaiyuehao1 小时前
算法43,外观数列,模拟算法+双指针
java·算法
智_永无止境1 小时前
一个Docker命令,40万首古诗词API开箱即用
docker·诗泉
程序员-Benothing2 小时前
Linux 用户与用户组管理:useradd usermod groupadd 实战
linux·运维·服务器