4. 状态切换之初始化到预操作:从站上电后的第一跳

各位好,我是你们的老朋友。今天咱们来啃第四块骨头——从站上电后,怎么从 Initialisation 自动蹦到 Pre-Operational。这一步看似简单,但很多新手在这里翻车。我当年调试第一个 CANopen 从站时,就卡在这个环节整整两天。

说白了,这就是从站的生命起点。你想想看,一个设备刚上电,它得先自检、加载配置、告诉主站“我活了”,然后才能进入工作状态。这个流程要是没走对,后面所有通信都是白搭。

4.1 初始化阶段的三个子状态

CANopen 规范里,Initialisation 不是单一状态,它内部还分了三个子阶段。我习惯把它们叫做“上电三部曲”:

子状态 主要动作 典型耗时
Reset Application 复位应用层参数,清空对象字典 1-5 ms
Reset Communication 复位通信模块,初始化 CAN 控制器 2-10 ms
Initialisation 加载默认配置,准备进入 Pre-Operational 5-20 ms

这三个子状态是自动串行执行的。从站硬件复位后,固件会依次走过这三个阶段。每个阶段都有对应的 NMT 状态码,你可以通过读取对象字典 0x1001 来确认当前走到了哪一步。

关键点:Reset Communication 阶段会清空所有 SDO 和 PDO 的配置参数。这意味着,如果你在应用层保存了自定义的通信参数,必须在 Initialisation 阶段重新加载到对象字典里。

4.2 自动切换的触发条件

从站不会自己凭空跳转。它需要满足三个条件,才会从 Initialisation 切换到 Pre-Operational:

  1. 硬件初始化完成——CAN 控制器配置成功,波特率设置正确,没有总线错误。
  2. 对象字典加载完毕——所有默认值写入对应索引,特别是 0x1000(设备类型)、0x1005(COB-ID SYNC)、0x1017(心跳生产者时间)等关键对象。
  3. 内部自检通过——没有致命错误,比如 EEPROM 校验失败、晶振频率异常等。

我记得有一次,客户反馈从站上电后一直卡在 Initialisation,死活进不了 Pre-Operational。我远程抓了 CAN 报文,发现从站根本没发 Boot-up 消息。后来一查,是晶振起振时间太长,导致 CAN 控制器初始化超时。嗯,这种硬件问题最坑人。

4.3 SDO 和 PDO 的初始化时机

这是本章的核心,也是很多工程师容易搞混的地方。我直接说结论:

  • SDO 服务器:在 Reset Communication 阶段末尾就已经初始化好了。也就是说,从站进入 Pre-Operational 的瞬间,SDO 通道就已经可以用了。
  • PDO:在 Initialisation 阶段只做配置加载,但不激活。PDO 的真正激活是在 Pre-Operational 状态里,由主站通过 SDO 配置完成后才生效。

为什么会这样?你想想看,PDO 是高速数据通道,如果从站刚上电就乱发 PDO,总线肯定乱套。所以 CANopen 规范规定:Pre-Operational 状态下只允许 SDO 通信,不允许 PDO 通信。PDO 要等到主站把从站切换到 Operational 状态后才能使用。

实战技巧:我建议在 Initialisation 阶段的末尾,把 PDO 的通信参数(0x1400-0x1BFF)和映射参数(0x1600-0x1BFF)都预加载好。这样主站一发 SDO 配置,PDO 就能立刻生效,不用再花时间加载参数。

4.4 Boot-up 消息的发送时机

从站完成 Initialisation 后,会立即发送一条 Boot-up 消息。这条消息的 CAN-ID 是 0x700 + 节点 ID,数据域只有一个字节:0x00。

这条消息的意义在于:告诉主站“我准备好了,可以接收 SDO 请求了”。主站收到 Boot-up 消息后,才会开始对从站进行配置。

我曾经遇到一个坑:某款 STM32 从站,Boot-up 消息总是发不出去。抓了逻辑分析仪才发现,是 CAN 发送邮箱满了,Boot-up 消息被堵在队列里。后来我在发送 Boot-up 前加了一个清空邮箱的操作,问题就解决了。

// Boot-up 消息发送示例(基于 STM32 HAL 库)
void CANopen_SendBootUp(uint8_t nodeID)
{
    CAN_TxHeaderTypeDef txHeader;
    uint8_t data[8] = {0};

    txHeader.StdId = 0x700 + nodeID;
    txHeader.ExtId = 0;
    txHeader.IDE = CAN_ID_STD;
    txHeader.RTR = CAN_RTR_DATA;
    txHeader.DLC = 1;
    txHeader.TransmitGlobalTime = DISABLE;

    // 清空发送邮箱,防止阻塞
    while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0)
    {
        // 等待,超时处理
    }

    HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &TxMailbox);
}

4.5 状态切换的完整时序

为了让大家看得更清楚,我画了一张流程图。这张图我当年做项目时贴在工位上,每次调试都看一眼。

从站上电状态切换流程图 上电 / 硬件复位 Reset Application Reset Communication Initialisation 自检通过? 是 发送 Boot-up 消息 Pre-Operational 否 错误处理 / 重试 SDO 已就绪,PDO 未激活

4.6 常见异常与排查方法

在实际项目中,从 Initialisation 到 Pre-Operational 的切换失败,通常有几种表现。我整理了一个排查表,大家调试时可以对照着看:

现象 可能原因 排查方法
没有 Boot-up 消息 CAN 控制器初始化失败 检查波特率配置、CAN 收发器供电
Boot-up 消息重复发送 状态机逻辑错误 检查 NMT 状态切换代码,确保只发一次
Boot-up 消息 ID 错误 节点 ID 配置异常 读取 0x1001 确认节点 ID
进入 Pre-Operational 后无法 SDO 通信 SDO 服务器未正确初始化 检查 0x1200 系列对象字典配置

避坑指南:我曾经遇到一个案例,从站 Boot-up 消息正常发送,但主站就是收不到。折腾了半天才发现,是 CAN 总线终端电阻没焊。嗯,这种低级错误最容易被忽略。所以调试前,先拿万用表量一下 CAN_H 和 CAN_L 之间的电阻,正常应该是 60 欧姆左右。

4.7 代码层面的实现要点

最后,我分享一下在 STM32 上实现这个状态切换的代码骨架。核心逻辑在 NMT 状态机处理函数里:

// NMT 状态机主循环(简化版)
void NMT_StateMachine(void)
{
    switch(current_state)
    {
        case NMT_INITIALISATION:
            // 1. 执行硬件初始化
            if(HAL_CAN_Init(&hcan) != HAL_OK)
            {
                Error_Handler();
                break;
            }

            // 2. 加载对象字典默认值
            OD_LoadDefaults();

            // 3. 自检通过后,发送 Boot-up
            CANopen_SendBootUp(nodeID);

            // 4. 切换到 Pre-Operational
            current_state = NMT_PRE_OPERATIONAL;
            break;

        case NMT_PRE_OPERATIONAL:
            // 只处理 SDO 请求,不处理 PDO
            ProcessSDO();
            break;

        // 其他状态...
    }
}

这段代码看起来简单,但有几个细节要注意:

  • 自检失败时,不能发送 Boot-up,也不能切换状态。应该进入错误处理循环,或者等待看门狗复位。
  • Boot-up 消息必须在切换状态之前发送。如果先切换状态再发消息,主站可能会认为从站已经准备好了,但实际上 Boot-up 还没到。
  • 对象字典的加载顺序有讲究。我习惯先加载通信相关对象(0x1000-0x1FFF),再加载应用层对象。因为通信参数是基础,基础没打好,后面都是空中楼阁。

好了,这一章的内容就到这里。从 Initialisation 到 Pre-Operational 的切换,是整个 NMT 状态机的基础。把这个流程吃透了,后面讲 Operational 和 Stopped 状态切换时,你就会觉得顺理成章。