Lely CANopen NMT Boot Slave:从 nmt_boot.c 看主站如何识别、配置并启动从站
@[toc]
1. 先给出核心判断
nmt_boot.c 实现的不是普通从站侧 NMT 状态机,也不是简单地发送一条 Start Remote Node 命令。它是 NMT manager 针对一个远端从站执行的启动编排状态机。
对每个被管理的从站,它把多种已有服务串成一条完整链路:
1 | 主站 DCF 中的从站声明 |
因此,阅读本文件时最重要的心智模型是:
co_nmt_boot_t不是新的 CANopen 传输协议,而是一个使用 NMT、SDO、对象字典、CAN receiver 和 CAN timer 的异步流程控制器。
本文不重复展开以下基础内容:
- NMT 命令帧、NMT 状态值和普通状态切换;
- Heartbeat producer/consumer 与 Node Guarding 的协议格式;
- SDO expedited、segmented、block transfer 的帧级状态机;
can_net_t、Timer、event loop 与 SocketCAN 的底层连接;- EDS/DCF 解析、
co_dev_t红黑树和 concise DCF 的基础格式。
这些模块在本篇中只说明它们如何被 nmt_boot.c 组合。
2. nmt_boot.c 在 Lely 分层中的位置
Lely CANopen C 核心是被动、异步的。协议对象不会自己创建线程,也不会主动读取系统时钟;CAN 帧和时间推进都通过 can_net_t 输入。nmt_boot.c 延续了相同设计:每次只发起一个非阻塞动作,完成后由回调推动下一状态。
1 | flowchart TB |
各对象职责如下:
| 对象 | 在 boot 流程中的职责 | 不负责什么 |
|---|---|---|
co_nmt_t |
管理全部从站、读取 0x1F80/0x1F81 策略、发起 boot、接收最终确认 | 不逐步执行单个从站的身份和软件检查 |
co_nmt_boot_t |
保存单个从站 boot 上下文,并按事件切换内部状态 | 不实现完整 SDO 传输算法 |
co_csdo_t |
执行远程 SDO upload/download | 不决定下一步该检查哪个对象 |
co_nmt_cfg_t |
执行配置恢复、concise DCF 或用户配置回调 | 不负责设备身份和程序版本检查 |
co_dev_t |
保存主站 DCF 中的远端期望值、文件属性和 NMT 参数 | 不直接向 CAN 总线发帧 |
can_recv_t |
接收目标节点的错误控制帧 | 不直接改变 boot 总流程 |
can_timer_t |
在指定截止时间触发状态超时事件 | 不创建线程,也不独立运行 |
这里有一个容易混淆的命名:
1 | NMT master boot slave |
其中的 slave 指被主站启动和检查的远端节点,不是说 nmt_boot.c 运行在从站侧。
3. 两个文件分别承担什么职责
3.1 nmt_boot.h:内部边界,不是面向应用的公共 API
src/co/nmt_boot.h 明确标为内部头文件,只暴露四类内容:
1 | typedef struct __co_nmt_boot co_nmt_boot_t; |
这组接口说明了三个边界:
co_nmt_boot_t是不透明内部对象,应用不应直接访问其字段。- 创建时注入
can_net_t、本地主站co_dev_t和所属co_nmt_t。 - 完成后通过
co_nmt_boot_con()回到 NMT 总控,而不是直接调用 C++ 用户回调。
面向应用的公共入口位于 <lely/co/nmt.h>:
1 | int co_nmt_boot_req(co_nmt_t *nmt, co_unsigned8_t id, int timeout); |
C++ 层再包装成:
1 | bool BasicMaster::Boot(uint8_t id); |
所以实际调用层次是:
1 | BasicMaster::Boot(id) |
3.2 nmt_boot.c:事件驱动状态机和具体流程
nmt_boot.c 主要包含四组实现:
co_nmt_boot_t与状态接口;- CAN 接收、Timer、SDO confirmation、configuration confirmation 的统一事件入口;
- 身份检查、节点状态检查、程序下载、配置更新和错误控制启动等状态;
- SDO upload/download、对象比较和 Node Guarding RTR 等辅助函数。
它不是按一个长函数顺序执行,而是把每个步骤拆成独立状态。
4. 状态对象为什么使用函数指针表
内部状态类型 co_nmt_boot_state_t 为不同异步事件预留了处理函数。根据 Doxygen 暴露的字段,可以抽象为:
1 | struct __co_nmt_boot_state { |
这里的代码是结构化示意,重点是事件类型,不应把它当成对目标提交逐字复制的声明。
各事件的来源如下:
| 事件槽 | 触发源 | 典型用途 |
|---|---|---|
on_enter |
进入新状态 | 发起 SDO、发送 NMT 命令、设置 timer |
on_leave |
离开状态 | 停止 receiver/timer、清理临时状态 |
on_recv |
can_recv_t 收到帧 |
处理 boot-up、heartbeat 或 guarding response |
on_time |
can_timer_t 到期 |
重试、转错误、再次轮询远端状态 |
on_dn_con |
SDO download 完成 | 判断写入成功、失败或需转下一程序状态 |
on_up_con |
SDO upload 完成 | 校验值、判断程序/配置状态 |
on_cfg_con |
configuration request 完成 | 判断配置是否成功并进入错误控制启动 |
统一分发链路为:
1 | flowchart LR |
这种设计有三个直接效果:
- CAN 接收回调不需要阻塞等待后续 SDO;
- SDO completion 只负责把结果交给当前 boot 状态;
- 同一个 boot 流程可以穿插 CAN 帧、SDO 和 timer,不需要线程或同步阻塞。
5. 创建、启动和销毁
5.1 创建不是立即启动
1 | co_nmt_boot_create(net, dev, nmt) |
创建的是可复用的单从站 boot 服务上下文。它把逻辑网络、主站设备描述和 NMT manager 联系起来,但尚未指定本次要启动哪个 Node-ID。
NMT 总对象为每个受管理从站保存一份 co_nmt_slave 状态,其中包括:
1 | assignment 0x1F81 对应的从站分配位 |
这说明 Lely 的模型不是“全网共享一个游标依次启动”,而是 NMT manager 对每个节点保留独立管理状态。具体是否并行启动及何时启动,由 co_nmt_t 的 startup 流程和对象 0x1F81 策略决定。
5.2 公共请求如何进入内部流程
应用调用:
1 | co_nmt_boot_req(nmt, id, timeout); |
内部最终进入:
1 | co_nmt_boot_boot_req( |
参数中的 timeout 是本次 boot 过程使用的 SDO timeout。公共 NMT 对象还提供:
1 | co_nmt_get_timeout(); |
默认 LELY_CO_NMT_TIMEOUT 为 100 ms,但它只控制 SDO 请求;等待 boot-up、Node Guarding RTR 和程序状态轮询使用其他独立宏。
5.3 销毁必须终止所有异步入口
co_nmt_boot_destroy() 的正确语义不是只释放一块内存。它必须确保:
- CAN receiver 不再回调此对象;
- CAN timer 不再回调此对象;
- 当前 Client-SDO 请求不再把 completion 投递到已释放上下文;
- 状态退出逻辑已完成。
这也是状态对象提供 on_leave 的原因之一:receiver 和 timer 的启停必须绑定状态生命周期,而不能散落在应用代码中。
6. co_nmt_boot_boot_req() 返回后,谁继续驱动 boot
这是理解 nmt_boot.c 最关键的问题。答案可以先压缩成一句话:
co_nmt_boot_boot_req()只启动状态机并建立第一个异步等待点;它返回后没有任何函数停在调用栈里等待。从站响应、SDO 完成、Timer 到期或配置完成时,底层事件源重新调用co_nmt_boot_t的事件入口,再根据boot->state继续执行。
因此要区分两种“结束”:
1 | 当前 C 函数返回 |
co_nmt_boot_t 是跨越多次回调长期存在的状态机上下文。当前状态、Node-ID、超时参数、错误状态、SDO 结果和 receiver/timer 等信息都保存在该对象及其关联服务中,而不是保存在 co_nmt_boot_boot_req() 的栈帧里。
6.1 从创建到第一次异步触发
创建时,boot 对象处于 co_nmt_boot_wait_state。真正请求 boot 时,公共入口完成本次请求参数的保存,并让 wait 状态通过 timer 异步启动流程。应用不应直接调用内部的 co_nmt_boot_wait_on_time();该函数只会由下面的链路触发:
1 | can_timer_t 到期 |
这里的 wait 是源码所称的“wait asynchronously”状态。它不让调用线程睡眠,也不在 nmt_boot.c 内创建线程。co_nmt_boot_boot_req() 启动 can_timer_t 后即可返回;只有当 can_net 的时间推进到定时点时,co_nmt_boot_timer() 才通过 co_nmt_boot_emit_time() 调用 co_nmt_boot_wait_on_time() 并进入后续状态。因此它建立的是“请求提交”和“状态机继续执行”之间的事件边界。在 io2/coapp 集成中,这个定时事件通常由 event loop 驱动;在直接使用 liblely-co 时,则由调用方推进 can_net 时间。是否发生线程切换取决于上层集成方式,不是 wait 状态本身的保证。
1 | sequenceDiagram |
6.2 co_nmt_boot_enter() 允许一次事件连续穿过多个同步状态
一个 state handler 返回 co_nmt_boot_state_t *,含义是“下一步进入哪个状态”。co_nmt_boot_enter() 负责:
- 调用旧状态的
on_leave; - 更新
boot->state; - 调用新状态的
on_enter; - 如果
on_enter又立即返回另一个 state,则继续进入下一个状态; - 遇到真正的异步操作后停止同步推进,当前调用栈返回。
可以用下面的结构化伪代码理解。它表达调用关系,不是源码逐字复制:
1 | static void |
这解释了两个看似矛盾的现象:
- 有些
on_enter()只做本地判断,立即返回下一个 state,因此一次 callback 内可能连续经过多个状态; - 有些
on_enter()发起 SDO、启动 receiver/timer 或提交 configuration request 后返回NULL,调用链立即退出,但boot->state仍指向当前等待状态。
所以不能把“函数已经 return”理解为“状态机已经结束”。它只是到达了一个异步边界。
6.3 五类事件如何重新进入当前 state
co_nmt_boot_emit_recv()、co_nmt_boot_emit_time()、co_nmt_boot_emit_dn_con()、co_nmt_boot_emit_up_con() 和 co_nmt_boot_emit_cfg_con() 是统一事件分发器。逻辑可以抽象为:
1 | handler = handler_of(boot->state, event); |
事件来源必须严格区分:
| boot 事件 | 直接来源 | 更底层的可能触发条件 |
|---|---|---|
on_recv |
co_nmt_boot_recv() |
boot 自己注册的 CAN receiver 收到 boot-up、Heartbeat 或 Node Guarding response |
on_time |
co_nmt_boot_timer() |
boot 自己的 can_timer_t 到期 |
on_up_con |
co_nmt_boot_up_con() |
Client-SDO upload 完成;可能是收到响应,也可能是 SDO timeout/abort |
on_dn_con |
co_nmt_boot_dn_con() |
Client-SDO download 完成;可能是收到响应,也可能是 SDO timeout/abort |
on_cfg_con |
co_nmt_boot_cfg_con() |
co_nmt_cfg_req() 子流程完成;可能包含 SDO,也可能等待应用调用 ConfigResult() |
因此,on_up_con 和 on_dn_con 不能简单称为“CAN 接收回调”。CAN 帧只是使 Client-SDO 状态机完成的一种原因;SDO 自身的 timer 到期同样可以生成 confirmation。on_cfg_con 更不是固定由一帧 CAN 报文直接触发,因为 OnConfig() 阶段可能暂停 boot,等待驱动或应用报告配置结果。
6.4 主函数此时在做什么
答案取决于使用的是哪一层集成方式,但两种方式都不是“阻塞在 boot 函数里等待”。
使用 liblely-coapp / io2 时
常见执行关系是:
1 | 应用调用 BasicMaster::Boot(id) |
主线程可能正在:
- 执行同一个 event loop 中的其他任务;
- 阻塞等待
epoll/Poll 的下一次 I/O; - 在多线程配置下执行应用自己的其他逻辑。
它不会为每个从站创建一个“boot 等待线程”,也不会一直停在 co_nmt_boot_boot_req() 内部。
只使用 liblely-co / can_net_t 时
此时没有 io2 自动代替应用驱动网络。集成代码必须持续完成两件事:
1 | 收到 CAN 帧 → can_net_recv() |
这些调用会让注册在 can_net_t 中的 receiver 和 timer 得到执行。仍然不是轮询 co_nmt_boot_t 本身;应用是在驱动 CAN 网络和时间,boot 状态机被事件间接推进。
6.5 其他模块是否不断查询 boot 是否完成
不是。co_nmt_is_booting()、BasicMaster::IsReady() 和 IsConfig() 都是观察状态的接口,不是状态机的驱动器:
1 | co_nmt_is_booting(id) |
真正的完成通知是推送式的:
1 | co_nmt_boot 状态机完成 |
应用可以查询状态做诊断或 UI 展示,但不需要通过轮询查询来使状态前进。
6.6 完整状态拓扑
下面的图按源码中的 state 对象组织。它表达状态之间的主拓扑和异步等待点;具体条件仍由 0x1F81、身份期望对象、程序下载对象、从站响应和 abort code 决定。
1 | flowchart TD |
这张图同时修正一个容易产生的顺序误解:
1 | check software |
co_nmt_boot_stop_prog_state 到 co_nmt_boot_wait_prog_state 是位于 check software 之后的可选子流程,不是在 co_nmt_boot_ec_state 之后执行;wait_prog 结束也不等于整个 NMT boot 结束,它还要继续配置检查和 error control。
6.7 按源码顺序阅读时应该建立的认知
从 co_nmt_boot_wait_on_time() 往下阅读,确实能够看到主要状态函数按流程大体排列,但“源码排列顺序”不能直接等价为“唯一的线性运行顺序”。更准确的读法是:
| 状态组 | 主要事件 | 到达异步边界的动作 |
|---|---|---|
wait/abort/error |
on_time/on_enter |
retry timer、结束 confirmation |
| 设备与 Identity | on_enter/on_up_con |
发起 SDO upload,等待 confirmation |
chk_node/reset_comm |
on_recv/on_time |
注册 CAN receiver、启动 timer、发送 RTR/NMT command |
chk_sw 与程序下载 |
on_up_con/on_dn_con/on_time |
SDO upload/download、程序状态轮询 timer |
| 配置日期与时间 | on_up_con |
连续 SDO upload |
up_cfg |
on_cfg_con |
co_nmt_cfg_req(),必要时等待应用 ConfigResult() |
ec |
on_recv/on_time |
等待第一帧 Heartbeat 或 Node Guarding confirmation |
还要注意:并不是每个等待状态都必须具有 on_enter。例如 co_nmt_boot_chk_cfg_time_state 只注册 on_up_con,说明发起对应 SDO upload 的动作可以发生在前一个状态的 handler 中,然后先更新当前 state,再等待未来 confirmation。状态对象表达的是“下一次事件到来时由谁处理”,不等于“所有动作都必须在本状态的 on_enter 内启动”。
7. 一条正常 boot 主流程
从 nmt_boot.c 暴露的状态集合可以把正常路径归纳为六段。实际是否经过每个状态,取决于 0x1F81 分配位、主站 DCF 中是否配置期望值、节点当前状态、是否启用程序下载以及配置日期/时间是否一致。
1 | flowchart TD |
7.1 第一步:确认该节点属于主站管理范围
对象 0x1F81 NMT slave assignment 是主站判断节点是否存在、是否需要 boot、是否 mandatory、是否允许发送 Reset Communication 等策略的核心。
dcfgen 中与其相关的常用 YAML 字段包括:
| YAML 字段 | 对应语义 |
|---|---|
boot |
是否由 master 配置和 boot,对应 0x1F81 bit 2 |
mandatory |
是否为 mandatory slave,对应 bit 3 |
reset_communication |
是否允许发送 Reset Communication,对应 bit 4 |
节点未列入 0x1F81 时,boot 最终返回错误状态 A。
7.2 第二步:校验设备类型与 Identity
状态机依次包含:
1 | check device type |
远端读取与主站本地期望对象的对应关系为:
| 远端从站对象 | 主站期望对象 | 不一致错误 |
|---|---|---|
0x1000:00 Device type |
0x1F84:<Node-ID> |
C |
0x1018:01 Vendor-ID |
0x1F85:<Node-ID> |
D |
0x1018:02 Product code |
0x1F86:<Node-ID> |
M |
0x1018:03 Revision number |
0x1F87:<Node-ID> |
N |
0x1018:04 Serial number |
0x1F88:<Node-ID> |
O |
执行方式不是手写五套值解码逻辑,而是:
1 | co_nmt_boot_up(remote index, sub-index) |
co_nmt_boot_chk() 负责把远端返回字节与本地对象字典对应子对象的期望值比较。
这体现了一个关键设计:
主站 DCF 不只是本节点对象字典,也保存了远端从站的期望身份和启动策略;boot 状态机通过本地 OD 驱动远端校验。
若对 0x1000 的首次 upload 完全没有响应,则是错误状态 B。官方配置说明专门为该状态提供默认 1000 ms 的再次尝试等待时间。
7.3 第三步:检查远端节点当前状态
co_nmt_boot_chk_node_state 负责确定远端节点处于何种 NMT 状态。源码提供:
1 | co_nmt_boot_chk_node_on_enter() |
这组接口说明该状态同时依赖 CAN receiver 与 timer。
当使用 Node Guarding 探测时:
1 | 进入 check node state |
默认 LELY_CO_NMT_BOOT_RTR_TIMEOUT 为 100 ms。
若节点使用 Heartbeat,则 boot 流程会结合已接收的状态或 Heartbeat 事件判断远端是否可继续;没有按期收到 Heartbeat 可形成错误 E。
错误 L 值得单独说明:
1 | L = NMT slave was initially operational |
它不是简单的“节点启动失败”,而是表示主站开始 boot 时发现从站已经在 Operational。此时 manager 可能继续管理其他节点,由上层策略决定后续动作。
7.4 第四步:必要时发送 Reset Communication
若流程需要通信复位,进入:
1 | co_nmt_boot_reset_comm_state |
运行链为:
1 | sequenceDiagram |
默认 LELY_CO_NMT_BOOT_RESET_TIMEOUT 为 1000 ms。其目的不是 SDO timeout,而是给从站重新初始化通信栈和发送 boot-up 留出时间。
复位刚完成时,从站 SDO server 可能尚未完全可用。为降低紧接复位后的 SDO 丢失概率,源码允许 SDO timeout 后重试,默认 LELY_CO_NMT_BOOT_SDO_RETRY = 3。
7.5 第五步:检查并可选更新程序软件
这部分对应 CiA 302-3 的 program download 流程。Lely 官方支持表把对象分成 manager 侧和 device 侧:
| 角色 | 对象 | 含义 |
|---|---|---|
| manager 本地 | 0x1F55 |
Expected software identification |
| manager 本地 | 0x1F58 |
Program data |
| remote device | 0x1F50 |
Program data |
| remote device | 0x1F51 |
Program control |
| remote device | 0x1F56 |
Program software identification |
| remote device | 0x1F57 |
Flash status identification |
nmt_boot.c 中对应的内部状态包括:
1 | check software |
主路径可概括为:
1 | flowchart LR |
根据传输能力和数据规模,程序数据下载可进入 block 或 segmented 路径。真正的分段、序号、CRC、窗口等行为仍由 co_csdo_t 执行,nmt_boot.c 只选择目标对象、提供文件/数据源并接收结果。
相关错误状态:
| 状态 | 含义 |
|---|---|
G |
程序下载对象未配置或相互不一致 |
H |
软件需要更新,但当前配置或状态不允许更新 |
I |
软件需要更新,但程序下载失败 |
程序擦除和启动通常不是立即完成,因此源码不会阻塞等待,而是每隔 LELY_CO_NMT_BOOT_CHECK_TIMEOUT 再次 upload:
1 | 0x1F57:01 flash status indication |
默认轮询间隔为 100 ms。
7.6 第六步:检查配置日期和时间
配置检查状态为:
1 | check configuration date |
相关对象:
| 角色 | 对象 | 含义 |
|---|---|---|
| remote device | 0x1020:01 |
Verify configuration date |
| remote device | 0x1020:02 |
Verify configuration time |
| manager 本地 | 0x1F26:<Node-ID> |
Expected configuration date/time |
Lely 官方支持表将 0x1020 标为 application-specific,而把 manager 侧 0x1F26 和 boot/check configuration 流程列为已实现。工程含义是:
- manager 能执行标准的比较和决策;
- 从站是否真正实现 0x1020 及如何保存日期/时间,仍由从站栈和应用决定。
如果日期和时间均符合预期,boot 可跳过配置下载;如果不一致,则进入 configuration request。
7.7 第七步:调用 configuration request 子流程
co_nmt_boot_up_cfg_on_enter() 不会在 nmt_boot.c 中重新实现 concise DCF 下载,而是发起:
1 | co_nmt_cfg_req(..., co_nmt_boot_cfg_con, boot); |
随后由 nmt_cfg.c 负责可能的:
1 | 恢复默认参数 |
配置完成后:
1 | co_nmt_cfg_con() |
若 SDO abort code 非零,boot 返回错误 J:configuration download failed。
这里需要注意 C++ OnConfig() 的语义:它不是通知“配置已经全部完成”,而是 boot 流程已到达用户扩展配置窗口。应用完成异步 SDO 后必须调用 ConfigResult(),否则 boot 状态机不会得到完成确认。
7.8 第八步:启动错误控制
最后进入:
1 | co_nmt_boot_ec_state |
这一状态根据配置启动 Heartbeat consumer 或 Node Guarding,并确认远端节点仍有有效响应。
1 | flowchart TD |
错误 K 与普通错误 E 的区别是发生阶段:
E:一般 Heartbeat 事件,没有收到被监控设备 Heartbeat;K:已经进入 start error control 阶段,但启动错误控制时仍未收到 Heartbeat。
完成时调用:
1 | co_nmt_boot_con(nmt, id, state_with_toggle, es); |
其中:
id是从站 Node-ID;st是远端状态,Node Guarding 场景可包含 toggle bit;es == 0表示成功;es为A~O表示具体 boot 错误状态。
8. A~O 错误状态完整含义
Lely C++ BasicMaster::OnBoot() 文档给出了完整映射。建议日志不要只打印字母,而是同时调用 co_nmt_es2str() 或使用 C++ what 字符串。
es |
Lely 含义 | 主要阶段 | 首要检查项 |
|---|---|---|---|
A |
节点未列在 0x1F81 | boot eligibility | master DCF / boot 字段 / Node-ID |
B |
upload 0x1000 无响应 | 首次 SDO 探测 | 总线、Node-ID、SDO server、timeout |
C |
0x1000 与 0x1F84 不一致 | identity | Device type |
D |
0x1018:01 与 0x1F85 不一致 | identity | Vendor-ID |
E |
Heartbeat 事件,未收到 Heartbeat | state/error control | 0x1016、0x1017、总线 |
F |
Node Guarding 请求无确认 | state/error control | RTR 支持、0x100C/0x100D、timeout |
G |
program download 对象缺失或配置不一致 | software | 1F50/51/55/56/57/58 与文件属性 |
H |
需要更新软件,但配置或当前状态不允许 | software | 0x1F81 策略、program control 状态 |
I |
程序下载失败 | software transfer | SDO abort、文件、flash 状态 |
J |
配置下载失败 | configuration | 1F20/1F22、OnConfig、SDO abort |
K |
start error control 阶段 Heartbeat 失败 | final error control | Heartbeat 启动时序与周期 |
L |
从站初始已处于 Operational | state check | 上电策略、master 接管时机 |
M |
0x1018:02 与 0x1F86 不一致 | identity | Product code |
N |
0x1018:03 与 0x1F87 不一致 | identity | Revision number |
O |
0x1018:04 与 0x1F88 不一致 | identity | Serial number |
这些状态是 boot 流程结果,不等同于 SDO abort code:
1 | SDO abort code |
例如同样是 I,其底层可能来自文件不存在、远端 0x1F50 不可写、block transfer 不支持、flash 返回失败或任意其他 SDO abort。诊断时必须同时保留底层 SDO 日志。
9. 五个默认超时分别解决什么问题
源码和官方配置文档给出以下默认值:
| 宏 | 默认值 | 作用 |
|---|---|---|
LELY_CO_NMT_TIMEOUT |
100 ms | boot/check configuration 中普通 SDO 请求 timeout,可运行期修改 |
LELY_CO_NMT_BOOT_WAIT_TIMEOUT |
1000 ms | error B 后再次尝试 boot 前的等待 |
LELY_CO_NMT_BOOT_SDO_RETRY |
3 次 | 复位附近的 SDO timeout 重试次数 |
LELY_CO_NMT_BOOT_RTR_TIMEOUT |
100 ms | Node Guarding RTR 的响应等待时间 |
LELY_CO_NMT_BOOT_RESET_TIMEOUT |
1000 ms | Reset Communication 后等待 boot-up 的时间 |
LELY_CO_NMT_BOOT_CHECK_TIMEOUT |
100 ms | 轮询 1F57:01 或 1F51:01 的间隔 |
不要只把 SetTimeout() 调大后认为所有 boot timeout 都已改变。C++:
1 | master.SetTimeout(std::chrono::milliseconds(500)); |
主要改变 LELY_CO_NMT_TIMEOUT 对应的 SDO timeout;其余等待属于编译期宏控制的不同阶段。
这组超时拆分是合理的,因为不同动作的时间尺度完全不同:
1 | SDO 单次应答:通常较短 |
10. SDO 所有权:为什么 boot 时普通远程读写可能失败
nmt_boot.c 在身份检查、程序下载和配置过程中持续占用目标节点的默认 Client-SDO。Lely 官方维护者明确说明:默认 SDO client 只有在 NMT master 不需要它执行 boot 时才可供普通应用使用。
C++ BasicMaster::GetSdo(id) 文档也规定:
1 | 若 master 不在 Pre-operational/Operational, |
因此以下做法存在竞争:
1 | master.Reset(); |
更可靠的生命周期是:
1 | Reset master |
唯一特殊窗口是 OnConfig(id):boot 流程主动把默认 Client-SDO 交给用户进行扩展配置,此时可以提交配置 SDO,但结束后必须调用 ConfigResult()。
1 | stateDiagram-v2 |
11. boot 完成不等于“从站已经被观测为 Operational”
CANopen NMT Start Remote Node 是无确认命令。Lely 维护者在官方 issue 中指出,OnBoot() 可能在从站实际切换到 Operational 之前被调用,因为 master 发出 NMT start 后不会获得协议级确认。
因此要区分:
1 | OnBoot(es == 0) |
对需要严格确认从站已运行的应用,建议使用双条件:
1 | boot_completed[id] = OnBoot(es == 0) |
如果 Heartbeat 未配置,master 可能无法在发出 Start 后立即获得新的状态反馈。此时需要明确使用 Node Guarding、应用层状态对象或其他可观测机制,不能把无确认的 NMT 命令当作远端执行确认。
12. 与 0x1F80、0x1F81 和 mandatory slave 的关系
12.1 0x1F80 决定 master 总体启动策略
0x1F80 NMT startup 控制 manager 在启动阶段的总体行为,例如是否执行 NMT master 启动、是否自动启动节点等。它属于 co_nmt_t 总控,不由单个 co_nmt_boot_t 独立决定。
12.2 0x1F81 决定每个从站的参与方式
0x1F81:<Node-ID> 是单节点策略来源,包括:
- 节点是否存在于网络列表;
- 是否参与 boot;
- 是否 mandatory;
- 是否允许 Reset Communication;
- 与该节点相关的其他 assignment 位。
12.3 mandatory 影响 master 自身何时进入 Operational
Lely 维护者说明,master 进入 Operational 通常要等待 mandatory slave 的 boot 流程完成。非 mandatory 节点可继续在后台完成 boot,而不会阻塞整个 master 的状态推进。
因此:
1 | 某个从站 OnBoot 成功 |
应用不应只从单个节点回调反推整个网络状态。
13. 从日志与抓包识别这条状态机
开启 Lely diagnostic trace 后,一条典型流程可能出现以下语义序列:
1 | NMT master 进入 Pre-operational |
总线侧对应的帧类别为:
| 阶段 | 典型 CAN-ID | 含义 |
|---|---|---|
| NMT 命令 | 0x000 |
Reset Communication、Start 等 |
| Client → Server SDO | 0x600 + Node-ID |
upload/download 请求 |
| Server → Client SDO | 0x580 + Node-ID |
SDO 响应或 abort |
| boot-up / Heartbeat / guarding | 0x700 + Node-ID |
状态与错误控制 |
| Node Guarding request | 0x700 + Node-ID, RTR |
请求从站返回状态 |
仅凭抓包看到 0x000 01 <id> 不能证明全部配置过程完成;仅看到 0x700+id 00 也只能证明节点发出了 boot-up。要判断 boot 流程,至少还要结合:
- 身份对象 SDO 访问是否成功;
- 是否产生配置或程序下载;
OnBoot的es;- 后续 Heartbeat/Node Guarding 状态;
- master 和 slave 的 NMT 状态回调。
14. 把整个实现压缩成一条调用链
1 | co_nmt_t startup / BasicMaster::Boot(id) |
最终可以用一句话概括:
nmt_boot.c的核心价值,是把主站 DCF 中对远端节点的“期望描述”,转换成一组按事件推进的 NMT、SDO、程序下载、配置下载和错误控制动作,并把复杂底层失败归一化为 A~O 的 boot 结果。
参考资料
- Lely core libraries GitLab repository
- Lely core libraries 2.4.0 Doxygen: src/co/nmt_boot.c
- Lely core libraries 2.4.0 Doxygen: src/co/nmt_boot.h
- Lely core libraries 2.4.0 Doxygen: include/lely/co/nmt.h
- Lely CANopen: Library overview
- Lely CANopen: Build configuration
- Lely CANopen: Standards support
- Lely CANopen: EDS/DCF tools
- Lely Doxygen: BasicMaster class reference










