简介针对STM32F767IGT6正点原子阿波罗开发板的EtherCAT主站SOEM工程源码适合工业以太网运动控制方向的嵌入式开发者。工程完整实现MAC初始化、EtherCAT网络配置、从站状态机轮询并采用CSP周期同步位置模式驱动单个电机完成正转、反转、停止及运行中急停按键KEY0使能、KEY1反转、KEY2正转、KEY_UP停止代码已逐行添加中文注释便于理解主站调度与CSP指令交互流程。压缩包共470个文件以144个H头文件、121个C源文件、60个D依赖文件及60个O目标文件为主同时包含Keil工程配置、链接脚本、Hex固件等相关文件整体约25.95MB。已有456人学习浏览目录结构清晰可直接基于正点原子阿波罗STM32F767平台导入测试是学习SOEM协议栈与CSP位置控制模式的实用参考。 前阵子有个做运动控制的朋友问我STM32F767 跑 SOEM 控制一个伺服电机转圈CSP 模式代码注释别人都写好了为什么我照着做还是吃不透我听完就想起自己第一次把 SOEM 移植到 F767 上的狼狈样上电后从站状态怎么都切不到 OP看门狗反复复位后来发现是周期任务根本没按预期跑起来。等把 EtherCAT 状态机、PDO 映射、控制字时序一条条理顺电机才老老实实转起来。这篇就当一次完整复盘从硬件接线讲起到 SOEM 移植再到 CSP 模式下目标位置怎么累加才能让电机转圈最后会专门聊聊我给这份源码加注释时的分层思路。适合手里有 F767 板子、想快速把 EtherCAT 主站跑通的人也适合那些已经在 PC 主站上跑过 SOEM、但没在单片机上碰过的人。1. 先搞清楚这个Demo的技术栈主站、从站与CSP是什么关系很多新手看到“EtherCAT”就兴奋觉得这是高级总线但真正理解主站和从站关系的人并不多。我先把这套系统的物理链路和逻辑链路拆开。1.1 物理链路与协议链路这套 Demo 的物理链路非常简单STM32F767 作为 EtherCAT 主站通过以太网口RMII 接口接 PHY 芯片常用 LAN8720A连接一个支持 EtherCAT 的伺服驱动器伺服驱动器再接伺服电机。主站不需要直接控制电机的相电流也不处理编码器信号它只负责在每一个通信周期把“目标位置、控制字”等数据发给驱动器再从驱动器收回“实际位置、状态字”等数据。逻辑链路上伺服驱动器通常使用 CANopen over EtherCATCoE协议而 CSPCyclic Synchronous Position周期同步位置模式是 CiA 402 驱动规范里的一种运行模式。主站和从站通过 PDO过程数据对象交换实时数据。很多人把 EtherCAT 和“高级 CAN”划等号其实不对。EtherCAT 是工业以太网它的帧在每个周期内经过所有从站时每个从站实时提取和插入数据整帧传输延迟极低而且是硬实时同步。1.2 CSP 模式下“转圈圈”的本质CSP 模式的核心特征主站每个周期给从站发送一个目标位置从站内部的位置环负责跟踪这个目标位置。所以“转圈圈”这个需求在 CSP 下实现起来极其简单——目标位置一直线性增长电机就会一直转。目标位置不变电机就会停在当前位置。请注意这里说的“转圈圈”不是 PWM 调速那种转法不是 FOC 那种靠三相桥驱动电机旋转的转法也不是 CAN 总线舵机那种关节控制。CSP 模式下的电机是伺服电机驱动器内部有完整的三环电流环、速度环、位置环位置环的输入就是主站下发的目标位置。主站的任务是“告诉电机去哪儿”而不是“告诉电机转多快”。这种架构下电机的实际速度由目标位置的变化速率决定不是由某个速度指令直接决定。先把这个技术栈认清楚后面看代码才不会晕。2. 为什么选F767和SOEM硬件选型与移植准备标题里写着 STM32F767不是随便选的。这个芯片做 SOEM 主站性价比和开发效率都卡得刚刚好。2.1 F767 比 F103/F405 强在哪做 EtherCAT 主站硬件上最核心的诉求是两样以太网 MAC 和够用的 CPU 主频。STM32F767 内置的是 10/100M 以太网 MAC主频 216MHzCortex-M7 内核浮点运算和数据处理能力对于跑 SOEM 协议栈绰绰有余。早期有人用 F103 外加 SPI 网口芯片跑 SOEM也能跑但带宽和实时性捉襟见肘一旦从站数量增加或者控制周期压缩到 500us就很容易抖动。F767 的另一个优势是 LAN8720A 这类 RMII 接口 PHY 芯片的方案非常成熟随便一块核心板上都有现成电路参考资料多驱动也简单。相比 F405F767 的缓存和主频更高调试支持也更完善跑 SOEM 时系统负载更低留给用户逻辑的余量更大。2.2 SOEM 移植的三个关键入口SOEMSimple Open EtherCAT Master是应用最广的开源 EtherCAT 主站协议栈作者是 rt-labs代码结构清爽比很多商业协议栈容易读。它原生面向 Linux 和 Windows 平台靠 raw socket 发以太网帧但它的架构里把平台相关代码抽得很干净移植到 STM32 主要动三个地方第一osal.c里的定时器与 sleep 实现。SOEM 里很多地方需要毫秒级延时等待从站状态切换你需要让它走 STM32 的HAL_GetTick()或者自研的 tick 计数不要用阻塞式HAL_Delay放在周期任务里。第二网卡收发层。SOEM 在 PC 上走 raw socket在单片机上需要把nicdrv.c里的ecx_setupnic、ecx_send_eo、ecx_recv_eo这几个函数对接到你板子的以太网驱动上。用 STM32 HAL 库加 DMA 收发是比较常见的做法也可以用 LwIP 的回调接口绕一层。第三应用层周期调度。SOEM 本身不包含应用调度器它只是告诉你“这个周期该发什么帧”。真正的周期节拍需要你自己用定时器中断或者实时系统任务来驱动。我建议直接用 F767 的高级定时器触发中断在中断服务函数里完成 PDO 数据的发送和接收优先级设为最高。硬件接线上有个非常容易踩的坑LAN8720A 的 RMII 接口需要 50MHz 参考时钟。常见方案是让 STM32 的 MCO1 引脚输出 50MHz 时钟给 PHY或者外接 50MHz 有源晶振。检查一下你的板子原理图确认时钟来源否则 PHY 起不来SOEM 扫描总线时一个从站都发现不了。3. 从Init到OP态状态机切换与CSP使能的前置检查EtherCAT 从站有严格的状态机Init → Pre-Op → Safe-Op → Op。拿电梯打比方Init 是断电检修状态Pre-Op 相当于机房通电但电梯不可运行Safe-Op 是轿厢门开了但电机还不能动Op 才是正常运行模式。你让伺服转圈必须让从站进入 Op同时让伺服驱动器的内部状态机进入 Operation Enabled。3.1 四态切换的完整顺序SOEM 提供了ec_statechange和ec_statecheck两个核心函数前者请求状态切换后者等待从站确认到达目标状态。代码流程一般是// 请求切换到 PRE-OP ec_slave[0].state EC_STATE_PRE_OP; ec_writestate(0); ec_statecheck(0, EC_STATE_PRE_OP, 3000); // 配置 PDO 映射建立 IOmap ec_config_map(IOmap); // 配置分布式时钟 DC ec_config_dc(); // 请求切换到 SAFE-OP ec_slave[0].state EC_STATE_SAFE_OP; ec_writestate(0); ec_statecheck(0, EC_STATE_SAFE_OP, 3000); // 请求切换到 OP ec_slave[0].state EC_STATE_OP; ec_writestate(0); ec_statecheck(0, EC_STATE_OP, 3000);每一步都不能跳。很多初学者图省事直接从 Init 拐到 OP结果从站状态字里的位一直不对。我之前调一块第三方伺服驱动器Pre-Op 和 Safe-Op 都过了OP 却迟迟进不去。后来抓了从站上报的错误码发现是 PDO 映射长度和驱动器实际配置不一致目标位置对象根本没映射进过程数据。3.2 伺服使能控制字从0x06到0x0F从站进入 OP 只是打通了 EtherCAT 通信链路驱动器本身还有一个 CiA 402 状态机。要让电机真正出力必须按顺序给控制字写三个值0x06Shutdown让驱动器进入 Ready to Switch On0x07Switch On让驱动器进入 Switched On0x0FEnable Operation让驱动器进入 Operation Enabled把 0x0F 直接给上不行很多驱动器直接不响应或者报 Fault。每个步骤之间最好轮询一下状态字确认上位机状态已经切换成功再发下一个控制字。状态字里要关注 bit0Ready to Switch On、bit1Switched On、bit2Operation Enabled、bit3Fault。如果 bit3 置 1说明驱动器报故障了需要先给控制字 0x80 复位故障再重新走 0x06→0x07→0x0F 流程。这一套时序我最初是看驱动器的《通信对象字典》文档才想明白的。不同品牌的驱动器细节略有差异但大框架一致用 0x6060 切换运行模式到 8代表 CSP用 0x6061 确认当前模式。3.3 我卡过最久的一次有一次我 GitHub 上抄了段代码控制字直接赋的 0x0F心里想着“这不就使能了吗”。上电后电机纹丝不动状态字 bit2 永远为 0。查了两小时最后发现是伺服驱动器内部参数配置里激活了“抱闸延时”从 enable 到实际出力之间有 300ms 延时而我的测试脚本只等了 50ms。这个不算代码 bug纯粹是没吃透驱动器特性。给你的建议同样的代码换个品牌的驱动器必须先查它的手册确认状态机细节。总线通信是标准的伺服内部行为并不完全标准。4. CSP转圈的代码逻辑PDO数据流、目标位置累加与周期调度这一节直接上干货。看懂这一段你就知道“转圈圈”在代码层面到底发生了什么。4.1 PDO 映射里到底传了什么CSP 模式下最少需要两组 PDORxPDO主站发给从站包含控制字0x604016位目标位置0x607A32位TxPDO从站发给主站包含状态字0x604116位实际位置0x606432位SOEM 在ec_config_map阶段会把这些对象映射进过程数据返回一个 IOmap 缓冲区之后每个周期你把数据按偏移填进去就行。偏移顺序跟你驱动器的 PDO 配置完全一致千万别搞反。我之前遇到过控制字和目标位置映射次序反了结果目标位置的低 16 位被驱动器当成了控制字电机刚转起来就触发 Fault。4.2 目标位置怎么算增量、转速与每圈计数CSP 模式下让电机“匀速转圈”最朴素的做法就是每个周期给目标位置叠加一个固定增量target_position position_increment;问题来了这个position_increment到底该取多少先看几个变量。假定电机转一圈需要 10000 个位置单位这个值由驱动器的电子齿轮比和编码器分辨率决定不同驱动器各不相同通信周期 Ts 1ms想要的目标转速是 60rpm每秒一圈。那么每秒需要累加 10000 个单位 每个周期需要累加 10000 × 0.001 10 个单位。推到一般公式增量 每转单位数 × 目标转速(rpm) × 通信周期(s) / 60代入例子10000 × 60 × 0.001 / 60 10。所以每个周期目标位置加 10电机就是 60rpm 匀速转。想换转速就换增量。很多人在这一步犯迷糊以为这是“扭矩控制”或者“速度控制”其实不是。你每个周期给的是一个“目标位置”只是这个位置在以固定速度改变电机跟着位置走速度自然就稳定了。4.3 周期性调度骨架接下来是一段伪代码性质的周期任务骨架代码注释我会在第六节细讲// 高级定时器中断1ms 周期 void TIM_PeriodElapsedCallback() { // 1. 补全 PDO 输出数据 output_pdo.controlword 0x0F; // 保持在 Operation Enabled output_pdo.target_position target_position; // 2. 发送上一周期的数据并接收当前周期反馈 ec_send_processdata(); int wkc ec_receive_processdata(1000); // 3. 读取从站反馈 statusword input_pdo.statusword; actual_position input_pdo.actual_position; // 4. 状态正常就累加目标位置 if ((statusword 0x004F) 0x004F) // 处于 Operation Enabled 且无故障 { target_position position_increment; } // 5. 下一轮循环回到步骤2开头通过 ec_send_processdata 把这轮数据发出去 }调通之后你会发现电机转圈不是发生在“某一条指令”里而是发生在“一个又一个周期的持续累加”里。这个思路跟传统 MCU 控制差别挺大但正好体现了运动控制的核心。5. 调试中最常见的几个坑从周期性丢帧到伺服报错跑通一轮 Demo 不难难的是跑顺。这里列几个我从实际项目中踩出来的高频问题。5.1 周期任务不稳定的根因中断里做printf第一个坑在通信周期中断里加printf调试。串口打印一个字符往往要几十微秒如果打印内容多周期被拉长伺服驱动器发现主站帧周期不稳定直接报同步错误电机抖动甚至停机。调试输出输出到内存缓冲区空闲时再批量打印。周期任务里除了发送接收和位置累加其他统统不要干。5.2 从站状态切不到 OP 的快速排查顺序如果你发现从站状态一直切不到 OP按这个顺序排查现象可能原因检查点扫描不到从站PHY 未初始化或网线顺序错LAN8720A 复位时序、RMII 时钟、主站与从站网口顺序Pre-Op 超时邮箱通信失败EEPROM 配置被改用 EC_READ_U16 读从站信息确认从站地址和厂商代码Safe-Op 超时PDO 映射和从站配置不一致检查ec_config_map返回的 IOmap 长度对比驱动器过程数据配置OP 超时输入输出数据长度错误确认 Ibytes/Obytes 是否正确必要时打印ec_slave[0].IbytesOP 后马上掉回 Safe-OpDC 同步没配好调用ec_config_dc()检查 SYNC0 周期与主站周期是否匹配其中 PDO 映射不一致是最常见的。SOEM 会根据你的映射函数向从站写 SMSyncManager和 FMMU 配置如果驱动器本身固件限制了 PDO 长度或者映射对象超出范围就会在 Safe-Op 切换时卡住。我建议你在代码里把ec_slave[0].Ibytes、Obytes断点打印出来和驱动器的 Process Data 长度对比差别大于 8 字节就要仔细查。5.3 转了几圈突然停住怎么判断是软件还是硬件电机转几圈后停下来通常是三种原因目标位置溢出、驱动器报 Fault、主站控制字被意外清零。先看状态字。如果 bit3 变成 1说明驱动器报故障去读 0x603F错误代码看具体值。如果状态字正常但目标位置没继续变多半是位置累加逻辑被某个条件卡住了比如我前面的代码里检查(statusword 0x004F) 0x004F如果你误写成 0x004F之外的位LED 会关断逻辑导致目标位置停止更新。还有一个容易忽略的问题目标位置是 32 位有符号数一直转圈累加下去最终会溢出。Demo 阶段无所谓如果长时间运行需要在累加后做取模或者回绕处理把目标位置控制在每转单位数的整数倍范围内。6. 给SOEM代码加注释的正确姿势不是翻译英文而是解释“为什么”标题里有“添加代码注释”几个字我最后专门聊聊这个话题。刚接触 SOEM 时大多数人会在每个函数上面补一行中文注释比如“定时器回调函数”、“发送过程数据”这种注释不能说错但价值很低。真正有价值的注释是解释“这里为什么必须这么做”。6.1 注释分三层协议层、驱动层、应用层第一层是协议层。比如ec_statechange各个状态切换为什么不能省略ec_config_map和 PDO 偏移有什么关系DC 分布式时钟在 CSP 里起什么作用。这些注释帮助读者理解 EtherCAT 标准本身。第二层是驱动层。比如nicdrv.c里收发缓冲区的处理为什么需要双缓冲DMA 描述符为什么要按 4 字节对齐。这些注释依赖 STM32 以太网驱动的背景知识。第三层是应用层。比如目标位置增量怎么由转速和周期推导出来状态字里哪些位必须检查控制字切换顺序为什么 0x06 要走在 0x0F 前面。这一层注释跟你自己的控制需求强相关。6.2 我重点加注释的位置我整理这份工程时注释最密集的地方有四个第一个是主循环里的状态机切换段。每行都标注了当前从站应该处于什么状态、如果超时说明哪部分配置可能有问题。第二个是 PDO 偏移地址的定义处。把每个偏移对应到驱动器手册中的对象字典条目将来换驱动器时不用重新翻手册。第三个是通信周期中断的调度函数。把发送、接收、位置累加的顺序讲清楚防止后人乱调整导致同步丢失。第四个是控制字的赋值处。标明每一步对应 CiA 402 的哪个状态迁移为什么不能直接跳到最后一步。6.3 学习建议不要试图一次性读懂整个 SOEM最后给学习者的建议不要试图一次性读懂整个 SOEM。我也不算完全读完只是把跟单轴 CSP 相关的链路读通了。最有效的路线是先不管邮箱协议、DC 底层细节只盯着四个文件看——ethercatmain.c中的初始化流程、ethercatconfig.c中的 PDO 映射、ethercatcoe.c中的 CoE 对象访问、以及你自己写的周期任务。跑通转圈之后再回头补 DC 同步和 WKC工作计数器的概念这时候你已经有实际经验了读源码会轻松很多。我在实际调试中最大的体会是注释比代码更难写。很多地方刚写完觉得懂了过几天再看又发现理解错了于是再查手册、再改注释。这份工程我反复改了好几轮注释最后才觉得“这地方总算说明白了”。如果你能把一份 SOEM Demo 的注释写到让别人照着能复现你对 EtherCAT 的理解就已经远超只看文章的水平了。本文还有配套的精品资源点击获取