Lely CANopen NMT Boot Slave:从 nmt_boot.c 看主站如何识别、配置并启动从站

在这里插入图片描述

@[toc]

1. 先给出核心判断

nmt_boot.c 实现的不是普通从站侧 NMT 状态机,也不是简单地发送一条 Start Remote Node 命令。它是 NMT manager 针对一个远端从站执行的启动编排状态机

对每个被管理的从站,它把多种已有服务串成一条完整链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
主站 DCF 中的从站声明

确认该节点是否允许/需要 boot

通过 Client-SDO 校验设备类型与 Identity

通过 Heartbeat 或 Node Guarding 判断节点状态

必要时发送 Reset Communication 并等待 boot-up

按配置决定是否检查、停止、清除和下载程序

比较配置日期/时间,必要时调用 configuration request

启动 Heartbeat/Node Guarding 错误控制

向 co_nmt_t 返回 boot 完成状态和 A~O 错误状态

因此,阅读本文件时最重要的心智模型是:

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
2
3
4
5
6
7
8
9
10
11
12
flowchart TB
APP[BasicMaster / 应用层] --> NMT[co_nmt_t\nNMT manager 总控]
NMT --> BOOT[co_nmt_boot_t\n单从站 boot 编排]

BOOT --> DEV[co_dev_t\n主站 DCF 与远端代理对象]
BOOT --> CSDO[co_csdo_t\n远端对象字典访问]
BOOT --> CFG[co_nmt_cfg_t\nconfiguration request 子流程]
BOOT --> NET[can_net_t]

NET --> RX[can_recv_t\nboot-up / heartbeat / guarding response]
NET --> TIMER[can_timer_t\n等待、重试、检查超时]
NET --> TX[NMT command / RTR / SDO CAN frame]

各对象职责如下:

对象 在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct __co_nmt_boot co_nmt_boot_t;

void co_nmt_boot_con(co_nmt_t *nmt,
co_unsigned8_t id, co_unsigned8_t st, char es);

co_nmt_boot_t *co_nmt_boot_create(
can_net_t *net, co_dev_t *dev, co_nmt_t *nmt);

void co_nmt_boot_destroy(co_nmt_boot_t *boot);

int co_nmt_boot_boot_req(co_nmt_boot_t *boot,
co_unsigned8_t id, int timeout,
co_csdo_ind_t *dn_ind, co_csdo_ind_t *up_ind,
void *data);

这组接口说明了三个边界:

  1. co_nmt_boot_t 是不透明内部对象,应用不应直接访问其字段。
  2. 创建时注入 can_net_t、本地主站 co_dev_t 和所属 co_nmt_t
  3. 完成后通过 co_nmt_boot_con() 回到 NMT 总控,而不是直接调用 C++ 用户回调。

面向应用的公共入口位于 <lely/co/nmt.h>

1
2
int co_nmt_boot_req(co_nmt_t *nmt, co_unsigned8_t id, int timeout);
int co_nmt_is_booting(const co_nmt_t *nmt, co_unsigned8_t id);

C++ 层再包装成:

1
2
3
4
bool BasicMaster::Boot(uint8_t id);
virtual void BasicMaster::OnBoot(
uint8_t id, NmtState st, char es,
const std::string& what) noexcept;

所以实际调用层次是:

1
2
3
4
5
6
7
8
BasicMaster::Boot(id)
→ co_nmt_boot_req(nmt, id, timeout)
→ 找到 nmt->slaves[id - 1].boot
→ co_nmt_boot_boot_req(...)
→ nmt_boot.c 内部状态机
→ co_nmt_boot_con(...)
→ co_nmt_t boot indication
→ BasicMaster::OnBoot(...)

3.2 nmt_boot.c:事件驱动状态机和具体流程

nmt_boot.c 主要包含四组实现:

  1. co_nmt_boot_t 与状态接口;
  2. CAN 接收、Timer、SDO confirmation、configuration confirmation 的统一事件入口;
  3. 身份检查、节点状态检查、程序下载、配置更新和错误控制启动等状态;
  4. SDO upload/download、对象比较和 Node Guarding RTR 等辅助函数。

它不是按一个长函数顺序执行,而是把每个步骤拆成独立状态。


4. 状态对象为什么使用函数指针表

内部状态类型 co_nmt_boot_state_t 为不同异步事件预留了处理函数。根据 Doxygen 暴露的字段,可以抽象为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct __co_nmt_boot_state {
co_nmt_boot_state_t *(*on_enter)(co_nmt_boot_t *boot);
void (*on_leave)(co_nmt_boot_t *boot);
co_nmt_boot_state_t *(*on_recv)(
co_nmt_boot_t *boot, const struct can_msg *msg);
co_nmt_boot_state_t *(*on_time)(
co_nmt_boot_t *boot, const struct timespec *tp);
co_nmt_boot_state_t *(*on_dn_con)(
co_nmt_boot_t *boot, co_unsigned32_t ac);
co_nmt_boot_state_t *(*on_up_con)(
co_nmt_boot_t *boot, co_unsigned32_t ac,
const void *ptr, size_t n);
co_nmt_boot_state_t *(*on_cfg_con)(
co_nmt_boot_t *boot, co_unsigned32_t ac);
};

这里的代码是结构化示意,重点是事件类型,不应把它当成对目标提交逐字复制的声明。

各事件的来源如下:

事件槽 触发源 典型用途
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flowchart LR
CAN[CAN frame] --> RCV[co_nmt_boot_recv]
TMR[CAN timer] --> TF[co_nmt_boot_timer]
DN[SDO download confirmation] --> DNF[co_nmt_boot_dn_con]
UP[SDO upload confirmation] --> UPF[co_nmt_boot_up_con]
CFG[configuration confirmation] --> CFGF[co_nmt_boot_cfg_con]

RCV --> ER[emit_recv]
TF --> ET[emit_time]
DNF --> ED[emit_dn_con]
UPF --> EU[emit_up_con]
CFGF --> EC[emit_cfg_con]

ER --> STATE[current state handler]
ET --> STATE
ED --> STATE
EU --> STATE
EC --> STATE

STATE --> ENTER[co_nmt_boot_enter next]

这种设计有三个直接效果:

  • 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
2
3
4
5
6
7
8
assignment     0x1F81 对应的从站分配位
est / rst 期望状态与已观测状态
es 最近一次 boot error status
booting 当前是否正在 boot
bootup 是否收到 boot-up
booted boot 流程是否已完成
boot 对应 co_nmt_boot_t
cfg 对应 configuration request 上下文

这说明 Lely 的模型不是“全网共享一个游标依次启动”,而是 NMT manager 对每个节点保留独立管理状态。具体是否并行启动及何时启动,由 co_nmt_t 的 startup 流程和对象 0x1F81 策略决定。

5.2 公共请求如何进入内部流程

应用调用:

1
co_nmt_boot_req(nmt, id, timeout);

内部最终进入:

1
2
3
4
5
co_nmt_boot_boot_req(
boot, id, timeout,
download_progress_ind,
upload_progress_ind,
user_data);

参数中的 timeout 是本次 boot 过程使用的 SDO timeout。公共 NMT 对象还提供:

1
2
co_nmt_get_timeout();
co_nmt_set_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
2
3
4
5
当前 C 函数返回
!= boot 流程结束

当前回调调用栈退出
!= co_nmt_boot_t 被销毁

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
2
3
4
5
6
7
can_timer_t 到期
→ co_nmt_boot_timer()
→ co_nmt_boot_emit_time()
→ boot->state->on_time()
→ co_nmt_boot_wait_on_time()
→ 返回 next state
→ co_nmt_boot_enter(boot, next)

这里的 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
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
sequenceDiagram
participant App as 应用/BasicMaster
participant Boot as co_nmt_boot_t
participant Net as can_net/timer
participant Driver as io2 event loop / can_net 驱动者
participant Sdo as Client-SDO
participant Nmt as co_nmt_t

App->>Boot: co_nmt_boot_boot_req(id, timeout, ...)
Boot->>Boot: 保存请求参数并保持 wait state
Boot->>Net: 启动 wait timer
Boot-->>App: 立即返回 0/-1
Note over App,Driver: 发起请求的调用栈结束,执行权返回上层

Driver->>Net: 驱动 CAN I/O 或推进网络时间
Net->>Boot: co_nmt_boot_timer()
Boot->>Boot: emit_time ->> wait_on_time ->> enter(next)
Boot->>Sdo: 发起一次 SDO upload/download
Boot-->>Driver: 当前事件回调返回

Sdo-->>Boot: up_con/dn_con(响应、abort 或 timeout)
Boot->>Boot: emit confirmation ->> current state handler
Boot->>Boot: enter(next state)

Boot->>Nmt: boot 完成或失败 confirmation
Nmt-->>App: OnBoot(id, state, error status, what)

6.2 co_nmt_boot_enter() 允许一次事件连续穿过多个同步状态

一个 state handler 返回 co_nmt_boot_state_t *,含义是“下一步进入哪个状态”。co_nmt_boot_enter() 负责:

  1. 调用旧状态的 on_leave
  2. 更新 boot->state
  3. 调用新状态的 on_enter
  4. 如果 on_enter 又立即返回另一个 state,则继续进入下一个状态;
  5. 遇到真正的异步操作后停止同步推进,当前调用栈返回。

可以用下面的结构化伪代码理解。它表达调用关系,不是源码逐字复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
static void
co_nmt_boot_enter(co_nmt_boot_t *boot, co_nmt_boot_state_t *next)
{
while (next != NULL) {
if (boot->state != NULL && boot->state->on_leave != NULL)
boot->state->on_leave(boot);

boot->state = next;
next = boot->state->on_enter != NULL
? boot->state->on_enter(boot)
: NULL;
}
}

这解释了两个看似矛盾的现象:

  • 有些 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
2
3
4
handler = handler_of(boot->state, event);
next = handler != NULL ? handler(boot, event_data) : NULL;
if (next != NULL)
co_nmt_boot_enter(boot, next);

事件来源必须严格区分:

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_conon_dn_con 不能简单称为“CAN 接收回调”。CAN 帧只是使 Client-SDO 状态机完成的一种原因;SDO 自身的 timer 到期同样可以生成 confirmation。on_cfg_con 更不是固定由一帧 CAN 报文直接触发,因为 OnConfig() 阶段可能暂停 boot,等待驱动或应用报告配置结果。

6.4 主函数此时在做什么

答案取决于使用的是哪一层集成方式,但两种方式都不是“阻塞在 boot 函数里等待”。

使用 liblely-coapp / io2

常见执行关系是:

1
2
3
4
5
6
7
8
9
应用调用 BasicMaster::Boot(id)
→ co_nmt_boot_req()
→ co_nmt_boot_boot_req()
→ 当前 executor task 返回
→ ev_loop 继续执行其他 task 或阻塞在 Poll
→ SocketCAN/timerfd 就绪
→ io_can_net 把帧或时间交给 can_net
→ can_net 调用 boot receiver/timer 或 Client-SDO 回调
→ 当前 state 继续推进

主线程可能正在:

  • 执行同一个 event loop 中的其他任务;
  • 阻塞等待 epoll/Poll 的下一次 I/O;
  • 在多线程配置下执行应用自己的其他逻辑。

它不会为每个从站创建一个“boot 等待线程”,也不会一直停在 co_nmt_boot_boot_req() 内部。

只使用 liblely-co / can_net_t

此时没有 io2 自动代替应用驱动网络。集成代码必须持续完成两件事:

1
2
收到 CAN 帧 → can_net_recv()
时间前进 → can_net_set_time()

这些调用会让注册在 can_net_t 中的 receiver 和 timer 得到执行。仍然不是轮询 co_nmt_boot_t 本身;应用是在驱动 CAN 网络和时间,boot 状态机被事件间接推进。

6.5 其他模块是否不断查询 boot 是否完成

不是。co_nmt_is_booting()BasicMaster::IsReady()IsConfig() 都是观察状态的接口,不是状态机的驱动器:

1
2
3
4
5
6
7
8
co_nmt_is_booting(id)
→ 查询是否仍处于 boot 流程

BasicMaster::IsReady(id)
→ 查询 boot 是否成功完成且之后未再次收到 boot-up

BasicMaster::IsConfig(id)
→ 查询是否停在 update configuration 阶段

真正的完成通知是推送式的:

1
2
3
4
5
co_nmt_boot 状态机完成
→ co_nmt_boot_con(...)
→ co_nmt_t 更新 slave.booting / booted / es / state
→ 调用 boot indication
→ BasicMaster::OnBoot(...)

应用可以查询状态做诊断或 UI 展示,但不需要通过轮询查询来使状态前进。

6.6 完整状态拓扑

下面的图按源码中的 state 对象组织。它表达状态之间的主拓扑和异步等待点;具体条件仍由 0x1F81、身份期望对象、程序下载对象、从站响应和 abort code 决定。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
flowchart TD
CREATE[co_nmt_boot_create] --> WAIT[wait state]
REQ[co_nmt_boot_boot_req] --> WAIT

WAIT -->|on_time: 节点不在网络/请求被撤销| ABORT[abort state]
ABORT -->|清理后需要重试| WAIT
ABORT -->|终止并报告| ERROR[error state]
WAIT -->|on_time: 可以开始| DEV[check device type]

subgraph IDENTITY[设备类型与 Identity 检查]
DEV -->|up_con: 通过或未配置比较| VENDOR[check vendor ID]
VENDOR -->|up_con| PRODUCT[check product code]
PRODUCT -->|up_con| REV[check revision]
REV -->|up_con| SERIAL[check serial number]
end

DEV -->|SDO失败/值不匹配| ERROR
VENDOR -->|值不匹配| ERROR
PRODUCT -->|值不匹配| ERROR
REV -->|值不匹配| ERROR
SERIAL -->|值不匹配| ERROR

SERIAL -->|up_con| CHKNODE[check node state]

subgraph NODESTATE[节点状态与通信复位]
CHKNODE -->|recv: 状态允许继续| CHKSW[check software]
CHKNODE -->|recv: 需要复位| RESET[reset communication]
CHKNODE -->|time: 无 Heartbeat/guarding confirmation| ERROR
RESET -->|recv: boot-up| CHKSW
RESET -->|time: 未收到 boot-up| ERROR
end

subgraph PROGRAM[可选程序下载分支]
CHKSW -->|无需更新或未配置程序检查| CFGDATE[check configuration date]
CHKSW -->|需要且允许更新| STOP[stop program]
CHKSW -->|需要但不允许/配置不一致| ERROR
STOP -->|dn_con/up_con| CLEAR[clear program]
CLEAR -->|dn_con: 支持 block| BLKDN[block download program]
CLEAR -->|dn_con: 使用 segmented| DN[segmented download program]
BLKDN -->|dn_con| WAITFLASH[wait for flashing]
DN -->|dn_con| WAITFLASH
WAITFLASH -->|time: 再次查询| WAITFLASH
WAITFLASH -->|up_con: flashing 完成| CHKPROG[check program SW-ID]
CHKPROG -->|up_con: 匹配| STARTPROG[start program]
CHKPROG -->|up_con: 不匹配| ERROR
STARTPROG -->|dn_con| WAITPROG[wait till program started]
WAITPROG -->|time: 再次查询| WAITPROG
WAITPROG -->|up_con: 已启动| CFGDATE
end

subgraph CONFIG[配置日期、时间与配置下发]
CFGDATE -->|up_con: 日期需继续判断| CFGTIME[check configuration time]
CFGDATE -->|无需配置或对象不可用时按策略跳过| EC[start error control]
CFGTIME -->|up_con: 配置一致| EC
CFGTIME -->|up_con: 配置不一致| UPCFG[update configuration]
UPCFG -->|cfg_con: 成功| EC
UPCFG -->|cfg_con: 失败| ERROR
end

EC -->|recv: error control 已建立| COMPLETE[co_nmt_boot_con success]
EC -->|time: 启动失败| ERROR
ERROR -->|co_nmt_boot_con failure| WAIT
COMPLETE --> WAIT

这张图同时修正一个容易产生的顺序误解:

1
2
3
4
5
6
check software
→ 可选 stop/clear/download/wait/check/start/wait program
→ check configuration date/time
→ update configuration(可选)
→ start error control
→ boot completion

co_nmt_boot_stop_prog_stateco_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
flowchart TD
START[boot request] --> LISTED{节点在 0x1F81 中且允许 boot?}
LISTED -- 否 --> EA[完成: error A]
LISTED -- 是 --> ID[设备类型与 Identity 校验]

ID --> NODE[检查远端 NMT 状态]
NODE -->|需通信复位| RESET[发送 Reset Communication\n等待 boot-up]
NODE -->|状态可继续| SW
RESET --> SW[检查软件版本]

SW -->|无需更新| CFGCHK[检查配置日期/时间]
SW -->|需要且允许| PROG[停止/清除/下载/启动程序]
SW -->|需要但不允许| EH[完成: error H]
PROG --> CFGCHK

CFGCHK -->|一致| EC[启动 error control]
CFGCHK -->|不一致| CFG[configuration request]
CFG -->|成功| EC
CFG -->|失败| EJ[完成: error J]

EC -->|收到预期确认| DONE[co_nmt_boot_con\nes = 0]
EC -->|error control 失败| EK[完成: error K/E/F]

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
2
3
4
5
check device type
check vendor ID
check product code
check revision number
check serial number

远端读取与主站本地期望对象的对应关系为:

远端从站对象 主站期望对象 不一致错误
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
2
3
4
5
co_nmt_boot_up(remote index, sub-index)
→ Client-SDO upload
→ co_nmt_boot_up_con()
→ 当前状态 on_up_con()
→ co_nmt_boot_chk(local index, local sub-index, ptr, n)

co_nmt_boot_chk() 负责把远端返回字节与本地对象字典对应子对象的期望值比较。

这体现了一个关键设计:

主站 DCF 不只是本节点对象字典,也保存了远端从站的期望身份和启动策略;boot 状态机通过本地 OD 驱动远端校验。

若对 0x1000 的首次 upload 完全没有响应,则是错误状态 B。官方配置说明专门为该状态提供默认 1000 ms 的再次尝试等待时间。

7.3 第三步:检查远端节点当前状态

co_nmt_boot_chk_node_state 负责确定远端节点处于何种 NMT 状态。源码提供:

1
2
3
4
co_nmt_boot_chk_node_on_enter()
co_nmt_boot_chk_node_on_recv()
co_nmt_boot_chk_node_on_time()
co_nmt_boot_chk_node_on_leave()

这组接口说明该状态同时依赖 CAN receiver 与 timer。

当使用 Node Guarding 探测时:

1
2
3
4
5
6
进入 check node state
→ co_nmt_boot_send_rtr()
→ CAN-ID = 0x700 + Node-ID,RTR
→ 等待 guarding response
→ 收到后提取 NMT state 和 toggle bit
→ 超过 RTR timeout 未响应则转 error F

默认 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
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant B as co_nmt_boot_t
participant N as co_nmt_t
participant S as Remote slave
participant T as can_timer_t

B->>N: 请求发送 NMT Reset Communication
N->>S: CAN-ID 0x000, data 82 <Node-ID>
B->>T: 设置 RESET_TIMEOUT
S-->>B: boot-up, 0x700 + Node-ID, data 00
B->>T: 取消等待
B->>B: 进入后续软件/配置检查

默认 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
2
3
4
5
6
7
8
9
check software
stop program
clear program
block download program
segmented download program
wait flash
check program software identification
start program
wait till program is started

主路径可概括为:

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart LR
A[读取远端 1F56\n当前软件标识] --> B{与本地 1F55 一致?}
B -- 是 --> DONE[跳过程序下载]
B -- 否 --> C{更新被允许且对象配置一致?}
C -- 否 --> H[error H 或 G]
C -- 是 --> STOP[写 1F51 停止程序]
STOP --> CLEAR[写 1F51 清除程序]
CLEAR --> DL[把本地 1F58 数据\n下载到远端 1F50]
DL --> FLASH[轮询 1F57:01]
FLASH --> VERIFY[再次检查 1F56]
VERIFY --> START[写 1F51 启动程序]
START --> WAIT[轮询 1F51:01]
WAIT --> DONE

根据传输能力和数据规模,程序数据下载可进入 block 或 segmented 路径。真正的分段、序号、CRC、窗口等行为仍由 co_csdo_t 执行,nmt_boot.c 只选择目标对象、提供文件/数据源并接收结果。

相关错误状态:

状态 含义
G 程序下载对象未配置或相互不一致
H 软件需要更新,但当前配置或状态不允许更新
I 软件需要更新,但程序下载失败

程序擦除和启动通常不是立即完成,因此源码不会阻塞等待,而是每隔 LELY_CO_NMT_BOOT_CHECK_TIMEOUT 再次 upload:

1
2
0x1F57:01  flash status indication
0x1F51:01 program control

默认轮询间隔为 100 ms。

7.6 第六步:检查配置日期和时间

配置检查状态为:

1
2
3
check configuration date
check configuration time
update configuration

相关对象:

角色 对象 含义
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
2
3
4
5
恢复默认参数
→ 0x1F20 Store DCF
→ 0x1F22 Concise DCF
→ 用户 OnConfig()/ConfigResult() 扩展配置
→ 必要时 Reset Communication/Reset Node

配置完成后:

1
2
3
4
co_nmt_cfg_con()
→ co_nmt_boot_cfg_con()
→ emit_cfg_con()
→ 当前 state.on_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
2
3
4
5
6
7
8
flowchart TD
A[start error control] --> B{配置 Heartbeat?}
B -- 是 --> HB[等待 Heartbeat]
B -- 否 --> NG[发送 guarding RTR / 建立 Node Guarding]
HB -->|收到| OK[boot complete]
HB -->|超时| K[error K]
NG -->|收到| OK
NG -->|超时| F[error F]

错误 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 表示成功;
  • esAO 表示具体 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
2
3
4
5
SDO abort code
= 某一次对象访问为什么失败

NMT boot error status A..O
= 整个从站 boot 流程最终在哪一类条件下失败

例如同样是 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
2
3
4
5
SDO 单次应答:通常较短
Node Guarding RTR:单帧响应,应更短
通信复位:需要重建整个通信对象,应更长
Flash 擦除:总时长可能较长,但使用短间隔轮询
error B 重试:避免持续紧密轰炸尚未上线的节点

10. SDO 所有权:为什么 boot 时普通远程读写可能失败

nmt_boot.c 在身份检查、程序下载和配置过程中持续占用目标节点的默认 Client-SDO。Lely 官方维护者明确说明:默认 SDO client 只有在 NMT master 不需要它执行 boot 时才可供普通应用使用。

C++ BasicMaster::GetSdo(id) 文档也规定:

1
2
3
若 master 不在 Pre-operational/Operational,
或 NMT master 正需要该 Client-SDO boot 节点,
返回 null。

因此以下做法存在竞争:

1
2
master.Reset();
master.SubmitRead<uint32_t>(1, 0x1018, 1, ...); // 可能过早

更可靠的生命周期是:

1
2
3
4
5
6
7
Reset master

等待 OnBoot(id, ..., es == 0)

IsReady(id) == true

再提交普通业务 SDO

唯一特殊窗口是 OnConfig(id):boot 流程主动把默认 Client-SDO 交给用户进行扩展配置,此时可以提交配置 SDO,但结束后必须调用 ConfigResult()

1
2
3
4
5
6
7
stateDiagram-v2
[*] --> BootOwnsSdo: Boot(id)
BootOwnsSdo --> UserConfigOwnsSdo: OnConfig(id)
UserConfigOwnsSdo --> BootOwnsSdo: ConfigResult(id, success)
BootOwnsSdo --> Ready: OnBoot(id, es=0)
BootOwnsSdo --> Failed: OnBoot(id, es!=0)
Ready --> AppOwnsSdo: GetSdo / SubmitRead / SubmitWrite

11. boot 完成不等于“从站已经被观测为 Operational”

CANopen NMT Start Remote Node 是无确认命令。Lely 维护者在官方 issue 中指出,OnBoot() 可能在从站实际切换到 Operational 之前被调用,因为 master 发出 NMT start 后不会获得协议级确认。

因此要区分:

1
2
3
4
5
6
OnBoot(es == 0)
= Lely 的 boot/config/error-control 流程成功完成

OnState(id, OPERATIONAL)
= master 后续通过 Heartbeat 或 Node Guarding
实际观察到从站处于 Operational

对需要严格确认从站已运行的应用,建议使用双条件:

1
2
3
4
5
boot_completed[id] = OnBoot(es == 0)
state_operational[id] = OnState(id, OPERATIONAL)

slave_ready_for_process_data =
boot_completed[id] && state_operational[id]

如果 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
2
3
某个从站 OnBoot 成功
≠ 所有 mandatory 从站均成功
≠ master 必然已经 Operational

应用不应只从单个节点回调反推整个网络状态。


13. 从日志与抓包识别这条状态机

开启 Lely diagnostic trace 后,一条典型流程可能出现以下语义序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NMT master 进入 Pre-operational
boot slave <id>
create/use client-SDO
upload 1000:00
upload 1018:01
upload 1018:02
upload 1018:03
upload 1018:04
check NMT state / reset communication
check software
check configuration date/time
update configuration
start error control
send NMT start
boot complete

总线侧对应的帧类别为:

阶段 典型 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 访问是否成功;
  • 是否产生配置或程序下载;
  • OnBootes
  • 后续 Heartbeat/Node Guarding 状态;
  • master 和 slave 的 NMT 状态回调。

14. 把整个实现压缩成一条调用链

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
33
34
35
36
37
co_nmt_t startup / BasicMaster::Boot(id)

co_nmt_boot_req()

co_nmt_boot_boot_req()

co_nmt_boot_enter(first state)

身份检查:
co_nmt_boot_up()
→ co_csdo upload
→ co_nmt_boot_up_con()
→ current_state->on_up_con()
→ co_nmt_boot_chk()

节点状态检查:
can_recv + can_timer
→ recv/time event
→ current_state->on_recv/on_time

可选软件更新:
1F55/1F58 ↔ 1F50/1F51/1F56/1F57
→ SDO download/upload confirmation

配置检查与更新:
1020 ↔ 1F26
→ co_nmt_cfg_req()
→ nmt_cfg.c
→ co_nmt_boot_cfg_con()

启动 Heartbeat/Node Guarding

co_nmt_boot_con(nmt, id, st, es)

co_nmt_t 更新 slave 状态并发出 boot indication

BasicMaster::OnBoot(id, st, es, what)

最终可以用一句话概括:

nmt_boot.c 的核心价值,是把主站 DCF 中对远端节点的“期望描述”,转换成一组按事件推进的 NMT、SDO、程序下载、配置下载和错误控制动作,并把复杂底层失败归一化为 A~O 的 boot 结果。


参考资料

  1. Lely core libraries GitLab repository
  2. Lely core libraries 2.4.0 Doxygen: src/co/nmt_boot.c
  3. Lely core libraries 2.4.0 Doxygen: src/co/nmt_boot.h
  4. Lely core libraries 2.4.0 Doxygen: include/lely/co/nmt.h
  5. Lely CANopen: Library overview
  6. Lely CANopen: Build configuration
  7. Lely CANopen: Standards support
  8. Lely CANopen: EDS/DCF tools
  9. Lely Doxygen: BasicMaster class reference