ARTICLE · 999071
读懂 EtherCAT 主站源码:SOEM 分层架构与 STM32H7+FPGA 移植实战
本文面向初学者,带你在不迷失于协议细节的前提下,通读一个真实的 EtherCAT 主站工程——基于 SOEM (Simple Open EtherCAT Master) 移植到 STM32F407 的代码,并最终给出向 STM32H7+FPGA 平台移植的完整路线。 一、为什么从源码读起 很多初学者上手 EtherCAT 时会被协议规范、XML 配置文件、各种寄存器地址吓退,觉得它"太重"。但当你真正打开一个能跑通的主站工程,会发现它的代码结构异常清晰:自上而下就是五层,每一层职责单一,层与层之间靠明确接口耦合。 一旦看懂了这五层,你会得到一个关键认知——移植到任何新平台,真正要改的代码不到工程总量的 5%。这不是夸张。下面我们就来验证这句话。二、五层架构:一张图看清边界 整个工程自上而下分为五层:
对初学者最关键的一点:移植分界线就画在 nicdrv.c 与 eth.c 之间。 nicdrv.c 负责帧缓冲管理与收发配对(保留在 STM32); eth.c 里的 bfin_EMAC_send() / bfin_EMAC_recv() 是真正打交道的硬件收发函数(交给 FPGA 替换)。记住这两个函数名,移植之路就豁然开朗了。 各层职责与对应源码 应用层 —— main.c 负责 OLED/按键显示,在 1ms SysTick 中调用 ecat_loop();ecatuser.c 封装了 ecat_init() 启动序列、ecat_loop() 周期收发、以及 PDO 结构体指针 outputs1/inputs1。 SOEM 核心库 —— ethercatmain.c 是状态机与过程数据交换;ethercatconfig.c 是从站扫描与 IOmap 映射;ethercatbase.c 是以太网帧/数据报组包。 HAL 层 —— nicdrv.c 用 16 个收发缓冲 + 索引号把"发出的帧"和"返回的帧"配对;osal.c 提供微秒级定时器(供超时判断)。 网卡驱动/时基 —— eth.c 的 bfin_EMAC_send/recv 是 SOEM 与硬件的唯一接口;tim.c 用 TIM2(µs)+TIM3(秒) 给 osal 提供时间。三、主站是怎么"跑起来"的:状态机四步走 EtherCAT 从站有四个核心状态:INIT → PRE_OP → SAFE_OP → OP。只有进入 OP,主从之间才会周期性交换过程数据。ecat_init() 按固定顺序完成整套握手: 1.ec_init(“YS-PWM”) —— 绑定网卡,初始化 16 组收发缓冲。 2.ec_config_init(FALSE) —— 扫描发现从站:发广播读(BRD)清点从站数量,再对每个从站自增读(APRD) EEPROM,填充全局数组 ec_slave[] 的厂商ID、产品ID、名字等信息。 3.配置从站 + DC + IOmap 映射 —— 注册 PO2SOconfig 回调(在从站进 SAFE_OP 前通过 SDO 写 PDO 映射参数 0x1C12/0x1C13),调用 ec_configdc() 配置分布式时钟,ec_config_map(&IOmap) 根据 FMMU 和同步管理器把输入输出数据映射到一块连续内存。 4.ec_statecheck(0, EC_STATE_SAFE_OP) —— 推动所有从站进入安全运行。 5.收发一帧 + ec_writestate → EC_STATE_OPERATIONAL —— 进入运行态,开启 1ms 周期循环。 这里有两个初学者容易忽略的重点: 重点一:PO2SOconfig 回调函数指针。 SOEM 在从站进入 SAFE_OP 前会自动调用它,这是 EtherCAT 协议规定的「PRE_OP 阶段配置点」——你只能在这个时机通过 SDO 写 PDO 映射,因为 SAFE_OP 后从站就锁定映射了。 重点二:WKC(工作计数器)。 expectedWKC = (outputsWKC * 2) + inputsWKC。每个从站处理一次数据报,WKC 自增;逻辑读写(LRW)对输出从站 +2(读+写)、输入从站 +1。主站每周期比对实际 WKC 与期望值,判断通信是否正常。这是 EtherCAT 最精巧的健壮性设计。 关键数据结构 ec_slavet 记住几个字段:outputs/inputs 指向 IOmap 里的输出/输入区(主站直接读写这两块内存即等于与从站通信)、Obytes/Ibytes 是字节长度、configadr 是配置地址、SM[]/FMMU[] 是同步管理器与内存映射单元四、周期过程数据交换:核心中的核心 进入 OP 后,每 1ms 在 SysTick 中断里调用 ecat_loop(),里面就两句:
ec_send_processdata();ec_receive_processdata();
这对函数构成了「一帧走完整个从站链」的总线式读写。它的机制是 EtherCAT 区别于普通以太网的根本所在——以太网帧是"一帧走到底"的总线拓扑: 1.构造 LRW 帧:主站用逻辑地址读写命令(LRW)一条数据报同时读写整段逻辑内存,效率最高。第一个帧里还追加一条 DC 时间戳数据报(EC_CMD_FRMW),读写 DC 主从站的系统时间寄存器,这是分布式时钟同步的来源。 2.帧穿越从站链:每个从站的 ESC(EtherCAT Slave Controller)看到帧经过,就按 FMMU 映射把自己的输入写进帧、把帧里的输出读出来,并让 WKC 自增。帧绕一圈回到主站。 3.帧返回接收:这是 nicdrv.c 的精华。SOEM 维护 16 组收发缓冲,每发一帧分配一个 index,发时置 TX 态,收时按帧里的 index 找回对应缓冲——即使收发乱序也能正确配对。 4.复制输入 + WKC 校验:收完后把帧里的输入数据 memcpy 回 IOmap 的输入区。主站直接读 inputs1->xxx 就拿到了从站数据,写 outputs1->xxx 就发给了从站。 为什么 STM32 只需保留这部分? 因为状态机、配置、SDO/邮箱通信都在 INIT→OP 阶段一次性做完;进入 OP 后每周期只跑"构造帧→穿越→收帧匹配"这一闭环,逻辑全在 SOEM 核心层,与具体硬件完全解耦——这正是 FPGA 接管硬件收发的可行性所在。五、移植到 STM32H7+FPGA:改两个函数就够了 理解了上面的机制,移植方案就水到渠成。目标架构:STM32H7 只跑过程数据逻辑,FPGA 负责以太网帧收发与 PHY 接口。不用改 / 直接复用(约 80% 代码) Ethercat/soem/* 整个 SOEM 核心库 —— 协议逻辑与硬件无关,原样移植到 H7 工程。 osal.c —— 仅把里面的 GetSec()/GetUSec() 指向 H7 的定时器即可。必须改写(移植的真正工作量集中在这 2 个函数)eth.c 里的 bfin_EMAC_send/recv,原实现是直接操作 STM32F4 ETH DMA,移植后改为操作 FPGA FIFO:
/* 原实现:操作 STM32F4 ETH MAC DMA */intbfin_EMAC_send(void *packet, int length){memcpy(&Tx_Buff[0][0], packet, length);low_level_output(Tx_Buff[0], length); // ← F4 HAL_ETH_TransmitFramereturn 0;}/* 移植后:把帧写入 FPGA 的 Tx FIFO(通过 FSMC 访问 FPGA 寄存器) */intbfin_EMAC_send(void *packet, int length){FPGA_WriteTxFifo(packet, length); // FIFO 满/超时处理return 0;}intbfin_EMAC_recv(uint8_t *packet, size_t size){/* 原实现:HAL_ETH_GetReceivedFrame 读 F4 DMA 描述符 *//* 移植后:从 FPGA Rx FIFO 读一帧 */int len = FPGA_ReadRxFifo(packet, size); // 非阻塞,无帧返回 -1return len;}
nicdrv.c 的 ecx_outframe / ecx_recvpkt 调用这两个函数,所以只要这两个函数语义不变(send 非阻塞发送、recv 非阻塞返回帧长或 -1),上层 SOEM 完全无感。 删除 / 不需要lan8720.c —— PHY 初始化由 FPGA 端处理。 MX_ETH_Init / HAL_ETH_MspInit —— F4 ETH 外设初始化,H7 端用 FSMC/SPI 初始化 FPGA 接口替代。 FPGA 端需要实现的逻辑
六、几个实操提醒 1.STM32H7 与 FPGA 的总线选型:过程数据帧不大(通常 <1500 字节),用 FSMC 并行总线(速度快)或 SPI(引脚少)均可。FSMC 更适合 EtherCAT 的实时性要求。 2.FIFO 深度:至少容纳 2~4 个完整帧(SOEM 允许多帧同时在途,EC_MAXBUF=16),避免丢帧。 3.DMA 描述符可去掉:F4 用了 DMA 描述符链,FPGA 方案里 FIFO 直接替代了描述符,逻辑更简单。 4.先用原工程跑通:移植前建议先在 F4 平台把通信调通,确认从站能进 OP、PDO 收发正常,再切到 H7+FPGA,便于隔离问题。 5.DC 同步不能省:ec_dcsync0(1, TRUE, 1000000, 0) 让从站每 1ms 产生 SYNC0 信号,使所有从站同步采样——这是 EtherCAT 微秒级同步的根基,移植时务必保留这段配置逻辑。七、收尾:把调用链刻进脑子里 最后,把过程数据一帧的完整调用链记住,你就真正读懂了这个工程:
只有两个 ★ 标记的函数需要改写,其余全部保留。这就是"5%"的由来。 EtherCAT 看似复杂,但它的设计哲学极其清晰:协议逻辑与硬件解耦、帧索引配对容忍乱序、WKC 自检保证健壮。当你把分界线(bfin_EMAC_send/recv)这一点想通,整个移植就变成了"换底座"的工程问题,而非协议重写。
读懂这套代码后,建议你先精读 ecatuser.c 的 ecat_init()(主站怎么跑起来)和 nicdrv.c 的 ecx_inframe()(帧怎么配对)——这两个分别是「主站初始化」和「帧收发配对」的核心,吃透它们,你的开发进度会快很多。
祝你移植顺利。