CANopen NMT:搞懂 NMT 运行流程
@[toc]
先给结论
NMT 是 CANopen 的网络管理状态机控制协议,Heartbeat 是配套的节点在线与状态上报机制。在 CANopenNode 里,CO_NMT_Heartbeat.c/h 不是单纯“收一帧命令就切状态”的代码,而是把以下功能合在一个对象 CO_NMT_t 里:
- NMT slave / NMT consumer:接收 CAN-ID
0x000的 NMT 命令,目标 Node-ID 为0或本节点时才处理。 - Heartbeat producer:用 CAN-ID
0x700 + nodeId周期发送本节点 NMT 状态;初始化阶段还会发 boot-up message。 - NMT 状态机执行点:真正状态切换不在 CAN 接收回调里完成,而是在
CO_NMT_process()周期调用中完成。 - 错误条件联动:可根据 CAN bus off、Heartbeat consumer 错误、Error register mask,把 Operational 自动降到 Pre-operational 或 Stopped。
- 复位请求返回:
Reset node/Reset communication不在 NMT 模块里直接重启 MCU,而是通过CO_NMT_process()返回CO_RESET_APP/CO_RESET_COMM交给应用层执行。
源码运行链路可以压缩成:
1 | CAN-ID 0x000 NMT command |
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 | # 启动所有节点进入 Operational |
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 |
含义:如果工程没有自己定义 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 | typedef enum { |
CO_NMT_process() 发送 heartbeat 时会把当前状态写到 HB_TXbuff->data[0]。所以调试 CAN 报文时看到:
1 | 0x705 [1] 7F # 节点 5 处于 Pre-operational |
3.3 CO_NMT_command_t:NMT 命令 byte0 的来源
1 | typedef enum { |
CO_NMT_receive() 从 NMT command 报文的 data[0] 读出这个值;如果本节点启用了 simple master,CO_NMT_sendCommand() 也会把这个值写回 NMT_TXbuff->data[0]。
3.4 CO_NMT_reset_cmd_t:复位命令为什么是返回值
1 | typedef enum { |
这体现了 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 | NMT->operatingState = CO_NMT_INITIALIZING; |
这里的关键是:初始状态固定为 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 | OD_get_u16(OD_1017_ProducerHbTime, 0, &HBprodTime_ms, true); |
随后它给 0x1017 安装 OD extension:
1 | NMT->OD_1017_extension.object = NMT; |
这样运行时如果通过 SDO 写 0x1017,OD_write_1017() 会同步更新 HBproducerTime_us,并把 HBproducerTimer 清零,使新的 heartbeat 周期立即生效。
4.3 配置 NMT 命令接收
1 | CO_CANrxBufferInit(NMT_CANdevRx, NMT_rxIdx, CANidRxNMT, 0x7FF, false, (void*)NMT, CO_NMT_receive); |
典型 CANidRxNMT 是 0x000。这一步把 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); |
典型 CANidTxHB 是 0x700 + nodeId,DLC 为 1。
5. CO_NMT_receive():接收回调只做“预处理”
CO_NMT_receive() 的逻辑很短,但很关键:
1 | uint8_t DLC = CO_CANrxMsg_readDLC(msg); |
这里有三个结论:
- NMT 命令必须 DLC=2,否则不处理。
- Node-ID 为 0 是广播,或者等于本节点 Node-ID 才处理。
- 接收回调不直接切状态,只写
NMT->internalCommand,真正动作留给CO_NMT_process()。
这种设计适合裸机和 RTOS:CAN 中断/驱动回调里只做极短预处理,复杂状态切换和可能触发的 reset 交给主循环或任务上下文。
6. CO_NMT_process():NMT 运行流程核心
CO_NMT_process() 是整个模块最重要的函数。它的执行顺序可以拆成 7 层。
6.1 更新 Heartbeat 定时器
1 | NMT->HBproducerTimer = (NMT->HBproducerTimer > timeDifference_us) |
timeDifference_us 是应用每次调用时传入的时间差。NMT 模块不自己读硬件定时器,而是依赖外部调度传入 elapsed time。
6.2 发送 boot-up 或 heartbeat
发送条件是:
1 | 当前是 Initializing |
发送内容是:
1 | NMT->HB_TXbuff->data[0] = (uint8_t)NMTstateCpy; |
如果当前状态是 CO_NMT_INITIALIZING,这帧就是 boot-up message。发完后,源码再根据 CO_NMT_STARTUP_TO_OPERATIONAL 决定初始运行状态:
1 | NMTstateCpy = (NMTcontrol & CO_NMT_STARTUP_TO_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 | switch (NMT->internalCommand) { |
注意:CO_NMT_RESET_NODE 和 CO_NMT_RESET_COMMUNICATION 不会在这里调用 NVIC_SystemReset()。源码只设置返回值,应用层必须处理这个返回值。
6.4 根据错误条件自动降级或恢复
源码读取 Emergency 模块中的错误状态:
1 | ErrBusOff = CO_isError(NMT->em, CO_EM_CAN_TX_BUS_OFF); |
然后结合 NMTcontrol 判断:
1 | busOff_HB = CO_NMT_ERR_ON_BUSOFF_HB && (busOff 或 heartbeat consumer 错误) |
最终策略:
| 当前状态 | 条件 | 结果 |
|---|---|---|
| Operational | busOff_HB 或 errRegMasked |
降级到 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_TIMERNEXT,CO_NMT_process() 会根据 heartbeat timer 更新 timerNext_us,供外部调度器决定下一次最晚什么时候再调用。
6.7 更新状态并返回 reset 命令
最后:
1 | NMT->operatingState = NMTstateCpy; |
应用层应该保存或检查 NMTstate,并处理 resetCommand。
7. CO_NMT_sendCommand():simple NMT master 不是完整主站
CO_NMT_sendCommand() 只有在 CO_CONFIG_NMT_MASTER 打开时才编译。它做两件事。
7.1 如果目标包含自己,先写本地 internalCommand
1 | if ((nodeID == 0U) || (nodeID == NMT->nodeId)) { |
这样本节点广播启动所有节点时,自己也会在下一次 CO_NMT_process() 中执行同一命令。
7.2 发送 NMT command 到总线
1 | NMT->NMT_TXbuff->data[0] = (uint8_t)command; |
这只负责发 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 | 1. 初始化 CAN/FDCAN 驱动和 CANopen 对象 |
伪代码:
1 | for (;;) { |
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 nodeId 或 01 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 的章节定位 |














