|
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 交给应用层执行。 源码运行链路可以压缩成: 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,仅通信部分复位 |
示例: # 启动所有节点进入 OperationalCAN-ID = 0x000, DLC = 2, Data = 01 00# 让节点 5 进入 Pre-operationalCAN-ID = 0x000, DLC = 2, Data = 80 05# 让节点 5 执行通信复位CAN-ID = 0x000, DLC = 2, Data = 82 051.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 默认配置 头文件中默认配置是: #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 发送的就是这个值 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 报文时看到: 0x705 [1] 7F # 节点 5 处于 Pre-operational0x705 [1] 05 # 节点 5 处于 Operational0x705 [1] 04 # 节点 5 处于 Stopped0x705 [1] 00 # boot-up message / Initializing3.3 CO_NMT_command_t:NMT 命令 byte0 的来源 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:复位命令为什么是返回值 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)),然后设置: 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() 会读取它,并保存到微秒单位: OD_get_u16(OD_1017_ProducerHbTime, 0, &HBprodTime_ms, true);NMT->HBproducerTime_us = (uint32_t)HBprodTime_ms * 1000U;随后它给 0x1017 安装 OD extension: 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 写 0x1017,OD_write_1017() 会同步更新 HBproducerTime_us,并把 HBproducerTimer 清零,使新的 heartbeat 周期立即生效。 4.3 配置 NMT 命令接收 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 打开时,才会编译和初始化: NMT->NMT_TXbuff = CO_CANtxBufferInit(NMT_CANdevTx, NMT_txIdx, CANidTxNMT, false, 2, false);这对应发送 CAN-ID 0x000、DLC 2 的 NMT command。 4.5 配置 Heartbeat 发送 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() 的逻辑很短,但很关键: 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 定时器 NMT->HBproducerTimer = (NMT->HBproducerTimer > timeDifference_us) ? (NMT->HBproducerTimer - timeDifference_us) : 0U;timeDifference_us 是应用每次调用时传入的时间差。NMT 模块不自己读硬件定时器,而是依赖外部调度传入 elapsed time。 6.2 发送 boot-up 或 heartbeat 发送条件是: 当前是 Initializing或0x1017 不为 0,并且 heartbeat timer 到期或 NMT 状态变化发送内容是: 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 决定初始运行状态: 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。 处理逻辑: 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_NODE 和 CO_NMT_RESET_COMMUNICATION 不会在这里调用 NVIC_SystemReset()。源码只设置返回值,应用层必须处理这个返回值。 6.4 根据错误条件自动降级或恢复 源码读取 Emergency 模块中的错误状态: 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 判断: 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_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,状态变化后会调用: NMT->pFunctNMT(NMTstateCpy);这适合应用层统一处理状态进入/退出,比如进入 Stopped 时关闭输出、进入 Operational 时允许业务周期运行。 6.6 可选 timerNext_us 如果打开 CO_CONFIG_FLAG_TIMERNEXT,CO_NMT_process() 会根据 heartbeat timer 更新 timerNext_us,供外部调度器决定下一次最晚什么时候再调用。 6.7 更新状态并返回 reset 命令 最后: 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 if ((nodeID == 0U) || (nodeID == NMT->nodeId)) { NMT->internalCommand = command;}这样本节点广播启动所有节点时,自己也会在下一次 CO_NMT_process() 中执行同一命令。 7.2 发送 NMT command 到总线 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. 初始化 CAN/FDCAN 驱动和 CANopen 对象2. 在 communication reset 段调用 CO_NMT_init()3. 主循环或 RTOS task 周期调用 CO_NMT_process()4. 根据返回的 resetCommand 决定是否通信复位或整机复位5. 根据 NMTstate 决定应用是否允许输出、是否允许 PDO 业务伪代码: 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。 建议调试阶段先设置一个容易观察的值,例如: 0x1017 = 1000 ms这样节点每秒发一次 heartbeat。CAN 分析仪上应该能看到: 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 的章节定位 |
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |