
简介这套基于QT平台与SOEM库的EtherCAT主站源码面向在Windows10/11下进行工业以太网从站调试的嵌入式或自动化工程师。资源针对1个IO模块实现了从站识别、进入OP状态、输入IO状态显示与输出IO控制并附带详细代码注释便于理解SOEM主站的网卡绑定、网络配置和状态机切换等核心流程。压缩包共134个文件以C/C源文件为主含77个头文件与35个C文件同时包含QT界面相关的cpp、ui文件以及lib/a格式的依赖库pro工程文件可方便直接打开编译整体仅478KB轻量而聚焦适合参考学习或二次开发。目前已有469人学习/下载。通过源码可快速搭建自己的EtherCAT主站调试环境获得从获取网卡信息到控制IO输出的完整思路并借助注释降低入门门槛为后续多从站扩展和工程落地提供实用基础。 前年年底我接了一台非标设备的上位机程序开发甲方提出一个硬性要求不用PLC不用触摸屏直接用PC通过网线控制一个IO箱把十几个传感器信号读进来再控制十几个继电器输出。最后落地的技术方案就是 Qt SOEMQt 负责界面和交互SOEM 作为开源的 EtherCAT 主站与一个数字量 IO 从站模块通信。整个项目在 Win10 和 Win11 两台工控机上反复调试最终以 1 个 IO 模块为 Demo 跑通了输入显示和输出控制的全部功能。这篇博文就按我自己做项目的顺序把选型理由、环境编译、核心代码、界面线程配合以及实际踩过的 Windows 兼容性坑完整梳理一遍。1. 为什么是 Qt SOEM 这套组合来扛 IO 控制1.1 商业主站授权不划算开源主站够用EtherCAT 主站方案常见的有倍福 TwinCAT、KPA 等商业产品功能当然很强但对于单台非标设备、只需要几十个 IO 点、客户预算卡得很死这种场景授权成本一下就把利润吃掉了。SOEMSimple Open EtherCAT Master是老牌开源主站EtherCAT 协议栈的底层功能基本都齐比如从站扫描、过程数据交换、DC 同步、断线检测等等。在 Linux 和 Windows 上都能跑资料虽然没有商业方案多但核心 API 很少啃一遍源码加例程就够上手了。SOEM 采用 GPL 类授权自己开发调试没问题但如果要做成商业产品对外分发建议还是走正版授权或找商业技术支持。我的做法是先在项目里做技术验证确认可行之后再评估商业化路径这个风险需要在立项时就跟决策层讲清楚。1.2 系统整体架构与数据流这套方案的数据流非常直接PC 的以太网口通过一条普通网线接到 EtherCAT 从站 IO 模块的 IN 口IO 模块另一边接传感器和继电器。SOEM 把网卡收到的 EtherCAT 帧解析出来Qt 界面读取输入点位状态同时把界面上的开关状态写成输出点位。演示用的从站模块选了常见的 16 路数字量输入 16 路数字量输出的 EtherCAT 耦合器供电需要单独的 24V 电源模块上一般有两个网口IN 口接主站OUT 口用来串联下一个从站。本次只演示 1 个模块所以 OUT 口空着就行。架构上分为三层界面层MainWindow 显示输入指示灯、输出开关、通信状态。数据层EtherCATWorker 线程周期性执行收发维护一块输入缓冲区和一块输出缓冲区。协议层SOEM 静态库封装 EtherCAT 帧的打包、解析和网卡读写。2. 环境搭建Qt 5.15.2 Npcap SOEM 源码编译2.1 工具链选型与版本搭配Qt 版本我选的是 5.15.2它是目前工业设备上位机里用得最稳的 LTS 版本网上的踩坑资料也最全。编译器建议用 MSVC2019 64bit因为 SOEM 在 Windows 下的官方例程基本都是 MSVC 环境编译验证过的用 MinGW 也能编但要处理 .lib 导入库的格式问题没必要给自己添麻烦。SOEM 本身不依赖 Qt它只依赖 Windows 下的 WinPcap 或 Npcap 开发库。我在 Win10 和 Win11 上都用的 Npcap安装的时候要特别注意勾选WinPcap API-Compatible Mode否则 SOEM 编译时找不到pcap.h和wpcap.lib会直接报头文件缺失的错误。Qt 5.15.2 的安装包在线下载地址是常规渠道安装时选择 MSVC 2019 64-bit 组件即可。顺便说一句5.15.2 之后 Qt 官方不再提供开源离线包所以更推荐直接在 Qt 在线安装器里选 5.15.2。2.2 SOEM 编译过程和 Qt 工程引用SOEM 源码直接从官方 GitHub 仓库拉取用 CMake 生成 Visual Studio 工程。我这里给出完整命令实际路径按你自己目录调整git clone https://github.com/OpenEtherCATsociety/SOEM.git cd SOEM mkdir build cd build cmake .. -G Visual Studio 16 2019 -A x64 cmake --build . --config Release如果 CMake 没有自动定位到 Npcap 开发库就需要手动指定。编译完成后会在build/Release下生成soem.lib静态库。在 Qt 工程里引用它只需要在.pro文件里加两行INCLUDEPATH D:/SOEM/soem LIBS D:/SOEM/build/Release/soem.lib还有一个关键依赖是 Npcap 的运行时库wpcap.dll发布程序时要把这个 DLL 一起放到 exe 同目录或者确保目标机器装了 Npcap 驱动。这一步经常被遗漏程序在自己电脑上能跑拷到客户机器上就报错后面第 5 章会专门讲这类问题。3. 核心代码拆解初始化、收发循环与数字量读写3.1 网卡初始化和从站扫描SOEM 最核心的初始化只有三步ec_init打开网卡ec_config_init扫描总线上所有从站ec_config_map做 IO 地址映射。这里我直接给出实际项目里带注释的代码标题说添加代码注释注释的价值就是把每个 SOEM 接口的坑写在代码旁边。// ec_init 参数在 Windows 下一般传 eth0、eth1 这种网卡索引别名 // 如果电脑有多块网卡建议启动时做成下拉框让用户选择避免绑错 if (ec_init(eth0) 0) { qCritical() ec_init 失败网卡未打开或当前用户没有底层访问权限; return -1; } // 扫描 EtherCAT 总线上的从站返回值是从站数量 // 第二个参数为 FALSE表示不启用冗余配置单网口场景足够 int slaveCount ec_config_init(FALSE); if (slaveCount 0) { qCritical() 没有扫描到任何 EtherCAT 从站请检查网线/供电/站号; ec_close(); return -1; } // 把从站的输入输出地址统一映射到 IOmap 缓冲区 ec_config_map(IOmap[0]);这里有一个必须记住的规则SOEM 的从站数组是从ec_slave[1]开始编号的ec_slave[0]是保留项。扫描到 1 个从站时有效对象就是ec_slave[1]它的inputs、outputs指针分别指向输入和输出数据区Ibits、Obits表示输入和输出的位宽。很多新手从这里就开始懵直接用[0]去访问结果读到的永远是脏数据。3.2 过程数据循环ec_send 与 ec_receive 的关系EtherCAT 运行起来之后主站需要周期性地把所有从站的过程数据打包成帧发出去再收回从站的反馈这个过程就是一次过程数据交换。SOEM 里面对应的就是一对函数// 把各个从站 outputs 区域的数据打包发送到总线上 ec_send_processdata(); // 接收本轮从站返回的数据同时把 inputs 区域更新到内存 // 参数 EC_TIMEOUTRET 是超时时间单位微秒 int wkc ec_receive_processdata(EC_TIMEOUTRET);wkc全称是 Working Counter它反映了从站实际响应的工作计数。这里要纠正一个常见误解wkc不等于从站数量它是所有读写操作计数的总和一个从站如果有输入又有输出会让wkc增加多个计数。我建议的做法是启动时先跑一次空循环记录正常时的wkc后续只要发现wkc expectedWkc就判定通信质量下降同时输出告警日志。这样比自己拍脑袋猜一个固定值靠谱得多。3.3 数字量读写位运算与注释要点对于 16DI/16DO 的数字量模块输入输出数据在内存里是按位排列的。每个字节存 8 路通道第 0~7 路在data[0]第 8~15 路在data[1]。读写的时候用移位和位与/位或运算。// 以第 1 个从站为例 uint8_t* inData ec_slave[1].inputs; uint8_t* outData ec_slave[1].outputs; // 读取输入第 0 路为 1 表示传感器导通 bool inCh0 (inData[0] 0) 0x01; // 读取输入第 15 路 bool inCh15 (inData[1] 7) 0x01; // 输出第 0 路置 1继电器吸合 outData[0] | 0x01; // 输出第 0 路清 0继电器断开 outData[0] ~0x01;这个位运算逻辑是所有 IO 显示和控制的基础。我给代码补注释时重点标注三件事一是字节序和位序二是读写方向三是outputs区的修改并不会立刻生效要等下一次ec_send_processdata()时才会真正发到从站。把这三个点写在注释里后来维护代码的人就不会再去翻 EtherCAT 协议文档。4. 界面线程与 EtherCAT 循环的配合方案4.1 用子线程跑周期收发别阻塞 UIEtherCAT 的 IO 刷新周期通常做到 1ms 到 5ms如果直接在 MainWindow 的槽函数里做 while 循环收发界面必然卡死。正确做法是把 EtherCAT 循环放进一个独立的QThread子线程里。线程内部用QElapsedTimer控制周期保证收发尽量稳定。class EtherCATWorker : public QThread { Q_OBJECT public: void stop() { m_running false; } signals: // 输入数据准备好了拷贝给主界面去刷新指示灯 void inputsReady(const QByteArray inputData); protected: void run() override { QElapsedTimer timer; while (m_running) { timer.restart(); // 把 UI 线程更新过的输出缓冲区拷到 SOEM 的 outputs 区 QMutexLocker locker(m_dataMutex); memcpy(ec_slave[1].outputs, m_outputData, m_outputBytes); locker.unlock(); ec_send_processdata(); int wkc ec_receive_processdata(EC_TIMEOUTRET); if (wkc 0) { QByteArray data(reinterpret_castconst char*(ec_slave[1].inputs), m_inputBytes); emit inputsReady(data); } int remain m_cycleMs - timer.elapsed(); if (remain 0) { msleep(remain); } else { // 实际耗时超过设定周期说明主站被系统卡顿拖累了 qWarning() EtherCAT 周期超时; } } } };这里我用了m_dataMutex来保护 UI 线程和 EtherCAT 线程共享的输出缓冲区避免一边写一边发产生半新半旧的脏数据。虽然这个锁的开销极小但它解决的并发问题很关键。4.2 输入状态用指示灯样式表刷新UI 上输入部分做成一排圆点指示灯每个通道对应一个QLabel。主界面收到inputsReady信号后遍历 16 路输入把对应QLabel的背景色改成绿色高电平或灰色低电平。void MainWindow::onInputsReady(const QByteArray data) { for (int i 0; i 16; i) { bool on (data.at(i / 8) (i % 8)) 0x01; m_ledLabels[i]-setStyleSheet( on ? background: #00cc00; border-radius: 8px; : background: #999999; border-radius: 8px;); } }用样式表而不是贴图片好处是改颜色方便后期想给输入信号加告警闪烁多写一个定时器翻转样式就行。4.3 输出控制走信号槽和共享缓冲区输出侧的控件我用了 16 个QCheckBox或者带状态的QPushButton点击时触发槽函数根据当前选中状态去修改共享输出缓冲区里对应的位。这里的关键是UI 线程不要直接去改ec_slave[1].outputs而是先更新自己的m_outputData由 EtherCAT 线程下一次发帧前拷贝过去。void MainWindow::onOutputToggled(int channel, bool checked) { QMutexLocker locker(m_dataMutex); if (checked) { m_outputData[channel / 8] | (1 (channel % 8)); } else { m_outputData[channel / 8] ~(1 (channel % 8)); } }这样拆分之后UI 线程完全不关心 SOEM 内部数据结构EtherCAT 线程也不用担心界面卡顿影响收发周期两边各干各的活。5. 实际调试记录win10/win11 上的坑与对策5.1 管理员权限、杀毒软件和网卡节能SOEM 在 Windows 下要对网卡做底层数据包捕获必须用管理员权限运行否则ec_init大概率能过但收发数据一直拿不到完整帧。开发阶段我直接把 Qt Creator 用管理员身份启动发布时给 exe 加requireAdministrator的 UAC 清单一劳永逸。还有两个很容易被忽略的系统因素。一是杀毒软件火绒、360 这类软件会扫描甚至拦截 EtherCAT 帧现场工控机上建议把程序目录加入白名单。二是网卡的电源管理必须到设备管理器里把允许计算机关闭此设备以节约电源关掉否则运行一段时间后会出现周期性卡顿IO 刷新偶尔掉几个周期排查起来非常隐蔽。5.2 Qt 部署时的 platform 插件报错程序刚拷到客户 Win11 机器上时最常见的报错就是no qt platform plugin could be initialized。原因基本就两个缺platforms/qwindows.dll或者QT_QPA_PLATFORM_PLUGIN_PATH环境变量指向错误。用 Qt 自带的windeployqt工具部署基本能解决命令很简单windeployqt YourApp.exe它会自动把 Qt 运行库、platforms目录等拷贝到 exe 同目录。如果手动打包一定要把plugins/platforms/qwindows.dll放到 exe 同级的platforms文件夹里。另外别忘了wpcap.dllNpcap 的运行时库同样要随程序分发否则程序启动后会提示找不到动态库。5.3 输入全灰、输出无响应时的排查顺序界面输入指示灯全灰、输出怎么点都没反应这基本是通信没建立成功。我的排查顺序是先硬件后软件模块供电是否正常模块上的电源灯有没有亮。网线插的是不是 IO 模块的 IN 口有些模块 OUT 口也能插但不会通信。从站模块的站号拨码是否正确和程序里索引是否一致。ec_config_init返回的从站数量是否等于预期。收发循环里的ec_receive_processdata返回值是否稳定。按这个顺序逐层打日志基本一轮下来就能定位。我在现场遇到过最刁钻的问题是某个 USB 转网口适配器不兼容 EtherCAT 帧换回板载有线网卡立刻正常。所以强烈建议做 EtherCAT 上位机尽量用 Intel 或 Realtek 的板载网卡别用转接卡凑合。最后再分享一个小经验。SOEM 的 API 看着不多但真正决定项目稳不稳的不是 API 本身而是通信异常的处理逻辑。wc不等于从站数。启动时先跑几轮空循环拿到基准值之后每次收发都对比一下一旦掉基准就提示报警并尝试重连这套逻辑在长时间运行的项目里非常省心。另外给代码补注释时别只写发送数据这种废话把为什么不用[0]索引为什么输出要等下个周期才生效这种背后逻辑写清楚三个月后你自己回来改代码都会感谢当时的判断。本文还有配套的精品资源点击获取