从零开发 EtherCAT 主站(六):SOEM 初始化流程详解,主站是如何发现所有从站的?

从零开发 EtherCAT 主站(六):SOEM 初始化流程详解,主站是如何发现所有从站的?

文章目录

  • [从零开发 EtherCAT 主站(六):SOEM 初始化流程详解,主站是如何发现所有从站的?](#从零开发 EtherCAT 主站(六):SOEM 初始化流程详解,主站是如何发现所有从站的?)
    • [一、从一个官方 Demo 看 EtherCAT Master 启动流程](#一、从一个官方 Demo 看 EtherCAT Master 启动流程)
    • [二、SOEM 为什么需要 Context?](#二、SOEM 为什么需要 Context?)
    • [三、第一步:ecx_init() --- 网卡初始化](#三、第一步:ecx_init() — 网卡初始化)
    • [四、第二步:ecx_config_init() --- 从站发现](#四、第二步:ecx_config_init() — 从站发现)
      • [4.1 为什么需要"发现"?](#4.1 为什么需要“发现”?)
      • [4.2 利用 Auto Increment Address 逐级扫描](#4.2 利用 Auto Increment Address 逐级扫描)
      • [4.3 ecx_config_init() 执行的核心步骤](#4.3 ecx_config_init() 执行的核心步骤)
    • [五、ec_slave_t --- SOEM 如何保存从站信息?](#五、ec_slave_t — SOEM 如何保存从站信息?)
    • [六、第三步:ecx_config_map_group() --- PDO 映射](#六、第三步:ecx_config_map_group() — PDO 映射)
    • [七、第四步:ecx_configdc() --- 分布式时钟配置](#七、第四步:ecx_configdc() — 分布式时钟配置)
    • [八、第五步:进入 SAFE_OP](#八、第五步:进入 SAFE_OP)
    • [九、第六步:进入 OP 前为什么要先发一次 PDO?](#九、第六步:进入 OP 前为什么要先发一次 PDO?)
    • [十、第七步:进入 OP --- 正式运行](#十、第七步:进入 OP — 正式运行)
    • [十一、SOEM 初始化完整流程总结](#十一、SOEM 初始化完整流程总结)
    • 十二、下一篇预告
    • 系列文章导航

一、从一个官方 Demo 看 EtherCAT Master 启动流程

在上一篇中,我们通过 SOEM 官方 simple.c 例程理解了主站的整体骨架:ec_init()ec_config_init()ec_config_map() → 状态切换 → 周期通信。

simple.c 使用的是传统全局 API(ec_initec_config_init 等全局函数)。而当前 SOEM 主推的编程模式是 Context 模式 ,即通过一个上下文结构体 ecx_contextt 来封装所有主站状态,实现多实例支持。

官方 simple_ng.c(NG = New Generation)就是 Context 模式的典型实现。本篇将以此为基础,深入分析 SOEM 的初始化流程:从网卡初始化,到调用 ecx_config_init() 自动发现 EtherCAT 从站的完整过程

先看 simple_ng.c 的主站启动入口:

c 复制代码
static void fieldbus_start(Fieldbus *fieldbus)
{
    printf("Initializing SOEM on '%s'... ", fieldbus->iface);

    // 1. 初始化网卡
    if (!ecx_init(&fieldbus->context, fieldbus->iface)) {
        printf("no socket connection\n");
        return;
    }

    // 2. 扫描从站
    printf("Finding autoconfig slaves... ");
    if (ecx_config_init(&fieldbus->context) <= 0) {
        printf("no slaves found\n");
        return;
    }

    // 3. 映射 PDO 到内存
    ecx_config_map_group(&fieldbus->context, fieldbus->map, fieldbus->group);

    // 4. 配置分布式时钟(可选)
    ecx_configdc(&fieldbus->context);

    // 5. 等待从站进入 SAFE_OP
    ecx_statecheck(&fieldbus->context, 0, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE * 4);

    // 6. 进入 OP
    ecx_slave_t *slave = fieldbus->context.slavelist;
    slave->state = EC_STATE_OPERATIONAL;
    ecx_writestate(&fieldbus->context, 0);
    ecx_statecheck(&fieldbus->context, 0, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE * 4);

    // 7. 周期通信
    fieldbus_roundtrip(fieldbus);
}

整个初始化流程可以概括为七个步骤,下面逐一深入分析。

二、SOEM 为什么需要 Context?

simple_ng.c 中,首先定义了一个 Fieldbus 结构体:

c 复制代码
typedef struct {
    ecx_contextt context;   // SOEM 上下文
    char *iface;            // 网卡名称
    uint8 group;            // 组 ID
    int roundtrip_time;     // 往返时间
    uint8 map[4096];        // PDO 映射缓冲区
} Fieldbus;

其中 ecx_contextt 是 SOEM Context 模式的核心数据结构,它封装了整个 EtherCAT 主站的运行时环境:

text 复制代码
ecx_contextt
    │
    ├── port → 网口操作句柄(socket/句柄)
    │
    ├── slavelist → 从站列表(ec_slave_t 数组)
    │
    ├── grouplist → 组列表(ec_group_t 数组)
    │
    ├── IOmap → 过程数据映射内存
    │
    ├── esibuf → ESI 数据缓存
    │
    ├── elist → 从站状态列表
    │
    └── ... 其他运行时状态

Context 模式与全局 API 模式的核心区别

全局 API Context 模式
数据结构 全局变量 ec_slave[]ec_group[] 用户管理的 ecx_contextt
多实例支持 ❌ 不支持 ✅ 支持
线程安全 好(各实例隔离)
典型用例 simple.c simple_ng.c(官方推荐)

简单理解:Context 就像一个"收纳盒",把所有 EtherCAT 主站相关的状态都装在一起,方便传递和管理

三、第一步:ecx_init() --- 网卡初始化

c 复制代码
if (!ecx_init(&fieldbus->context, fieldbus->iface)) {
    printf("no socket connection\n");
    return;
}

ecx_init() 负责建立主站与物理网卡的连接。在 Linux 下,它会打开一个原始套接字(raw socket),绑定到指定的网络接口(如 eth0)。

ecx_init() 内部执行的核心操作:

  1. 通过 if_nametoindex() 获取网卡索引
  2. 创建 PF_PACKET、SOCK_RAW 类型的 socket
  3. 绑定到指定网卡,设置为混杂模式
  4. 初始化 ecx_contextt 中的 port 结构体
  5. 设置 socket 超时时间
  6. 初始化 redport(冗余端口,用于环网冗余)
  7. 清空 slavelist,为后续扫描做准备

这里要注意:ecx_init() 只是让主站具备了发送和接收 EtherCAT 帧的能力,它并不会主动搜索从站 。真正的从站发现,由下一步的 ecx_config_init() 完成。

四、第二步:ecx_config_init() --- 从站发现

c 复制代码
if (ecx_config_init(&fieldbus->context) <= 0) {
    printf("no slaves found\n");
    return;
}

ecx_config_init() 是整个 SOEM 初始化阶段最核心的函数。如果说 ecx_init() 是"打开眼睛",那么 ecx_config_init() 就是"环顾四周,看清有哪些设备"。

4.1 为什么需要"发现"?

EtherCAT 和普通以太网的一个重要区别在于:普通以太网设备有 IP 地址、MAC 地址,而 EtherCAT 从站在上电后还没有任何地址信息。主站必须通过一种特殊机制,从零开始找出总线上有哪些设备。

4.2 利用 Auto Increment Address 逐级扫描

EtherCAT 从站在上电后,会处于一个特殊的状态:每个从站的默认地址就是它的物理位置偏移量,即 Auto Increment Address

自动递增地址实际上是一个从站相对于主站的物理位置偏移量。当主站发送一个寻址到某个 Auto Increment Address 的报文时,报文经过的每个从站会先检查这个地址值是否与自己匹配,如果不匹配,则先将地址值减 1,再转发给下一个从站。这个机制使得主站可以通过"试探"不同的地址偏移量,逐个定位到每个从站。

SOEM 的典型扫描流程

text 复制代码
Master 发送广播报文:读取第一个从站的 EEPROM
                    │
                    ▼
               Slave 1 响应(地址 0)
                    │
                    ▼
          Master 知道 Slave 1 存在
                    │
                    ▼
   Master 发送报文:读取下一个从站(地址 -1)
                    │
                    ▼
               Slave 2 响应(地址 0)
                    │
                    ▼
          Master 知道 Slave 2 存在
                    │
                    ▼
   Master 继续扫描,直到无响应 → 扫描完成

关键点是:SOEM 通过发送带 Auto Increment Read 的 Datagram,配合 Working Counter(WKC) 的返回值来判断从站是否响应。WKC 是 EtherCAT 帧中的一个计数器,从站每处理一个报文就会递增它。主站发送报文后检查 WKC 是否变化,就能知道该地址是否存在从站。

4.3 ecx_config_init() 执行的核心步骤

ecx_config_init() 在内部依次完成:

  1. 重置所有从站:发送 Broadcast 报文,清除从站状态
  2. Auto Increment 扫描:从地址 0 开始,逐个读取从站信息
  3. 读取 EEPROM:获取每个从站的 Vendor ID、Product Code、Revision、Serial Number
  4. 分配 Configured Address:为每个从站分配唯一的站点别名地址
  5. 初始化 slavelist :填充 ec_slave_t 结构体数组
  6. 检测 Mailbox 能力:判断从站是否支持 CoE、FoE 等协议
  7. 初始化 PDO 信息:读取从站的 PDO 映射配置(来自 EEPROM)

ecx_config_init() 完成后,context->slavelist 中已经包含了所有从站的完整信息,主站已经"知道"了网络上都有哪些设备。

五、ec_slave_t --- SOEM 如何保存从站信息?

扫描完成后,SOEM 会建立 context->slavelist[] 数组,每个元素是一个 ec_slave_t 结构体。

该结构体的关键字段:

字段 说明
state 当前状态(INIT/PRE_OP/SAFE_OP/OP)
eeprom EEPROM 内容
name 设备名称
vendor 厂商 ID
productcode 产品代码
revision 版本号
serial 序列号
Ibytes / Obytes 输入/输出数据字节数
Obits / Ibits 输入/输出数据位数
PDOassign PDO 分配索引
PDOconfig PDO 配置索引
SM SyncManager 配置

例如,扫描到一个伺服驱动器后:

c 复制代码
slavelist[1].vendor = 0x0000009A;      // 某厂商 ID
slavelist[1].productcode = 0x12345678; // 产品代码
slavelist[1].revision = 0x00010001;    // 版本号
slavelist[1].eeprom = ...;             // EEPROM 原始数据

在 SOEM 中,slavelist[0] 是特殊的------它不是一个实际物理从站,而是一个 虚拟主站从站,用于广播控制(如同时将 OP 命令广播给所有从站)。

simple_ng.c 中进入 OP 状态时:

c 复制代码
ecx_slave_t *slave = fieldbus->context.slavelist;
slave->state = EC_STATE_OPERATIONAL;   // 修改 slavelist[0]
ecx_writestate(&fieldbus->context, 0); // 广播给所有从站

这里操作的就是 slavelist[0],然后通过 ecx_writestate() 广播到所有从站,要求所有从站进入 OP 状态。

六、第三步:ecx_config_map_group() --- PDO 映射

c 复制代码
ecx_config_map_group(&fieldbus->context, fieldbus->map, fieldbus->group);

ecx_config_init() 完成从站发现后,ecx_config_map_group() 进一步配置过程数据映射。

这个函数的核心作用是:

  1. 遍历指定 Group 中的所有从站
  2. 根据 EEPROM 中的 PDO 配置,计算每个从站的输入/输出数据大小
  3. IOmap 中为每个从站分配空间
  4. 配置 SyncManager 和 FMMU

fieldbus->map 就是实际的过程数据缓冲区,后续周期通信时,主站就是通过读写这块内存来交换 PDO 数据。

text 复制代码
fieldbus->map(IOmap)
┌─────────────────────────────────────────────────────────┐
│ Slave 1 Out │ Slave 1 In │ Slave 2 Out │ Slave 2 In │ ... │
└─────────────────────────────────────────────────────────┘

七、第四步:ecx_configdc() --- 分布式时钟配置

c 复制代码
ecx_configdc(&fieldbus->context);

如果从站支持分布式时钟(DC),这个函数会:

  1. 识别支持 DC 的从站
  2. 配置 DC 参数
  3. 启用 DC 同步模式

DC(Distributed Clock)是 EtherCAT 实现高精度同步的核心机制,它让所有从站共享同一个时间基准,同步精度可达亚微秒级。对于伺服驱动器的同步运动控制,DC 是必须正确配置的功能。

八、第五步:进入 SAFE_OP

c 复制代码
ecx_statecheck(&fieldbus->context, 0, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE * 4);

这里等待所有从站进入 SAFE_OP 状态。

在 SAFE_OP 状态下:

  • 输入 PDO 已经可以读取(主站可以获取从站的反馈数据)
  • 输出 PDO 被禁止(主站不能改变从站的输出状态)

这种设计是为了安全:在设备正式运行前,主站可以先确认输入数据正常,再正式启用输出。

为什么不能直接从 PRE_OP 跳到 OP? 因为 EtherCAT 协议要求状态必须逐步切换(INIT → PRE_OP → SAFE_OP → OP),每一步都有对应的配置和检查。跳过 SAFE_OP 直接进入 OP 会导致从站拒绝状态切换请求。

九、第六步:进入 OP 前为什么要先发一次 PDO?

simple_ng.c 中,进入 OP 之前调用了 fieldbus_roundtrip(fieldbus),它的内部是:

c 复制代码
ecx_send_processdata(&fieldbus->context);
wkc = ecx_receive_processdata(&fieldbus->context, EC_TIMEOUTRET);

这一步看似普通,但有一个很微妙的工程细节:

很多 EtherCAT 从站在进入 OP 之前,需要看到一次正常的过程数据交换,才能确认 PDO 通道已经建立,然后才允许进入 OP 状态。

如果跳过这一步,某些从站可能拒绝进入 OP 状态,或者进入 OP 后 PDO 数据不更新。这是 EtherCAT 开发中常见的调试陷阱之一。

十、第七步:进入 OP --- 正式运行

c 复制代码
ecx_slave_t *slave = fieldbus->context.slavelist;
slave->state = EC_STATE_OPERATIONAL;
ecx_writestate(&fieldbus->context, 0);
ecx_statecheck(&fieldbus->context, 0, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE * 4);

最后,主站请求所有从站进入 OP(Operational) 状态。如果所有从站都成功进入 OP,EtherCAT 网络就具备了进行正常实时周期通信的条件。

十一、SOEM 初始化完整流程总结

text 复制代码
                    Application
                         │
                 fieldbus_start()
                         │
                    ecx_init()
                         │
              ┌──────────┴──────────┐
              │  NIC Initialization │  打开网卡,初始化 socket
              └──────────┬──────────┘
                         │
                 ecx_config_init()
                         │
              ┌──────────┴──────────┐
              │   Slave Discovery   │  Auto Increment 扫描
              └──────────┬──────────┘
                         │
              ┌──────────┴──────────┐
              │    EEPROM Read      │  读取 Vendor ID / Product Code...
              └──────────┬──────────┘
                         │
              ┌──────────┴──────────┐
              │  Create slavelist[] │  建立从站信息数组
              └──────────┬──────────┘
                         │
              ecx_config_map_group()
                         │
              ┌──────────┴──────────┐
              │    PDO Mapping      │  配置 SyncManager & FMMU
              └──────────┬──────────┘
                         │
                ecx_configdc()
                         │
              ┌──────────┴──────────┐
              │   Distributed Clocks│  配置 DC 同步
              └──────────┬──────────┘
                         │
                ecx_statecheck()
                         │
              ┌──────────┴──────────┐
              │      SAFE_OP        │  输入可用,输出禁止
              └──────────┬──────────┘
                         │
         ecx_send_processdata() / ecx_receive_processdata()
                         │
              ┌──────────┴──────────┐
              │    Process Data     │  建立 PDO 通道
              └──────────┬──────────┘
                         │
                ecx_writestate()
                         │
              ┌──────────┴──────────┐
              │       OP            │  正式运行
              └─────────────────────┘

十二、下一篇预告

本文分析了 SOEM 初始化流程中最核心的步骤------从 ecx_init() 网卡初始化,到 ecx_config_init() 完成从站发现和 slavelist 建立,再到状态切换进入 OP 的完整过程。

其中,ecx_config_map_group() 涉及 SyncManager(SM)FMMU(Fieldbus Memory Management Unit) 的配置,这是理解 EtherCAT 过程数据交换机制的关键。

下一篇将深入这两个核心机制:

《从零开发 EtherCAT 主站(七):SOEM 如何配置 PDO?深入理解 SyncManager 与 FMMU》

重点分析:

  • PDO Mapping 的原理与配置流程
  • SyncManager 的作用与配置方法
  • FMMU 如何将物理地址映射到主站内存
  • IOmap 中的数据是如何组织的

系列文章导航

章节 标题 状态
第一篇 EtherCAT 为什么能成为工业实时通信的主流?
第二篇 EtherCAT 通信原理详解
第三篇 EtherCAT 状态机(ESM)完整解析
第四篇 PDO、Mailbox、CoE、FoE 到底是什么?
第五篇 SOEM 框架源码解析
第六篇 SOEM 初始化流程详解(本文)
第七篇 SyncManager 与 FMMU 详解 待更新
相关推荐
老当益壮梁奶奶25 分钟前
Linux软件编程学习笔记(九):消息队列、共享内存与信号灯详解
linux·c语言·笔记·学习
MartinYeung527 分钟前
[论文学习]激活差异揭示后门:SAE架构对比研究
人工智能·学习·架构
小雪崩30 分钟前
嵌入式学习 day35:进程间通信-消息队列、共享空间及有名信号量
linux·c语言·学习
从零开始的嵌入式之旅43 分钟前
day33
linux·c语言·经验分享·笔记·嵌入式硬件
RedJACK~1 小时前
Windows11一级右键菜单添加Alacritty教程
单片机·嵌入式硬件
学习星球1 小时前
# 6G通感一体化(ISAC)技术深度解析——从原理到实战> <br />
go·信息与通信·媒体
智闲电子设计1 小时前
STM32 DAC 实战:用 DMA 把定时器表变成波形,自制信号发生器
stm32·单片机·嵌入式硬件
zcmodeltech1 小时前
反应装置模型控制系统设计与实现:多设备协同联动方案
java·网络·数据库·stm32·嵌入式硬件·能源·制造
深圳市恒锐丰科技杨生1 小时前
CL4054HL 36V 耐压 600mA 单节锂电池线性充电芯片|SOT23‑5 小型便携充电方案
嵌入式硬件·硬件工程