3. 状态转换实战:使用 ec_writestate() 和 ec_readstate() 实现从机状态切换

好,咱们直接进入正题。EtherCAT 从机状态机,说白了就是一条「单向车道」——从 Init 到 Pre-Op,再到 Safe-Op,最后到 Op。你只能往前走,不能跳级。我刚开始接触时,总想着能不能从 Init 直接蹦到 Op,结果从机根本不搭理我。

这一节,我们就用 SOEM 的两个核心函数——ec_writestate()ec_readstate(),亲手把从机从 Init 推到 Op。代码跑通了,你才算真正入门。

3.1 状态机的基础认知

先看这张图,我把它画成了流程图,你一眼就能看明白状态怎么走:

EtherCAT 从机状态机转换流程 Init Pre-Operational Safe-Operational Operational ec_writestate() ec_writestate() ec_writestate() 错误恢复 关键规则: • 状态转换必须按顺序:Init → Pre-Op → Safe-Op → Op,不可跳级 • 每次写入状态后,必须用 ec_readstate() 确认从机是否成功响应

这张图我画得比较直白。每个箭头代表一次 ec_writestate() 调用。注意那个红色的虚线箭头——那是错误恢复路径。从机如果出错了,会自己掉回 Init 或 Pre-Op,你得重新推上去。

3.2 ec_writestate() 到底干了什么?

这个函数,说白了就是往从机的 AL Control 寄存器(0x0120) 里写一个值。你告诉从机:「嘿,请切换到 XX 状态。」

函数原型很简单:

int ec_writestate(uint16_t slave, int state);

参数说明:

  • slave:从机编号,从 1 开始。0 表示所有从机。
  • state:目标状态,用宏定义:EC_STATE_INITEC_STATE_PRE_OPEC_STATE_SAFE_OPEC_STATE_OP

返回值:成功返回 0,失败返回 -1。

我的小习惯:每次调用 ec_writestate() 之后,我都会加一个 10ms 左右的延时。为什么?因为从机需要时间处理状态切换请求。我曾经遇到过一上来就连续读写,结果从机根本没反应过来的情况。

3.3 ec_readstate() 怎么用?

写完了状态,你得确认从机到底切没切过去。这就是 ec_readstate() 的活。

它读取从机的 AL Status 寄存器(0x0130),返回当前实际状态。

int ec_readstate(uint16_t slave);

返回的就是当前状态值。你可以跟目标状态做比较:

int current_state = ec_readstate(slave);
if (current_state == target_state) {
    printf("状态切换成功!\n");
} else {
    printf("状态不对,当前是 0x%02X\n", current_state);
}
注意:ec_readstate() 返回的是从机 AL Status 寄存器的原始值。如果从机报错,这个值会包含错误标志位(比如 bit 4 置位表示错误)。你需要用 EC_STATE_ERROR 宏来检测。

3.4 实战:从 Init 推到 Op

好,咱们直接上代码。这是一个完整的从机状态切换流程:

#include <stdio.h>
#include <osal.h>
#include <ethercat.h>

int main(int argc, char *argv[]) {
    int slave = 1;  // 操作第一个从机
    int target_state;
    int current_state;
    int ret;

    // 1. 初始化网络
    if (ec_init("eth0") <= 0) {
        printf("初始化失败!\n");
        return -1;
    }
    printf("网络初始化成功\n");

    // 2. 扫描从机
    if (ec_config_init(FALSE) <= 0) {
        printf("没有发现从机!\n");
        ec_close();
        return -1;
    }
    printf("发现 %d 个从机\n", ec_slavecount);

    // 3. 配置映射(略,后续章节会讲)

    // 4. 状态切换:Init -> Pre-Op
    printf("正在切换到 Pre-Op...\n");
    target_state = EC_STATE_PRE_OP;
    ret = ec_writestate(slave, target_state);
    if (ret != 0) {
        printf("写入状态失败!\n");
        ec_close();
        return -1;
    }
    osal_usleep(10000);  // 等待 10ms

    current_state = ec_readstate(slave);
    if (current_state != target_state) {
        printf("切换到 Pre-Op 失败,当前状态: 0x%02X\n", current_state);
        ec_close();
        return -1;
    }
    printf("已切换到 Pre-Op\n");

    // 5. Pre-Op -> Safe-Op
    printf("正在切换到 Safe-Op...\n");
    target_state = EC_STATE_SAFE_OP;
    ret = ec_writestate(slave, target_state);
    if (ret != 0) {
        printf("写入状态失败!\n");
        ec_close();
        return -1;
    }
    osal_usleep(10000);

    current_state = ec_readstate(slave);
    if (current_state != target_state) {
        printf("切换到 Safe-Op 失败,当前状态: 0x%02X\n", current_state);
        ec_close();
        return -1;
    }
    printf("已切换到 Safe-Op\n");

    // 6. Safe-Op -> Op
    printf("正在切换到 Op...\n");
    target_state = EC_STATE_OP;
    ret = ec_writestate(slave, target_state);
    if (ret != 0) {
        printf("写入状态失败!\n");
        ec_close();
        return -1;
    }
    osal_usleep(10000);

    current_state = ec_readstate(slave);
    if (current_state != target_state) {
        printf("切换到 Op 失败,当前状态: 0x%02X\n", current_state);
        ec_close();
        return -1;
    }
    printf("已切换到 Op,从机正常运行!\n");

    // 7. 运行主循环(略)

    ec_close();
    return 0;
}

这段代码我写得很啰嗦,但每一步都做了状态确认。你想想看,如果中间某一步失败了,你至少知道卡在哪。

3.5 错误恢复:从机掉线了怎么办?

实际项目中,从机不会永远乖乖听话。我遇到过好几次,从机在运行中突然报错,状态掉回 Init。这时候你得做错误恢复。

错误恢复的流程是这样的:

  1. 检测到从机状态异常(比如不是 Op)
  2. 读取 AL Status 寄存器,检查错误码
  3. 重新从 Init 开始,一步步推上去

看代码:

int recover_slave(int slave) {
    int state;
    int retry = 3;  // 最多重试 3 次

    while (retry--) {
        printf("尝试恢复从机 %d...\n", slave);

        // 先强制写 Init
        ec_writestate(slave, EC_STATE_INIT);
        osal_usleep(20000);

        // 再推 Pre-Op
        ec_writestate(slave, EC_STATE_PRE_OP);
        osal_usleep(10000);
        state = ec_readstate(slave);
        if (state != EC_STATE_PRE_OP) continue;

        // 推 Safe-Op
        ec_writestate(slave, EC_STATE_SAFE_OP);
        osal_usleep(10000);
        state = ec_readstate(slave);
        if (state != EC_STATE_SAFE_OP) continue;

        // 推 Op
        ec_writestate(slave, EC_STATE_OP);
        osal_usleep(10000);
        state = ec_readstate(slave);
        if (state == EC_STATE_OP) {
            printf("从机 %d 恢复成功!\n", slave);
            return 0;
        }
    }

    printf("从机 %d 恢复失败,请检查硬件!\n", slave);
    return -1;
}
避坑指南:我曾经在恢复流程中只写了 Op 状态,没从 Init 开始推。结果从机一直报错,折腾了半天才发现问题。记住:错误恢复必须从头开始,不能偷懒。

3.6 状态切换的常见问题

现象 可能原因 解决方法
ec_writestate() 返回 -1 网络不通或从机未响应 检查网线、电源、从机地址
状态卡在 Init 不动 邮箱通信未配置好 检查 SDO 配置或从机 EEPROM
Safe-Op 推不到 Op PDO 映射或过程数据有问题 检查 DC 配置和同步模式
状态来回跳变 看门狗超时或通信干扰 调整看门狗参数,检查布线

嗯,这张表是我多年踩坑的总结。你如果遇到状态切换的问题,先对照这张表排查,大概率能解决。

3.7 总结

这一节我们干了三件事:

  • 理解了 EtherCAT 状态机的单向转换规则
  • 学会了用 ec_writestate() 写状态、用 ec_readstate() 读状态
  • 写了一个完整的从 Init 到 Op 的切换代码,还加了错误恢复

我个人觉得,状态切换是 EtherCAT 开发中最基础也最容易出问题的一环。你只要把这一节吃透了,后面配置 PDO、跑过程数据,都会顺手很多。

好,代码都在上面了。你拿去跑一跑,有问题随时回来翻这张表。


专注资料整理