EtherCAT主站:從編譯到運動控制實戰(zhàn))
1. 為什么是QT加SOEM一個老牌主站庫的二次生命力做嵌入式上位機開發(fā)這些年我踩過不少協(xié)議棧的坑最后在EtherCAT主站這個方向上SOEMSimple Open EtherCAT Master是我目前最推薦用來搭配QT做上位機的一套組合。原因很簡單SOEM是開源庫代碼量適中移植性極強而且它不依賴Linux內(nèi)核補丁——這意味著你可以輕松地在Windows、Linux、甚至RTOS上跑起來而QT恰好又是跨平臺界面開發(fā)的首選兩者配合起來幾乎是無縫銜接。先說一個很多人容易混淆的點EtherCAT主站協(xié)議棧和從站協(xié)議棧完全是兩碼事。從站一般由從站控制器ESC硬件來完成比如LAN9252、AX58100這類芯片芯片出廠就固化了EtherCAT從站協(xié)議的處理邏輯你不用去碰協(xié)議棧的底層。而主站這邊就不一樣了主站的任務(wù)是生成報文、管理從站狀態(tài)機、處理分布式時鐘、維護過程數(shù)據(jù)映射這些邏輯全部要跑在CPU上所以主站的“協(xié)議?!焙脡闹苯記Q定了整個運動控制系統(tǒng)的實時性和穩(wěn)定性。SOEM最初是由德國的一家自動化公司貢獻給開源社區(qū)的后來被納入了IgH的生態(tài)體系??赡苡腥藭柤热籌gHEtherLab那么有名為什么不去用IgH我的實測體會是IgH功能雖然全但它是基于Linux內(nèi)核模塊實現(xiàn)的驅(qū)動模型復雜編譯和加載都要在內(nèi)核態(tài)完成而且每次內(nèi)核升級都要重新編譯這對很多做產(chǎn)品的人來說是件很頭疼的事情。SOEM則完全是用戶態(tài)的庫API簡潔編譯出來就是一個靜態(tài)庫或者動態(tài)庫QT工程里直接引用就行開發(fā)效率高得多。還有一點很關(guān)鍵SOEM對嵌入式平臺的適配相當好。我試過STM32MP1、i.MX8M Plus、瑞薩RZ/G2L這些平臺SOEM都能很好地跑起來。對于需要做HMI人機界面的項目比如點膠機、貼片機、機器人控制柜QT畫界面SOEM跑實時通信這套架構(gòu)在生產(chǎn)線上已經(jīng)經(jīng)過了大量驗證。這篇文章我就從頭到尾梳理一遍從庫的編譯到QT工程的集成再到實際應(yīng)用中的關(guān)鍵細節(jié)和踩坑記錄幫助大家少走彎路。2. SOEM主站協(xié)議庫的架構(gòu)與核心機制2.1 主站協(xié)議棧的分層思想理解SOEM之前先得理解EtherCAT主站協(xié)議棧的層次劃分。EtherCAT的通信模型其實很簡潔主站發(fā)送一個以太網(wǎng)幀幀里面串聯(lián)了所有從站的數(shù)據(jù)每個從站從幀中提取自己的輸出數(shù)據(jù)同時把輸入數(shù)據(jù)插入到幀的相應(yīng)位置然后幀繼續(xù)傳給下一個從站。整個過程是“飛讀飛寫”也就是所謂的過程數(shù)據(jù)Process Data交換。SOEM的核心代碼主要分為這幾個層次底層套接字層負責原始以太網(wǎng)幀的收發(fā)在Linux下走的是AF_PACKET協(xié)議族在Windows下走的是WinPcap/Npcap庫這一層是平臺相關(guān)的。鏈路層抽象SOEM通過ecx_setup系列函數(shù)來初始化網(wǎng)卡和從站掃描真正把以太網(wǎng)幀封裝成EtherCAT報文。應(yīng)用層接口這一層是我們最常碰到的包括狀態(tài)機管理郵箱通信、狀態(tài)轉(zhuǎn)換、過程數(shù)據(jù)映射、分布式時鐘等。在SOEM中有一個全局的結(jié)構(gòu)體ecx_context它把整個主站運行狀態(tài)都封裝在里面。對于多線程應(yīng)用SOEM允許你創(chuàng)建多個ecx_context實例從而實現(xiàn)多主站并行——比如一個主站控制關(guān)節(jié)伺服另一個主站控制IO模塊這在復雜機器人系統(tǒng)里非常實用。2.2 SOEM與IGH的定位差異很多人一上來就會問SOEM和IGH到底選哪個我個人的結(jié)論是如果追求絕對最高的實時性并且團隊有Linux內(nèi)核開發(fā)能力項目周期長那IGH確實可以做得很極致。如果追求快速落地、跨平臺、嵌入式友好尤其是要搭配QT做上位機/觸控屏那么SOEM絕對是性價比最高的選擇。IGH把主站做成內(nèi)核模塊實時性確實是用戶態(tài)方案比不了的但付出的代價是調(diào)試困難、移植麻煩、GUI集成費勁。SOEM走的是用戶態(tài)雖然實時性上限略低但通過配合PREEMPT_RT補丁或者綁定CPU核心CPU Affinity在百微秒級別的周期控制中完全夠用。對于絕大多數(shù)使用EtherCAT的場景——伺服周期1ms、4ms、甚至8ms都常見——SOEM的性能毫無壓力。2.3 狀態(tài)機與通信流程EtherCAT通信的核心是狀態(tài)機從站設(shè)備在上電之后要依次經(jīng)過Init、Pre-Operational、Safe-Operational、Operational四個狀態(tài)主站負責驅(qū)動這些狀態(tài)轉(zhuǎn)換。SOEM的API封裝了這些狀態(tài)轉(zhuǎn)換ec_statechange檢查狀態(tài)變化ec_writestate讓從站切換到目標狀態(tài)ec_readstate讀取所有從站的當前狀態(tài)每一個狀態(tài)都有各自允許的通信服務(wù)Init只能進行寄存器讀寫用來獲取從站信息。Pre-Op郵箱通信開啟可以配置PDO映射、加載Coe對象字典。Safe-Op過程數(shù)據(jù)開始刷新但此時從站不輸出只處理輸入。Op完全運行狀態(tài)輸出有效伺服使能。我在調(diào)試時習慣寫一個狀態(tài)機等待函數(shù)在切換狀態(tài)之后輪詢所有從站是否都到了預期狀態(tài)如果超時就把錯誤寄存器讀出來這樣能快速定位從站配置的問題。3. 從零開始編譯SOEM庫3.1 源碼準備與CMake構(gòu)建SOEM的源碼托管在GitHub上倉庫地址是OpenEtherCATsociety/SOEM。建議不要直接下載master分支的代碼而是選擇一個穩(wěn)定的release標簽版本目前我常用的是1.4.0版本這個版本在ARM平臺和x86平臺上的測試都比較充分。源碼下載回來之后解壓到本地目錄SOEM本身采用CMake構(gòu)建系統(tǒng)這也是它能輕松集成到QT工程的前提之一。在Linux平臺下構(gòu)建過程很簡單mkdir build cd build cmake .. make sudo make install但這里有幾個細節(jié)需要注意。默認的CMake配置會生成動態(tài)庫和示例程序如果你想得到一個精簡的靜態(tài)庫用于嵌入式平臺可以在cmake時加上參數(shù)cmake -DBUILD_SHARED_LIBSOFF -DSOEM_BUILD_TESTSOFF -DSOEM_BUILD_EXAMPLESOFF ..這樣得到的就是一個純凈的libsoem.a靜態(tài)庫體積很小適合直接丟到嵌入式ARM的交叉編譯工具鏈里面去鏈接。SOEM支持兩種構(gòu)建方式一種是在主機上編譯然后通過交叉編譯工具鏈重新構(gòu)建另一種是直接把SOEM源碼加入QT工程一并編譯。對于產(chǎn)品化項目我更推薦后者理由后面細說。3.2 Windows平臺上的編譯特殊性Windows平臺下編譯SOEM會比Linux多一些步驟。SOEM的底層套接字在Windows上依賴WinPcap或Npcap的SDK原因在于EtherCAT主站需要自己構(gòu)造以太網(wǎng)幀的MAC地址和EtherType0x88A4標準的Windows Socket API是干不了這件事的必須借助Npcap的底層抓包接口。你需要先安裝Npcap并勾選“安裝SDK”的選項然后在CMake配置時指定Npcap SDK的路徑。如果直接用QT的MinGW工具鏈要特別留意Npcap SDK和MinGW的兼容性。實測下來MSVC2019配合Npcap是最穩(wěn)定的組合MinGW環(huán)境下偶爾會出現(xiàn)頭文件找不到的情況解決辦法是手動把Npcap的Include目錄和Lib目錄加進工程文件。3.3 引入QT工程的兩種姿勢把SOEM用到QT工程中主要有兩種方式。第一種是把SOEM編譯成靜態(tài)庫然后在QT的.pro文件里通過LIBS 來引用。這種方式的優(yōu)點是工程結(jié)構(gòu)清晰QT只負責界面和業(yè)務(wù)邏輯SOEM作為獨立的庫存在缺點是當你想修改SOEM源碼時需要單獨編譯庫。第二種方式是把SOEM的全部源文件soem/目錄下直接加入到QT工程中參與整體編譯。這種方式對源碼的調(diào)試非常方便打斷點直接就能進入SOEM的內(nèi)部函數(shù)對于想深入學習EtherCAT主站協(xié)議的人來說特別有價值。我自己的項目就采用的第二種方式在.pro文件里添加INCLUDEPATH $$PWD/soem/soem SOURCES \ $$PWD/soem/soem/ethercat.c \ $$PWD/soem/soem/ethercatcoe.c \ $$PWD/soem/soem/ethercatdc.c \ $$PWD/soem/soem/ethercatfoe.c \ $$PWD/soem/soem/ethercatmain.c \ $$PWD/soem/soem/ethercatprint.c \ $$PWD/soem/soem/ethercatsoe.c \ $$PWD/soem/soem/osal/osal.c \ $$PWD/soem/soem/osal/osal_win32.c注意文件列表要根據(jù)平臺調(diào)整Windows下需要osal_win32.cLinux下則需要osal_linux.c。另外Npcap的依賴也要加進去win32 { INCLUDEPATH C:/Program Files/Npcap LIBS -LC:/Program Files/Npcap/Lib -lwpcap -lPacket }直接參與編譯還有一個好處是可以用上QT的構(gòu)建套件Kit比如你在Qt Creator里配置了交叉編譯套件那么SOEM也會跟著交叉編譯到目標平臺完全不用手動維護交叉編譯鏈的環(huán)境變量。4. 核心API應(yīng)用實戰(zhàn)主站掃描、配置與過程數(shù)據(jù)交換4.1 初始化網(wǎng)卡與從站掃描無論多復雜的EtherCAT系統(tǒng)起步操作都是類似的打開網(wǎng)卡、掃描總線、獲取從站信息。SOEM提供了一個極其實用的API以下是典型的初始化流程#include ethercat.h #include QDebug char IOmap[4096]; int slaveCount 0; bool setupEtherCAT(const QString ifname) { // 打開網(wǎng)卡返回套接字句柄 if (ec_init(ifname.toLocal8Bit().data()) 0) { qCritical() Failed to initialize NIC: ifname; return false; } qInfo() NIC initialized: ifname; // 掃描總線上所有從站 slaveCount ec_config_init(FALSE); if (slaveCount 0) { qCritical() No slaves found on the bus.; return false; } qInfo() Total slaves: slaveCount; // 配置過程數(shù)據(jù)映射 int result ec_config_map(IOmap); if (result 0) { qCritical() PDO mapping failed.; return false; } qInfo() PDO mapping done, bytes used: result; // 請求切換到OP狀態(tài) ec_slave[0].state EC_STATE_OPERATIONAL; ec_writestate(0); return true; }這里有幾個關(guān)鍵點。ec_config_init(TRUE)中的參數(shù)表示是否使用保留地址reserved address進行配置一般設(shè)為FALSE即可。掃描完成后總線上的每個從站信息會填充到全局數(shù)組ec_slave[]中每個元素的eep_id、eep_man、eep_rev這些字段存儲的就是從站的Vendor ID、Product ID等信息。ec_config_map(IOmap)這個函數(shù)是理解EtherCAT從站映射的關(guān)鍵它會為每個從站計算PDO的映射關(guān)系返回所有從站輸出和輸入數(shù)據(jù)占用的總字節(jié)數(shù)。IOmap這個緩沖區(qū)就是你接下來讀寫過程數(shù)據(jù)的“舞臺”。4.2 理解Vendor ID與從站配置的坑在調(diào)試新從站時很多人會遇到一個“詭異”現(xiàn)象從站明明被掃描到了但狀態(tài)始終切不到OP。這時大概率是從站的配置信息不完整尤其是FMMU映射出了問題。FMMUFieldbus Memory Management Unit是EtherCAT從站內(nèi)部用來把過程數(shù)據(jù)映射到本地地址的機制SOEM在ec_config_map時會按EEPROM里的配置自動設(shè)置但是如果從站的EEPROM沒有燒錄或者配置錯誤映射就會失敗。這時可以用SOEM提供的工具函數(shù)讀取從站的EEPROM信息qDebug() Slave i Vendor: hex ec_slave[i].eep_man Product: hex ec_slave[i].eep_id Revision: hex ec_slave[i].eep_rev;通常從站出廠時廠商會把這些信息寫好。但如果是自己開發(fā)的從站比如基于LAN9252或者AX58100的板子在初次調(diào)試時就要特別留意很多從站不支持自動配置FMMU必須在上位機中顯式地設(shè)置PDO的映射參數(shù)。這種情況下光靠SOEM的自動映射是不夠的你可能需要直接操作ESC寄存器來手動配置。一個我非常推薦的做法是在正式控制邏輯之前先打印所有從站的輸入輸出長度和映射地址確認每個從站占用的IOmap偏移是否正確for (int i 1; i slaveCount; i) { qDebug() Slave i InputStart: ec_slave[i].Istart OutputStart: ec_slave[i].Ostart InputBits: ec_slave[i].Ibits OutputBits: ec_slave[i].Obits; }這個過程能幫你發(fā)現(xiàn)在后續(xù)訪問過程數(shù)據(jù)時該把指針指向哪個偏移位置尤其是總線上有多個不同類型的從站時這一步絕不能省。4.3 過程數(shù)據(jù)的讀寫PDO映射完成之后我們就可以通過IOmap來讀寫數(shù)據(jù)了。SOEM的思想是把所有從站的輸入輸出緊湊排列在一段連續(xù)的內(nèi)存中然后由協(xié)處理器和主站代碼通過指針來操作。為了方便管理我在實際項目中會為每個從站定義一個結(jié)構(gòu)體再把這些結(jié)構(gòu)體按映射順序重疊到IOmap中。比如常見的伺服驅(qū)動器通常使用CiA402協(xié)議控制字Controlword、狀態(tài)字Statusword、目標位置Target Position和實際位置Actual Position這些對象都是標準化的于是我可以這樣定義typedef struct { uint16_t controlword; int32_t target_position; int32_t target_velocity; uint16_t mode_of_operation; } ServoOutput_t; typedef struct { uint16_t statusword; int32_t actual_position; int32_t actual_velocity; uint16_t mode_of_operation_display; } ServoInput_t; ServoOutput_t* servoOut[16]; ServoInput_t* servoIn[16]; // 在配置完成后按每個從站的IOmap偏移綁定地址 for (int i 1; i slaveCount; i) { servoOut[i] (ServoOutput_t*)(IOmap ec_slave[i].Ostart); servoIn[i] (ServoInput_t*)(IOmap ec_slave[i].Istart); }這樣綁定之后讀寫過程數(shù)據(jù)就成了結(jié)構(gòu)體成員訪問那么簡單。比如要讓1號伺服進入位置模式并開始運動我只需要servoOut[1]-controlword 0x000F; servoOut[1]-mode_of_operation 0x01; // CSP模式 servoOut[1]-target_position 100000; // 單位取決于電子齒輪比然后調(diào)用ec_send_processdata()和ec_receive_processdata(0)把這一幀數(shù)據(jù)發(fā)送出去。這里要特別強調(diào)一個細節(jié)SOEM的ec_send_processdata()和ec_receive_processdata(0)必須成對調(diào)用并且建議在一個獨立的實時線程中以固定周期循環(huán)調(diào)用。發(fā)送函數(shù)構(gòu)造報文接收函數(shù)解析從站反饋兩者的間隔通常在幾十微秒以內(nèi)超時會返回EC_ERR。為了監(jiān)控通信質(zhì)量我習慣在接收之后檢查返回值如果在連續(xù)多個周期內(nèi)返回錯誤就觸發(fā)報警。4.4 從站狀態(tài)切換的狀態(tài)機封裝從站從啟動到進入OP狀態(tài)中間要經(jīng)歷多次狀態(tài)切換每一次切換都要給從站留出足夠的時間去處理郵箱通信和PDO配置。SOEM官方示例中的方式比較初級只是簡單地把目標狀態(tài)寫進ec_slave[0].state然后調(diào)用ec_writestate而實際項目中每臺從站的狀態(tài)切換速度不一樣尤其是一些復雜的伺服驅(qū)動器可能需要幾百毫秒才能完成參數(shù)初始化。因此我在QT工程里封裝了一個帶超時管理的狀態(tài)切換函數(shù)bool waitForState(int slaveIndex, uint16_t targetState, int timeoutMs) { QElapsedTimer timer; timer.start(); while (timer.elapsed() timeoutMs) { ec_slave[slaveIndex].state targetState; ec_writestate(slaveIndex); // 讀取實際狀態(tài) ec_readstate(); uint16_t actual ec_slave[slaveIndex].state; // 檢查是否到達目標狀態(tài) if (actual targetState) { return true; } // 如果從站報錯讀取AL狀態(tài)碼 if (actual EC_STATE_ERROR) { uint16_t alStatusCode ec_slave[slaveIndex].ALstatuscode; qWarning() Slave slaveIndex error, AL status code: hex alStatusCode; return false; } QThread::msleep(10); } qWarning() Slave slaveIndex state timeout; return false; }從站無法進入OP狀態(tài)時SOEM會在ec_slave[i].ALstatuscode中給出具體原因常見的有AL狀態(tài)碼含義處理建議0x0012無效的郵箱配置檢查SMSyncManager配置0x0013無效的PDO映射檢查PDO映射并重新配置0x0014同步錯誤檢查DC時鐘配置0x001E從站本地錯誤查看從站手冊確認錯誤來源有了這個等待函數(shù)切OP之前的流程就清晰可查了先切Pre-Op然后寫入PDO映射再切Safe-Op最后切Op。每一層都能驗證。5. 分布式時鐘與同步讓多軸動得整齊劃一5.1 為什么需要同步EtherCAT最吸引人的地方之一就是它的分布式時鐘Distributed Clock簡稱DC。如果沒有DC同步總線上每個從站都是按照各自的晶振節(jié)拍來采樣和輸出晶振的頻率誤差雖然不大但累積到一定時間就會導致軸與軸之間出現(xiàn)明顯的偏移。在高精度的運動控制場合這種時間偏差會直接表現(xiàn)為軌跡跟隨的抖動。DC的原理簡單來說就是第一個具有DC功能的從站作為參考時鐘主站周期性發(fā)送時鐘同步報文其他從站讀取參考時鐘的時間戳并通過本地時鐘補償算法來校準自身的本地時間。這樣一來所有從站的采樣時刻就對齊到了同一個時間基準上。SOEM對DC的支持很到位在ec_config_map之后只需要調(diào)用以下幾個函數(shù)就可以完成DC初始化ec_configdc();這個函數(shù)會自動檢測從站的DC能力并對有DC功能的從站進行時鐘同步設(shè)置。如果總線上混合了有DC和無DC的從站SOEM會以第一個支持DC的從站的時鐘作為參考也就是所謂的SYNC0事件由DC0來觸發(fā)。5.2 周期同步與SYNC信號在實際項目中伺服驅(qū)動器的位置環(huán)、速度環(huán)甚至電流環(huán)都需要依賴從站的SYNC事件來觸發(fā)。SYNC事件是DC時鐘在到達預設(shè)時刻時生成的硬件中斷信號驅(qū)動內(nèi)部的數(shù)字信號處理器DSP會響應(yīng)這個中斷進行采樣和計算。也就是說周期控制的精度直接取決于DC同步鏈路的質(zhì)量和SYNC信號周期的設(shè)置。在SOEM中可以通過ec_dcsync0來設(shè)置SYNC0的周期和偏移// slaveIndex為參考從站索引 // cycleTimeNs為周期時間單位是納秒 // shiftTimeNs為偏移時間 ec_dcsync0(slaveIndex, TRUE, cycleTimeNs, shiftTimeNs);例如對于1ms的控制周期ec_dcsync0(1, TRUE, 1000000, 0);這里有一個心得SYNC0的偏移時間不要一律設(shè)置為0。在總線上有多個從站時如果所有從站都在同一個時間點觸發(fā)SYNC可能會造成總線上瞬時電流峰值過大尤其在伺服驅(qū)動器的使能瞬間。通常我會把偏移時間錯開幾十微秒讓各驅(qū)動器的采樣點稍微錯開能有效減少總線上的干擾。5.3 DC同步誤差的觀察與調(diào)優(yōu)怎么判斷DC同步是否正常一個可行的辦法是循環(huán)讀取參考從站的時鐘時間戳和本地從站的系統(tǒng)時間計算時間偏差。SOEM提供了ec_dcreg相關(guān)的寄存器讀取函數(shù)可以讀取從站的0x092CSystem Time寄存器和0x0920Receive Time寄存器實時打印偏差。我在調(diào)試時發(fā)現(xiàn)當DC同步正常時時間偏差一般能穩(wěn)定在幾百納秒以內(nèi)如果偏差出現(xiàn)毫秒級的漂移那就要檢查參考時鐘從站的選擇是否正確或者從站之間的線纜連接是不是接觸不良。還有一點容易被忽略如果總線上有非DC能力的從站它的報文轉(zhuǎn)發(fā)延遲會對DC同步產(chǎn)生可見的影響這種情況下盡量把DC參考點設(shè)在離主站最近的DC從站上或者調(diào)整DC偏移參數(shù)來補償。同步是所有運動控制系統(tǒng)的生命線SOEM在這塊已經(jīng)封裝得很好但用戶必須理解這些參數(shù)的含義才能合理使用。切忌直接復制網(wǎng)上的參數(shù)不同品牌伺服、不同拓撲結(jié)構(gòu)最優(yōu)配置是不同的。6. 基于QT的完整應(yīng)用框架設(shè)計6.1 界面線程與實時線程如何協(xié)作很多初次接觸QT結(jié)合EtherCAT的人最容易犯的錯誤就是把通信直接塞進GUI線程。QT的GUI線程運行著事件循環(huán)一旦被長時間阻塞界面就會卡死這是絕對不可接受的。正確做法是把EtherCAT周期通信放在一個單獨的實時線程中用信號槽機制與界面線程通信。在設(shè)計這個框架時我會把功能分成三層界面層負責顯示狀態(tài)、接收用戶輸入比如目標位置、速度完全不接觸任何EtherCAT API??刂茖右粋€獨立的QThread運行周期通信循環(huán)調(diào)用SOEM的收發(fā)函數(shù)并維護一個簡化的狀態(tài)機。數(shù)據(jù)層定義好控制指令和反饋數(shù)據(jù)的結(jié)構(gòu)體用互斥鎖或者隊列在控制層和界面層之間傳遞數(shù)據(jù)。對于運動控制系統(tǒng)我通常定義一個EtherCATWorker類繼承自QObject并把它移動到QThread中執(zhí)行class EtherCATWorker : public QObject { Q_OBJECT public: explicit EtherCATWorker(QObject* parent nullptr); public slots: void start(); void stop(); void setTargetPosition(int axis, int32_t pos); signals: void statusUpdated(const QString status); void positionUpdated(int axis, int32_t pos); void errorOccurred(const QString error); private: void cycleLoop(); bool runningFlag; };start()插槽里執(zhí)行前面說到的初始化和狀態(tài)切換然后進入一個while循環(huán)以QElapsedTimer控制周期節(jié)拍在循環(huán)內(nèi)部調(diào)用ec_send_processdata和ec_receive_processdata。這里有個性能細節(jié)周期循環(huán)中不要使用QThread::msleep()來粗略延時因為它的精度不足以支撐1ms以下的控制周期而且受系統(tǒng)調(diào)度影響很大。更可靠的做法是使用QElapsedTimer做自旋等待或者干脆加上std::this_thread::sleep_for(std::chrono::microseconds(...))配合高精度定時器。如果是在Linux平臺還可以配合timerfd或者clock_nanosleep這些實時接口。6.2 跨線程數(shù)據(jù)傳遞不丟不重在EtherCATWorker中更新的伺服位置、狀態(tài)字等數(shù)據(jù)界面需要實時刷新。直接用普通成員變量跨線程讀寫有數(shù)據(jù)競爭的風險用QT信號槽傳輸又可能出現(xiàn)高頻下的丟包或延遲。我的做法是在Worker內(nèi)部用三個環(huán)形緩沖區(qū)分別存儲位置、狀態(tài)和報警數(shù)據(jù)界面線程按需取用用QMutex保護讀寫指針。void EtherCATWorker::cycleLoop() { while (runningFlag) { // 等待周期節(jié)拍 // 發(fā)起過程數(shù)據(jù)交換 ec_send_processdata(); int wkc ec_receive_processdata(0); // 讀取各從站的反饋 for (int i 1; i slaveCount; i) { int32_t pos servoIn[i]-actual_position; uint16_t status servoIn[i]-statusword; mutex.lock(); posBuffer[i] pos; statusBuffer[i] status; mutex.unlock(); } // 向界面發(fā)信號刷新的通知 emit dataRefreshed(); // 周期節(jié)拍控制 } }界面端的刷新槽函數(shù)只需要加鎖拷貝數(shù)據(jù)然后更新控件即可。這樣的設(shè)計下不管界面刷新頻率快慢底層通信都不會受到影響。當然有人會說直接用emit positionUpdated(axis,pos)不是更簡單嗎在軸數(shù)少、通信周期慢的場景下確實夠用但是在16軸甚至32軸、1ms周期的系統(tǒng)里高頻信號的發(fā)射和槽函數(shù)的調(diào)用會引入不必要的線程切換開銷。環(huán)形緩沖區(qū)加通知信號的方案在這類場景下明顯更穩(wěn)。6.3 日志記錄與在線升級產(chǎn)品化的系統(tǒng)不能只跑通就完事。我們需要日志來追蹤異常需要OTA升級能力來修復現(xiàn)場問題。SOEM的FOEFile over EtherCAT協(xié)議可以在EtherCAT總線上傳輸文件最常見的就是給從站升級固件。SOEM提供了ec_foe_write和ec_foe_read函數(shù)使用起來很直接uint32_t err ec_FOEwrite(1, filename, password, buffer, size, timeout);返回值為0代表成功非0值則是從站返回的錯誤碼。在給伺服驅(qū)動器升級固件時注意事項比較多升級前必須確保從站處于Pre-Op狀態(tài)過程數(shù)據(jù)交換要暫停升級期間不要斷電??偩€上多個從站升級時務(wù)必一臺上完再切下一臺絕不能“并發(fā)”。日志方面QT的qInstallMessageHandler可以把運行日志同時輸出到控制臺、文件和網(wǎng)絡(luò)配合SOEM的通信錯誤返回值記錄這樣現(xiàn)場如果出了問題能快速定位是通信鏈路問題還是控制邏輯問題。我見過太多項目因為缺少日志現(xiàn)場調(diào)試像大海撈針一樣。7. 實戰(zhàn)中的常見問題與排查技巧7.1 網(wǎng)卡選型與實時性優(yōu)化SOEM跑不好很多時候問題不出在協(xié)議棧本身而是網(wǎng)卡選錯了。EtherCAT主站對網(wǎng)卡的要求非常嚴格網(wǎng)卡必須支持接收所有以太網(wǎng)幀混雜模式并且發(fā)送和接收的時間戳越精確越好。普通消費級USB網(wǎng)卡是絕對不可用的延遲大還不穩(wěn)定。目前做EtherCAT主站最常用的方案是Intel的I210、I211以及部分瑞昱的千兆網(wǎng)卡芯片。I210因為支持硬件時間戳在DC同步精度要求高的場合幾乎是首選。英特爾的I210網(wǎng)卡成本不算高很多工控主板也直接板載。在Linux下可以通過ethtool -i查看網(wǎng)卡驅(qū)動如果是igb驅(qū)動那基本上就沒問題了。為了提高通信穩(wěn)定性我還會做以下幾個優(yōu)化在多核處理器上把EtherCAT線程綁定到獨立CPU核心避免線程被系統(tǒng)調(diào)度到其他核上。在Linux下使用chrt命令把實時線程設(shè)為SCHED_FIFO優(yōu)先級保證周期調(diào)度的確定性。關(guān)閉網(wǎng)卡的節(jié)能模式、中斷合并Interrupt Moderation盡量降低報文延遲。這些優(yōu)化看似不起眼但對系統(tǒng)的長期穩(wěn)定運行非常關(guān)鍵。7.2 總線掉線、丟幀與從站超時EtherCAT總線上最怕的就是掉從站。運行中一旦某個從站斷線主站發(fā)出的幀就無法正常返回所有從站過程數(shù)據(jù)都可能失效。SOEM的ec_receive_processdata返回值是“工作計數(shù)器”Working Counter的總和通過檢查它可以判斷這一幀通信是否成功。我封裝過一個檢查函數(shù)bool checkWorkingCounter(int expectedWkc) { int wkc ec_receive_processdata(0); if (wkc expectedWkc) { qWarning() WKC mismatch, expected: expectedWkc actual: wkc; // 可以在這里記錄是哪一幀、哪個從站失聯(lián) return false; } return true; }如果報文丟失頻繁首要懷疑點就是網(wǎng)線接頭。EtherCAT要求使用屏蔽雙絞線至少Cat5e且屏蔽層要良好接地。很多現(xiàn)場問題最終都查出來是網(wǎng)線壓接不合格這一段我踩過很多次。總線末端必須接上終結(jié)電阻否則信號反射會導致通信不穩(wěn)定。7.3 從站EEPROM異?;謴瓦€有一種常見的現(xiàn)場故障從站的EEPROM損壞或者數(shù)據(jù)被意外改寫導致SOEM掃描時上報錯誤的從站信息甚至根本掃描不到。針對這種情況SOEM提供了寄存器級的訪問接口可以強制讀寫EEPROM。在實際操作中只要從站的ESC芯片本身沒有損壞就可以通過ec_escread和ec_escrwrite直接訪問EEPROM接口恢復出廠配置。不過要注意裸操作EEPROM風險較大如果誤改了廠商區(qū)域的數(shù)據(jù)從站可能無法正常運行。最穩(wěn)妥的方式是聯(lián)系從站廠商獲取原廠配置工具而不是自行修復?,F(xiàn)場應(yīng)急時可以把錯誤從站隔離然后重新掃描其他從站至少保證單站故障不拖垮整條線。7.4 QT串口界面卡死與崩潰最后聊一個很常見的QT層面的坑。很多人在用QT開發(fā)監(jiān)控界面時習慣在槽函數(shù)里直接更新表格控件比如QTableWidget的行列變化。但如果數(shù)據(jù)刷新頻率太高控件頻繁重建內(nèi)存碎片化會導致界面越來越卡甚至直接崩潰。我建議的做法是在界面上使用模型/視圖架構(gòu)Model/View預先分配足夠大的表格數(shù)據(jù)模型刷新時只更新數(shù)據(jù)而不重建控件。對于大量波形類數(shù)據(jù)的監(jiān)控建議用QCustomPlot或QCharts的增量刷新模式而不是整個圖重繪。如果界面線程和EtherCAT線程之間通過信號槽傳遞高頻數(shù)據(jù)記得用Qt::DirectConnection連接方式并且信號參數(shù)用const QByteArray這種隱式共享類型減少拷貝開銷。8. 一個完整的起步項目單軸伺服的點動控制為了讓大家把前面的知識串起來我給出一個最簡單的完整示例QT界面上放一個按鈕和一個進度條按下按鈕后1號伺服以固定速度運行到指定位置。8.1 界面與業(yè)務(wù)邏輯分離界面很簡單一個QPushButton用于觸發(fā)運動一個QLabel顯示當前位置。整個QT工程的關(guān)鍵代碼集中在Worker線程。void MainWindow::on_startButton_clicked() { if (worker nullptr) { worker new EtherCATWorker(); thread new QThread(); worker-moveToThread(thread); connect(thread, QThread::started, worker, EtherCATWorker::start); connect(worker, EtherCATWorker::positionUpdated, this, MainWindow::updatePosition); connect(worker, EtherCATWorker::statusUpdated, this, MainWindow::updateStatus); connect(worker, EtherCATWorker::errorOccurred, this, MainWindow::showError); thread-start(); } // 發(fā)一個位置指令 worker-setTargetPosition(1, 50000); }EtherCATWorker::start()先初始化SOEM成功之后進入周期循環(huán)。設(shè)置目標位置時注意給數(shù)據(jù)加鎖void EtherCATWorker::setTargetPosition(int axis, int32_t pos) { QMutexLocker locker(cmdMutex); targetPositionMap[axis] pos; }然后在周期循環(huán)里讀取這個目標位置并寫入PDOint32_t pos; { QMutexLocker locker(cmdMutex); pos targetPositionMap[1]; } servoOut[1]-target_position pos; servoOut[1]-controlword 0x000F;這樣單軸點動控制的骨架就出來了。8.2 防止誤觸發(fā)的安全邏輯控制類軟件里“防止誤觸發(fā)”是必須考慮的功能。我在按鈕槽函數(shù)里加入了一個鎖定機制只有從站狀態(tài)都正常、伺服使能成功、急停信號未觸發(fā)時按鈕才有效。bool MainWindow::checkSystemReady() { if (isEmergencyStop) { showError(Emergency stop active!); return false; } if (!worker-isOperational()) { showError(EtherCAT not in OP state); return false; } return true; }很多初學者在調(diào)試時會忽略急停信號。實際上EtherCAT的AL狀態(tài)機和急停邏輯是兩回事——急停信號通常通過硬接線接入IO模塊或者伺服驅(qū)動器的STOSafe Torque Off端子軟件層面只能檢測和報警不能替代硬件的安全功能。這一點必須提醒大家安全回路的設(shè)計一定不要依賴上位機軟件這是人命關(guān)天的事情不能有任何僥幸心理。8.3 從簡單到復雜的演進路線如果你是從零開始接觸這套架構(gòu)我建議的路徑是先跑通官方示例simple_test再用QT封裝一個單軸點動然后擴展成多軸聯(lián)動最后加入DC同步和FOE升級。不要一上來就追求大而全否則出了Bug你根本不知道是協(xié)議棧的問題還是自己代碼的問題。單軸跑通之后再嘗試在兩臺伺服上做同步運動對比DC同步開啟和關(guān)閉時的曲線差異。有了實際的實驗數(shù)據(jù)你對EtherCAT的同步機制才會有真正的體感。9. 個人經(jīng)驗總結(jié)寫這篇文章的時候我回想這幾年在EtherCAT主站上走過的彎路最想傳達的一點是SOEM雖然叫“Simple”但它的內(nèi)部機制并不簡單只是把復雜性封裝得比較友好而已。真正用好的關(guān)鍵還是要把EtherCAT的協(xié)議原理和從站的行為摸透。QT加SOEM這套組合在目前的開源生態(tài)里可以說是“能打”的。QT負責跨平臺的界面、豐富的控件庫和完善的調(diào)試工具SOEM提供輕量、高效、可嵌入的EtherCAT主站能力兩者疊加無論是做實驗室原型還是量產(chǎn)設(shè)備都有足夠的底子。最后分享一個很小的技巧在開發(fā)和調(diào)試階段把SOEM的調(diào)試打印打開能看到每個從站的寄存器讀寫過程對理解協(xié)議和排查問題幫助極大。SOEM在編譯時用DEBUG宏來控制這些輸出QT工程中可以在.pro里加一句DEFINES DEBUG來啟用。等你在實際項目中積累了自己的工具函數(shù)庫和模板工程之后開發(fā)效率會飛躍一個臺階。希望這篇實戰(zhàn)指南能幫你把QT加SOEM的框架從編譯到落地完整搭建起來少踩一些我當年踩過的坑。