简介:这份资源面向嵌入式工程师与运动控制开发者,提供将开源SOEM协议栈移植到STM32平台、构建EtherCAT主站的完整工程源码,可用于驱动伺服电机等从站设备,解决工业实时以太网主站开发门槛高、参考实现稀缺的问题。压缩包共267个文件,约1.63MB,以139个.h头文件与119个.c源文件为主体,涵盖STM32F7系列HAL库驱动、以太网MAC与PHY配置、SOEM协议栈适配及RTOS集成代码,另含uvprojx/uvoptx工程文件、hex烧录固件、s汇编启动文件与bat编译脚本,可直接用Keil打开编译。资源已有18343人学习下载,热度较高。读者可从中获得主站移植的完整目录结构与初始化流程,参考PDO/SDO通信、PDO映射验证方法,并借鉴作者记录的bug排查与实时性优化思路,适合具备STM32与以太网基础的开发者进阶学习。
1. 用 STM32 跑 EtherCAT 主站:为什么有人死磕 SOEM 这条路
如果你手里有一块 STM32 开发板,又恰好被伺服电机、远程 IO 或运动控制项目卡住,大概率会刷到“STM32 做 EtherCAT 主站”这个方向。EtherCAT 是工业现场总线的硬通货,周期能做到 1ms 甚至更低,同步抖动小,接线简单,但主站方案长期被几款专用芯片和商业协议栈把持。SOEM 是开源 EtherCAT 主站协议栈里最常被拿来折腾的一个,纯 C 写,不依赖操作系统,理论上只要有网口就能跑。把 SOEM 移植到 STM32 上,意味着你不再需要一台工控机或 Linux 盒子,一块几十块钱的 MCU 就能当主站,直接控制伺服电机和 IO 模块。这条路适合谁?适合做小型运动控制器、教学实验平台、或者想低成本验证 EtherCAT 拓扑的嵌入式工程师。但别高兴太早,STM32 的以太网外设和 SOEM 的时序要求之间有一道坎,翻过去是生产力,翻不过去就是玄学。
2. 拆开 SOEM 的骨架:从协议栈到 STM32 的移植路径
2.1 SOEM 到底做了什么,没做什么
SOEM 的全称是 Simple Open EtherCAT Master,它实现了 EtherCAT 主站的核心状态机:初始化、预运行、安全运行、运行。它负责扫描从站、配置邮箱通信、映射过程数据、发送周期帧。但它不提供实时操作系统,不提供网卡驱动,也不管你的硬件定时器怎么配。换句话说,SOEM 是一套协议逻辑,它假设你有一个能收发原始以太网帧的底层接口。在 Linux 上,这个接口是 raw socket;在 STM32 上,你得自己用 ETH 外设或者外挂 MAC 芯片去填这个坑。SOEM 的源码结构很清晰,osal目录是操作系统抽象层,oshw目录是硬件抽象层,soem目录是协议核心。移植的本质就是替换osal和oshw里的几个函数,让它们适配 STM32 的裸机或 RTOS 环境。
2.2 硬件选型:哪些 STM32 能扛住 EtherCAT 主站
不是所有 STM32 都能跑。EtherCAT 主站需要发送和接收原始以太网帧,帧类型是 0x88A4,普通 TCP/IP 协议栈处理不了。你需要一个能绕过 TCP/IP 协议栈、直接操作 MAC 层的外设。STM32F4 和 F7 系列自带以太网 MAC,配合外部 PHY 芯片可以做到。STM32H7 系列也有,而且主频更高,适合做周期更短的主站。但要注意,STM32 的以太网外设是 DMA 驱动的,收发描述符环的管理直接影响实时性。我一般会选 STM32F407 或 STM32F767 作为起步平台,前者资料多,后者性能余量大。PHY 芯片选常见的 LAN8720 或 DP83848,RMII 接口,50MHz 时钟。别用 SPI 接口的 ENC28J60,那玩意儿延迟太大,跑 EtherCAT 会翻车。
2.3 移植第一步:把 SOEM 的硬件抽象层换成 STM32 的 ETH 驱动
SOEM 在oshw目录下有一个nicdrv.c文件,里面定义了网卡收发的接口。你需要实现两个关键函数:osal_eth_send和osal_eth_recv。在 STM32 上,这两个函数直接操作 ETH 外设的发送和接收描述符。下面是一个简化的发送函数示例,基于 STM32 HAL 库:
// 发送原始以太网帧,绕过 LwIP int osal_eth_send(uint8_t *buffer, int length) { // 等待上一个发送描述符空闲 while (ETH->DMASR & ETH_DMASR_TBUS) { // 可以在这里喂看门狗或切换任务 } // 设置发送缓冲区地址和长度 ETH->DMATDLAR = (uint32_t)buffer; ETH->DMATDAR = (uint32_t)buffer; // 配置发送描述符控制字,设置帧长度和校验和卸载 ETH_TxDesc[0].DESC0 = (uint32_t)buffer; ETH_TxDesc[0].DESC1 = length | ETH_TDESC_TCH; // 触发发送 ETH->DMASR = ETH_DMASR_TBUS; ETH->DMATPDR = 0; return length; }这段代码的逻辑是:先检查 DMA 发送忙状态,然后把缓冲区地址写入发送描述符,设置帧长度和“插入 CRC”标志,最后触发发送。参数buffer必须是对齐的,长度不能超过以太网最大帧 1518 字节。接收函数类似,需要轮询接收描述符,把数据拷贝出来,然后释放描述符。注意,SOEM 期望收到的是完整的以太网帧,包括帧头,但不包括前导码和 CRC。STM32 的 ETH 外设会自动处理前导码和 CRC,所以你在接收时拿到的就是帧头开始的数据。
2.4 定时器与中断:让主站周期稳下来
EtherCAT 主站的周期发送通常由一个硬件定时器触发。我一般用 TIM2 或 TIM5 做微秒级定时,在中断里调用ec_send_processdata和ec_receive_processdata。但这里有个坑:STM32 的 ETH 中断和定时器中断优先级要配好,ETH 接收中断优先级要高于定时器中断,否则接收会被延迟,导致从站看门狗超时。另外,SOEM 的ec_receive_processdata需要在一个周期内多次调用,直到收到所有从站的返回数据。常见做法是在定时器中断里发帧,然后在主循环里轮询接收,或者用 ETH 接收中断置标志位,主循环处理。周期时间设多少?起步建议 2ms,稳定后再往 1ms 压。STM32F407 跑 168MHz,处理 SOEM 协议栈和以太网收发,1ms 周期勉强能撑住,但余量不大。
3. 从零搭建:STM32 工程配置与 SOEM 集成步骤
3.1 工程骨架:用 CubeMX 生成 ETH 和定时器初始化代码
先用 STM32CubeMX 建工程,选好芯片型号,配置 ETH 外设。关键参数:RMII 模式,自动协商开启,接收描述符和发送描述符各设 4 个以上,接收缓冲区设大一点,比如 1536 字节。然后开一个定时器,比如 TIM2,预分频到 1MHz,自动重装载值设成周期微秒数,开启更新中断。生成代码后,把 SOEM 的源码拷贝到工程目录,在 IDE 里添加soem、osal、oshw三个文件夹的源文件。注意,SOEM 默认的osal是给 Linux 或 Windows 用的,你需要把osal.c里跟文件系统、线程相关的函数删掉或替换成空实现。我一般会新建一个osal_stm32.c,只保留osal_current_time、osal_mutex_lock这些必须的接口,用 STM32 的 HAL_GetTick 和空宏实现。
3.2 网络接口初始化:绕过 LwIP,直接操作 ETH 外设
CubeMX 生成的代码默认会初始化 LwIP,但 SOEM 不需要 TCP/IP 协议栈。你可以在 CubeMX 里把 LwIP 关掉,只保留 ETH 外设的初始化。然后自己写一个eth_raw_init函数,配置 MAC 地址、DMA 描述符、开启收发。下面是一个初始化片段:
void eth_raw_init(uint8_t *mac) { // 设置 MAC 地址 ETH->MACA0HR = (mac[5] << 8) | mac[4]; ETH->MACA0LR = (mac[3] << 24) | (mac[2] << 16) | (mac[1] << 8) | mac[0]; // 配置 DMA 突发长度和描述符 ETH->DMABMR = ETH_DMABMR_AAB | ETH_DMABMR_FB | ETH_DMABMR_RDP_32Beat; // 初始化发送描述符 for (int i = 0; i < TX_DESC_CNT; i++) { ETH_TxDesc[i].DESC0 = (uint32_t)&TxBuff[i][0]; ETH_TxDesc[i].DESC1 = 0; ETH_TxDesc[i].DESC2 = 0; ETH_TxDesc[i].DESC3 = 0; } // 初始化接收描述符 for (int i = 0; i < RX_DESC_CNT; i++) { ETH_RxDesc[i].DESC0 = (uint32_t)&RxBuff[i][0]; ETH_RxDesc[i].DESC1 = ETH_RDESC_OWN; ETH_RxDesc[i].DESC2 = 0; ETH_RxDesc[i].DESC3 = 0; } // 启动 DMA 收发 ETH->DMATDLAR = (uint32_t)Ð_TxDesc[0]; ETH->DMARDLAR = (uint32_t)Ð_RxDesc[0]; ETH->DMASR = ETH_DMASR_TS; ETH->DMAOMR = ETH_DMAOMR_ST | ETH_DMAOMR_SR; }这段代码直接操作寄存器,把发送和接收描述符环建好,然后启动 DMA。参数mac是 6 字节数组,建议用本地管理地址,比如02:00:00:00:00:01。注意,接收描述符的DESC1要设置OWN位,表示 DMA 拥有该描述符,可以往里面写数据。发送描述符的DESC1初始为 0,发送时再设置长度和控制位。
3.3 扫描从站与 PDO 映射:用 SOEM 的 API 完成配置
底层通了之后,就可以调用 SOEM 的 API 了。典型流程是:ec_init初始化,ec_config_init扫描从站,ec_config_map映射过程数据,ec_configdc配置分布式时钟,然后ec_statecheck等待从站进入 OP 状态。下面是一个配置示例:
// 初始化 SOEM,绑定网卡名(在 STM32 上随便填一个) if (ec_init("stm32_eth") <= 0) { printf("ec_init failed\n"); return -1; } // 扫描从站 if (ec_config_init(FALSE) <= 0) { printf("no slaves found\n"); return -1; } // 映射过程数据到 IOmap ec_config_map(&IOmap); // 配置分布式时钟 ec_configdc(); // 请求所有从站进入 OP 状态 ec_slave[0].state = EC_STATE_OPERATIONAL; ec_writestate(0); // 等待状态切换 do { ec_statecheck(0, EC_STATE_OPERATIONAL, 50000); } while (ec_slave[0].state != EC_STATE_OPERATIONAL);这段代码里,ec_init的参数在 STM32 上没有实际意义,因为底层已经绑定了 ETH 外设,随便传一个字符串就行。ec_config_map会把所有从站的输入输出数据映射到IOmap数组里,你后续读写伺服电机的目标位置、实际位置,都是操作这个数组。ec_configdc是配置分布式时钟,如果从站支持 DC,这一步能让主站和从站的时间同步。注意,ec_statecheck的超时时间单位是微秒,50000 就是 50ms,如果从站没反应,会一直卡在这里,建议加个重试次数。
3.4 周期任务:在定时器中断里收发过程数据
配置完成后,主站需要周期性发送过程数据。我一般把周期任务放在定时器中断里,或者用一个高优先级任务。下面是一个简化的周期处理函数:
void ethercat_cycle(void) { // 发送过程数据 ec_send_processdata(); // 接收过程数据,最多重试 3 次 int wkc = 0; for (int i = 0; i < 3; i++) { wkc = ec_receive_processdata(EC_TIMEOUTRET); if (wkc >= expected_wkc) break; } // 检查工作计数器,判断从站是否响应 if (wkc < expected_wkc) { error_count++; // 可以在这里触发从站状态检查或报警 } // 处理应用层逻辑,比如更新伺服目标位置 app_motion_update(); }ec_send_processdata会把IOmap里的输出数据打包成 EtherCAT 帧发出去。ec_receive_processdata会尝试接收从站返回的帧,返回值是工作计数器,表示有多少从站正确响应了。expected_wkc是你期望的工作计数器值,通常等于从站数量乘以每个从站的响应数。如果wkc小于期望值,说明有从站掉线或数据错误。EC_TIMEOUTRET是接收超时时间,单位是微秒,一般设 2000 左右。这个函数要在每个周期内调用,周期时间由定时器决定。
4. 避坑指南:STM32 跑 SOEM 最常见的五个翻车点
4.1 现象:从站扫描不到,ec_config_init返回 0
原因:STM32 的 ETH 外设没有正确接收到 EtherCAT 帧,或者发送的帧类型不对。EtherCAT 帧类型是 0x88A4,如果你用 LwIP 的ethernet_input处理,会被直接丢弃。另外,PHY 芯片的链路状态没起来,或者 RMII 时钟没配好,也会导致收发失败。
解决:先用示波器看 PHY 的 50MHz 时钟有没有,再看 ETH 的DMASR寄存器,确认收发描述符的OWN位有没有变化。如果发送描述符的OWN位一直是 1,说明 DMA 没启动。检查ETH->DMAOMR的ST和SR位有没有置 1。另外,MAC 地址不要用广播地址,用本地管理地址。
4.2 现象:周期运行一段时间后,从站报看门狗超时
原因:主站的周期抖动太大,或者接收处理太慢,导致从站等不到下一个帧。STM32 的 ETH 中断优先级如果低于定时器中断,接收会被延迟。另外,ec_receive_processdata如果只调用一次,可能收不全所有从站的数据。
解决:把 ETH 接收中断优先级设到最高,定时器中断次之。在周期函数里,ec_receive_processdata至少调用两次,或者用中断方式接收,收到一帧就置标志位。周期时间先设 2ms,稳定后再往 1ms 压。如果还是超时,检查IOmap的大小,别映射太多数据。
4.3 现象:伺服电机使能后抖动或飞车
原因:PDO 映射的数据类型和从站定义的不一致,比如把int32当int16解析,或者字节序搞反了。EtherCAT 是小端模式,STM32 也是小端,但如果你用htons转换过,就会出错。另外,分布式时钟没配好,主站和从站的时间偏差太大,也会导致抖动。
解决:用 SOEM 的ec_slave结构体打印每个从站的 PDO 映射,确认偏移量和数据类型。不要手动转换字节序,直接按小端读写。如果从站支持 DC,确保ec_configdc在ec_config_map之后调用,并且ec_slave[0].hasdc为真。伺服使能前,先让从站进入 OP 状态,再发目标位置。
4.4 现象:编译报错,osal里的函数找不到定义
原因:SOEM 的osal层默认依赖 Linux 的头文件,比如pthread.h、sys/time.h。在 STM32 的裸机环境里,这些都不存在。
解决:把osal.c里跟线程、文件系统相关的函数全部删掉,只保留osal_current_time、osal_mutex_lock、osal_mutex_unlock这几个。用 STM32 的HAL_GetTick实现osal_current_time,用空宏实现互斥锁。如果用了 FreeRTOS,可以用信号量实现互斥锁。另外,oshw里的nicdrv.c也要改,把osal_eth_send和osal_eth_recv换成 STM32 的 ETH 驱动。
4.5 现象:主站能扫描到从站,但进不了 OP 状态
原因:从站的邮箱通信没配好,或者 PDO 映射参数不对。有些伺服驱动器需要先写特定的 SDO 参数才能进入 OP,比如设置同步模式、配置 PDO 映射。另外,ec_config_map之后,IOmap的指针可能没对齐,导致从站拒绝数据。
解决:用ec_SDOread和ec_SDOwrite检查从站的 SDO 参数,确认 PDO 映射和同步模式。IOmap建议用__attribute__((aligned(4)))对齐。如果从站支持 DC,确保ec_configdc返回的偏移量正确。进 OP 之前,先让从站进入 SAFE_OP,再切 OP。如果还是不行,用 SOEM 的ec_slave结构体打印错误码。
5. 进阶技巧:用分布式时钟和 PDO 轮询把周期压到 1ms 以内
5.1 分布式时钟的配置与验证
分布式时钟是 EtherCAT 实现高精度同步的关键。主站需要发送一个广播读命令,测量每个从站的时钟偏移和传输延迟,然后写入从站的 DC 寄存器。SOEM 的ec_configdc会自动完成这个过程,但前提是从站支持 DC。配置完成后,你可以用ec_slave[i].dc结构体查看每个从站的偏移量。验证方法:让主站和从站同时输出一个方波,用示波器看两个方波的相位差,如果小于 1 微秒,说明 DC 配好了。如果相位差很大,检查ec_configdc的调用时机,必须在ec_config_map之后、进 OP 之前。
5.2 PDO 轮询与中断的取舍
SOEM 默认用轮询方式接收过程数据,但在 STM32 上,轮询会占用大量 CPU 时间。我一般用 ETH 接收中断,收到帧后置一个标志位,主循环或定时器任务里再调用ec_receive_processdata。这样 CPU 占用率能降下来,周期也能压得更短。但要注意,中断里不要直接调用 SOEM 的函数,因为 SOEM 不是线程安全的。中断只负责把数据从 ETH 描述符拷贝到临时缓冲区,然后置标志位。主循环里再处理。下面是一个中断处理的示例:
// ETH 接收中断服务函数 void ETH_IRQHandler(void) { if (ETH->DMASR & ETH_DMASR_RS) { // 清除接收状态标志 ETH->DMASR = ETH_DMASR_RS; // 置接收标志位,通知主循环 eth_rx_flag = 1; } }主循环里检查eth_rx_flag,如果置位,就调用ec_receive_processdata。注意,ec_receive_processdata内部会遍历接收描述符,所以你要确保 ETH 的接收描述符没有被 DMA 覆盖。我一般会在中断里把数据拷贝到另一个缓冲区,然后释放描述符,这样 DMA 可以继续接收。
5.3 把周期压到 1ms 的参数调优清单
| 参数 | 建议值 | 说明 |
|---|---|---|
| ETH 接收中断优先级 | 最高 | 高于定时器中断,确保接收及时 |
| 定时器周期 | 1000us | 1ms 周期,先跑 2ms 稳定后再压 |
| 接收描述符数量 | 8 | 太少会丢帧,太多占内存 |
| 发送描述符数量 | 4 | 发送帧不多,4 个够用 |
EC_TIMEOUTRET | 2000 | 接收超时,单位微秒 |
ec_receive_processdata调用次数 | 2 | 至少两次,确保收全 |
IOmap对齐 | 4 字节 | 用aligned(4)属性 |
| CPU 主频 | 168MHz 以上 | F407 起步,H7 更稳 |
调优的顺序是:先保证 2ms 周期稳定运行 10 分钟不报错,再把定时器改成 1ms,观察wkc是否稳定。如果wkc波动,先降回 2ms,检查 ETH 中断延迟和ec_receive_processdata的调用时机。另外,把不用的外设中断关掉,减少干扰。我习惯在周期任务里翻转一个 GPIO,用示波器看周期抖动,如果抖动超过 100us,说明中断被阻塞了,需要调整优先级。
从那以后我每次移植 SOEM 到新平台,都强制先跑一遍ec_config_init和ec_config_map,确认从站扫描和 PDO 映射没问题,再动周期任务。这个顺序能省下大量排查时间。希望帮到你。
本文还有配套的精品资源,点击获取