戰(zhàn):從協(xié)議棧到GD32應(yīng)用)
簡(jiǎn)介CANopen完整源碼包面向嵌入式系統(tǒng)開(kāi)發(fā)者提供基于CAN總線、符合國(guó)際標(biāo)準(zhǔn)的高層協(xié)議棧實(shí)現(xiàn)適用于工業(yè)自動(dòng)化、運(yùn)動(dòng)控制、機(jī)器人及分布式控制等需要設(shè)備互聯(lián)的場(chǎng)合。壓縮包僅347KB含77個(gè)文件其中30個(gè)h頭文件與28個(gè)c源文件構(gòu)成協(xié)議棧核心覆蓋NMT網(wǎng)絡(luò)管理、SDO服務(wù)數(shù)據(jù)對(duì)象、PDO過(guò)程數(shù)據(jù)對(duì)象、LSS層設(shè)置、心跳、同步、緊急事件等機(jī)制另有7個(gè)Markdown文檔、3個(gè)HTML頁(yè)面用于說(shuō)明與索引2個(gè)Makefile可編譯工程1個(gè)EDS文件用于設(shè)備配置并含Doxygen配置、代碼風(fēng)格規(guī)范、許可證等輔助文件整體結(jié)構(gòu)清晰、便于查閱。已有929人學(xué)習(xí)該資源。除完整協(xié)議外包內(nèi)還提供Linux socketCAN驅(qū)動(dòng)適配層、EEPROM離線存儲(chǔ)、對(duì)象字典自動(dòng)生成、空白驅(qū)動(dòng)模板和基礎(chǔ)main示例可幫助讀者從驅(qū)動(dòng)到應(yīng)用完整打通直接作為項(xiàng)目基底進(jìn)行移植或二次開(kāi)發(fā)是理解CANopen協(xié)議實(shí)現(xiàn)并快速落地的實(shí)用資料。 做嵌入式也有些年頭了手頭用過(guò)的CANopen協(xié)議棧源碼也有好幾套從最早的簡(jiǎn)單Slave實(shí)現(xiàn)到后來(lái)支持LSS、SDO Block Transfer的完整版都接觸過(guò)。最近剛好幫一個(gè)老項(xiàng)目做控制器升級(jí)又把CANopen完整源碼翻出來(lái)重新整理了一遍順手把整個(gè)工程從新唐的M0核移植到GD32F450上全程還比較順利中間積累了不少經(jīng)驗(yàn)。這篇就把我對(duì)一套可用、完整的CANopen源碼的理解和實(shí)際操作心得整理出來(lái)希望能幫到正在找源碼、準(zhǔn)備移植或者想搞懂內(nèi)部機(jī)制的同行。1. 拿到一套完整源碼先別急著編譯——你得先看清它包含哪三類(lèi)東西很多人在網(wǎng)上看到有人分享CANopen源碼或者從某個(gè)開(kāi)源倉(cāng)庫(kù)點(diǎn)了下載就以為拿到了一堆.c和.h文件。實(shí)際上一套真正完整、能落地到產(chǎn)品里的CANopen源碼遠(yuǎn)不止一個(gè)協(xié)議棧核心那么單薄。它最少應(yīng)該包含以下三類(lèi)東西缺了哪一類(lèi)你后面做工程化都會(huì)很難受。第一類(lèi)是協(xié)議棧核心也就是CTCommunication Target部分負(fù)責(zé)處理CAN報(bào)文收發(fā)、對(duì)象字典維護(hù)、NMT狀態(tài)機(jī)、PDO/SDO心跳等基礎(chǔ)協(xié)議功能。這部分是核心中的核心通常占源碼量的六成以上。你需要確認(rèn)的是它支持的CANopen功能子集比如是只支持PDO和SDO還是連緊急報(bào)文、同步報(bào)文、時(shí)間戳都齊全支不支持SDO分段傳輸和塊傳輸NMT從站節(jié)點(diǎn)是否完善。第二類(lèi)是硬件抽象層與驅(qū)動(dòng)它直接決定了這套源碼能不能移植到你手上的MCU。完整的源碼一定針對(duì)不同的MCU和CAN控制器做了驅(qū)動(dòng)適配比如STM32的bxCAN、NXP的FlexCAN或者獨(dú)立的MCP2515 SPI接口CAN控制器。如果源碼的自述文件里列出了好幾個(gè)硬件平臺(tái)說(shuō)明這套源碼是活的應(yīng)用過(guò)驗(yàn)證的而不是誰(shuí)在某個(gè)角落寫(xiě)完了從沒(méi)跑過(guò)的死代碼。第三類(lèi)是工具與平臺(tái)代碼包括配套的上位機(jī)測(cè)試工具、對(duì)象字典生成器、以及必要的文檔。比如有的源碼帶一個(gè)cangen或者candump的集成腳本有的帶一個(gè)Excel表形式的對(duì)象字典配置模板還有的帶E DS文件的導(dǎo)入導(dǎo)出工具。這些輔助內(nèi)容在你做量產(chǎn)配置、一致性測(cè)試時(shí)特別有用沒(méi)有它們你只能手動(dòng)改源碼里的各個(gè)索引改錯(cuò)一個(gè)字節(jié)都查半天。我見(jiàn)過(guò)不少朋友拿到的所謂“完整源碼”其實(shí)是個(gè)半殘品協(xié)議棧核心看起來(lái)挺全但驅(qū)動(dòng)層只有一個(gè)空殼工具鏈部分直接缺失。所以拿到源碼的第一件事是去讀它的README和移植說(shuō)明把源碼的目錄結(jié)構(gòu)吃透搞清楚哪個(gè)目錄是核心、哪個(gè)目錄是BSP、哪個(gè)目錄是示例工程。實(shí)際上這一步比編譯通過(guò)更有價(jià)值因?yàn)樗鼪Q定了你后續(xù)自研的比例到底有多大。2. 從NMT到SDO源碼內(nèi)部到底在跑什么——一個(gè)完整源碼的模塊拆解CANopen完整源碼的核心價(jià)值在于它實(shí)現(xiàn)了一條完整的通信鏈路而不只是收發(fā)幾個(gè)CAN報(bào)文幀。很多剛剛接觸CANopen協(xié)議棧源碼的人看了幾天還是一頭霧水根本原因是只盯著某一兩個(gè)源文件猛看卻沒(méi)有站在全局模塊層面去理解整個(gè)協(xié)議棧的分工。這里我把一套規(guī)范協(xié)議棧的核心模塊拆開(kāi)講一遍幫你先建立整體認(rèn)知。2.1 對(duì)象字典模塊OD整個(gè)CANopen的“內(nèi)存數(shù)據(jù)庫(kù)”對(duì)象字典是CANopen協(xié)議的核心中的核心。你不要把對(duì)象字典當(dāng)成一堆結(jié)構(gòu)體變量它本質(zhì)上是一個(gè)“地址映射表”所有通信對(duì)象都是靠訪問(wèn)這個(gè)字典來(lái)讀寫(xiě)實(shí)際設(shè)備參數(shù)的。完整的源碼里對(duì)象字典模塊通常會(huì)提供一個(gè)OD入口數(shù)組里面每一項(xiàng)包含索引、子索引、數(shù)據(jù)類(lèi)型、訪問(wèn)屬性、存儲(chǔ)地址等屬性。比如你看到一個(gè)0x2000的制造商自定義對(duì)象它靠的就是去OD表里面查對(duì)應(yīng)的value地址然后通過(guò)SDO命令讀寫(xiě)。實(shí)際使用中很多移植失敗的問(wèn)題不是CAN收發(fā)不行而是OD表的數(shù)據(jù)類(lèi)型長(zhǎng)度定義錯(cuò)了導(dǎo)致訪問(wèn)越界或者返回長(zhǎng)度異常。套用實(shí)際經(jīng)驗(yàn)在修改OD表時(shí)數(shù)據(jù)類(lèi)型的長(zhǎng)度定義要嚴(yán)格對(duì)照CANopen標(biāo)準(zhǔn)的基本數(shù)據(jù)類(lèi)型一個(gè)unsigned16你寫(xiě)成8位后面讀出來(lái)全是錯(cuò)亂的。2.2 NMT模塊從站設(shè)備的狀態(tài)管家網(wǎng)絡(luò)管理NMT模塊負(fù)責(zé)CANopen節(jié)點(diǎn)的狀態(tài)管理與啟動(dòng)控制。完整源碼里面NMT的狀態(tài)機(jī)通常會(huì)有四個(gè)狀態(tài)初始化、預(yù)操作、運(yùn)行、停止。你可能在調(diào)試時(shí)發(fā)現(xiàn)從站連上主站但一直不進(jìn)入運(yùn)行狀態(tài)大概率就是NMT狀態(tài)機(jī)沒(méi)有處理主站發(fā)來(lái)的啟動(dòng)命令。源碼里NMT模塊一般會(huì)封裝成NMT_Slave_StateMachine()這樣一個(gè)周期處理入口在定時(shí)中斷或者主循環(huán)里調(diào)用。好的源碼還會(huì)帶有命令執(zhí)行記錄或者日志接口方便你定位的是自己沒(méi)進(jìn)狀態(tài)機(jī)還是報(bào)文沒(méi)收到。我之前就遇到過(guò)一次問(wèn)題主站發(fā)的NMT命令從站收不到排查到最后發(fā)現(xiàn)是驗(yàn)收濾波器把11位ID的NMT報(bào)文過(guò)濾掉了這也是完整源碼也不會(huì)給你避免的硬件配置坑。2.3 SDO與PDO兩種完全不同的數(shù)據(jù)通道服務(wù)數(shù)據(jù)對(duì)象SDO是點(diǎn)對(duì)點(diǎn)的、有確認(rèn)的傳輸方式適合參數(shù)配置與少量數(shù)據(jù)讀寫(xiě)。它在源碼里通常對(duì)應(yīng)一個(gè)比較龐大的狀態(tài)機(jī)因?yàn)樾枰幚矸侄螀f(xié)議、塊傳輸、中止報(bào)文等復(fù)雜邏輯。PDO則是廣播式的實(shí)時(shí)數(shù)據(jù)傳輸傳輸沒(méi)有確認(rèn)機(jī)制數(shù)據(jù)載荷最多8字節(jié)適合周期性的過(guò)程數(shù)據(jù)。你如果要做伺服控制里的位置給定和實(shí)際值回讀走的一定是PDO而不是SDO。一套完整的CANopen源碼中SDO與PDO的緩沖區(qū)管理、事件觸發(fā)條件、同步周期計(jì)數(shù)這些細(xì)節(jié)通常是開(kāi)源的你在移植時(shí)注意區(qū)分它們是基于查詢(xún)還是中斷機(jī)制實(shí)現(xiàn)的因?yàn)檫@兩種機(jī)制對(duì)實(shí)時(shí)性影響很大。在完整源碼的內(nèi)部邏輯中PDO的同步窗口處理往往還涉及到SYNC報(bào)文的計(jì)時(shí)精度這部分如果實(shí)現(xiàn)得不到位多伺服同步就會(huì)產(chǎn)生累計(jì)偏差。2.4 心跳與心跳消費(fèi)者系統(tǒng)健康的哨兵心跳報(bào)文Heartbeat是CANopen里節(jié)點(diǎn)狀態(tài)監(jiān)測(cè)的關(guān)鍵機(jī)制。一個(gè)節(jié)點(diǎn)以生產(chǎn)者方式周期發(fā)出當(dāng)前狀態(tài)另一個(gè)節(jié)點(diǎn)以消費(fèi)者方式監(jiān)測(cè)如果超時(shí)未收到則認(rèn)為對(duì)端節(jié)點(diǎn)故障。這個(gè)機(jī)制在你做設(shè)備安全聯(lián)鎖時(shí)特別有用。源碼里通常有單獨(dú)的Heartbeat_Producer()和Heartbeat_Consumer()函數(shù)你要注意區(qū)分它們對(duì)應(yīng)的對(duì)象字典參數(shù)0x1017是生產(chǎn)者心跳周期0x1016是消費(fèi)者心跳超時(shí)配置。很多初用者搞混心跳和節(jié)點(diǎn)守護(hù)Node Guarding的區(qū)別實(shí)際在源碼里它們的實(shí)現(xiàn)路徑完全不同而且心跳的實(shí)現(xiàn)更加簡(jiǎn)潔。我在實(shí)際項(xiàng)目的經(jīng)驗(yàn)是新設(shè)計(jì)的系統(tǒng)一律優(yōu)先用心跳機(jī)制因?yàn)樗渲煤?jiǎn)單、雙向監(jiān)控也方便主站對(duì)多個(gè)從站的在線狀態(tài)做全局管理無(wú)需像Node Guarding那樣頻繁發(fā)請(qǐng)求幀。3. 移植的真實(shí)工作量跟網(wǎng)上說(shuō)的“改改函數(shù)就行”相去甚遠(yuǎn)拿到完整源碼以后的第一個(gè)實(shí)戰(zhàn)任務(wù)必然是移植。我見(jiàn)過(guò)網(wǎng)上很多說(shuō)法是“只需修改幾個(gè)接口函數(shù)就能跑”實(shí)際操作下來(lái)你會(huì)發(fā)現(xiàn)這句話(huà)說(shuō)得太輕巧了。完整的移植工作分幾個(gè)層面每一個(gè)層面都可能卡住你半天以上。3.1 定時(shí)器與時(shí)間基準(zhǔn)最先要做的事CANopen協(xié)議棧的時(shí)間敏感度非常高SDO超時(shí)、PDO事件周期、心跳發(fā)送周期、SYNC同步等等全部依賴(lài)一個(gè)穩(wěn)定的時(shí)基。絕大多數(shù)CANopen源碼會(huì)定義一個(gè)類(lèi)似Timer_Init()、Timer_GetTick()的接口你需要把它對(duì)接上MCU的硬件定時(shí)器。這里有個(gè)關(guān)鍵細(xì)節(jié)時(shí)基的粒度最好選用1ms如果選10msPDO事件周期的最小變化步長(zhǎng)就成了10ms如果你需要3ms周期的PDO就會(huì)無(wú)能為力。我當(dāng)時(shí)移植到GD32上是用了一個(gè)32位自由運(yùn)行的定時(shí)器每次系統(tǒng)節(jié)拍中斷里調(diào)用一個(gè)Timer_IncTick()函數(shù)給協(xié)議棧累計(jì)時(shí)基。千萬(wàn)注意如果你用的是FreeRTOS這類(lèi)操作系統(tǒng)不要直接用vTaskDelay()做時(shí)間基準(zhǔn)延時(shí)因?yàn)閰f(xié)議棧內(nèi)部很多狀態(tài)機(jī)是查詢(xún)式的阻塞延時(shí)會(huì)導(dǎo)致?tīng)顟B(tài)機(jī)翻轉(zhuǎn)不及時(shí)。正確做法是協(xié)議棧的周期任務(wù)以系統(tǒng)節(jié)拍的形式被調(diào)用純粹的非阻塞模型。3.2 CAN底層驅(qū)動(dòng)收發(fā)函數(shù)的封裝要點(diǎn)移植的第二個(gè)大頭是CAN收發(fā)接口。源碼里一般會(huì)給出CAN驅(qū)動(dòng)的模板文件你需要實(shí)現(xiàn)CAN控制器的初始化、報(bào)文發(fā)送、接收中斷處理、錯(cuò)誤處理等幾個(gè)函數(shù)。這里最容易踩的坑是過(guò)濾器配置。CANopen的11位CAN ID分配有明確規(guī)則NMT是0x000SDO請(qǐng)求是0x600加節(jié)點(diǎn)號(hào)SDO響應(yīng)是0x580加節(jié)點(diǎn)號(hào)PDO則根據(jù)TPDO/RPDO各有不同范圍。如果驅(qū)動(dòng)層沒(méi)有正確配置驗(yàn)收過(guò)濾器收發(fā)全亂。我給當(dāng)時(shí)用的GD32F450配置過(guò)濾器的時(shí)候就用了一種比較省事的方式全部以屏蔽模式只接收前兩位ID匹配“11”和“10”的幀這是CANopen框架的常用區(qū)分方式剩下的交給協(xié)議棧篩選。這樣硬件接收中斷的壓力小協(xié)議棧內(nèi)部的DBT動(dòng)態(tài)分配地址也能正常工作。此外驅(qū)動(dòng)層還得把CAN控制器的錯(cuò)誤狀態(tài)中斷接進(jìn)來(lái)好的完整源碼會(huì)利用總線錯(cuò)誤計(jì)數(shù)器實(shí)現(xiàn)總線的錯(cuò)誤主動(dòng)、錯(cuò)誤被動(dòng)、總線關(guān)閉等狀態(tài)的反饋上報(bào)這是做設(shè)備診斷的前提。3.3 心跳、同步、LSS的移植細(xì)節(jié)如何驗(yàn)證——逐級(jí)點(diǎn)亮移植完成后你不能只看一個(gè)心跳報(bào)文能發(fā)出來(lái)就認(rèn)為大功告成。驗(yàn)證要分階段走首先是SDO讀寫(xiě)對(duì)象字典里的幾個(gè)關(guān)鍵對(duì)象比如讀0x1000設(shè)備類(lèi)型、0x1001錯(cuò)誤寄存器然后是PDO的動(dòng)態(tài)映射改0x1A00系列對(duì)象再重新映射TPDO看看數(shù)據(jù)有沒(méi)有按新映射關(guān)系發(fā)出來(lái)再測(cè)同步報(bào)文看PDO是否能按同步計(jì)數(shù)發(fā)送最后如果源碼支持LSS還要驗(yàn)證LSS快速掃描能否在總線上找到你設(shè)置的節(jié)點(diǎn)號(hào)。這個(gè)過(guò)程中我最常向同事推薦的做法是用主站軟件打開(kāi)日志回放功能把啟動(dòng)階段的總線流量全部記錄成日志文件。一旦從站響應(yīng)不對(duì)就對(duì)比日志里面CAN幀的時(shí)間戳和ID序列能迅速定位問(wèn)題是出在狀態(tài)機(jī)沒(méi)啟動(dòng)還是對(duì)象字典的響應(yīng)異常。這條排查思路幫我解決過(guò)不少隱蔽的時(shí)序問(wèn)題比反復(fù)搭邏輯分析儀還直觀。4. 一張CAN卡配一個(gè)從站調(diào)試工具如何驗(yàn)證“完整源碼”是真的完整拿到源碼并完成移植后你得驗(yàn)證源碼功能到底全不全而不是它聲稱(chēng)全不全。驗(yàn)證手段主要靠總線上的實(shí)際報(bào)文交互。這里推薦一套基礎(chǔ)但很實(shí)用的驗(yàn)證環(huán)境一張兼容CANopen協(xié)議的USB轉(zhuǎn)CAN卡如周立功、PEAK等一個(gè)CAN調(diào)試軟件如CANTest、PCAN-View再加上你燒錄了源代碼的從站設(shè)備。驗(yàn)證最優(yōu)先級(jí)的是SDO通信。在CAN調(diào)試軟件里發(fā)一條SDO讀命令比如要讀取0x1000數(shù)據(jù)類(lèi)型為32位無(wú)符號(hào)就發(fā)送COB-ID 0x600節(jié)點(diǎn)號(hào)的報(bào)文字節(jié)40 00 10 00 00 00 00 00。正常源碼頭實(shí)現(xiàn)的對(duì)端會(huì)立刻回一條帶數(shù)據(jù)的SDO響應(yīng)。如果沒(méi)回復(fù)先看軟件層有沒(méi)有進(jìn)SDO處理函數(shù)再看硬件CAN收發(fā)是不是正常。最經(jīng)典的是很多人漏了SDO的傳輸類(lèi)型檢查導(dǎo)致響應(yīng)發(fā)不出來(lái)。接下來(lái)是PDO的動(dòng)態(tài)行為驗(yàn)證。先通過(guò)SDO把TPDO1的映射參數(shù)改為你想要的數(shù)據(jù)對(duì)象并設(shè)置傳輸類(lèi)型為同步周期1即每收到一個(gè)SYNC就發(fā)送一次然后周期性地發(fā)送SYNC報(bào)文COB-ID 0x80數(shù)據(jù)長(zhǎng)度0字節(jié)觀察從站是否會(huì)每隔一個(gè)SYNC就發(fā)出TPDO報(bào)文。能跑通這一步說(shuō)明PDO的動(dòng)態(tài)配置也完整。心跳節(jié)點(diǎn)驗(yàn)證更簡(jiǎn)單配置從站的0x1017心跳周期為100ms然后在調(diào)試軟件里以SDO方式寫(xiě)入觀察總線上的心跳報(bào)文COB-ID 0x700節(jié)點(diǎn)號(hào)是否以100ms周期穩(wěn)定發(fā)出。如果還想全面一點(diǎn)可以再測(cè)一下緊急報(bào)文人為制造一個(gè)錯(cuò)誤事件比如把從站的一個(gè)模擬量輸入通道短接到地看0x80節(jié)點(diǎn)號(hào)的緊急報(bào)文有沒(méi)有按錯(cuò)誤碼主動(dòng)上報(bào)。這一整套流程跑下來(lái)你對(duì)手上這套源碼的能力邊界就心里有數(shù)了。哪些功能能用、哪些還有bug、哪些實(shí)際是沒(méi)實(shí)現(xiàn)完的一目了然。5. 選型回看為什么說(shuō)完整源碼比你自己拼裝協(xié)議更值得最后聊一個(gè)繞不開(kāi)的問(wèn)題為什么我們不自己直接從CAN驅(qū)動(dòng)層開(kāi)始逐字實(shí)現(xiàn)CANopen非要找一套完整源碼很多團(tuán)隊(duì)做產(chǎn)品時(shí)受“掌控一切”的心態(tài)驅(qū)使覺(jué)得論文里都寫(xiě)清了協(xié)議原理自己動(dòng)手實(shí)現(xiàn)一個(gè)CANopen從站也不難。但實(shí)際做下去會(huì)發(fā)現(xiàn)CANopen的復(fù)雜度藏在一堆細(xì)節(jié)里對(duì)象字典的異常處理、SDO分段協(xié)議的狀態(tài)翻轉(zhuǎn)、TPDO事件時(shí)間窗抖動(dòng)、多主站切換時(shí)的狀態(tài)協(xié)調(diào)……這些如果用專(zhuān)業(yè)工具做一致性測(cè)試到處都是坑。完整源碼的價(jià)值就在于它把這些坑從“你可能踩到”變成了“已經(jīng)有人踩過(guò)并修補(bǔ)好了”。從項(xiàng)目成本看借助完整源碼能節(jié)省大量市場(chǎng)驗(yàn)證時(shí)間。尤其是當(dāng)你的產(chǎn)品需要跟不同品牌的主站設(shè)備配合時(shí)源碼的協(xié)議兼容性直接決定了你在客戶(hù)現(xiàn)場(chǎng)的調(diào)試時(shí)間。以我自己的經(jīng)驗(yàn)選擇一套具備多個(gè)真實(shí)項(xiàng)目支持經(jīng)歷并持續(xù)維護(hù)的完整源碼在長(zhǎng)遠(yuǎn)角度不管是穩(wěn)定性還是可維護(hù)性上都比從零攢協(xié)議省力得多。當(dāng)然你也得根據(jù)自己的實(shí)際需求選擇源碼的具體方向——做產(chǎn)品商用的話(huà)留意許可證協(xié)議開(kāi)源的有LGPL、BSD等商用授權(quán)也有做學(xué)習(xí)研究就找文檔和注釋詳細(xì)的開(kāi)源版本更重視多主站支持和冗余功能的話(huà)可以?xún)?yōu)先選擇帶CiA305支持的完整實(shí)現(xiàn)。這些都是在選型時(shí)值得額外花時(shí)間研究的點(diǎn)。最后再分享一個(gè)實(shí)操習(xí)慣你在移植過(guò)程中可以在對(duì)象字典里加一個(gè)自定義的診斷對(duì)象把自己移植調(diào)試時(shí)的內(nèi)部狀態(tài)機(jī)值通過(guò)SDO實(shí)時(shí)讀出來(lái)。這個(gè)方法在我做復(fù)雜狀態(tài)定位的時(shí)候幫了大忙尤其適合在總線日志之外快速看清系統(tǒng)內(nèi)部的狀態(tài)流轉(zhuǎn)。本文還有配套的精品資源點(diǎn)擊獲取