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。
我画了一张状态机图,你一看就明白。
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 实战建议
最后,分享几个我在项目中总结的经验:
- Boot-up报文一定要发。主站靠它知道节点上线了。我见过有人把Boot-up漏了,结果主站一直等,节点一直不发,两边干瞪眼。
- 状态切换要加保护。比如从Pre-operational切Operational前,检查PDO映射是否完整。别让节点在Operational状态下发乱码。
- 心跳周期要可配置。通过对象字典0x1017让用户自己设。调试时设短点(100ms),正式运行时设长点(500ms-1000ms)。
- Stopped状态要处理干净。节点进入Stopped后,所有定时器、发送队列、接收缓冲区都要清掉。别留尾巴。
NMT这块看起来简单,但坑不少。你想想看,整个CANopen网络就靠NMT来协调,如果状态机跑偏了,后面PDO和SDO再漂亮也没用。把状态机吃透,你的从站就稳了一半。