4. NMT网络管理:节点状态机与心跳守护

NMT(Network Management)是CANopen的神经中枢。说白了,它负责三件事:让节点知道自己在哪个状态、让节点能切换状态、让主站知道节点还活着。我见过不少开发者把精力全放在PDO和SDO上,结果NMT没处理好,整个网络跑着跑着就崩了。

这一章,我们就把NMT的底裤扒干净。

4.1 节点状态机:四个状态,一条命

CANopen节点有四个状态:Initialization、Pre-operational、Operational、Stopped。每个状态能干什么、不能干什么,CiA 301写得清清楚楚。

核心原则:节点上电后必须走完Initialization,然后自动进入Pre-operational。只有收到主站的NMT启动命令,才能进入Operational。

我画了一张状态机图,你一看就明白。

CANopen NMT 节点状态机 Initialization 上电 → 初始化对象字典 自动 Pre-operational SDO通信,无PDO 启动 Operational 全功能运行 停止 Stopped 仅NMT通信 停止 启动 复位 复位节点 重新初始化 复位通信 保留对象字典 复位通信 说明: • 所有状态切换由主站通过NMT命令控制 • 节点上电后自动从Initialization进入Pre-operational • 复位节点会重新初始化对象字典,复位通信只重置通信参数

4.2 各状态详解

4.2.1 Initialization(初始化)

节点上电后,第一件事就是跑初始化。这个阶段会做:

  • 硬件自检(寄存器、RAM、外设)
  • 加载对象字典默认值
  • 配置通信参数(波特率、节点ID)
  • 发送Boot-up报文(0x700 + Node-ID)

我的习惯:在Boot-up报文中带一个字节的附加状态码,方便主站判断节点是否初始化成功。虽然CiA 301没强制要求,但我在项目里吃过亏——节点初始化到一半卡住了,主站还以为它正常。

4.2.2 Pre-operational(预操作)

这是开发阶段最常用的状态。节点能响应SDO请求,但不会发送PDO。我建议所有从站上电后默认停在这个状态,等主站确认配置完毕再切到Operational。

Pre-operational能干的事:

  • SDO读写对象字典
  • 发送心跳报文
  • 接收NMT命令
  • 发送紧急报文(如果配置了)

不能干的事:

  • 发送/接收PDO
  • 同步报文处理(SYNC)

4.2.3 Operational(运行)

全功能状态。节点该干活了。PDO开始飞,同步开始跑,所有通信对象都激活。

注意:从Pre-operational切到Operational时,PDO映射必须已经配置好。我曾经遇到一个坑:主站发启动命令太快,从站的PDO映射还没写完,结果Operational状态下PDO发出去的全是垃圾数据。

4.2.4 Stopped(停止)

这个状态比较特殊。节点只响应NMT命令,其他通信全部关闭。说白了就是"装死"状态。主站发停止命令后,节点必须立即停止所有通信,包括心跳。

嗯,这里要注意:Stopped状态下,节点不能发心跳。所以主站如果依赖心跳来监控节点,会发现节点"掉线"了。这不是故障,是正常行为。

4.3 状态切换命令

NMT命令通过CAN报文发送,COB-ID固定为0x000。报文格式很简单:

字节 内容 说明
Byte 0 命令码 见下表
Byte 1 节点ID 0x00 = 所有节点

常用的NMT命令码:

命令码 功能 目标状态
0x01 启动节点 Operational
0x02 停止节点 Stopped
0x80 进入预操作 Pre-operational
0x81 复位节点 Initialization
0x82 复位通信 Initialization

举个例子,主站想启动节点ID为5的从站:

CAN报文:0x000  [0x01, 0x05]
COB-ID: 0x000
数据:   01 05
长度:   2字节

如果想启动所有节点:

CAN报文:0x000  [0x01, 0x00]
数据:   01 00

关键点:节点ID为0x00是广播命令,所有节点都会响应。但广播命令不会收到确认——每个节点自己默默切换状态,不发回复。

4.4 心跳机制

心跳是CANopen的"保命符"。主站通过心跳知道从站是否还活着。从站定期发送心跳报文,主站如果超时没收到,就知道出事了。

4.4.1 心跳报文格式

心跳的COB-ID是0x700 + Node-ID。数据只有一个字节:节点当前状态。

状态值 含义
0x00 Boot-up(初始化完成)
0x04 Stopped
0x05 Operational
0x7F Pre-operational

比如节点ID为5的从站处于Operational状态,心跳报文就是:

COB-ID: 0x705
数据:   0x05
长度:   1字节

4.4.2 心跳生产者(从站侧)

从站需要配置两个对象字典条目:

  • 0x1017:心跳生产时间(单位ms,0表示禁止心跳)
  • 0x1016:心跳消费时间(主站用,从站一般不用配)

我一般把心跳周期设为100ms到1000ms之间。太快了浪费总线带宽,太慢了主站反应迟钝。

/* 心跳生产者代码示例 */
void heartbeat_task(void)
{
    uint16_t producer_time = objdict_read_16(0x1017);
    if (producer_time == 0) return;  /* 禁止心跳 */
    
    uint8_t state = get_nmt_state();
    can_send(0x700 + node_id, &state, 1);
    
    /* 启动定时器,下次发送 */
    start_timer(producer_time, heartbeat_task);
}

4.4.3 心跳消费者(主站侧)

主站通过对象字典0x1016配置每个从站的心跳超时时间。格式是:

字节 内容
Byte 0-1 心跳超时时间(ms)
Byte 2 节点ID

主站收到心跳后,重置对应节点的定时器。如果超时没收到,就触发心跳事件——通常的做法是尝试复位节点或者报警。

避坑指南:我曾经把心跳超时设成和生产时间一样,结果网络稍微有点延迟就误报。建议超时时间设为生产时间的2-3倍。比如生产周期100ms,超时设250ms比较稳妥。

4.5 节点守护(Node Guarding)

节点守护是心跳的前身,CiA 301里还保留着,但新项目我建议直接用心跳。节点守护的机制是主站主动轮询从站,从站回复状态。

节点守护的报文:

  • 主站发送:COB-ID = 0x700 + Node-ID,数据为空(RTR帧)
  • 从站回复:COB-ID = 0x700 + Node-ID,数据 = 状态字节

状态字节的格式和心跳一样。区别在于:心跳是从站主动发,节点守护是主站主动问。

注意:节点守护和心跳不能同时启用。如果对象字典0x1017(心跳生产时间)不为0,节点守护自动失效。我建议统一用心跳,省事。

4.6 实战建议

最后,分享几个我在项目中总结的经验:

  1. Boot-up报文一定要发。主站靠它知道节点上线了。我见过有人把Boot-up漏了,结果主站一直等,节点一直不发,两边干瞪眼。
  2. 状态切换要加保护。比如从Pre-operational切Operational前,检查PDO映射是否完整。别让节点在Operational状态下发乱码。
  3. 心跳周期要可配置。通过对象字典0x1017让用户自己设。调试时设短点(100ms),正式运行时设长点(500ms-1000ms)。
  4. Stopped状态要处理干净。节点进入Stopped后,所有定时器、发送队列、接收缓冲区都要清掉。别留尾巴。

NMT这块看起来简单,但坑不少。你想想看,整个CANopen网络就靠NMT来协调,如果状态机跑偏了,后面PDO和SDO再漂亮也没用。把状态机吃透,你的从站就稳了一半。