☰
STM32移植SOEM EtherCAT主站控制伺服电机实战指南
2026/10/20 13:32:14 网站建设 项目流程

简介:这份资源面向嵌入式工程师、运动控制开发者与高校自动化方向学生,提供将开源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 全双工的以太网外设。

我选的是 STM32F407ZGT6,192KB 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 到 STM32:osal 与 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 周期稳不稳就看这里

多轴伺服要同步,必须开 DC(Distributed 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 映射长度和从站实际配置不匹配,或者 SM(SyncManager)配置不对。也有可能是从站需要先写特定参数才允许进 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 wkc=3

如果状态卡在 SAFEOP,说明 PDO 映射或 DC 配置还有问题,回第 5 章查。

第二步,Wireshark 抓包看帧间隔。把 PC 的网卡接在 EtherCAT 总线的镜像口上(或者用支持端口镜像的交换机),过滤eth.type == 0x88a4。正常 1ms 周期下,你应该看到每 1ms 一帧的过程数据帧,帧长随 PDO 数量变化。如果帧间隔忽大忽小,说明主循环周期不稳,回去检查定时器配置。

第三步,用ec_readstate()周期性打印从站状态字和实际位置。伺服使能后手动转一下电机轴,看actual_pos有没有变化。如果数值在动但方向反了,改一下 PDO 里的位置符号或者驱动器参数。

/* 验证用的状态打印 */ printf("slave1 status=0x%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,都强制走一遍抓包验证,不再只看串口打印就下结论。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询