Lely CANopen NMT Heartbeat、srv与cfg 机制源码学习
@[toc]
本文把以下六个内部文件放在同一条运行链上分析:
1 | src/co/nmt_hb.c |
它们并不是三套彼此独立的协议实现,而是 co_nmt_t 周围的三类内部组件:
nmt_hb:针对一个远端 Node-ID 维护 Heartbeat consumer 的接收、超时和状态事件;nmt_srv:根据本节点当前 NMT 状态装配或拆除 SDO、PDO、SYNC、TIME、EMCY、LSS 服务;nmt_cfg:在主站 boot slave 流程中,执行恢复默认值、文本 DCF、concise DCF 和用户扩展配置。
先建立一个总的心智模型:
1 | co_nmt_t |
这三部分共同依赖 can_net_t。它们自身不创建线程、不读取操作系统时钟,也不直接操作 SocketCAN;CAN 帧和时间推进都由外部把事件送入 can_net_t,再由 receiver、timer 和异步完成回调推动状态机。
源码基线与证据边界:本文以 Lely 官方 GitLab 项目、官方 Doxygen 2.4.0、官方功能/配置说明以及用户现有的 NMT boot 源码笔记为依据。本文没有声称完成当前
master分支的本地构建、单元测试或目标板验证;涉及内部函数关系的结论以官方 Doxygen 暴露的函数、结构字段和文档为准,涉及设计动机的内容会明确写成分析。
1. 六个文件分别解决什么问题
| 文件 | 核心对象 | 主要职责 | 不负责什么 |
|---|---|---|---|
nmt_hb.h |
co_nmt_hb_t |
声明单节点 Heartbeat consumer 内部接口 | 不公开给普通应用直接使用 |
nmt_hb.c |
struct __co_nmt_hb |
CAN 接收、超时定时、Heartbeat 错误发生/恢复、远端状态变化 | 不管理整个 0x1016 数组,不决定系统错误策略 |
nmt_srv.h |
struct co_nmt_srv |
定义服务位掩码和 NMT 服务管理器 | 不实现 SDO/PDO/SYNC 等协议本体 |
nmt_srv.c |
co_nmt_srv_set() |
创建、启动、停止和销毁 SDO/PDO/SYNC/TIME/EMCY/LSS | 不决定本节点应该进入哪个 NMT 状态 |
nmt_cfg.h |
co_nmt_cfg_t |
声明 configuration request 内部接口 | 不提供通用对象字典下载 API |
nmt_cfg.c |
struct __co_nmt_cfg |
编排 restore、0x1F20、0x1F22、用户配置和复位等待 |
不实现 SDO 分段/块传输,不判断是否需要配置 |
最容易混淆的是 nmt_srv 和 nmt_cfg:
1 | nmt_srv |
前者属于本地服务生命周期,后者属于主站对远端节点的配置流程。
2. 三组对象在 NMT 总体架构中的位置
下面这张图重点观察所有权和事件方向:co_nmt_t 是上层总控,三个内部组件都把结果回送给它,但实际 CAN 收发和定时仍由 can_net_t 驱动。
1 | flowchart TB |
关键边界如下:
co_nmt_t决定状态和策略;co_nmt_srv执行本地服务集合切换;co_nmt_hb只维护一个远端节点的错误控制上下文;co_nmt_cfg只执行一次目标明确的配置请求;co_csdo_t才是实际发送 SDO 请求、处理响应和超时的传输状态机。
3. nmt_srv:NMT 状态与协议服务之间的装配层
3.1 为什么不能在创建 NMT 时一次性创建全部服务
CANopen 的通信能力受 NMT 状态限制。典型行为是:
- Initialisation/Reset 阶段不运行正常通信服务;
- Pre-operational 允许 SDO、SYNC、TIME、EMCY 等配置和管理通信,但 PDO 不进入正常过程数据交换;
- Operational 才启用 PDO;
- Stopped 只保留 NMT 和错误控制相关能力。
Lely 没有把这些状态判断散落到每个服务内部,而是引入 co_nmt_srv,把“当前应启用哪些服务”压缩成一个服务位掩码。nmt.c 的状态入口决定目标掩码,co_nmt_srv_set() 执行差异切换。
nmt_srv.h 中可确认的服务位包括:
1 | CO_NMT_SRV_SDO |
这里要注意:
CO_NMT_PREOP_SRV、CO_NMT_START_SRV和CO_NMT_STOP_SRV的具体组合定义在nmt.c,不在本文分析的六个文件中。nmt_srv.c只负责执行该组合,不负责制定状态策略。
3.2 struct co_nmt_srv 保存什么
官方 Doxygen 显示该结构至少包含:
- 指向父
co_nmt_t的指针; - Server-SDO 数组和数量
nssdo; - Client-SDO 数组和数量
ncsdo; - RPDO 数组和数量
nrpdo; - TPDO 数组和数量
ntpdo; - SYNC、TIME、EMCY、LSS 服务指针;
- 当前启用服务位掩码。
它不是一个协议状态机,而是一个聚合对象和生命周期控制器。可以抽象成:
1 | struct co_nmt_srv { |
上面是结构关系的等价表达,不是逐字复制源码。实际字段是否存在还受功能裁剪宏影响。
3.3 co_nmt_srv_init() 只初始化管理器,不代表全部服务已运行
co_nmt_srv_init() 建立管理器与父 NMT 的关系,并初始化内部指针、数量和状态。它与 co_nmt_srv_set() 的职责不同:
1 | co_nmt_srv_init() |
因此,新建 co_nmt_t 后不会立即拥有全部 SDO/PDO 服务。官方概览也明确说明,新创建的 NMT 服务处于 Initialisation 状态,不创建正常通信服务;应用可以先注册回调,再通过 reset node 启动完整状态机。
3.4 co_nmt_srv_set() 的核心是“集合差分”
可以把它理解为下面的逻辑:
1 | old_mask = 当前已启用集合 |
随后分别调用:
1 | co_nmt_srv_init_sdo() / co_nmt_srv_fini_sdo() |
这不是简单的 start/stop 开关。对数组型服务,例如 SDO 和 PDO,初始化函数需要从对象字典中发现配置对象,创建对应数量的服务实例,并把它们保存到管理器数组中;结束函数则要按相反顺序停止、销毁并释放数组。
1 | flowchart LR |
这种设计的工程价值是:
- 状态机只表达“要什么”,不直接管理每种服务的分配细节;
- 服务管理器集中处理部分初始化失败和销毁顺序;
co_nmt_get_ssdo()、co_nmt_get_csdo()、co_nmt_get_rpdo()、co_nmt_get_tpdo()等公开查询接口可以统一从 NMT 中取得当前实例;- 功能裁剪时,相应初始化分支可在编译期移除。
3.5 SDO 服务如何从对象字典变成实例
co_nmt_srv_init_sdo() 面向两类对象:
| 对象范围 | 服务类型 | 当前节点角色 |
|---|---|---|
0x1200..0x127F |
co_ssdo_t |
作为 SDO server,被远端访问 |
0x1280..0x12FF |
co_csdo_t |
作为 SDO client,主动访问远端 |
管理器不是预设固定数量,而是依据 co_dev_t 中实际存在且可用的通信参数创建实例。每个实例仍由自己的 ssdo.c 或 csdo.c 状态机处理分段、块传输、toggle、CRC 和超时;nmt_srv.c 不重复实现这些协议。
3.6 PDO 服务为什么要同时处理映射、SYNC 和错误回调
co_nmt_srv_init_pdo() 需要关联:
1 | RPDO communication parameter 0x1400.. |
创建 RPDO/TPDO 后,管理器还要把回调接回 NMT:
co_nmt_srv_rpdo_err():把 RPDO 长度、超时或映射相关通信错误送入 NMT 默认错误处理;co_nmt_srv_sync_err():把 SYNC 超时或数据长度错误送入 NMT;co_nmt_srv_sync_ind():在 SYNC 到来后协调同步 RPDO/TPDO,并最终调用 NMT 的 SYNC indication。
因此 nmt_srv 不只是“放一组指针”,它还是协议服务与 NMT 统一错误策略之间的适配层。
3.7 服务销毁顺序为何重要
PDO 可能依赖 SYNC,多个服务都可能持有 can_recv_t 或 can_timer_t。销毁时必须先阻止新的 CAN/定时事件进入,再释放服务对象和数组。否则会出现:
1 | receiver/timer 仍挂在 can_net_t |
co_nmt_srv_fini_*() 的存在说明 Lely 把这种生命周期约束集中处理。应用侧不应绕过 NMT 管理器,私自销毁由它创建的 SDO/PDO/SYNC 等实例。
4. nmt_hb:一个实例只监控一个远端节点
4.1 co_nmt_hb_t 的粒度
co_nmt_hb_t 不是整个 0x1016 Consumer heartbeat time 数组,也不是全网 Heartbeat 管理器。它对应一个远端 Node-ID,保存该节点的:
1 | CAN receiver |
官方 Doxygen 对关键字段的说明包括:
net:所属can_net_t;nmt:父 NMT 服务;st:不含 toggle bit 的节点状态;ms:consumer heartbeat time,单位毫秒;state:Heartbeat 错误当前是CO_NMT_EC_OCCURRED还是CO_NMT_EC_RESOLVED。
多个 0x1016 子索引会由 nmt.c 创建和管理多个 co_nmt_hb_t。nmt_hb.c 只关心自己的一个配置项。
4.2 创建阶段:先准备 receiver 和 timer
co_nmt_hb_create(net, nmt) 内部遵循 Lely 常见的四段式生命周期:
1 | alloc |
初始化阶段会创建并绑定:
- 一个
can_recv_t,回调为co_nmt_hb_recv(); - 一个
can_timer_t,回调为co_nmt_hb_timer(); - 父
co_nmt_t指针和初始状态。
创建完成不等于已经监听某个 CAN-ID。真正的 Node-ID 和超时来自 co_nmt_hb_set_1016()。
4.3 co_nmt_hb_set_1016():把对象字典配置落到 receiver 和 timer
公开 NMT API co_dev_cfg_hb() 会更新对象 0x1016。对象字典 download indication 最终让 NMT 找到对应 Heartbeat consumer,并调用:
1 | co_nmt_hb_set_1016(hb, id, ms); |
其职责可归纳为:
- 保存新的 producer Node-ID 和 consumer time;
- 停止旧 receiver/timer,避免旧 Node-ID 继续产生事件;
- 在配置有效时,将 receiver 绑定到:
1 | CO_NMT_EC_CANID(id) = 0x700 + id |
- 根据 Heartbeat 监控状态维护超时定时器;
- 清理或重置与旧配置相关的错误状态。
当 ms == 0 时,Heartbeat consumption 被禁用。Node-ID 不在有效远端节点范围时,也不应保持有效监听。
需要区分两个时间:
1 | 远端 0x1017 |
工程上通常要求 consumer time 大于 producer time,并保留调度和总线抖动裕量。Lely 的 dcfgen 默认可用 heartbeat multiplier 从 producer time 生成 consumer time。
4.4 接收路径:CAN 帧只包含远端 NMT 状态
Heartbeat 与 boot-up 共用:
1 | CAN-ID = 0x700 + Node-ID |
co_nmt_hb_recv() 的主线可以理解为:
1 | flowchart TD |
这里有三个不同事件,不应合并:
| 事件 | 含义 |
|---|---|
boot-up 0x00 |
远端完成通信初始化或复位;上层 boot/error-control 逻辑可能继续推进 |
| Heartbeat timeout | 在监控窗口内没有收到有效帧 |
| NMT state change | 收到帧,但状态与 NMT 维护的参考状态不同 |
co_nmt_hb_ind() 的参数包含:
1 | id |
这让父 co_nmt_t 可以同时更新:
- Heartbeat 错误事件;
- 远端节点状态;
- boot-up 检测;
- 主站错误处理策略;
- 应用注册的 Heartbeat/state indication。
4.5 超时路径:timer 只报告事件,不决定系统动作
当 consumer timer 到期时,co_nmt_hb_timer() 将错误状态切换为 CO_NMT_EC_OCCURRED,并通过:
1 | co_nmt_hb_ind(nmt, id, OCCURRED, TIMEOUT, st) |
把事件交给父 NMT。
它不直接执行:
1 | 发送 EMCY |
这些动作属于 co_nmt_on_hb()、co_nmt_node_err_ind()、对象 0x1029 error behavior、主站 boot/error handler 或用户回调。这样 nmt_hb.c 保持为纯错误检测器,而不是系统策略执行器。
4.6 co_nmt_hb_set_st() 的作用
co_nmt_hb_set_st(hb, st) 设置父 NMT 认为该远端节点应该处于的状态参考值。典型调用时机包括:
- 主站向节点发送 Start/Stop/Pre-operational 命令;
- boot slave 流程确认节点状态;
- 收到 boot-up 后更新节点上下文;
- 应用或管理器显式改变期望状态。
之后收到 Heartbeat 时,nmt_hb 可以区分:
1 | 节点还在线,但状态不符合预期 |
与:
1 | 节点完全没有继续发 Heartbeat |
这正是 reason = CO_NMT_EC_STATE 和 reason = CO_NMT_EC_TIMEOUT 分开的原因。
5. nmt_cfg:主站配置请求的异步编排状态机
5.1 它在 boot slave 流程中的入口
nmt_boot.c 在检查身份、软件和配置日期/时间后,如果判断需要更新配置,会调用:
1 | co_nmt_cfg_req(nmt, id, timeout, con, data); |
公开 API 位于 nmt.h/nmt.c,真正的子状态机位于 nmt_cfg.c。完成后回调:
1 | co_nmt_cfg_con_t(nmt, id, abort_code, data); |
所以 nmt_cfg 是 nmt_boot 的一个可复用子流程:
1 | nmt_boot 判断“是否需要配置” |
5.2 struct __co_nmt_cfg 保存的不是配置数据本体
该对象主要保存流程上下文:
can_net_t *net;co_dev_t *dev,即主站本地 DCF;co_nmt_t *nmt;- 当前目标 Node-ID;
- 使用的
co_csdo_t; - request timeout;
- 当前状态指针;
- CAN receiver/timer,用于等待 reset 后的 boot-up;
- 下载确认、用户配置结果和最终 confirmation 回调;
- 文本 DCF 解析/迭代所需的临时对象。
真正的远端传输数据仍由:
1 | co_csdo_dn_req() |
等 Client-SDO API 发送。nmt_cfg 负责选择下一步,不负责重新实现 SDO 协议。
5.3 状态机主线
从官方 Doxygen 暴露的状态入口函数可以还原出如下主链:
1 | restore |
1 | stateDiagram-v2 |
这里的状态名对应 nmt_cfg.c 可见的函数族:
1 | co_nmt_cfg_restore_on_enter / on_dn_con |
5.4 restore:根据 0x1F8A 决定是否写远端 0x1011
主站 DCF 的 0x1F8A Restore configuration 指定每个从站是否以及通过哪个子索引恢复默认参数。若目标节点配置了 restore,nmt_cfg 会向远端对象:
1 | 0x1011:<restore sub-index> |
下载标准签名:
1 | "load" = 0x64616F6C(按 CANopen little-endian 编码发送) |
若从站接受该请求,恢复动作可能要求 Reset Communication 或 Reset Node 才生效。nmt_cfg 随后进入 reset 等待状态:
- 发送相应 NMT reset 命令;
- receiver 监听目标节点
0x700 + id; - timer 使用
LELY_CO_NMT_CFG_RESET_TIMEOUT; - 收到 boot-up
data[0] == 0后继续; - 超时则以 SDO/NMT 相关错误结束配置。
这解释了 nmt_cfg.c 为什么除 CSDO 外,还持有 CAN receiver 和 timer:只有 SDO completion 不能证明从站已经完成 reset 并重新上线。
5.5 0x1F20 Store DCF:文本 DCF 的差异下载
Lely 官方发布说明明确:如果主站对象 0x1F20:<Node-ID> 包含文本 DCF,配置流程会读取该 DCF,并对其中满足以下条件的对象生成 SDO 写入:
1 | 对象可写 |
因此 store_1f20 分支不是把整个文本文件作为一个 DOMAIN 原样写给从站,而是:
1 | 主站本地 0x1F20:<id> 指向/包含文本 DCF |
co_nmt_cfg_store_1f20_on_leave() 的存在也说明该分支需要释放或关闭文本 DCF 解析上下文,而不仅是提交一次 SDO。
该自动差异写入存在一个重要边界:PDO 配置通常要求严格的“先禁用 PDO → 修改通信/映射参数 → 再启用 PDO”顺序。早期自动文本 DCF 差异机制不能仅凭无序单对象写入安全完成全部 PDO 重配置。因此工程上更适合由 dcfgen 生成有序 concise DCF,或在用户配置回调中显式控制顺序。
5.6 0x1F22 Concise DCF:按记录顺序执行 SDO 写入
0x1F22:<Node-ID> 保存该从站的 concise DCF。其内容是紧凑的二进制记录序列:
1 | 记录数量 |
store_1f22 分支把该数据交给 Client-SDO 的 concise DCF download 能力,按文件顺序逐项写入远端对象字典。与 0x1F20 的“根据 ParameterValue/DefaultValue 生成写入”不同,0x1F22 已经明确规定了:
- 写哪些对象;
- 按什么顺序写;
- 每项写入多少字节;
- 哪些 disable/enable 操作先后发生。
这也是 Lely dcfgen 为每个从站生成 .bin 文件的主要用途。master.yml 的 configuration_file 最终映射到 0x1F22。
5.7 user configuration:自动配置之后的扩展窗口
文本 DCF 与 concise DCF 处理完成后,co_nmt_cfg_user_on_enter() 通过父 NMT 触发:
1 | co_nmt_cfg_ind_t(nmt, id, sdo, data); |
C++ BasicMaster/AsyncMaster 中对应 OnConfig()。这里传入可用的 Client-SDO,应用可以继续执行设备专用配置。
这个回调不是“配置已经完成”通知。若应用发起异步 SDO,请求完成后必须调用:
1 | co_nmt_cfg_res(nmt, id, abort_code); |
C++ 层通常对应 ConfigResult()。只有 nmt_cfg 收到结果后,状态机才会进入成功或 abort 结束状态。
完整顺序是:
1 | 0x1F20 自动配置 |
5.8 所有异步入口最终汇聚到统一事件分发
nmt_cfg.c 可见四类事件入口:
1 | co_nmt_cfg_emit_dn_con() SDO download 完成 |
这些入口不会在各处写一套 switch(state),而是把事件交给当前状态的函数指针:
1 | state->on_dn_con |
状态切换由 co_nmt_cfg_enter() 统一执行 on_leave 和 on_enter。这种模式与 nmt_boot.c、csdo.c、ssdo.c 一致:
状态对象定义当前能接受什么事件;事件回调只转发;下一状态由当前状态返回或显式进入。
6. 三个模块怎样在一次主站启动中连接起来
下面按一个典型主站启动从站的顺序串联三组源码。
6.1 本地主站先通过 nmt_srv 建立运行能力
1 | 应用创建 co_nmt_t |
其中,后续 boot/config 必须依赖可用的 Client-SDO。若主站 DCF 中没有相应 0x1280.. CSDO 参数,或构建时禁用了 master/CSDO 能力,nmt_cfg 就没有传输通道可用。
6.2 nmt_hb 持续维护远端在线与状态信息
主站从 0x1016 创建 Heartbeat consumer:
1 | 0x1016:<sub-index> |
收到 boot-up/Heartbeat 后,父 NMT 更新节点状态。超时或状态异常时,Heartbeat indication 进入 NMT error handler。
6.3 nmt_boot 在需要时调用 nmt_cfg
1 | boot slave |
6.4 启动完成后,nmt_srv 再启用 PDO
当主站和从站进入 Operational,本节点 NMT 状态入口把服务 mask 切换到包含 PDO 的集合:
1 | co_nmt_srv_set(Operational mask) |
这说明配置期和运行期有明确边界:
1 | 配置期主要依赖 SDO/CSDO |
6.5 完整时序
1 | sequenceDiagram |
7. 配置变化如何穿透到运行对象
7.1 修改 0x1016 不是只改一个整数
当 SDO 写本地 0x1016 时:
1 | SDO download |
所以修改 0x1016 会立即改变 CAN 接收注册和定时行为。它不是延迟到下次复位才生效的普通参数。
7.2 NMT 状态变化不是只改 nmt->st
1 | NMT command 或内部状态切换 |
因此从 Pre-operational 进入 Operational 的可观察结果不只是 Heartbeat 的 Byte0 从 0x7F 变成 0x05,还包括 PDO receiver、timer 和 TPDO 触发链真正开始工作。
7.3 修改主站 DCF 的 0x1F20/0x1F22 不会自动立即写从站
这些对象是 configuration manager 的配置来源。只有在以下入口触发后才执行:
- boot slave 判断配置需要更新;
- 应用显式调用
co_nmt_cfg_req(); - 通过
0x1F25 Configuration request触发相关流程。
对象字典保存“要执行什么”,nmt_cfg 状态机决定“什么时候执行”。
参考资料
- Lely Industries, lely-core GitLab repository: https://gitlab.com/lely_industries/lely-core
- Lely Industries, Lely core libraries Doxygen 2.4.0: https://lely_industries.gitlab.io/lely-core/doxygen/
- Lely CANopen, Library overview: https://opensource.lely.com/canopen/docs/overview/
- Lely CANopen, Standards support: https://opensource.lely.com/canopen/docs/standards/
- 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, v2.1.0 release notes — Automatic node configuration: https://opensource.lely.com/canopen/release/v2.1.0/
- CiA 301 V4.2.0,CANopen application layer and communication profile。
- CiA 302-2 V4.1.0,Network management。
- CiA 302-3 V4.1.0,Configuration and program download。










