简介:这份资源面向嵌入式与工业自动化方向的开发者,提供将开源SOEM方案移植到STM32平台、构建EtherCAT主站的完整工程代码,可用于伺服电机等从站设备的实时通信与运动控制实践。资源包共267个文件,以139个h头文件与119个c源文件为主体,涵盖STM32F7系列HAL库驱动、以太网MAC与PHY配置、SOEM协议栈适配及RTOS集成代码,另含uvprojx与uvoptx工程文件、hex烧录固件、s启动文件及bat编译脚本等,压缩包约1.63MB,可直接用Keil打开编译。目前已有18343人学习下载,适合希望掌握EtherCAT主站开发、PDO与SDO通信调试及实时性优化的中高级工程师参考,也能帮助读者理解从底层驱动到应用层电机控制的完整链路,并借鉴作者在移植与排错中积累的思路。
1. 从零搭建 EtherCAT 主站:为什么我选了 STM32 + SOEM 这套组合
第一次在 STM32 上跑 EtherCAT 主站,我踩的坑不是代码写错,而是版本没对齐。当时手头一块 STM32F407 开发板,网上找了一份 SOEM 1.3.1 的移植例程,编译通过、烧录成功,但伺服电机就是不响应,PDO 数据全是零。折腾两天才发现,问题出在 LAN8720 PHY 的地址配置和 SOEM 的 ecx_setupnic 参数对不上。EtherCAT 主站和普通网络通信不一样,它对时序和底层网卡驱动的要求非常苛刻,STM32 上跑 SOEM 属于典型的“资源受限 + 实时性要求高”场景,装不好连从站都扫不到。这套方案适合做运动控制的嵌入式开发者,尤其是需要多轴伺服同步、又不想上专用主站芯片的项目。下面把我从环境准备到跑通验证的完整过程拆开讲,照着走能少走不少弯路。
2. 环境准备与版本选型:STM32 型号、SOEM 版本、PHY 芯片怎么搭
2.1 STM32 型号选择与资源评估
SOEM 主站对 MCU 的要求集中在三点:RAM 够大、有以太网外设、主频别太低。我实测下来,STM32F407 是最低门槛,192KB RAM 里 SOEM 协议栈加缓冲区大概吃掉 40~60KB,剩下给应用层。如果从站数量超过 8 个,或者 PDO 数据量大,建议直接上 STM32F429 或 F767,RAM 到 256KB 以上会从容很多。
| MCU 型号 | RAM | 以太网 | 主频 | 适用场景 |
|---|---|---|---|---|
| STM32F407 | 192KB | 有 | 168MHz | 3~5 轴,小数据量 |
| STM32F429 | 256KB | 有 | 180MHz | 5~8 轴,中等 PDO |
| STM32F767 | 512KB | 有 | 216MHz | 8 轴以上,大数据量 |
我一般会留出 30% 的 RAM 余量,因为 SOEM 在运行过程中会有动态内存分配,尤其是 ec_config_init 阶段。如果 RAM 吃紧,可以把 EC_MAXEEPBUF 和 EC_MAXBUF 调小,但别低于 128,否则从站多的时候会丢帧。
2.2 SOEM 版本差异与获取方式
SOEM 目前主流版本是 1.3.1 和 1.4.0。1.3.1 稳定但功能少,1.4.0 增加了对 DC 同步更好的支持,但移植时对底层网卡驱动的接口有改动。我建议新手先用 1.3.1 跑通,再考虑升级。源码从官方仓库拿,不要用来路不明的移植包,很多坑都是别人改了一半留下的。
# 获取 SOEM 源码,指定 1.3.1 标签 git clone https://github.com/OpenEtherCATsociety/SOEM.git cd SOEM git checkout v1.3.1代码块里就是标准的源码拉取流程,checkout 到 v1.3.1 是为了避开 1.4.0 里新增的 ecx_configdc 接口变动。如果你用的是 STM32CubeIDE,直接把 soem 目录下的 src 和 include 拷进工程,别用 Makefile 编译,否则中断向量表容易冲突。
2.3 PHY 芯片与硬件连接确认
STM32 自带的 MAC 需要外接 PHY,常见的是 LAN8720 和 DP83848。LAN8720 便宜但地址配置容易错,DP83848 贵一点但稳定。硬件上要确认 RMII 接口的 50MHz 时钟来源,如果是 STM32 输出,PHY 的 REF_CLK 要接对;如果是外部晶振,STM32 的 ETH_RMII_REF_CLK 要配置成输入模式。
// LAN8720 地址配置,PHY_ADDRESS 根据硬件原理图确定 #define LAN8720_PHY_ADDRESS 0x00 // 常见值,也可能是 0x01 // 在 ethernetif.c 中初始化时传入 heth.Init.PhyAddress = LAN8720_PHY_ADDRESS;这段配置决定了 STM32 能不能和 PHY 通信。PhyAddress 由 PHY 芯片的 PHYAD0 引脚电平决定,悬空或接地对应不同地址。我遇到过一块板子地址是 0x01,但例程默认 0x00,结果 ETH_ReadPHYRegister 一直返回 0xFFFF,查了半天才发现是硬件跳线问题。
3. 移植 SOEM 到 STM32:从网卡驱动到定时器配置
3.1 网卡驱动适配:ecx_setupnic 与底层收发对接
SOEM 本身不直接操作寄存器,它通过 nic 结构体里的函数指针调用底层收发。移植的核心就是实现这几个函数:发送、接收、获取链路状态。STM32 上用 HAL 库的 ETH_Transmit 和 ETH_Receive 包一层就行。
// 底层发送函数,对接 SOEM 的 ecx_setupnic int stm32_send(void *ptr, int size) { HAL_StatusTypeDef ret; ret = HAL_ETH_Transmit(&heth, (uint8_t *)ptr, size, 100); // 100ms 超时 if (ret != HAL_OK) { return 0; // 发送失败返回 0,SOEM 会重试 } return size; } // 底层接收函数,注意要处理非阻塞情况 int stm32_recv(void *ptr, int size) { uint32_t len = 0; HAL_ETH_GetReceivedFrame_IT(&heth); // 中断方式接收 if (heth.RxFrameInfos.length > 0) { len = heth.RxFrameInfos.length; memcpy(ptr, heth.RxFrameInfos.buffer, len); HAL_ETH_ReleaseRxFrame(&heth); } return len; }发送函数里超时设 100ms 是经验值,EtherCAT 周期通常 1~10ms,超时太长会阻塞主循环。接收函数用中断方式,避免轮询浪费 CPU。注意 memcpy 的长度要判断,不能超过 SOEM 给的缓冲区大小,否则会踩内存。
3.2 定时器与中断优先级配置
EtherCAT 主站需要周期性发送过程数据,我用 TIM2 做 1ms 中断,在中断里调用 ec_send_processdata 和 ec_receive_processdata。中断优先级必须高于串口和普通 GPIO,但低于以太网中断,否则会丢包。
// TIM2 中断配置,1ms 周期 void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); // 发送过程数据 ec_send_processdata(); // 接收过程数据,超时 200us wkc = ec_receive_processdata(EC_TIMEOUTRET); if (wkc < expected_wkc) { // 工作计数器不匹配,记录错误 error_count++; } } }中断里只做收发和计数,复杂逻辑放到主循环。ec_receive_processdata 的超时参数 EC_TIMEOUTRET 默认是 2000us,我改成 200us 是为了避免中断执行时间过长。如果从站响应慢,可以适当放宽,但别超过 500us,否则会影响下一个周期。
3.3 内存与缓冲区配置
SOEM 的缓冲区大小在 ec_config.h 里定义,STM32 上要按实际从站数量调整。EC_MAXBUF 默认 128,如果从站有 16 个,每个 PDO 64 字节,那至少要 1024。我一般会算一下总 PDO 长度,然后乘 1.5 倍留余量。
// ec_config.h 中的关键配置 #define EC_MAXBUF 512 // 根据从站数量调整 #define EC_MAXEEPBUF 1024 // EEPROM 缓冲区,一般不用改 #define EC_MAXSM 8 // 每个从站的最大 SM 数量 #define EC_MAXPORT 4 // 端口数,STM32 单网口设为 1 也行EC_MAXBUF 改大后要确认 RAM 够用,编译完看 map 文件里 .bss 段大小。如果超了,先把 EC_MAXEEPBUF 减小,那个只在配置阶段用,跑起来后可以释放。
4. 配置从站与 PDO 映射:让伺服电机真正动起来
4.1 扫描从站与状态机切换
上电后第一步是 ec_config_init,它会扫描总线上的从站并分配地址。如果这一步失败,后面都白搭。常见问题是网线质量差或者从站供电不足,导致扫描不到。
// 初始化 SOEM 并扫描从站 if (ec_init("eth0") <= 0) { // STM32 上通常传 NULL 或自定义接口名 printf("ec_init failed\n"); return -1; } if (ec_config_init(FALSE) <= 0) { printf("no slaves found\n"); return -1; } printf("%d slaves found\n", ec_slavecount); // 切换到 PRE-OP 状态,配置 PDO 映射 ec_statecheck(0, EC_STATE_PRE_OP, EC_TIMEOUTSTATE);ec_config_init 的参数 FALSE 表示不等待所有从站进入 INIT 状态,STM32 上建议用 FALSE,避免卡死。扫描到从站后,用 ec_statecheck 等所有从站进入 PRE-OP,超时时间 EC_TIMEOUTSTATE 默认 2000ms,够用。
4.2 PDO 映射与伺服使能
伺服电机的 PDO 映射决定了控制字、状态字、目标位置这些数据在过程数据里的偏移。不同品牌的伺服映射不一样,要对着手册配。我一般先用 ec_config_map 自动映射,然后打印出来核对。
// 配置 PDO 映射 ec_config_map(&IOmap); // 打印每个从站的 PDO 偏移 for (int i = 1; i <= ec_slavecount; i++) { printf("slave %d: output %d bytes, input %d bytes\n", i, ec_slave[i].Obytes, ec_slave[i].Ibytes); } // 切换到 SAFE-OP 再切 OP ec_statecheck(0, EC_STATE_SAFE_OP, EC_TIMEOUTSTATE); ec_slave[0].state = EC_STATE_OPERATIONAL; ec_send_processdata(); ec_receive_processdata(EC_TIMEOUTRET); ec_writestate(0);ec_config_map 把 PDO 数据映射到 IOmap 数组,之后读写 IOmap 就是读写从站。Obytes 和 Ibytes 是输出和输入字节数,如果和手册对不上,说明映射错了。切 OP 之前必须先切 SAFE-OP,否则从站会报错。
4.3 伺服使能与运动控制
让电机转起来需要发控制字 0x06(Shutdown)→ 0x07(Switch On)→ 0x0F(Enable Operation),然后给目标位置。这些值通过 IOmap 写进去。
// 假设第一个从站是伺服,输出偏移 0 uint16_t *control_word = (uint16_t *)(IOmap + ec_slave[1].outputs); int32_t *target_pos = (int32_t *)(IOmap + ec_slave[1].outputs + 2); *control_word = 0x06; // Shutdown ec_send_processdata(); ec_receive_processdata(EC_TIMEOUTRET); HAL_Delay(10); *control_word = 0x07; // Switch On ec_send_processdata(); ec_receive_processdata(EC_TIMEOUTRET); HAL_Delay(10); *control_word = 0x0F; // Enable Operation *target_pos = 10000; // 目标位置 ec_send_processdata(); ec_receive_processdata(EC_TIMEOUTRET);控制字和状态字的位定义要查伺服手册,不同品牌有差异。目标位置是增量还是绝对,取决于伺服模式。如果电机不转,先看状态字是否到了 Operation Enabled,再看目标位置有没有写对。
5. 避坑与常见问题:从站扫不到、PDO 不更新、电机抖动
5.1 从站扫不到,ec_config_init 返回 0
现象:串口打印 “no slaves found”,但网线插好了,从站供电也正常。
原因:STM32 的 RMII 时钟配置错误,或者 PHY 地址不对。LAN8720 需要 50MHz 时钟,如果 STM32 的 ETH_RMII_REF_CLK 没输出,PHY 根本不工作。
解决:用示波器量 PHY 的 REF_CLK 引脚,没有 50MHz 就检查 RCC 配置。另外确认 PHY 地址,用 ETH_ReadPHYRegister 读 ID 寄存器,能读到 0x0007 和 0xC0F1 说明通信正常。
5.2 PDO 数据不更新,工作计数器不变
现象:ec_receive_processdata 返回的 wkc 一直是 0,IOmap 里的数据不动。
原因:从站没进入 OP 状态,或者 PDO 映射和从站实际配置不匹配。
解决:先 ec_statecheck 确认所有从站是 OP。如果状态不对,检查 ec_config_map 之后的 ec_writestate 有没有执行。另外用 ec_slave[i].Obytes 和手册对比,映射错了就手动改 ec_config_pdo 里的条目。
5.3 电机抖动或飞车
现象:使能后电机剧烈抖动,或者直接冲到限位。
原因:控制字切换太快,或者目标位置单位搞错。有些伺服默认是增量模式,给绝对位置会飞车。
解决:控制字切换之间加 10ms 以上延时,别连续发。目标位置先给 0,确认电机锁死后再给小的增量。如果是绝对模式,先回零。
5.4 定时器中断里收发导致丢包
现象:运行一段时间后 wkc 偶尔变小,电机有异响。
原因:中断优先级配置不当,以太网中断被 TIM2 打断,导致接收不完整。
解决:把 ETH 中断优先级设得比 TIM2 高,或者用 DMA 接收。我一般会把 ETH 中断设为抢占优先级 1,TIM2 设为 2,这样以太网先处理。
5.5 编译报错 EC_MAXBUF 溢出
现象:编译时提示 .bss 段超出 RAM。
原因:EC_MAXBUF 设太大,或者从站数量多但缓冲区没算对。
解决:先算总 PDO 长度,EC_MAXBUF 设为总长度的 1.5 倍。如果 RAM 还是不够,把 EC_MAXEEPBUF 从 1024 降到 512,那个只在配置阶段用。
6. 装完怎么验证跑通:用 Wireshark 抓包和状态字判断
验证 EtherCAT 主站是否跑通,最直接的方法是看工作计数器和从站状态字。我一般会先让电机使能但不转,观察状态字是否稳定在 0x0027(Operation Enabled),然后给一个小的位置增量,看电机是否平滑转动。
// 验证代码:打印状态字和工作计数器 uint16_t *status_word = (uint16_t *)(IOmap + ec_slave[1].inputs); printf("status: 0x%04X, wkc: %d\n", *status_word, wkc); // 状态字 0x0027 表示 Operation Enabled if ((*status_word & 0x006F) == 0x0027) { printf("servo enabled\n"); } else { printf("servo not ready, status=0x%04X\n", *status_word); }状态字的位定义:bit0-3 是状态,bit4 是电压使能,bit5 是快速停止,bit6 是开关使能。0x0027 表示电压使能、操作使能、无故障。如果状态字是 0x0021,说明还没使能;0x0028 说明有故障,要查故障码。
另一个验证手段是用 Wireshark 抓 EtherCAT 帧。STM32 上不方便直接抓,可以在主站和从站之间串一个支持端口镜像的交换机,用 PC 抓包。正常的 EtherCAT 周期帧长度在 60~1500 字节之间,如果看到大量重传或帧长度异常,说明底层驱动有问题。
我还会做一个压力测试:让电机以 1ms 周期做正弦运动,跑 10 分钟,统计 wkc 错误次数。如果错误次数为 0,说明时序和缓冲区都配对了。如果有错误,先看是不是中断优先级问题,再查网线屏蔽层有没有接地。
从那以后我每次移植 SOEM 到新板子,都强制走一遍“扫描从站 → 切 OP → 读状态字 → 小增量运动 → 压力测试”这个流程,少一步都可能留下隐患。希望帮到你。
本文还有配套的精品资源,点击获取