从零开发 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_init、ec_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() 内部执行的核心操作:
- 通过
if_nametoindex()获取网卡索引 - 创建 PF_PACKET、SOCK_RAW 类型的 socket
- 绑定到指定网卡,设置为混杂模式
- 初始化
ecx_contextt中的port结构体 - 设置 socket 超时时间
- 初始化
redport(冗余端口,用于环网冗余) - 清空
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() 在内部依次完成:
- 重置所有从站:发送 Broadcast 报文,清除从站状态
- Auto Increment 扫描:从地址 0 开始,逐个读取从站信息
- 读取 EEPROM:获取每个从站的 Vendor ID、Product Code、Revision、Serial Number
- 分配 Configured Address:为每个从站分配唯一的站点别名地址
- 初始化
slavelist:填充ec_slave_t结构体数组 - 检测 Mailbox 能力:判断从站是否支持 CoE、FoE 等协议
- 初始化 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() 进一步配置过程数据映射。
这个函数的核心作用是:
- 遍历指定 Group 中的所有从站
- 根据 EEPROM 中的 PDO 配置,计算每个从站的输入/输出数据大小
- 在
IOmap中为每个从站分配空间 - 配置 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),这个函数会:
- 识别支持 DC 的从站
- 配置 DC 参数
- 启用 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 详解 | 待更新 |