STM32移植SOEM EtherCAT主站控制伺服电机实战指南
简介这份资源面向嵌入式工程师、运动控制开发者与高校自动化方向学生提供将开源SOEM协议栈移植到STM32平台、构建EtherCAT主站的完整工程源码。EtherCAT作为高精度运动控制领域的实时工业以太网协议通常依赖Linux主站而本工程让STM32F767等高性能MCU直接承担主站角色配合伺服电机完成PDO/SDO通信与PDO映射验证降低了开发门槛。压缩包共267个文件约1.63MB以139个.h头文件与119个.c源文件为主体涵盖STM32F7 HAL库、以太网驱动与SOEM适配代码另含uvprojx/uvoptx Keil工程、hex烧录文件、s启动文件及bat脚本便于直接编译与调试。目前已有18343人学习下载。读者可从中获取主站移植的初始化流程、中断与RTOS集成思路、实时性优化要点以及针对通信bug的日志分析与排错参考并在此基础上扩展电机控制等应用层算法。1. 从一块吃灰的 STM32 板子说起为什么我劝你先别急着焊伺服手头这块 STM32F407 开发板在抽屉里躺了大半年直到某天接到一个运动控制的小活——三轴伺服要求 1ms 总线周期抖动还得压得住。PLC 方案报价太高商用主站协议栈授权费也不便宜翻来覆去最后盯上了 SOEM 这个开源 EtherCAT 主站方案。EtherCAT 主站、SOEM、STM32、伺服电机这几个词凑一块网上资料看着不少真动手才发现坑全在细节里PHY 芯片选错、时钟没配对、PDO 映射对不上、从站死活进不了 OP 状态。这篇笔记就是把我从零搭这套环境的过程完整记下来包括版本怎么选、代码怎么改、配置怎么调、跑不通怎么查。适合手里有 STM32 板子、想用 SOEM 做 EtherCAT 主站控制伺服电机的开发者新手照着能走通熟手可以重点看版本差异和参数取舍那几段。2. 环境准备SOEM 版本、STM32 型号与网络硬件的选型清单2.1 SOEM 版本怎么选1.4.0 和 2.0 的取舍SOEM 目前主流两个分支1.4.0 稳定版和 2.0 开发版。我一开始图新用了 2.0结果在 STM32 上编译时发现它默认依赖了一些 POSIX 接口移植层要改的地方比 1.4.0 多不少。后来退回 1.4.0配合osal和oshw两层抽象移植工作量明显小。具体来说1.4.0 的目录结构是这样的soem/ ├── soem/ # 协议栈核心 ├── osal/ # 操作系统抽象层 │ ├── linux/ │ ├── win32/ │ └── intime/ └── oshw/ # 硬件抽象层 ├── linux/ ├── win32/ └── macosx/我们要做的就是新增osal/stm32和oshw/stm32两个目录把底层收发和定时器接口对接上去。2.0 版本虽然 API 更现代但它的ecx_系列函数改动较大网上能找到的 STM32 移植参考大多基于 1.4.0遇到问题更容易搜到答案。提示如果你不是非要追新特性1.4.0 是更稳妥的选择。我用的具体版本是 SOEM 1.4.0从官方仓库拉取即可。2.2 STM32 型号与 PHY 芯片的硬性要求不是所有 STM32 都能跑 EtherCAT 主站。核心要求两点一是要有足够的 RAM 放过程数据映像二是要有一路能跑 100Mbps 全双工的以太网外设。我选的是 STM32F407ZGT6192KB SRAM自带 10/100M MAC外挂一颗 LAN8720A PHY。这里有个坑LAN8720A 的 RMII 参考时钟需要 50MHz可以从 STM32 的 MCO 引脚输出也可以由 PHY 自己产生。我一开始让 PHY 自产时钟结果链路能起来但丢包严重后来改成 STM32 MCO1 输出 50MHz 给 PHY问题消失。如果你用的是 STM32H743 这类带 FMC 的型号也可以走外扩以太网方案但软件移植复杂度会上升。新手建议从 F407 LAN8720A 这个组合起步资料最多。2.3 软件工具链清单工具版本用途STM32CubeMX6.x生成 HAL 初始化代码ARM GCC10.3-2021.10交叉编译OpenOCD0.11 以上烧录调试SOEM1.4.0EtherCAT 主站协议栈Wireshark3.x EtherCAT 插件总线抓包分析工具链装好后先确认arm-none-eabi-gcc -v能正常输出版本号OpenOCD 能连上板子。这两步没过后面全是白搭。3. 移植 SOEM 到 STM32osal 与 oshw 两层怎么改3.1 新建 osal/stm32定时器与临界区实现SOEM 的osal层需要你提供几个基础能力毫秒级延时、微秒级延时、定时器获取、互斥锁单核可以空实现。在 STM32 上我用 TIM2 做微秒计时SysTick 做毫秒延时。新建osal/stm32/osal.c核心实现如下/* osal/stm32/osal.c */ #include osal.h #include stm32f4xx_hal.h /* 微秒延时基于 DWT 计数器比 TIM 方案更省外设 */ void osal_usleep(uint32 usec) { uint32_t start DWT-CYCCNT; uint32_t ticks usec * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); } /* 毫秒延时直接调 HAL */ void osal_msleep(uint32 msec) { HAL_Delay(msec); } /* 获取当前微秒数用于超时判断 */ uint32 osal_current_time(void) { return (uint32)(DWT-CYCCNT / (SystemCoreClock / 1000000U)); } /* 单核环境互斥锁空实现 */ void osal_mutex_lock(void) {} void osal_mutex_unlock(void) {}DWT 是 Cortex-M 内核自带的调试监视器用它做微秒延时不需要额外占用定时器。注意要在初始化时使能 DWT/* 在 main 初始化里加这段 */ CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;osal_current_time返回的是微秒计数SOEM 内部用它做超时判断。如果你发现从站扫描总是超时先检查这个函数返回值对不对。3.2 新建 oshw/stm32网卡收发对接oshw层负责实际的数据帧收发。SOEM 期望你实现oshw_send和oshw_recv两个函数以及一个oshw_get_buffer返回发送缓冲区指针。STM32F407 的以太网外设用 HAL 库驱动我采用轮询方式收发避免中断嵌套带来的时序抖动/* oshw/stm32/oshw.c */ #include oshw.h #include stm32f4xx_hal.h extern ETH_HandleTypeDef heth; static uint8_t tx_buf[EC_BUFSIZE]; static uint8_t rx_buf[EC_BUFSIZE]; /* 返回发送缓冲区SOEM 往里填数据 */ uint8_t *oshw_get_buffer(void) { return tx_buf; } /* 发送一帧len 为帧长度 */ int oshw_send(uint8_t *buf, int len) { if (HAL_ETH_Transmit(heth, (ETH_BufferTypeDef *)buf, len, 100) ! HAL_OK) return 0; return len; } /* 接收一帧返回实际长度0 表示无数据 */ int oshw_recv(uint8_t *buf, int timeout_us) { uint32_t start osal_current_time(); while ((osal_current_time() - start) timeout_us) { if (HAL_ETH_GetReceivedFrame(heth) HAL_OK) { uint32_t len heth.RxFrameInfos.length; memcpy(buf, (void *)heth.RxFrameInfos.buffer, len); HAL_ETH_ReleaseRxFrame(heth); return len; } } return 0; }这里有个关键点EtherCAT 帧是裸以太网帧类型字段是0x88A4不走 IP 层。所以 STM32 的 MAC 要配置成能接收所有帧或者至少能识别0x88A4类型。我在 CubeMX 里把以太网外设的Receive All Packets打开省得过滤规则没配对导致丢帧。另外EC_BUFSIZE在 SOEM 里默认是 1518但如果你要跑大 PDO 或者多从站建议改成 2048 甚至更大。改的位置在soem/ethercattype.h里。3.3 编译配置把新目录加进构建系统SOEM 默认用 CMake 构建但 STM32 这边我直接用 Makefile 把需要的源文件编进去。需要加入编译的 SOEM 文件清单soem/ethercatbase.c soem/ethercatcoe.c soem/ethercatconfig.c soem/ethercatdc.c soem/ethercatfoe.c soem/ethercatmain.c soem/ethercatprint.c soem/ethercatsoe.c osal/stm32/osal.c oshw/stm32/oshw.c在 Makefile 里加-I路径指向soem/、osal/stm32/、oshw/stm32/然后编译。如果报undefined reference to osal_mutex_lock之类的错说明 osal 文件没编进去或者函数签名对不上回去检查头文件里的声明。4. 配置主站参数从站扫描、PDO 映射与 DC 同步4.1 从站扫描与状态机推进主站初始化流程按 SOEM 的标准套路走/* 主循环前的初始化 */ ec_init(stm32_eth); /* 绑定我们实现的网卡 */ if (ec_config_init(FALSE) 0) { printf(no slave found\r\n); while (1); } printf(%d slaves found\r\n, ec_slavecount); /* 配置过程数据映射 */ ec_config_map(IOmap); ec_configdc(); /* 配置分布式时钟 */ /* 请求进入 OP 状态 */ ec_statecheck(0, EC_STATE_OPERATIONAL, EC_TIMEOUTSTATE * 4);ec_config_init的参数FALSE表示不跳过未配置的从站。如果你只接了伺服驱动器一般能直接扫到。扫不到的话先确认物理链路——用 Wireshark 抓一下看有没有0x88A4的帧发出去。ec_config_map会把所有从站的输入输出映射到IOmap这块内存里。IOmap的大小要提前算好我一般给 4096 字节三轴伺服够用。4.2 PDO 映射伺服电机最容易翻车的地方PDO 映射是 EtherCAT 主站控制伺服的核心。每个伺服驱动器的 PDO 配置不一样但套路相同把控制字、目标位置、目标速度这些输出对象映射到 RxPDO把状态字、实际位置、实际速度映射到 TxPDO。以某款支持 CiA402 协议的伺服为例典型映射如下/* 假设从站 1 是伺服偏移量从 IOmap 里算 */ uint8_t *out ec_slave[1].outputs; uint8_t *in ec_slave[1].inputs; /* RxPDO主站发给伺服 */ *(uint16_t *)(out 0) control_word; /* 控制字 0x6040 */ *(int32_t *)(out 2) target_pos; /* 目标位置 0x607A */ *(int32_t *)(out 6) target_vel; /* 目标速度 0x60FF */ /* TxPDO伺服回给主站 */ status_word *(uint16_t *)(in 0); /* 状态字 0x6041 */ actual_pos *(int32_t *)(in 2); /* 实际位置 0x6064 */ actual_vel *(int32_t *)(in 6); /* 实际速度 0x606C */偏移量必须和从站的 PDO 配置严格对应。我踩过的坑是驱动器出厂默认 PDO 映射和手册里写的不一样导致控制字写进去没反应。解决办法是用ec_readPDOmap把实际映射读出来打印对照着改偏移。注意不同品牌的伺服 PDO 映射差别很大一定要先读从站的对象字典确认不要照抄别家的偏移量。4.3 分布式时钟配置1ms 周期稳不稳就看这里多轴伺服要同步必须开 DCDistributed Clocks。ec_configdc()会自动配置从站的 DC 参数但主站这边还要做两件事一是把ec_slave[0].hasdc置位二是在主循环里调用ec_send_processdata和ec_receive_processdata的时机要跟 DC 同步。我用的 1ms 周期主循环结构while (1) { /* 等 DC 同步信号 */ ec_send_processdata(); wkc ec_receive_processdata(EC_TIMEOUTRET); if (wkc expected_wkc) { /* 工作计数不对说明有从站没响应 */ error_count; } /* 控制逻辑 */ servo_update(); osal_msleep(1); /* 简单周期控制要求高时用定时器 */ }wkc是工作计数正常应该等于从站数量加一。如果持续偏小先查线缆和从站供电再查 DC 配置。5. 避坑与排查从站进不了 OP、丢帧、PDO 对不上怎么办5.1 从站一直停在 PREOP 状态现象ec_statecheck返回的状态一直是 PREOP进不了 SAFEOP 或 OP。原因最常见的是 PDO 映射长度和从站实际配置不匹配或者 SMSyncManager配置不对。也有可能是从站需要先写特定参数才允许进 OP。解决先用ec_readstate()打印每个从站的状态码对照 ETG 规范查错误码含义。如果是 SM 配置问题检查ec_config_map之后ec_slave[i].SM的长度是否和从站手册一致。有些伺服需要先通过 SDO 写0x1C12和0x1C13来激活 PDO 映射这一步不能省。5.2 工作计数 wkc 不稳定偶尔少 1现象大部分周期wkc正常但每隔几十个周期就少 1伺服偶尔报位置偏差过大。原因我遇到的是 LAN8720A 的时钟抖动导致偶发丢帧。另外如果主循环里osal_msleep(1)被其他中断打断周期会拉长DC 同步就会偏。解决把 PHY 时钟源改成 STM32 MCO 输出同时在主循环里用硬件定时器触发发送不要依赖HAL_Delay。如果还不行在oshw_recv里加统计计数看是发送丢还是接收丢。5.3 PDO 数据写进去没反应现象控制字写了但伺服不动状态字也没变化。原因偏移量算错了或者 PDO 映射里根本没包含你要写的对象。解决用ec_readPDOmap读出实际映射打印每个对象的索引和长度然后重新算偏移。我一般会在初始化时把映射表打印出来确认无误再进主循环。5.4 编译时报 osal 函数重复定义现象链接阶段报multiple definition of osal_current_time。原因SOEM 源码里可能已经包含了一个默认的 osal 实现你又加了自己的两个一起编进去了。解决检查 Makefile 里是不是把osal/linux或osal/win32也编进去了只保留osal/stm32。5.5 上电后从站扫描不到任何设备现象ec_config_init返回 0一个从站都没扫到。原因物理层问题居多——网线没插好、PHY 没复位、MAC 没使能。也可能是 EtherCAT 帧类型没配对。解决先用 Wireshark 抓包看 STM32 有没有发出0x88A4的帧。如果没有检查oshw_send有没有被调用如果有发但没回检查从站供电和网线。我遇到过一次是 CubeMX 里以太网引脚配置错了RMII 的 REF_CLK 引脚没设成正确模式。6. 装完怎么验证用 Wireshark 抓包确认主站真的跑起来了代码编过、烧进去、串口有打印不代表 EtherCAT 主站真的在工作。我一般用三步验证法缺一步都不算跑通。第一步串口打印从站数量和状态。正常输出应该是这样的2 slaves found Slave 1: PREOP - SAFEOP - OP Slave 2: PREOP - SAFEOP - OP wkc3如果状态卡在 SAFEOP说明 PDO 映射或 DC 配置还有问题回第 5 章查。第二步Wireshark 抓包看帧间隔。把 PC 的网卡接在 EtherCAT 总线的镜像口上或者用支持端口镜像的交换机过滤eth.type 0x88a4。正常 1ms 周期下你应该看到每 1ms 一帧的过程数据帧帧长随 PDO 数量变化。如果帧间隔忽大忽小说明主循环周期不稳回去检查定时器配置。第三步用ec_readstate()周期性打印从站状态字和实际位置。伺服使能后手动转一下电机轴看actual_pos有没有变化。如果数值在动但方向反了改一下 PDO 里的位置符号或者驱动器参数。/* 验证用的状态打印 */ printf(slave1 status0x%04X pos%d vel%d\r\n, *(uint16_t *)(ec_slave[1].inputs 0), *(int32_t *)(ec_slave[1].inputs 2), *(int32_t *)(ec_slave[1].inputs 6));这三步走完基本能确认主站从物理层到应用层都通了。从那以后我每次移植完 SOEM都强制走一遍抓包验证不再只看串口打印就下结论。希望帮到你。本文还有配套的精品资源点击获取