CANopen NMT:搞懂 NMT 运行流程

在这里插入图片描述

@[toc]

在这里插入图片描述

先给结论

NMT 是 CANopen 的网络管理状态机控制协议,Heartbeat 是配套的节点在线与状态上报机制。在 CANopenNode 里,CO_NMT_Heartbeat.c/h 不是单纯“收一帧命令就切状态”的代码,而是把以下功能合在一个对象 CO_NMT_t 里:

  1. NMT slave / NMT consumer:接收 CAN-ID 0x000 的 NMT 命令,目标 Node-ID 为 0 或本节点时才处理。
  2. Heartbeat producer:用 CAN-ID 0x700 + nodeId 周期发送本节点 NMT 状态;初始化阶段还会发 boot-up message。
  3. NMT 状态机执行点:真正状态切换不在 CAN 接收回调里完成,而是在 CO_NMT_process() 周期调用中完成。
  4. 错误条件联动:可根据 CAN bus off、Heartbeat consumer 错误、Error register mask,把 Operational 自动降到 Pre-operational 或 Stopped。
  5. 复位请求返回Reset node / Reset communication 不在 NMT 模块里直接重启 MCU,而是通过 CO_NMT_process() 返回 CO_RESET_APP / CO_RESET_COMM 交给应用层执行。

源码运行链路可以压缩成:

1
2
3
4
5
6
CAN-ID 0x000 NMT command
-> CO_NMT_receive() 校验 DLC 和 Node-ID
-> NMT->internalCommand = command
-> CO_NMT_process() 周期执行
-> 切换 NMT 状态或返回 resetCommand
-> 发送 boot-up / heartbeat:0x700 + nodeId, data[0] = NMT state

1. 先看协议:NMT 解决什么问题

1.1 NMT 的定位

CANopen 节点不是上电后立刻就能随便发 PDO。协议把设备运行分成几个 NMT 状态,主站或管理端通过 NMT 命令控制节点进入启动、停止、预运行、运行或复位流程。

在工程上,NMT 主要解决三件事:

问题 NMT/Heartbeat 的做法
什么时候允许 PDO 工作 只有 Operational 状态下 PDO 才工作
主站如何启动、停止或复位从站 发送 CAN-ID 0x000 的 NMT command
主站如何知道从站当前在线状态 从站周期发送 Heartbeat,数据字节携带 NMT state

CANopenNode 官方仓库说明它支持 NMT slave、simple NMT master,以及 heartbeat producer/consumer error control。CiA 的 NMT 公开说明也明确:NMT 协议由 active NMT manager 发送,接收后会强制 CANopen 设备转入被命令的 NMT 状态;报文是单帧 2 字节,CAN-ID 为 0

1.2 NMT 命令帧格式

NMT command 固定使用最高优先级的标准 CAN-ID:

字段 值/含义
CAN-ID 0x000
DLC 2
Byte 0 NMT command specifier
Byte 1 目标 Node-ID;0 表示广播所有节点

常用命令值在 CO_NMT_command_t 中直接定义:

CANopenNode 枚举 数值 协议含义
CO_NMT_ENTER_OPERATIONAL 0x01 Start remote node,进入 Operational
CO_NMT_ENTER_STOPPED 0x02 Stop remote node,进入 Stopped
CO_NMT_ENTER_PRE_OPERATIONAL 0x80 Enter pre-operational
CO_NMT_RESET_NODE 0x81 Reset node,完整节点复位
CO_NMT_RESET_COMMUNICATION 0x82 Reset communication,仅通信部分复位

示例:

1
2
3
4
5
6
7
8
# 启动所有节点进入 Operational
CAN-ID = 0x000, DLC = 2, Data = 01 00

# 让节点 5 进入 Pre-operational
CAN-ID = 0x000, DLC = 2, Data = 80 05

# 让节点 5 执行通信复位
CAN-ID = 0x000, DLC = 2, Data = 82 05

1.3 Heartbeat 与 boot-up 报文格式

Heartbeat 是错误控制协议的一部分,用于确认网络参与者仍然在线,并且仍处于预期的 NMT FSA 状态。

字段 值/含义
CAN-ID 0x700 + Node-ID
DLC 1
Byte 0 当前 NMT state

状态值在 CO_NMT_internalState_t 中定义:

状态 数值 运行含义
CO_NMT_INITIALIZING 0 初始化中;也用于 boot-up message 的数据字节
CO_NMT_PRE_OPERATIONAL 127 / 0x7F 预运行;SDO 等对象可用,PDO 不工作
CO_NMT_OPERATIONAL 5 / 0x05 运行;PDO 开始工作
CO_NMT_STOPPED 4 / 0x04 停止;只保留少量通信能力
CO_NMT_UNKNOWN -1 Heartbeat consumer 侧用于表示未知状态

注意:boot-up message 和 heartbeat 使用同一个 CAN-ID 规则。boot-up 是节点初始化完成时发出的状态值 0,后续 heartbeat 则周期性发送当前状态。

1.4 四个状态要先分清

在这里插入图片描述

CANopenNode 在头文件注释里给出了状态边界:

状态 CANopenNode 注释中的语义 对 STM32 从机的直接影响
Initializing CANopen 初始化完成前的活动状态 还没进入正常 CANopen 通信流程
Pre-operational 除 PDO 外所有 CANopen 对象可用 常用于 SDO 配置、参数下载、映射检查
Operational PDO 也可用 正式跑过程数据
Stopped 只保留 Heartbeat producer 和 NMT consumer 应用输出应进入安全/停止策略

2. 再看源码:文件分工和阅读顺序

在这里插入图片描述

建议阅读顺序如下:

顺序 位置 先看什么 目的
1 CO_NMT_Heartbeat.h 顶部 Doxygen 模块说明、NMT/Heartbeat 报文内容 建立协议边界
2 CO_NMT_internalState_t NMT 状态值 对照 heartbeat byte0
3 CO_NMT_command_t NMT 命令值 对照 NMT command byte0
4 CO_NMT_reset_cmd_t 返回给应用的复位动作 理解 reset 不在模块内部直接执行
5 CO_NMT_control_t NMTcontrol 的位域 理解自启动、错误降级、错误恢复
6 CO_NMT_t 状态、定时器、CAN buffer、回调 理解运行时对象保存了什么
7 CO_NMT_init() 初始化对象、OD 0x1017、CAN RX/TX 理解初始化阶段做了什么
8 CO_NMT_receive() 接收 NMT 命令 理解接收回调只做预处理
9 CO_NMT_process() 状态机主逻辑 这是运行流程核心
10 CO_NMT_sendCommand() 可选 simple NMT master 发送能力 理解 CO_CONFIG_NMT_MASTER 不是完整主站

3. CO_NMT_Heartbeat.h:先把协议值和对象结构看懂

3.1 CO_CONFIG_NMT 默认配置

头文件中默认配置是:

1
2
3
#ifndef CO_CONFIG_NMT
#define CO_CONFIG_NMT (CO_CONFIG_GLOBAL_FLAG_CALLBACK_PRE | CO_CONFIG_GLOBAL_FLAG_TIMERNEXT)
#endif

含义:如果工程没有自己定义 CO_CONFIG_NMT,默认打开两个公共能力:

flag 含义
CO_CONFIG_GLOBAL_FLAG_CALLBACK_PRE 收到 NMT CAN 帧预处理后可触发回调,常用于 RTOS 唤醒处理任务
CO_CONFIG_GLOBAL_FLAG_TIMERNEXT CO_NMT_process() 可给出下一次建议调用时间 timerNext_us

如果想让本节点也能主动发 NMT command,需要额外打开 CO_CONFIG_NMT_MASTER。这只表示“simple NMT master 发送能力”,不是完整主站配置器。

3.2 CO_NMT_internalState_t:Heartbeat 发送的就是这个值

1
2
3
4
5
6
7
typedef enum {
CO_NMT_UNKNOWN = -1,
CO_NMT_INITIALIZING = 0,
CO_NMT_PRE_OPERATIONAL = 127,
CO_NMT_OPERATIONAL = 5,
CO_NMT_STOPPED = 4
} CO_NMT_internalState_t;

CO_NMT_process() 发送 heartbeat 时会把当前状态写到 HB_TXbuff->data[0]。所以调试 CAN 报文时看到:

1
2
3
4
0x705  [1]  7F   # 节点 5 处于 Pre-operational
0x705 [1] 05 # 节点 5 处于 Operational
0x705 [1] 04 # 节点 5 处于 Stopped
0x705 [1] 00 # boot-up message / Initializing

3.3 CO_NMT_command_t:NMT 命令 byte0 的来源

1
2
3
4
5
6
7
8
typedef enum {
CO_NMT_NO_COMMAND = 0,
CO_NMT_ENTER_OPERATIONAL = 1,
CO_NMT_ENTER_STOPPED = 2,
CO_NMT_ENTER_PRE_OPERATIONAL = 128,
CO_NMT_RESET_NODE = 129,
CO_NMT_RESET_COMMUNICATION = 130
} CO_NMT_command_t;

CO_NMT_receive() 从 NMT command 报文的 data[0] 读出这个值;如果本节点启用了 simple master,CO_NMT_sendCommand() 也会把这个值写回 NMT_TXbuff->data[0]

3.4 CO_NMT_reset_cmd_t:复位命令为什么是返回值

1
2
3
4
5
6
typedef enum {
CO_RESET_NOT = 0,
CO_RESET_COMM = 1,
CO_RESET_APP = 2,
CO_RESET_QUIT = 3
} CO_NMT_reset_cmd_t;

这体现了 CANopenNode 的边界:协议栈识别“需要复位”,但不直接决定 MCU 怎么复位。

  • CO_RESET_COMM:应用进入通信复位段,重建 CANopen 通信对象。
  • CO_RESET_APP:应用执行完整设备复位,常见做法是 MCU reset 或回到主初始化入口。
  • CO_RESET_NOT:正常运行。

3.5 NMTcontrol:不是协议帧,而是本地策略开关

NMTcontrol 是传给 CO_NMT_init() 的本地控制位域,用于决定自启动和错误行为:

作用
CO_NMT_ERR_REG_MASK 低 8 位作为 Error register mask
CO_NMT_STARTUP_TO_OPERATIONAL 初始化后直接进入 Operational,否则进入 Pre-operational
CO_NMT_ERR_ON_BUSOFF_HB Operational 下遇到 CAN bus off 或 heartbeat consumer 错误时降级
CO_NMT_ERR_ON_ERR_REG Operational 下 Error register 命中 mask 时降级
CO_NMT_ERR_TO_STOPPED 错误触发时降到 Stopped,否则降到 Pre-operational
CO_NMT_ERR_FREE_TO_OPERATIONAL 错误消失后可从 Pre-operational 自动恢复到 Operational

这一组宏是源码学习时很容易漏掉的点。它解释了一个现象:节点没有收到新的 NMT command,也可能因为本地错误自动从 Operational 掉到 Pre-operational 或 Stopped。


4. CO_NMT_init():初始化时把协议入口全部接好

CO_NMT_init() 必须在 communication reset section 调用。它主要做 6 件事。

4.1 清空对象并写入初始状态

源码先 memset(NMT, 0, sizeof(CO_NMT_t)),然后设置:

1
2
3
4
5
6
NMT->operatingState = CO_NMT_INITIALIZING;
NMT->operatingStatePrev = CO_NMT_INITIALIZING;
NMT->nodeId = nodeId;
NMT->NMTcontrol = NMTcontrol;
NMT->em = em;
NMT->HBproducerTimer = (uint32_t)firstHBTime_ms * 1000U;

这里的关键是:初始状态固定为 CO_NMT_INITIALIZING,后面第一次 CO_NMT_process() 会发送 boot-up message,然后再进入 Pre-operational 或 Operational。

4.2 读取并扩展 OD 0x1017 Producer heartbeat time

0x1017 是 producer heartbeat time。CO_NMT_init() 会读取它,并保存到微秒单位:

1
2
OD_get_u16(OD_1017_ProducerHbTime, 0, &HBprodTime_ms, true);
NMT->HBproducerTime_us = (uint32_t)HBprodTime_ms * 1000U;

随后它给 0x1017 安装 OD extension:

1
2
3
4
NMT->OD_1017_extension.object = NMT;
NMT->OD_1017_extension.read = OD_readOriginal;
NMT->OD_1017_extension.write = OD_write_1017;
OD_extension_init(OD_1017_ProducerHbTime, &NMT->OD_1017_extension);

这样运行时如果通过 SDO 写 0x1017OD_write_1017() 会同步更新 HBproducerTime_us,并把 HBproducerTimer 清零,使新的 heartbeat 周期立即生效。

4.3 配置 NMT 命令接收

1
CO_CANrxBufferInit(NMT_CANdevRx, NMT_rxIdx, CANidRxNMT, 0x7FF, false, (void*)NMT, CO_NMT_receive);

典型 CANidRxNMT0x000。这一步把 CAN 接收 buffer 和 CO_NMT_receive() 绑定起来。

4.4 可选配置 simple NMT master 发送

只有 CO_CONFIG_NMT_MASTER 打开时,才会编译和初始化:

1
NMT->NMT_TXbuff = CO_CANtxBufferInit(NMT_CANdevTx, NMT_txIdx, CANidTxNMT, false, 2, false);

这对应发送 CAN-ID 0x000、DLC 2 的 NMT command。

4.5 配置 Heartbeat 发送

1
NMT->HB_TXbuff = CO_CANtxBufferInit(HB_CANdevTx, HB_txIdx, CANidTxHB, false, 1, false);

典型 CANidTxHB0x700 + nodeId,DLC 为 1。


5. CO_NMT_receive():接收回调只做“预处理”

CO_NMT_receive() 的逻辑很短,但很关键:

1
2
3
4
5
6
7
8
9
uint8_t DLC = CO_CANrxMsg_readDLC(msg);
const uint8_t* data = CO_CANrxMsg_readData(msg);
CO_NMT_command_t command = (CO_NMT_command_t)data[0];
uint8_t nodeId = data[1];

if ((DLC == 2U) && ((nodeId == 0U) || (nodeId == NMT->nodeId))) {
NMT->internalCommand = command;
... optional callback ...
}

这里有三个结论:

  1. NMT 命令必须 DLC=2,否则不处理。
  2. Node-ID 为 0 是广播,或者等于本节点 Node-ID 才处理。
  3. 接收回调不直接切状态,只写 NMT->internalCommand,真正动作留给 CO_NMT_process()

这种设计适合裸机和 RTOS:CAN 中断/驱动回调里只做极短预处理,复杂状态切换和可能触发的 reset 交给主循环或任务上下文。


6. CO_NMT_process():NMT 运行流程核心

在这里插入图片描述

CO_NMT_process() 是整个模块最重要的函数。它的执行顺序可以拆成 7 层。

6.1 更新 Heartbeat 定时器

1
2
3
NMT->HBproducerTimer = (NMT->HBproducerTimer > timeDifference_us)
? (NMT->HBproducerTimer - timeDifference_us)
: 0U;

timeDifference_us 是应用每次调用时传入的时间差。NMT 模块不自己读硬件定时器,而是依赖外部调度传入 elapsed time。

6.2 发送 boot-up 或 heartbeat

发送条件是:

1
2
3
当前是 Initializing

0x1017 不为 0,并且 heartbeat timer 到期或 NMT 状态变化

发送内容是:

1
2
NMT->HB_TXbuff->data[0] = (uint8_t)NMTstateCpy;
CO_CANsend(NMT->HB_CANdevTx, NMT->HB_TXbuff);

如果当前状态是 CO_NMT_INITIALIZING,这帧就是 boot-up message。发完后,源码再根据 CO_NMT_STARTUP_TO_OPERATIONAL 决定初始运行状态:

1
2
3
NMTstateCpy = (NMTcontrol & CO_NMT_STARTUP_TO_OPERATIONAL)
? CO_NMT_OPERATIONAL
: CO_NMT_PRE_OPERATIONAL;

所以:

NMTcontrol 设置 boot-up 后进入
未设置 CO_NMT_STARTUP_TO_OPERATIONAL Pre-operational
设置 CO_NMT_STARTUP_TO_OPERATIONAL Operational

6.3 处理收到的内部命令

internalCommand 可能来自两个入口:

  • CO_NMT_receive():总线上收到 NMT command。
  • CO_NMT_sendCommand():本节点作为 simple NMT master 发送命令时,如果目标是自己或广播,也会写入本地 internalCommand

处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch (NMT->internalCommand) {
case CO_NMT_ENTER_OPERATIONAL:
NMTstateCpy = CO_NMT_OPERATIONAL;
break;
case CO_NMT_ENTER_STOPPED:
NMTstateCpy = CO_NMT_STOPPED;
break;
case CO_NMT_ENTER_PRE_OPERATIONAL:
NMTstateCpy = CO_NMT_PRE_OPERATIONAL;
break;
case CO_NMT_RESET_NODE:
resetCommand = CO_RESET_APP;
break;
case CO_NMT_RESET_COMMUNICATION:
resetCommand = CO_RESET_COMM;
break;
}
NMT->internalCommand = CO_NMT_NO_COMMAND;

注意:CO_NMT_RESET_NODECO_NMT_RESET_COMMUNICATION 不会在这里调用 NVIC_SystemReset()。源码只设置返回值,应用层必须处理这个返回值。

6.4 根据错误条件自动降级或恢复

源码读取 Emergency 模块中的错误状态:

1
2
3
ErrBusOff = CO_isError(NMT->em, CO_EM_CAN_TX_BUS_OFF);
ErrHbCons = CO_isError(NMT->em, CO_EM_HEARTBEAT_CONSUMER);
ErrHbConsRemote = CO_isError(NMT->em, CO_EM_HB_CONSUMER_REMOTE_RESET);

然后结合 NMTcontrol 判断:

1
2
busOff_HB = CO_NMT_ERR_ON_BUSOFF_HB && (busOff 或 heartbeat consumer 错误)
errRegMasked = CO_NMT_ERR_ON_ERR_REG && (Error register 命中 NMTcontrol 低 8 位 mask)

最终策略:

当前状态 条件 结果
Operational busOff_HBerrRegMasked 降级到 Pre-operational,或在 CO_NMT_ERR_TO_STOPPED 设置时降到 Stopped
Pre-operational CO_NMT_ERR_FREE_TO_OPERATIONAL 设置,且错误条件全部消失 自动恢复到 Operational

这解释了调试中常见的“明明没发 NMT stop,节点为什么掉到 Pre-op”:通常是 NMTcontrol 配置让错误寄存器或 heartbeat consumer 错误参与了 NMT 状态控制。

6.5 可选状态变化回调

如果打开 CO_CONFIG_NMT_CALLBACK_CHANGE,状态变化后会调用:

1
NMT->pFunctNMT(NMTstateCpy);

这适合应用层统一处理状态进入/退出,比如进入 Stopped 时关闭输出、进入 Operational 时允许业务周期运行。

6.6 可选 timerNext_us

如果打开 CO_CONFIG_FLAG_TIMERNEXTCO_NMT_process() 会根据 heartbeat timer 更新 timerNext_us,供外部调度器决定下一次最晚什么时候再调用。

6.7 更新状态并返回 reset 命令

最后:

1
2
3
4
5
NMT->operatingState = NMTstateCpy;
if (NMTstate != NULL) {
*NMTstate = NMTstateCpy;
}
return resetCommand;

应用层应该保存或检查 NMTstate,并处理 resetCommand


7. CO_NMT_sendCommand():simple NMT master 不是完整主站

CO_NMT_sendCommand() 只有在 CO_CONFIG_NMT_MASTER 打开时才编译。它做两件事。

7.1 如果目标包含自己,先写本地 internalCommand

1
2
3
if ((nodeID == 0U) || (nodeID == NMT->nodeId)) {
NMT->internalCommand = command;
}

这样本节点广播启动所有节点时,自己也会在下一次 CO_NMT_process() 中执行同一命令。

7.2 发送 NMT command 到总线

1
2
3
NMT->NMT_TXbuff->data[0] = (uint8_t)command;
NMT->NMT_TXbuff->data[1] = nodeID;
return CO_CANsend(NMT->NMT_CANdevTx, NMT->NMT_TXbuff);

这只负责发 0x000 NMT command,不包含:

  • 自动扫描节点;
  • 自动配置对象字典;
  • 自动写 PDO 映射;
  • 自动监控所有 heartbeat;
  • 自动恢复故障节点。

所以 CO_CONFIG_NMT_MASTER 的准确理解是:让本节点具备发送 NMT command 的能力,不是把 STM32 从机“变成完整 CANopen 主站”。


8. STM32 从机里应该怎么用 NMT

在这里插入图片描述

8.1 最小运行链路

普通 STM32 CANopen 从机通常只需要默认 NMT slave + Heartbeat producer:

1
2
3
4
5
1. 初始化 CAN/FDCAN 驱动和 CANopen 对象
2. 在 communication reset 段调用 CO_NMT_init()
3. 主循环或 RTOS task 周期调用 CO_NMT_process()
4. 根据返回的 resetCommand 决定是否通信复位或整机复位
5. 根据 NMTstate 决定应用是否允许输出、是否允许 PDO 业务

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (;;) {
uint32_t dt_us = get_elapsed_us();
uint32_t timerNext_us = UINT32_MAX;
CO_NMT_internalState_t nmtState;

CO_NMT_reset_cmd_t reset = CO_NMT_process(CO->NMT, &nmtState, dt_us, &timerNext_us);

if (reset == CO_RESET_COMM) {
/* exit to communication reset section */
break;
}
if (reset == CO_RESET_APP) {
/* execute application reset policy, for example MCU reset */
system_reset();
}

if (nmtState == CO_NMT_OPERATIONAL) {
/* allow normal process-data behavior */
} else {
/* keep outputs safe or skip operational-only behavior */
}
}

8.2 0x1017 Producer heartbeat time 怎么配

0x1017 的单位是毫秒,CANopenNode 读出后换算成微秒保存到 HBproducerTime_us

建议调试阶段先设置一个容易观察的值,例如:

1
0x1017 = 1000 ms

这样节点每秒发一次 heartbeat。CAN 分析仪上应该能看到:

1
0x700 + nodeId  [1]  7F / 05 / 04

如果 0x1017 = 0,周期 heartbeat 被禁用;但初始化阶段的 boot-up message 仍由 CO_NMT_process() 在 Initializing 状态下发送。

8.3 主站不发 Start 时,为什么 PDO 不工作

如果没有设置 CO_NMT_STARTUP_TO_OPERATIONAL,节点 boot-up 后默认进入 Pre-operational。这个状态下 SDO 可用,但 PDO 不工作。

这时有两种选择:

方式 行为 适用场景
主站发送 01 nodeId01 00 从站进入 Operational 规范主站管理流程
本地设置 CO_NMT_STARTUP_TO_OPERATIONAL boot-up 后自动进入 Operational 简单单节点、实验或固定系统

8.4 收到 reset command 后该做什么

NMT reset 命令不是“协议栈内部立即复位”。应用必须检查 CO_NMT_process() 返回值:

返回值 应用侧动作
CO_RESET_COMM 退出当前通信循环,重新初始化 CANopen 通信对象,通常不需要重启 MCU
CO_RESET_APP 执行完整应用复位策略,可能包括 MCU reset
CO_RESET_NOT 正常继续

9. 一句话总结

CO_NMT_Heartbeat.c/h 的核心不是“发心跳”这么简单,而是:CO_NMT_t 保存本节点 NMT 状态和 heartbeat 定时器,用 CO_NMT_receive() 把总线 NMT 命令转成内部命令,再由 CO_NMT_process() 在主循环/任务上下文中统一完成 boot-up、heartbeat、状态切换、错误降级和复位请求返回。


10. 资料依据

类型 资料 本文使用点
协议公开说明 CiA:Network management NMT command 由 active NMT manager 发送;CAN-ID 0;DLC 2;byte0 为命令,byte1 为 node-ID;node-ID 为 0 表示所有节点
协议公开说明 CiA:Error control protocols Heartbeat 用于确认网络参与者仍在线且处于预期 NMT FSA 状态
官方实现资料 CANopenNode:NMT and Heartbeat Doxygen CO_NMT_init()CO_NMT_process()、回调、CO_NMT_sendCommand() 的官方语义
官方实现资料 CANopenNode:CO_NMT_Heartbeat.h Doxygen NMT 状态、NMT command、Heartbeat/NMT 报文内容、NMTcontrol
官方仓库 CANopenNode GitHub CANopenNode 是 ANSI C 协议栈,支持 NMT slave、simple NMT master、Heartbeat producer/consumer
上传源码 CO_NMT_Heartbeat.h / CO_NMT_Heartbeat.c 本文源码流程、状态值、命令值、初始化与 process 逻辑
上传规范译注 《CiA301 V4.2.0(中文注释版)》 NMT、boot-up、状态机、Heartbeat/Node guarding、对象 0x1017 的章节定位