Lely CANopen NMT Heartbeat、srv与cfg 机制源码学习

在这里插入图片描述

@[toc]

本文把以下六个内部文件放在同一条运行链上分析:

1
2
3
4
5
6
src/co/nmt_hb.c
src/co/nmt_hb.h
src/co/nmt_srv.c
src/co/nmt_srv.h
src/co/nmt_cfg.c
src/co/nmt_cfg.h

它们并不是三套彼此独立的协议实现,而是 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
2
3
4
5
co_nmt_t
├─ 管本节点 NMT 状态机与主站启动策略
├─ 通过 co_nmt_srv 管本节点协议服务生命周期
├─ 为每个被监控远端节点创建 co_nmt_hb
└─ 在需要更新从站配置时创建/调用 co_nmt_cfg

这三部分共同依赖 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、0x1F200x1F22、用户配置和复位等待 不实现 SDO 分段/块传输,不判断是否需要配置

最容易混淆的是 nmt_srvnmt_cfg

1
2
3
4
5
nmt_srv
= 管“本节点当前应该运行哪些协议服务”

nmt_cfg
= 管“主站怎样把配置写入一个远端从站”

前者属于本地服务生命周期,后者属于主站对远端节点的配置流程。


2. 三组对象在 NMT 总体架构中的位置

下面这张图重点观察所有权和事件方向:co_nmt_t 是上层总控,三个内部组件都把结果回送给它,但实际 CAN 收发和定时仍由 can_net_t 驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flowchart TB
APP["应用或 coapp Node"] --> NMT["co_nmt_t\nNMT 总控"]
DEV["co_dev_t\n本地 DCF 与对象字典"] --> NMT
NET["can_net_t\nCAN receiver 与 timer 调度"] --> NMT

NMT --> SRV["co_nmt_srv\n本地服务装配器"]
SRV --> SSDO["Server-SDO"]
SRV --> CSDO["Client-SDO"]
SRV --> PDO["RPDO 与 TPDO"]
SRV --> AUX["SYNC TIME EMCY LSS"]

NMT --> HBSET["多个 co_nmt_hb\n每个远端节点一个实例"]
NET --> HBSET
HBSET -->|"超时或状态事件"| NMT

NMT --> CFG["co_nmt_cfg\n单个从站配置请求"]
CFG --> CSDO
DEV --> CFG
NET --> CFG
CFG -->|"abort code 或成功"| NMT

关键边界如下:

  1. co_nmt_t 决定状态和策略;
  2. co_nmt_srv 执行本地服务集合切换;
  3. co_nmt_hb 只维护一个远端节点的错误控制上下文;
  4. co_nmt_cfg 只执行一次目标明确的配置请求;
  5. 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
2
3
4
5
6
CO_NMT_SRV_SDO
CO_NMT_SRV_PDO
CO_NMT_SRV_SYNC
CO_NMT_SRV_TIME
CO_NMT_SRV_EMCY
CO_NMT_SRV_LSS

这里要注意:

CO_NMT_PREOP_SRVCO_NMT_START_SRVCO_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct co_nmt_srv {
co_nmt_t *nmt;
unsigned mask;

co_ssdo_t **ssdos;
size_t nssdo;
co_csdo_t **csdos;
size_t ncsdo;

co_rpdo_t **rpdos;
size_t nrpdo;
co_tpdo_t **tpdos;
size_t ntpdo;

co_sync_t *sync;
co_time_t *time;
co_emcy_t *emcy;
co_lss_t *lss;
};

上面是结构关系的等价表达,不是逐字复制源码。实际字段是否存在还受功能裁剪宏影响。

3.3 co_nmt_srv_init() 只初始化管理器,不代表全部服务已运行

co_nmt_srv_init() 建立管理器与父 NMT 的关系,并初始化内部指针、数量和状态。它与 co_nmt_srv_set() 的职责不同:

1
2
3
4
5
6
7
8
co_nmt_srv_init()
建立空的服务管理器

co_nmt_srv_set(mask)
把当前服务集合切换到 mask

co_nmt_srv_fini()
拆除仍存在的服务并清理管理器

因此,新建 co_nmt_t 后不会立即拥有全部 SDO/PDO 服务。官方概览也明确说明,新创建的 NMT 服务处于 Initialisation 状态,不创建正常通信服务;应用可以先注册回调,再通过 reset node 启动完整状态机。

3.4 co_nmt_srv_set() 的核心是“集合差分”

可以把它理解为下面的逻辑:

1
2
3
4
5
old_mask = 当前已启用集合
new_mask = NMT 状态要求的集合

need_disable = old_mask & ~new_mask
need_enable = new_mask & ~old_mask

随后分别调用:

1
2
3
4
5
6
co_nmt_srv_init_sdo()   / co_nmt_srv_fini_sdo()
co_nmt_srv_init_pdo() / co_nmt_srv_fini_pdo()
co_nmt_srv_init_sync() / co_nmt_srv_fini_sync()
co_nmt_srv_init_time() / co_nmt_srv_fini_time()
co_nmt_srv_init_emcy() / co_nmt_srv_fini_emcy()
co_nmt_srv_init_lss() / co_nmt_srv_fini_lss()

这不是简单的 start/stop 开关。对数组型服务,例如 SDO 和 PDO,初始化函数需要从对象字典中发现配置对象,创建对应数量的服务实例,并把它们保存到管理器数组中;结束函数则要按相反顺序停止、销毁并释放数组。

1
2
3
4
5
6
7
8
flowchart LR
STATE["NMT 状态入口"] --> MASK["目标服务 mask"]
MASK --> SET["co_nmt_srv_set"]
SET --> DIFF{"比较当前 mask"}
DIFF -->|"新增位"| INIT["init_sdo pdo sync time emcy lss"]
DIFF -->|"移除位"| FINI["fini_sdo pdo sync time emcy lss"]
INIT --> ACTIVE["更新后的本地服务集合"]
FINI --> ACTIVE

这种设计的工程价值是:

  • 状态机只表达“要什么”,不直接管理每种服务的分配细节;
  • 服务管理器集中处理部分初始化失败和销毁顺序;
  • 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.ccsdo.c 状态机处理分段、块传输、toggle、CRC 和超时;nmt_srv.c 不重复实现这些协议。

3.6 PDO 服务为什么要同时处理映射、SYNC 和错误回调

co_nmt_srv_init_pdo() 需要关联:

1
2
3
4
RPDO communication parameter  0x1400..
RPDO mapping parameter 0x1600..
TPDO communication parameter 0x1800..
TPDO mapping parameter 0x1A00..

创建 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_tcan_timer_t。销毁时必须先阻止新的 CAN/定时事件进入,再释放服务对象和数组。否则会出现:

1
2
3
4
5
receiver/timer 仍挂在 can_net_t

目标服务对象已释放

后续 CAN 帧或定时事件访问失效地址

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
2
3
4
5
6
7
CAN receiver
CAN timer
父 co_nmt_t
远端 Node-ID
consumer heartbeat time ms
NMT 状态参考值 st
当前错误状态 state

官方 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_tnmt_hb.c 只关心自己的一个配置项。

4.2 创建阶段:先准备 receiver 和 timer

co_nmt_hb_create(net, nmt) 内部遵循 Lely 常见的四段式生命周期:

1
2
3
4
5
alloc
→ init
→ 使用
→ fini
→ free

初始化阶段会创建并绑定:

  • 一个 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);

其职责可归纳为:

  1. 保存新的 producer Node-ID 和 consumer time;
  2. 停止旧 receiver/timer,避免旧 Node-ID 继续产生事件;
  3. 在配置有效时,将 receiver 绑定到:
1
CO_NMT_EC_CANID(id) = 0x700 + id
  1. 根据 Heartbeat 监控状态维护超时定时器;
  2. 清理或重置与旧配置相关的错误状态。

ms == 0 时,Heartbeat consumption 被禁用。Node-ID 不在有效远端节点范围时,也不应保持有效监听。

需要区分两个时间:

1
2
3
4
5
远端 0x1017
= producer 实际发送周期

本地 0x1016 低 16 位
= consumer 允许的最大接收间隔

工程上通常要求 consumer time 大于 producer time,并保留调度和总线抖动裕量。Lely 的 dcfgen 默认可用 heartbeat multiplier 从 producer time 生成 consumer time。

4.4 接收路径:CAN 帧只包含远端 NMT 状态

Heartbeat 与 boot-up 共用:

1
2
3
CAN-ID = 0x700 + Node-ID
DLC = 1
Byte0 = NMT state

co_nmt_hb_recv() 的主线可以理解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart TD
RX["can_net_t 匹配到 0x700 + id"] --> CHECK{"DLC 和帧类型有效?"}
CHECK -->|"否"| DROP["忽略"]
CHECK -->|"是"| ST["读取 data[0] 并去除 toggle bit"]
ST --> ARM["重新安排 consumer timeout"]
ARM --> OLD{"之前存在 Heartbeat 错误?"}
OLD -->|"是"| RESOLVE["上报 CO_NMT_EC_RESOLVED"]
OLD -->|"否"| CMP["比较 NMT 状态参考值"]
RESOLVE --> CMP
CMP --> CHANGE{"状态是否发生变化或不符合期望?"}
CHANGE -->|"是"| EVENT["co_nmt_hb_ind reason=STATE"]
CHANGE -->|"否"| DONE["保持监控"]
EVENT --> DONE

这里有三个不同事件,不应合并:

事件 含义
boot-up 0x00 远端完成通信初始化或复位;上层 boot/error-control 逻辑可能继续推进
Heartbeat timeout 在监控窗口内没有收到有效帧
NMT state change 收到帧,但状态与 NMT 维护的参考状态不同

co_nmt_hb_ind() 的参数包含:

1
2
3
4
id
state = OCCURRED / RESOLVED
reason = TIMEOUT / STATE
st = 远端状态

这让父 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
2
3
4
发送 EMCY
停止所有节点
复位从站
切换本节点 NMT 状态

这些动作属于 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_STATEreason = 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_cfgnmt_boot 的一个可复用子流程:

1
2
3
4
5
nmt_boot 判断“是否需要配置”

nmt_cfg 执行“怎样配置”

nmt_boot 根据 abort code 决定继续或报 boot error J

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
2
3
co_csdo_dn_req()
co_csdo_dn_val_req()
co_csdo_dn_dcf_req()

等 Client-SDO API 发送。nmt_cfg 负责选择下一步,不负责重新实现 SDO 协议。

5.3 状态机主线

从官方 Doxygen 暴露的状态入口函数可以还原出如下主链:

1
2
3
4
5
6
7
8
9
10
restore
→ reset/wait boot-up(按需)
→ store_1f20
→ store_1f22
→ user configuration
→ confirmation

任一步失败
→ abort
→ confirmation(abort code)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
stateDiagram-v2
[*] --> Restore
Restore --> ResetWait: restored defaults require reset
Restore --> StoreText: no reset required
ResetWait --> StoreText: boot-up received
ResetWait --> Abort: timeout
StoreText --> StoreConcise: text DCF complete or absent
StoreText --> Abort: SDO or parse failure
StoreConcise --> UserConfig: concise DCF complete or absent
StoreConcise --> Abort: SDO failure
UserConfig --> Complete: user reports success
UserConfig --> Abort: user reports abort code
Complete --> [*]
Abort --> [*]

这里的状态名对应 nmt_cfg.c 可见的函数族:

1
2
3
4
5
6
co_nmt_cfg_restore_on_enter / on_dn_con
co_nmt_cfg_reset_on_enter / on_recv / on_time
co_nmt_cfg_store_1f20_on_enter / on_dn_con / on_leave
co_nmt_cfg_store_1f22_on_enter / on_dn_con
co_nmt_cfg_user_on_enter / on_res
co_nmt_cfg_abort_on_enter

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 等待状态:

  1. 发送相应 NMT reset 命令;
  2. receiver 监听目标节点 0x700 + id
  3. timer 使用 LELY_CO_NMT_CFG_RESET_TIMEOUT
  4. 收到 boot-up data[0] == 0 后继续;
  5. 超时则以 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
2
3
对象可写
并且
ParameterValue != DefaultValue

因此 store_1f20 分支不是把整个文本文件作为一个 DOMAIN 原样写给从站,而是:

1
2
3
4
5
6
7
8
9
主站本地 0x1F20:<id> 指向/包含文本 DCF

解析设备配置

遍历需要更新的可写对象

逐个提交 Client-SDO download

每次 dn_con 后推进到下一个对象

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
2
3
4
5
记录数量
+ index
+ sub-index
+ 数据长度
+ 数据字节

store_1f22 分支把该数据交给 Client-SDO 的 concise DCF download 能力,按文件顺序逐项写入远端对象字典。与 0x1F20 的“根据 ParameterValue/DefaultValue 生成写入”不同,0x1F22 已经明确规定了:

  • 写哪些对象;
  • 按什么顺序写;
  • 每项写入多少字节;
  • 哪些 disable/enable 操作先后发生。

这也是 Lely dcfgen 为每个从站生成 .bin 文件的主要用途。master.ymlconfiguration_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
2
3
4
5
6
7
8
9
0x1F20 自动配置

0x1F22 自动配置

用户 OnConfig 扩展配置

ConfigResult

最终 configuration confirmation

5.8 所有异步入口最终汇聚到统一事件分发

nmt_cfg.c 可见四类事件入口:

1
2
3
4
co_nmt_cfg_emit_dn_con()  SDO download 完成
co_nmt_cfg_emit_recv() 收到 reset 后的 boot-up
co_nmt_cfg_emit_time() 等待超时
co_nmt_cfg_emit_res() 用户配置结果

这些入口不会在各处写一套 switch(state),而是把事件交给当前状态的函数指针:

1
2
3
4
state->on_dn_con
state->on_recv
state->on_time
state->on_res

状态切换由 co_nmt_cfg_enter() 统一执行 on_leaveon_enter。这种模式与 nmt_boot.ccsdo.cssdo.c 一致:

状态对象定义当前能接受什么事件;事件回调只转发;下一状态由当前状态返回或显式进入。


6. 三个模块怎样在一次主站启动中连接起来

下面按一个典型主站启动从站的顺序串联三组源码。

6.1 本地主站先通过 nmt_srv 建立运行能力

1
2
3
4
5
6
7
8
9
应用创建 co_nmt_t

触发 reset node / reset communication

NMT 进入 Pre-operational

co_nmt_srv_set(PREOP service mask)

创建本地 CSDO、SSDO、SYNC、TIME、EMCY、LSS 等允许服务

其中,后续 boot/config 必须依赖可用的 Client-SDO。若主站 DCF 中没有相应 0x1280.. CSDO 参数,或构建时禁用了 master/CSDO 能力,nmt_cfg 就没有传输通道可用。

6.2 nmt_hb 持续维护远端在线与状态信息

主站从 0x1016 创建 Heartbeat consumer:

1
2
3
4
5
6
0x1016:<sub-index>
↓ 解析 producer id 和 consumer time
co_nmt_hb_set_1016()

receiver = 0x700 + id
timer = expected interval

收到 boot-up/Heartbeat 后,父 NMT 更新节点状态。超时或状态异常时,Heartbeat indication 进入 NMT error handler。

6.3 nmt_boot 在需要时调用 nmt_cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
boot slave

身份检查、软件检查、配置日期/时间检查

需要更新配置

co_nmt_cfg_req()

restore → 1F20 → 1F22 → user config

co_nmt_cfg_con(abort code)

boot slave 继续启动错误控制

6.4 启动完成后,nmt_srv 再启用 PDO

当主站和从站进入 Operational,本节点 NMT 状态入口把服务 mask 切换到包含 PDO 的集合:

1
2
3
4
5
co_nmt_srv_set(Operational mask)

创建/启动 RPDO 与 TPDO

SYNC、事件定时器、接收 deadline、映射读写开始工作

这说明配置期和运行期有明确边界:

1
2
3
配置期主要依赖 SDO/CSDO
运行期过程数据主要依赖 PDO
Heartbeat 在两者之外持续提供错误控制

6.5 完整时序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
sequenceDiagram
participant App as Application
participant Nmt as co_nmt_t
participant Srv as co_nmt_srv
participant Boot as nmt_boot
participant Cfg as nmt_cfg
participant Csdo as co_csdo_t
participant Hb as co_nmt_hb
participant Slave as Remote slave

App->>Nmt: reset node
Nmt->>Srv: set pre-operational services
Srv->>Csdo: create and start Client-SDO

Nmt->>Boot: boot slave request
Boot->>Csdo: identity and version SDO requests
Csdo->>Slave: SDO upload or download
Slave-->>Csdo: SDO response

Boot->>Cfg: configuration request
Cfg->>Csdo: restore or DCF downloads
Csdo->>Slave: ordered SDO writes
Slave-->>Csdo: confirmations
Csdo-->>Cfg: download confirmation
Cfg-->>Boot: configuration confirmation

Slave-->>Hb: boot-up or heartbeat
Hb-->>Nmt: state or error-control indication
Boot-->>Nmt: boot complete
Nmt->>Srv: set operational services
Srv->>Srv: create RPDO and TPDO
Nmt-->>App: OnBoot or state indication

7. 配置变化如何穿透到运行对象

7.1 修改 0x1016 不是只改一个整数

当 SDO 写本地 0x1016 时:

1
2
3
4
5
6
7
8
9
10
11
SDO download

co_1016_dn_ind()

校验 Node-ID、重复配置和时间值

找到或重配 co_nmt_hb

停止旧 receiver/timer

绑定新 0x700 + id 并更新超时

所以修改 0x1016 会立即改变 CAN 接收注册和定时行为。它不是延迟到下次复位才生效的普通参数。

7.2 NMT 状态变化不是只改 nmt->st

1
2
3
4
5
6
7
8
9
NMT command 或内部状态切换

co_nmt_enter(new_state)

state.on_enter()

co_nmt_srv_set(target mask)

协议服务真实创建/停止

因此从 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 状态机决定“什么时候执行”。


参考资料

  1. Lely Industries, lely-core GitLab repository: https://gitlab.com/lely_industries/lely-core
  2. Lely Industries, Lely core libraries Doxygen 2.4.0: https://lely_industries.gitlab.io/lely-core/doxygen/
  3. Lely CANopen, Library overview: https://opensource.lely.com/canopen/docs/overview/
  4. Lely CANopen, Standards support: https://opensource.lely.com/canopen/docs/standards/
  5. Lely CANopen, Build configuration: https://opensource.lely.com/canopen/docs/configuration/
  6. Lely CANopen, EDS/DCF tools: https://opensource.lely.com/canopen/docs/dcf-tools/
  7. Lely CANopen, v2.1.0 release notes — Automatic node configuration: https://opensource.lely.com/canopen/release/v2.1.0/
  8. CiA 301 V4.2.0,CANopen application layer and communication profile。
  9. CiA 302-2 V4.1.0,Network management。
  10. CiA 302-3 V4.1.0,Configuration and program download。