1. CANopen与NMT协议基础

各位同学,咱们今天正式开始CANopenNode从站开发的实战之旅。第一节课,我打算先把CANopen协议栈和NMT状态机这两个最基础的概念讲透。你想想看,搞嵌入式通信,如果连设备的状态机都搞不清楚,后面调试起来那真是寸步难行。我个人习惯,每接触一个新协议栈,第一件事就是先把状态转换图画出来贴在工位上。

1.1 CANopen协议栈概述

CANopen,说白了就是基于CAN总线的一套高层应用协议。它定义了一套标准化的通信机制和设备描述方法。为什么工业现场这么多设备都用它?因为它的实时性好,而且主站从站分工明确,调试起来相对顺手。

一个典型的CANopen协议栈,从底往上大致分这么几层:

  • 物理层:就是CAN收发器,比如TJA1050,负责电平转换。
  • 数据链路层:CAN控制器,比如STM32内部的bxCAN或FDCAN,处理帧的收发和仲裁。
  • 应用层:CANopen协议栈核心,包括对象字典、NMT、PDO、SDO等。

我在项目中遇到过不少新手,上来就拿着CANopenNode的源码硬啃,结果被对象字典的EDS文件搞得一头雾水。其实不用急,咱们先抓住主线——NMT状态机。只要状态机搞明白了,设备的行为你就掌握了七八成。

核心要点:CANopenNode是一个开源的CANopen协议栈实现,支持主站和从站模式。它的代码结构清晰,非常适合在STM32上做二次开发。咱们这门课,全程基于CANopenNode v4.x版本。

1.2 NMT状态机定义

NMT,全称Network Management,网络管理。它负责控制CANopen设备的状态。每个CANopen从站,都必须实现一个标准的状态机。这个状态机有四个核心状态:初始化、预操作、操作和停止。

嗯,这里要注意,有些资料会把“初始化”拆成“初始化”和“应用层复位”两个子状态,但咱们先按最标准的四状态模型来讲。

NMT状态机转换图 初始化 (Initialisation) 预操作 (Pre-Operational) 操作 (Operational) 停止 (Stopped) 自动转换 启动远程节点 停止远程节点 进入预操作 停止远程节点 复位节点 复位通信 复位通信 复位通信 说明:实线箭头表示NMT主站通过CAN报文触发的状态转换,虚线箭头(初始化→预操作)为设备上电后自动完成。 每个状态转换都对应一个特定的NMT命令标识符(CS)。

1.3 各状态详解

1.3.1 初始化状态

设备上电或硬件复位后,首先进入初始化状态。在这个状态里,设备做三件事:

  • 初始化CAN控制器和收发器。
  • 加载对象字典的默认值。
  • 发送Boot-up报文(0x700 + Node-ID),告诉主站“我活了”。

初始化完成后,设备自动进入预操作状态。不需要主站发任何命令。我记得第一次调CANopenNode时,忘了配置Boot-up报文的发送,结果主站一直报“节点未上线”,查了半天才发现是初始化回调里少写了一行代码。

调试技巧:用CAN分析仪抓一下总线,如果上电后看到0x700 + Node-ID的报文,说明初始化成功了。没看到?先检查硬件复位和时钟配置。

1.3.2 预操作状态

预操作状态,是设备最常用的调试状态。在这个状态下:

  • SDO通信正常:主站可以通过SDO读写对象字典,配置参数。
  • PDO通信禁止:不会自动发送或接收过程数据。
  • NMT命令响应:可以接收主站的NMT命令。

说白了,预操作就是“配置模式”。你想想看,如果一上电就开始跑PDO,参数都还没配好,那不乱套了吗?所以工业现场的标准流程是:上电→预操作→配置参数→切换到操作状态。

1.3.3 操作状态

操作状态,是设备的正常工作模式。在这个状态下:

  • PDO通信使能:按照配置好的映射,周期或事件触发地收发过程数据。
  • SDO通信仍然可用:但一般不建议在运行时频繁读写。
  • NMT命令响应:可以接收主站的NMT命令。

我曾经在一个伺服驱动项目里,把PDO的发送周期设成了1ms,结果总线负载直接飙到80%以上。后来在预操作状态下把参数调好,再切到操作状态,问题就解决了。所以预操作状态真的是调试利器。

1.3.4 停止状态

停止状态,设备进入“静默”模式:

  • 禁止SDO通信:不能读写对象字典。
  • 禁止PDO通信:不发送任何过程数据。
  • 只响应NMT命令:只能接收复位节点或复位通信命令。

这个状态一般用于紧急停止或故障后的安全隔离。嗯,这里要注意,停止状态下的设备虽然不通信,但CAN控制器还是在工作的,只是应用层不处理报文。

1.4 状态转换触发条件

状态转换由NMT主站通过发送特定的CAN报文来触发。报文格式如下:

COB-ID 字节0 (CS) 字节1 说明
0x000 0x01 Node-ID 启动远程节点 → 操作状态
0x000 0x02 Node-ID 停止远程节点 → 停止状态
0x000 0x80 Node-ID 进入预操作 → 预操作状态
0x000 0x81 Node-ID 复位节点 → 初始化状态
0x000 0x82 Node-ID 复位通信 → 初始化状态

CS是Command Specifier,命令标识符。Node-ID为0时,表示广播给总线上所有节点。

避坑指南:我曾经在调试时,主站发了复位通信命令(CS=0x82),结果从站没有重新发送Boot-up报文。后来发现是CANopenNode的NMT回调函数里,复位通信后没有重新初始化对象字典的默认值。这个坑,大家移植时一定要检查。

1.5 CANopenNode中的NMT实现

在CANopenNode源码里,NMT状态机主要在CO_NMT.c和CO_NMT.h中实现。核心数据结构是CO_NMT_t,里面有一个state成员变量,记录当前状态。

// CANopenNode NMT状态枚举
typedef enum {
    CO_NMT_INITIALISATION  = 0x00,
    CO_NMT_PRE_OPERATIONAL = 0x7F,
    CO_NMT_OPERATIONAL     = 0x05,
    CO_NMT_STOPPED         = 0x04
} CO_NMT_state_t;

状态转换的逻辑在CO_NMT_process()函数里。每次收到NMT报文,都会调用这个函数,根据当前状态和CS值,决定是否转换以及执行什么动作。

我个人习惯,在调试阶段会在CO_NMT_process()里加一个串口打印,把每次状态转换都打出来。这样主站发了什么命令,从站有没有正确响应,一目了然。

实战要点:在STM32上移植CANopenNode时,NMT状态机的核心代码基本不用改。你只需要确保:1) CAN中断能正确接收报文;2) 定时器能提供1ms的时基;3) 对象字典的默认值符合你的应用需求。

好了,这一节的内容就到这里。NMT状态机是CANopen的基石,后面的PDO、SDO、心跳报文,全都依赖这个状态机来协调。把这四个状态和它们的转换条件记熟,后面调试起来会顺手很多。