CANopen NMT搞懂 NMT 运行流程文章目录CANopen NMT搞懂 NMT 运行流程先给结论1. 先看协议NMT 解决什么问题1.1 NMT 的定位1.2 NMT 命令帧格式1.3 Heartbeat 与 boot-up 报文格式1.4 四个状态要先分清2. 再看源码文件分工和阅读顺序3. CO_NMT_Heartbeat.h先把协议值和对象结构看懂3.1 CO_CONFIG_NMT 默认配置3.2 CO_NMT_internalState_tHeartbeat 发送的就是这个值3.3 CO_NMT_command_tNMT 命令 byte0 的来源3.4 CO_NMT_reset_cmd_t复位命令为什么是返回值3.5 NMTcontrol不是协议帧而是本地策略开关4. CO_NMT_init()初始化时把协议入口全部接好4.1 清空对象并写入初始状态4.2 读取并扩展 OD 0x1017 Producer heartbeat time4.3 配置 NMT 命令接收4.4 可选配置 simple NMT master 发送4.5 配置 Heartbeat 发送5. CO_NMT_receive()接收回调只做“预处理”6. CO_NMT_process()NMT 运行流程核心6.1 更新 Heartbeat 定时器6.2 发送 boot-up 或 heartbeat6.3 处理收到的内部命令6.4 根据错误条件自动降级或恢复6.5 可选状态变化回调6.6 可选 timerNext_us6.7 更新状态并返回 reset 命令7. CO_NMT_sendCommand()simple NMT master 不是完整主站7.1 如果目标包含自己先写本地 internalCommand7.2 发送 NMT command 到总线8. STM32 从机里应该怎么用 NMT8.1 最小运行链路8.2 0x1017 Producer heartbeat time 怎么配8.3 主站不发 Start 时为什么 PDO 不工作8.4 收到 reset command 后该做什么9. 一句话总结10. 资料依据先给结论NMT 是 CANopen 的网络管理状态机控制协议Heartbeat 是配套的节点在线与状态上报机制。在 CANopenNode 里CO_NMT_Heartbeat.c/h不是单纯“收一帧命令就切状态”的代码而是把以下功能合在一个对象CO_NMT_t里NMT slave / NMT consumer接收 CAN-ID0x000的 NMT 命令目标 Node-ID 为0或本节点时才处理。Heartbeat producer用 CAN-ID0x700 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交给应用层执行。源码运行链路可以压缩成CAN-ID 0x000 NMT command - CO_NMT_receive() 校验 DLC 和 Node-ID - NMT-internalCommand command - CO_NMT_process() 周期执行 - 切换 NMT 状态或返回 resetCommand - 发送 boot-up / heartbeat0x700 nodeId, data[0] NMT state1. 先看协议NMT 解决什么问题1.1 NMT 的定位CANopen 节点不是上电后立刻就能随便发 PDO。协议把设备运行分成几个 NMT 状态主站或管理端通过 NMT 命令控制节点进入启动、停止、预运行、运行或复位流程。在工程上NMT 主要解决三件事问题NMT/Heartbeat 的做法什么时候允许 PDO 工作只有 Operational 状态下 PDO 才工作主站如何启动、停止或复位从站发送 CAN-ID0x000的 NMT command主站如何知道从站当前在线状态从站周期发送 Heartbeat数据字节携带 NMT stateCANopenNode 官方仓库说明它支持 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-ID0x000DLC2Byte 0NMT command specifierByte 1目标 Node-ID0表示广播所有节点常用命令值在CO_NMT_command_t中直接定义CANopenNode 枚举数值协议含义CO_NMT_ENTER_OPERATIONAL0x01Start remote node进入 OperationalCO_NMT_ENTER_STOPPED0x02Stop remote node进入 StoppedCO_NMT_ENTER_PRE_OPERATIONAL0x80Enter pre-operationalCO_NMT_RESET_NODE0x81Reset node完整节点复位CO_NMT_RESET_COMMUNICATION0x82Reset communication仅通信部分复位示例# 启动所有节点进入 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 051.3 Heartbeat 与 boot-up 报文格式Heartbeat 是错误控制协议的一部分用于确认网络参与者仍然在线并且仍处于预期的 NMT FSA 状态。字段值/含义CAN-ID0x700 Node-IDDLC1Byte 0当前 NMT state状态值在CO_NMT_internalState_t中定义状态数值运行含义CO_NMT_INITIALIZING0初始化中也用于 boot-up message 的数据字节CO_NMT_PRE_OPERATIONAL127 / 0x7F预运行SDO 等对象可用PDO 不工作CO_NMT_OPERATIONAL5 / 0x05运行PDO 开始工作CO_NMT_STOPPED4 / 0x04停止只保留少量通信能力CO_NMT_UNKNOWN-1Heartbeat consumer 侧用于表示未知状态注意boot-up message 和 heartbeat 使用同一个 CAN-ID 规则。boot-up 是节点初始化完成时发出的状态值0后续 heartbeat 则周期性发送当前状态。1.4 四个状态要先分清CANopenNode 在头文件注释里给出了状态边界状态CANopenNode 注释中的语义对 STM32 从机的直接影响InitializingCANopen 初始化完成前的活动状态还没进入正常 CANopen 通信流程Pre-operational除 PDO 外所有 CANopen 对象可用常用于 SDO 配置、参数下载、映射检查OperationalPDO 也可用正式跑过程数据Stopped只保留 Heartbeat producer 和 NMT consumer应用输出应进入安全/停止策略2. 再看源码文件分工和阅读顺序建议阅读顺序如下顺序位置先看什么目的1CO_NMT_Heartbeat.h顶部 Doxygen模块说明、NMT/Heartbeat 报文内容建立协议边界2CO_NMT_internalState_tNMT 状态值对照 heartbeat byte03CO_NMT_command_tNMT 命令值对照 NMT command byte04CO_NMT_reset_cmd_t返回给应用的复位动作理解 reset 不在模块内部直接执行5CO_NMT_control_t宏NMTcontrol的位域理解自启动、错误降级、错误恢复6CO_NMT_t状态、定时器、CAN buffer、回调理解运行时对象保存了什么7CO_NMT_init()初始化对象、OD 0x1017、CAN RX/TX理解初始化阶段做了什么8CO_NMT_receive()接收 NMT 命令理解接收回调只做预处理9CO_NMT_process()状态机主逻辑这是运行流程核心10CO_NMT_sendCommand()可选 simple NMT master 发送能力理解CO_CONFIG_NMT_MASTER不是完整主站3.CO_NMT_Heartbeat.h先把协议值和对象结构看懂3.1CO_CONFIG_NMT默认配置头文件中默认配置是#ifndefCO_CONFIG_NMT#defineCO_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_TIMERNEXTCO_NMT_process()可给出下一次建议调用时间timerNext_us如果想让本节点也能主动发 NMT command需要额外打开CO_CONFIG_NMT_MASTER。这只表示“simple NMT master 发送能力”不是完整主站配置器。3.2CO_NMT_internalState_tHeartbeat 发送的就是这个值typedefenum{CO_NMT_UNKNOWN-1,CO_NMT_INITIALIZING0,CO_NMT_PRE_OPERATIONAL127,CO_NMT_OPERATIONAL5,CO_NMT_STOPPED4}CO_NMT_internalState_t;CO_NMT_process()发送 heartbeat 时会把当前状态写到HB_TXbuff-data[0]。所以调试 CAN 报文时看到0x705 [1] 7F # 节点 5 处于 Pre-operational 0x705 [1] 05 # 节点 5 处于 Operational 0x705 [1] 04 # 节点 5 处于 Stopped 0x705 [1] 00 # boot-up message / Initializing3.3CO_NMT_command_tNMT 命令 byte0 的来源typedefenum{CO_NMT_NO_COMMAND0,CO_NMT_ENTER_OPERATIONAL1,CO_NMT_ENTER_STOPPED2,CO_NMT_ENTER_PRE_OPERATIONAL128,CO_NMT_RESET_NODE129,CO_NMT_RESET_COMMUNICATION130}CO_NMT_command_t;CO_NMT_receive()从 NMT command 报文的data[0]读出这个值如果本节点启用了 simple masterCO_NMT_sendCommand()也会把这个值写回NMT_TXbuff-data[0]。3.4CO_NMT_reset_cmd_t复位命令为什么是返回值typedefenum{CO_RESET_NOT0,CO_RESET_COMM1,CO_RESET_APP2,CO_RESET_QUIT3}CO_NMT_reset_cmd_t;这体现了 CANopenNode 的边界协议栈识别“需要复位”但不直接决定 MCU 怎么复位。CO_RESET_COMM应用进入通信复位段重建 CANopen 通信对象。CO_RESET_APP应用执行完整设备复位常见做法是 MCU reset 或回到主初始化入口。CO_RESET_NOT正常运行。3.5NMTcontrol不是协议帧而是本地策略开关NMTcontrol是传给CO_NMT_init()的本地控制位域用于决定自启动和错误行为宏作用CO_NMT_ERR_REG_MASK低 8 位作为 Error register maskCO_NMT_STARTUP_TO_OPERATIONAL初始化后直接进入 Operational否则进入 Pre-operationalCO_NMT_ERR_ON_BUSOFF_HBOperational 下遇到 CAN bus off 或 heartbeat consumer 错误时降级CO_NMT_ERR_ON_ERR_REGOperational 下 Error register 命中 mask 时降级CO_NMT_ERR_TO_STOPPED错误触发时降到 Stopped否则降到 Pre-operationalCO_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-operatingStateCO_NMT_INITIALIZING;NMT-operatingStatePrevCO_NMT_INITIALIZING;NMT-nodeIdnodeId;NMT-NMTcontrolNMTcontrol;NMT-emem;NMT-HBproducerTimer(uint32_t)firstHBTime_ms*1000U;这里的关键是初始状态固定为CO_NMT_INITIALIZING后面第一次CO_NMT_process()会发送 boot-up message然后再进入 Pre-operational 或 Operational。4.2 读取并扩展 OD0x1017 Producer heartbeat time0x1017是 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 extensionNMT-OD_1017_extension.objectNMT;NMT-OD_1017_extension.readOD_readOriginal;NMT-OD_1017_extension.writeOD_write_1017;OD_extension_init(OD_1017_ProducerHbTime,NMT-OD_1017_extension);这样运行时如果通过 SDO 写0x1017OD_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_TXbuffCO_CANtxBufferInit(NMT_CANdevTx,NMT_txIdx,CANidTxNMT,false,2,false);这对应发送 CAN-ID0x000、DLC 2 的 NMT command。4.5 配置 Heartbeat 发送NMT-HB_TXbuffCO_CANtxBufferInit(HB_CANdevTx,HB_txIdx,CANidTxHB,false,1,false);典型CANidTxHB是0x700 nodeIdDLC 为 1。5.CO_NMT_receive()接收回调只做“预处理”CO_NMT_receive()的逻辑很短但很关键uint8_tDLCCO_CANrxMsg_readDLC(msg);constuint8_t*dataCO_CANrxMsg_readData(msg);CO_NMT_command_t command(CO_NMT_command_t)data[0];uint8_tnodeIddata[1];if((DLC2U)((nodeId0U)||(nodeIdNMT-nodeId))){NMT-internalCommandcommand;...optional callback...}这里有三个结论NMT 命令必须 DLC2否则不处理。Node-ID 为 0 是广播或者等于本节点 Node-ID 才处理。接收回调不直接切状态只写NMT-internalCommand真正动作留给CO_NMT_process()。这种设计适合裸机和 RTOSCAN 中断/驱动回调里只做极短预处理复杂状态切换和可能触发的 reset 交给主循环或任务上下文。6.CO_NMT_process()NMT 运行流程核心CO_NMT_process()是整个模块最重要的函数。它的执行顺序可以拆成 7 层。6.1 更新 Heartbeat 定时器NMT-HBproducerTimer(NMT-HBproducerTimertimeDifference_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(NMTcontrolCO_NMT_STARTUP_TO_OPERATIONAL)?CO_NMT_OPERATIONAL:CO_NMT_PRE_OPERATIONAL;所以NMTcontrol设置boot-up 后进入未设置CO_NMT_STARTUP_TO_OPERATIONALPre-operational设置CO_NMT_STARTUP_TO_OPERATIONALOperational6.3 处理收到的内部命令internalCommand可能来自两个入口CO_NMT_receive()总线上收到 NMT command。CO_NMT_sendCommand()本节点作为 simple NMT master 发送命令时如果目标是自己或广播也会写入本地internalCommand。处理逻辑switch(NMT-internalCommand){caseCO_NMT_ENTER_OPERATIONAL:NMTstateCpyCO_NMT_OPERATIONAL;break;caseCO_NMT_ENTER_STOPPED:NMTstateCpyCO_NMT_STOPPED;break;caseCO_NMT_ENTER_PRE_OPERATIONAL:NMTstateCpyCO_NMT_PRE_OPERATIONAL;break;caseCO_NMT_RESET_NODE:resetCommandCO_RESET_APP;break;caseCO_NMT_RESET_COMMUNICATION:resetCommandCO_RESET_COMM;break;}NMT-internalCommandCO_NMT_NO_COMMAND;注意CO_NMT_RESET_NODE和CO_NMT_RESET_COMMUNICATION不会在这里调用NVIC_SystemReset()。源码只设置返回值应用层必须处理这个返回值。6.4 根据错误条件自动降级或恢复源码读取 Emergency 模块中的错误状态ErrBusOffCO_isError(NMT-em,CO_EM_CAN_TX_BUS_OFF);ErrHbConsCO_isError(NMT-em,CO_EM_HEARTBEAT_CONSUMER);ErrHbConsRemoteCO_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)最终策略当前状态条件结果OperationalbusOff_HB或errRegMasked降级到 Pre-operational或在CO_NMT_ERR_TO_STOPPED设置时降到 StoppedPre-operationalCO_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_TIMERNEXTCO_NMT_process()会根据 heartbeat timer 更新timerNext_us供外部调度器决定下一次最晚什么时候再调用。6.7 更新状态并返回 reset 命令最后NMT-operatingStateNMTstateCpy;if(NMTstate!NULL){*NMTstateNMTstateCpy;}returnresetCommand;应用层应该保存或检查NMTstate并处理resetCommand。7.CO_NMT_sendCommand()simple NMT master 不是完整主站CO_NMT_sendCommand()只有在CO_CONFIG_NMT_MASTER打开时才编译。它做两件事。7.1 如果目标包含自己先写本地internalCommandif((nodeID0U)||(nodeIDNMT-nodeId)){NMT-internalCommandcommand;}这样本节点广播启动所有节点时自己也会在下一次CO_NMT_process()中执行同一命令。7.2 发送 NMT command 到总线NMT-NMT_TXbuff-data[0](uint8_t)command;NMT-NMT_TXbuff-data[1]nodeID;returnCO_CANsend(NMT-NMT_CANdevTx,NMT-NMT_TXbuff);这只负责发0x000NMT command不包含自动扫描节点自动配置对象字典自动写 PDO 映射自动监控所有 heartbeat自动恢复故障节点。所以CO_CONFIG_NMT_MASTER的准确理解是让本节点具备发送 NMT command 的能力不是把 STM32 从机“变成完整 CANopen 主站”。8. STM32 从机里应该怎么用 NMT8.1 最小运行链路普通 STM32 CANopen 从机通常只需要默认 NMT slave Heartbeat producer1. 初始化 CAN/FDCAN 驱动和 CANopen 对象 2. 在 communication reset 段调用 CO_NMT_init() 3. 主循环或 RTOS task 周期调用 CO_NMT_process() 4. 根据返回的 resetCommand 决定是否通信复位或整机复位 5. 根据 NMTstate 决定应用是否允许输出、是否允许 PDO 业务伪代码for(;;){uint32_tdt_usget_elapsed_us();uint32_ttimerNext_usUINT32_MAX;CO_NMT_internalState_t nmtState;CO_NMT_reset_cmd_t resetCO_NMT_process(CO-NMT,nmtState,dt_us,timerNext_us);if(resetCO_RESET_COMM){/* exit to communication reset section */break;}if(resetCO_RESET_APP){/* execute application reset policy, for example MCU reset */system_reset();}if(nmtStateCO_NMT_OPERATIONAL){/* allow normal process-data behavior */}else{/* keep outputs safe or skip operational-only behavior */}}8.20x1017 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_OPERATIONALboot-up 后自动进入 Operational简单单节点、实验或固定系统8.4 收到 reset command 后该做什么NMT reset 命令不是“协议栈内部立即复位”。应用必须检查CO_NMT_process()返回值返回值应用侧动作CO_RESET_COMM退出当前通信循环重新初始化 CANopen 通信对象通常不需要重启 MCUCO_RESET_APP执行完整应用复位策略可能包括 MCU resetCO_RESET_NOT正常继续9. 一句话总结CO_NMT_Heartbeat.c/h的核心不是“发心跳”这么简单而是用CO_NMT_t保存本节点 NMT 状态和 heartbeat 定时器用CO_NMT_receive()把总线 NMT 命令转成内部命令再由CO_NMT_process()在主循环/任务上下文中统一完成 boot-up、heartbeat、状态切换、错误降级和复位请求返回。10. 资料依据类型资料本文使用点协议公开说明CiANetwork managementNMT command 由 active NMT manager 发送CAN-ID 0DLC 2byte0 为命令byte1 为 node-IDnode-ID 为 0 表示所有节点协议公开说明CiAError control protocolsHeartbeat 用于确认网络参与者仍在线且处于预期 NMT FSA 状态官方实现资料CANopenNodeNMT and Heartbeat DoxygenCO_NMT_init()、CO_NMT_process()、回调、CO_NMT_sendCommand()的官方语义官方实现资料CANopenNodeCO_NMT_Heartbeat.hDoxygenNMT 状态、NMT command、Heartbeat/NMT 报文内容、NMTcontrol宏官方仓库CANopenNode GitHubCANopenNode 是 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的章节定位