戰(zhàn):差分信號(hào)、仲裁機(jī)制與調(diào)試技巧)
CAN 總線這東西搞嵌入式的遲早要碰。不管是做汽車電子、工業(yè)控制還是什么無(wú)人機(jī)、機(jī)械臂只要涉及多個(gè)節(jié)點(diǎn)之間穩(wěn)定通信CAN 總是那個(gè)繞不開的選項(xiàng)。用一句話概括它一對(duì)差分線多節(jié)點(diǎn)掛載無(wú)主機(jī)依賴靠報(bào)文 ID 裁決優(yōu)先級(jí)。這套機(jī)制聽起來簡(jiǎn)單實(shí)際用起來細(xì)節(jié)極多像位時(shí)序采樣、終端電阻匹配、總線仲裁、錯(cuò)誤處理、過濾器配置每一個(gè)環(huán)節(jié)都可能讓你調(diào)試到懷疑人生。我?guī)啄昵暗谝淮巫?CAN 通信的項(xiàng)目時(shí)也被速率配不對(duì)、收發(fā)超時(shí)這類問題折磨過。這篇文章不聊虛的直接從原理講到 STM32 上的手寫代碼把我踩過的坑和常用的套路一并交代清楚。這篇文章適合誰(shuí)看如果你是剛接觸 CAN 的初學(xué)者可以從頭讀到尾建立起完整的知識(shí)框架如果你已經(jīng)在用 STM32 做開發(fā)但只是照抄例程、不太清楚底層怎么回事那重點(diǎn)看協(xié)議細(xì)節(jié)、寄存器配置和調(diào)試經(jīng)驗(yàn)部分能解決現(xiàn)實(shí)中不少疑惑。整篇文章會(huì)用大量類比和實(shí)例盡量讓非汽車電子背景的讀者也能輕松跟上。1. 從物理層看懂 CAN為什么它這么抗干擾1.1 差分信號(hào)一對(duì)線解決所有問題CAN 總線物理層最核心的設(shè)計(jì)就是差分傳輸。常規(guī)串口用一根信號(hào)線對(duì)地傳遞電平受地電位差和電磁干擾影響大。CAN 用 CANH 和 CANL 兩根線傳輸電壓差接收端比較兩根線的電壓差來判斷邏輯電平而不是比較某根線對(duì)地的絕對(duì)電壓。干擾信號(hào)通常是共模形式的差分接收天然就把這種共模干擾抵消掉了所以 CAN 在電機(jī)、逆變器這類強(qiáng)電磁干擾環(huán)境里依然能穩(wěn)定工作。CAN 的顯性電平也就是邏輯 0對(duì)應(yīng) CANH 和 CANL 的電壓差約為 2V隱性電平對(duì)應(yīng)邏輯 1電壓差約為 0V。如果你用示波器看總線波形空閑時(shí)總線電壓大約在 2.5V 附近波動(dòng)顯性位會(huì)被拉低到 1.5V 左右而 CANH 會(huì)被拉高到 3.5V 左右。這種電平定義直接決定了總線仲裁機(jī)制的實(shí)現(xiàn)基礎(chǔ)后文會(huì)詳細(xì)展開。1.2 終端電阻不能省也不能亂接標(biāo)準(zhǔn) CAN 總線要求在總線兩端各接一個(gè) 120Ω 終端電阻作用是匹配傳輸線阻抗減少信號(hào)反射。如果只接一個(gè)、接在中間或者干脆不接板子在短距離低速通信時(shí)可能還能工作但波特率一旦提高或者線長(zhǎng)達(dá)到幾十米就會(huì)在隱性到顯性的跳變沿上產(chǎn)生嚴(yán)重的信號(hào)振鈴導(dǎo)致采樣錯(cuò)誤。很多用 STM32 的人第一次調(diào)試 CAN 不通過就是因?yàn)闆]接終端電阻。我自己測(cè)試時(shí)一般直接把 120Ω 電阻做在轉(zhuǎn)接板上兩端的節(jié)點(diǎn)才各放一個(gè)。還有一個(gè)常見誤區(qū)是兩個(gè)節(jié)點(diǎn)通信時(shí)哪怕只有兩個(gè)設(shè)備也需要在各自端各接一個(gè) 120Ω合起來總線上的等效阻抗才是 60Ω這是 CAN 收發(fā)器輸出驅(qū)動(dòng)設(shè)計(jì)的目標(biāo)負(fù)載。如果兩端各接一個(gè)但中間還有其他節(jié)點(diǎn)那就是過度端接同樣會(huì)出問題。注意部分開發(fā)板上已經(jīng)集成了終端電阻并且有跳線開關(guān)控制使用前務(wù)必確認(rèn)開關(guān)狀態(tài)以免出現(xiàn)兩個(gè)電阻并聯(lián)變成了 60Ω 的異常負(fù)載。1.3 收發(fā)器選型與電平轉(zhuǎn)換STM32 內(nèi)部的 CAN 控制器只是協(xié)議控制器負(fù)責(zé)數(shù)據(jù)鏈路層的幀處理、錯(cuò)誤管理和位時(shí)序它本身不能直接接總線??刂破鬏敵龅氖?TX/RX 數(shù)字信號(hào)必須經(jīng)過 CAN 收發(fā)器轉(zhuǎn)換成 CANH/CANL 差分電平才能掛上總線。常用收發(fā)器有 TJA1050、TJA1040、SN65HVD230 等。選型的時(shí)候要看速率、工作電壓和待機(jī)電流大部分應(yīng)用選 TJA1050 這類就足夠穩(wěn)定。如果你的 MCU 是 3.3V而收發(fā)器是 5V 供電像 TJA1050即使單片機(jī)引腳是 3.3V 推挽輸出CAN 控制器和它對(duì)接也一般沒問題因?yàn)槭瞻l(fā)器的 TXD 輸入閾值兼容 3.3V 邏輯。但要注意有些老式收發(fā)器的高電平輸入閾值偏高最好選擇支持 3.3V 邏輯輸入的型號(hào)或者做電平轉(zhuǎn)換否則偶發(fā)性的發(fā)送異常就很難查了。2. 數(shù)據(jù)鏈路層拆解報(bào)文格式、仲裁和位填充2.1 標(biāo)準(zhǔn)幀與擴(kuò)展幀別把 ID 位寬搞混CAN 2.0 協(xié)議定義了兩種幀格式標(biāo)準(zhǔn)幀和擴(kuò)展幀。標(biāo)準(zhǔn)幀的標(biāo)識(shí)符 ID 是 11 位擴(kuò)展幀是 29 位后者在多節(jié)點(diǎn)復(fù)雜網(wǎng)絡(luò)中更常見因?yàn)樗苋菁{更多不同優(yōu)先級(jí)的報(bào)文類型。兩者的幀頭字段區(qū)分就在 IDE 位。幀的基本結(jié)構(gòu)包括 SOF 起始幀、仲裁段、控制段、數(shù)據(jù)段、CRC 校驗(yàn)段、ACK 應(yīng)答段和 EOF 結(jié)束段。其中控制段里的 DLC 表示數(shù)據(jù)長(zhǎng)度CAN 規(guī)定單幀數(shù)據(jù)最多 8 個(gè)字節(jié)。很多人會(huì)問為什么不用更多這是當(dāng)年總線速率和工業(yè)場(chǎng)景下的折中設(shè)計(jì)保證短幀快速傳輸和低延遲。雖然后來出現(xiàn)了 CAN FD數(shù)據(jù)段最長(zhǎng)可到 64 字節(jié)但直到今天大量設(shè)備仍然跑經(jīng)典 CAN應(yīng)用層協(xié)議也大多兼容 8 字節(jié)格式。2.2 總線仲裁機(jī)制誰(shuí)優(yōu)先級(jí)高誰(shuí)先走這是 CAN 最核心的設(shè)計(jì)之一也是它和普通串口總線最大的區(qū)別。多個(gè)節(jié)點(diǎn)同時(shí)發(fā)送時(shí)總線不會(huì)像 I2C 那樣需要主機(jī)仲裁協(xié)調(diào)而是每一位進(jìn)行比較發(fā)送節(jié)點(diǎn)一邊發(fā) ID 位一邊監(jiān)聽總線電平。如果某個(gè)節(jié)點(diǎn)發(fā)送的是隱性位1卻在總線上讀到顯性位0說明有其他節(jié)點(diǎn)在發(fā)送優(yōu)先級(jí)更高的報(bào)文這個(gè)節(jié)點(diǎn)就立刻退出發(fā)送轉(zhuǎn)為接收下次再自動(dòng)重發(fā)。這種機(jī)制要求 ID 值越小優(yōu)先級(jí)越高。因?yàn)轱@性位 0 能覆蓋隱性位 1ID 前面越多的 0越容易在仲裁中勝出。在工程上通常把控制類報(bào)文分配低 ID 值比如剎車、急停這類實(shí)時(shí)性要求極高的優(yōu)先分配 0x0xx信息類報(bào)文分配高 ID比如狀態(tài)監(jiān)測(cè)、溫度上報(bào)這些允許一定延遲的。設(shè)計(jì)應(yīng)用層時(shí)如果一開始沒有規(guī)劃好 ID 分配策略后期增加節(jié)點(diǎn)會(huì)很痛苦這是實(shí)際項(xiàng)目中容易被低估的問題。2.3 位填充和同步機(jī)制CAN 協(xié)議規(guī)定連續(xù)發(fā)送 5 個(gè)相同電平的位之后必須插入一個(gè)相反電平的填充位。接收端在接收時(shí)會(huì)把這個(gè)填充位移除。這樣做的好處是保證總線上不會(huì)出現(xiàn)長(zhǎng)時(shí)間無(wú)跳變沿的情況因?yàn)榻邮展?jié)點(diǎn)的時(shí)鐘同步依賴于電平跳變沿。收發(fā)雙方的時(shí)鐘始終存在微小誤差如果沒有頻繁的跳變沿來重新同步位時(shí)序時(shí)間一長(zhǎng)采樣點(diǎn)就會(huì)偏移到錯(cuò)誤的位區(qū)間通信立刻出錯(cuò)。這里有個(gè)很容易踩的坑位填充規(guī)則不影響 CRC 計(jì)算的原始數(shù)據(jù)但發(fā)送時(shí) CRC 序列本身也要做填充處理接收端校驗(yàn)時(shí)則需要先剝離填充位。所以調(diào)試時(shí)別只看數(shù)據(jù)幀內(nèi)容如果抓到的波形異??紤]是不是位填充邏輯和 CRC 邊界沒對(duì)齊特別是你自己實(shí)現(xiàn) CAN 協(xié)議或者拿到非標(biāo)準(zhǔn)控制器做兼容時(shí)。3. STM32 bxCAN 外設(shè)過濾器、報(bào)文存儲(chǔ)和中斷3.1 為什么叫 bxCAN它的內(nèi)存結(jié)構(gòu)是什么樣的STM32 里集成的 CAN 控制器叫 bxCAN基于此前某半導(dǎo)體大廠的 IP 核。它有一個(gè)專門的報(bào)文存儲(chǔ)區(qū)不是簡(jiǎn)單地給每個(gè)郵箱分配固定地址而是按照 SRAM 方式訪問控制邏輯來管理。bxCAN 提供 3 個(gè)發(fā)送郵箱和 2 個(gè)接收 FIFO每個(gè) FIFO 深度為 3 個(gè)報(bào)文。對(duì)絕大多數(shù)應(yīng)用來說這個(gè)硬件緩沖池完全夠用。bxCAN 還有一個(gè)顯著特點(diǎn)每個(gè)節(jié)點(diǎn)可以配置多個(gè)過濾器組STM32F103 系列提供 14 組過濾器F405 等系列更多。過濾器的作用是只讓感興趣的報(bào)文進(jìn)入接收 FIFO不屬于當(dāng)前節(jié)點(diǎn)的報(bào)文直接在硬件層面被丟棄這樣能大幅降低 MCU 的中斷負(fù)載。不理解過濾器的工程師容易把接收中斷寫成高頻率觸發(fā)結(jié)果 CPU 大部分時(shí)間都在處理無(wú)關(guān)中斷系統(tǒng)響應(yīng)自然就慢了。3.2 過濾器模式列表模式和掩碼模式怎么選bxCAN 的過濾器支持兩種工作模式列表模式和掩碼模式。列表模式就是精確匹配規(guī)則里寫死的 ID 必須完全一致才接收。掩碼模式則是按位匹配把掩碼中為 1 的位視為需要嚴(yán)格匹配為 0 的位視為不關(guān)心。實(shí)際項(xiàng)目中掩碼模式更適合需要接收一組連續(xù) ID 報(bào)文的場(chǎng)景比如接收 ID 0x100 到 0x10F 之間的所有報(bào)文只需把掩碼低 4 位置 0其余位置 1。配置過濾器的時(shí)候要注意32 位過濾器寬度下標(biāo)準(zhǔn)幀的 ID 在寄存器中存儲(chǔ)位置和擴(kuò)展幀不同而且數(shù)據(jù)位要左移對(duì)齊。不少初學(xué)者直接拿例程改發(fā)現(xiàn)濾出來的 ID 總是不對(duì)其實(shí)是因?yàn)闆]理解寄存器位域的布局。建議配置完過濾器后先開一個(gè)全匹配的掩碼模式驗(yàn)證接收鏈路通了再逐步收窄掩碼這樣能減少排查難度。3.3 發(fā)送郵箱機(jī)制與中斷標(biāo)志發(fā)送時(shí)bxCAN 提供 3 個(gè)發(fā)送郵箱每個(gè)郵箱可以獨(dú)立提交報(bào)文。軟件提交后硬件自動(dòng)按照優(yōu)先級(jí)順序發(fā)送郵箱中的報(bào)文并處理總線仲裁和重發(fā)。發(fā)送完成的標(biāo)志是郵箱的 TXOK 位置 1這個(gè)標(biāo)志可以通過中斷或輪詢方式讀取。一個(gè)常見的工程坑是不考慮郵箱是否空閑就直接往同一個(gè)郵箱反復(fù)寫數(shù)據(jù)導(dǎo)致丟幀。正確的做法是每次發(fā)送前檢查郵箱空滿狀態(tài)或者直接使用發(fā)送完成中斷在中斷回調(diào)里處理下一幀數(shù)據(jù)。多個(gè)節(jié)點(diǎn)或高頻率周期性發(fā)送時(shí)三個(gè)郵箱很容易被占滿如果發(fā)送失敗標(biāo)志出現(xiàn)一定要記錄錯(cuò)誤狀態(tài)并做重發(fā)策略不能悶頭發(fā)。3.4 中斷事件全貌接收中斷、發(fā)送中斷、錯(cuò)誤中斷和喚醒中斷bxCAN 的中斷可分四類接收 FIFO 有報(bào)文時(shí)觸發(fā)接收中斷發(fā)送完成時(shí)觸發(fā)發(fā)送中斷發(fā)生錯(cuò)誤時(shí)觸發(fā)錯(cuò)誤中斷還有 CAN 喚醒中斷和睡眠中斷。用 STM32 開發(fā)時(shí)有些開發(fā)者只開了接收中斷錯(cuò)誤中斷沒開結(jié)果總線一出錯(cuò)代碼就跟傻了一樣沒有反饋。真正穩(wěn)定的通信系統(tǒng)錯(cuò)誤中斷必須開起來哪怕只是在中斷里把一個(gè)錯(cuò)誤計(jì)數(shù)器累加也對(duì)現(xiàn)場(chǎng)定位問題極其有幫助。接收中斷也分 FIFO0 和 FIFO1 兩個(gè)獨(dú)立中斷入口對(duì)應(yīng)不同優(yōu)先級(jí)的報(bào)文策略。可以這樣設(shè)計(jì)將關(guān)鍵控制報(bào)文配置到 FIFO0 并開啟高優(yōu)先級(jí)中斷普通狀態(tài)報(bào)文進(jìn) FIFO1 用低優(yōu)先級(jí)處理或者直接輪詢這樣高實(shí)時(shí)性報(bào)文的中斷延遲大大縮短這是從實(shí)際項(xiàng)目里總結(jié)出的實(shí)用技巧。4. 手把手 STM32 配置從 CubeMX 到標(biāo)準(zhǔn)庫(kù)實(shí)戰(zhàn)4.1 波特率計(jì)算位時(shí)間與采樣點(diǎn)CAN 波特率計(jì)算公式是波特率 APB1 時(shí)鐘頻率 / (預(yù)分頻器 × (同步段 傳播段 相位緩沖段1 相位緩沖段2))。STM32F1 系列的 APB1 時(shí)鐘是 36MHz如果目標(biāo)波特率是 500kbps可以這樣算預(yù)分頻 4時(shí)間單元 36MHz / 4 9MHz位時(shí)間 9000000 / 500000 18 個(gè)時(shí)間單元。常見分配方式同步段 1TQ傳播段 1TQ相位緩沖段1 為 8TQ相位緩沖段2 為 8TQ總位時(shí)間 18TQ。這些段的分配決定采樣點(diǎn)位置。采樣點(diǎn)一般設(shè)置在 75% 到 85% 之間比較穩(wěn)定這是綜合考慮線纜延遲、收發(fā)器傳播延遲和時(shí)鐘誤差的結(jié)果。如果你用 CubeMX 的默認(rèn)配置BS1 8TQBS2 7TQ同步段 1TQ總位時(shí)間就是 16TQ采樣點(diǎn)約 87.5%對(duì)大多數(shù)短距離總線來說也行。但如果總線長(zhǎng)度較長(zhǎng)或者波特率高于 1Mbps建議手動(dòng)設(shè)置采樣點(diǎn)在 80% 左右更穩(wěn)妥。4.2 引腳復(fù)用和時(shí)鐘配置用 CubeMX 時(shí)CAN 引腳默認(rèn)在 PA11 和 PA12CAN RX 和 TX但確認(rèn)一下目標(biāo)板原理圖如果默認(rèn)引腳被占用或者板子引出了其他復(fù)用引腳就需要重新映射。F1 系列 CAN 也支持復(fù)用映射到 PB8、PB9配置時(shí)要選擇正確的復(fù)用功能。初始化順序建議是先配置 GPIO再使能 CAN 外設(shè)時(shí)鐘然后初始化 CAN 參數(shù)最后配置過濾器。如果順序顛倒可能出現(xiàn) CAN 初始化失敗卻查不到原因的情況。需要提醒的是CAN 外設(shè)時(shí)鐘和 GPIO 時(shí)鐘可能掛在不同的 AHB1/APB1 總線上有些人初始化時(shí)忘記開某個(gè)時(shí)鐘就導(dǎo)致外設(shè)不工作這也是常見的低階錯(cuò)誤。檢查時(shí)鐘樹時(shí)確認(rèn) CAN 和 GPIO 所在總線都已使能。這一點(diǎn)看起來基礎(chǔ)但調(diào)試中出現(xiàn)莫名問題后還是值得先排查一遍。4.3 初始化代碼參考下面給出一段基于標(biāo)準(zhǔn)庫(kù)風(fēng)格的初始化參考代碼核心實(shí)現(xiàn)步驟與寄存器操作對(duì) HAL 庫(kù)同樣適用void CAN_Init_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; // RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; // TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); CAN_DeInit(CAN1); CAN_StructInit(CAN_InitStructure); CAN_InitStructure.CAN_TTCM DISABLE; CAN_InitStructure.CAN_ABOM ENABLE; // 自動(dòng)離線恢復(fù) CAN_InitStructure.CAN_AWUM ENABLE; CAN_InitStructure.CAN_NART DISABLE; // 自動(dòng)重傳 CAN_InitStructure.CAN_RFLM DISABLE; // FIFO 不鎖定 CAN_InitStructure.CAN_TXFP DISABLE; // 按 ID 優(yōu)先級(jí)發(fā)送 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure); CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0 | CAN_IT_ERR, ENABLE); }初始化里比較關(guān)鍵的兩個(gè)參數(shù)是 ABOM自動(dòng)離線恢復(fù)和 NART禁止自動(dòng)重傳。ABOM 開啟后如果總線因嚴(yán)重錯(cuò)誤進(jìn)入離線狀態(tài)硬件會(huì)自動(dòng)等待恢復(fù)條件再重新參與通信不需要軟件干預(yù)。NART 關(guān)閉意味著自動(dòng)重傳開啟發(fā)送失敗的幀會(huì)硬件自動(dòng)重傳直到成功或出錯(cuò)。這個(gè)配置對(duì)實(shí)時(shí)性要求高的場(chǎng)景很重要。如果不想讓一幀失敗數(shù)據(jù)一直占用郵箱可以考慮打開 NART自己管理重傳邏輯。提示在雙 CAN 的 STM32 芯片上CAN2 的時(shí)鐘來自 CAN1要啟用 CAN2 必須先初始化 CAN1。這個(gè)坑很多人到后面才遇到。4.4 發(fā)送報(bào)文輪詢與中斷兩種姿勢(shì)先寫一個(gè)發(fā)送函數(shù)核心操作是選擇一個(gè)空閑郵箱、把 ID 和數(shù)據(jù)按位域填充進(jìn)寄存器、提交發(fā)送請(qǐng)求。標(biāo)準(zhǔn)庫(kù)寫成這樣uint8_t CAN_SendStdFrame(uint32_t stdId, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t i; TxMessage.StdId stdId; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len; for (i 0; i len; i) { TxMessage.Data[i] data[i]; } if (CAN_Transmit(CAN1, TxMessage) ! CAN_TxStatus_Ok) { return 1; } return 0; }這個(gè)函數(shù)只做了發(fā)送提交并不保證報(bào)文真的已經(jīng)上總線。如果需要確認(rèn)發(fā)送完成需要再輪詢郵箱狀態(tài)或者開發(fā)送完成中斷。實(shí)際項(xiàng)目中我一般用一個(gè)環(huán)形緩沖區(qū)保存待發(fā)送報(bào)文發(fā)送完成中斷里取下一個(gè)發(fā)送防止高頻調(diào)用發(fā)送函數(shù)時(shí)因?yàn)猷]箱全滿而丟幀。另一個(gè)建議是發(fā)送函數(shù)返回錯(cuò)誤時(shí)把錯(cuò)誤碼記錄下來配合錯(cuò)誤中斷去判斷是總線錯(cuò)誤、仲裁丟失還是郵箱滿。如果發(fā)送一直失敗總線電壓、終端電阻、波特率和節(jié)點(diǎn) ID 沖突是最先要排查的四個(gè)方向。4.5 接收?qǐng)?bào)文中斷回調(diào)解析接收用中斷最直接。在中斷服務(wù)函數(shù)里讀取 FIFO 報(bào)文處理完必須清標(biāo)志。標(biāo)準(zhǔn)庫(kù)的接收中斷處理大致如下void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 根據(jù) RxMessage.StdId 分發(fā)處理 // 放到應(yīng)用層環(huán)形緩沖區(qū)再置事件標(biāo)志 }如果 FIFO 溢出中斷可能會(huì)進(jìn)不來此時(shí)要檢查溢出標(biāo)志 CAN_FLAG_FOV0。一旦發(fā)生溢出說明接收處理太慢或丟棄策略有問題需要優(yōu)化中斷里的處理邏輯盡量減少耗時(shí)操作只做存 buffer 和置標(biāo)志具體解析放到主循環(huán)或?qū)S萌蝿?wù)里。5. 不得不說的坑波特率容差、錯(cuò)誤狀態(tài)機(jī)和總線離線恢復(fù)5.1 波特率容差到底有多大CAN 通信要求每個(gè)節(jié)點(diǎn)的位時(shí)序誤差保持在特定的容差范圍內(nèi)。以 500kbps 為例位時(shí)間 2us在 16TQ 的采樣點(diǎn)配置下采樣窗口大約是 1.75us 處。晶振精度一般 ±0.1% 到 ±0.5%這是沒問題的。問題是很多人用了內(nèi)部 RC 振蕩器精度可能到 ±1% 甚至更高在很多情況下也能跑通但總線一旦拉長(zhǎng)、干擾變大就容易偶發(fā)錯(cuò)誤。所以 CAN 通信推薦使用外部晶振或者至少保證所有節(jié)點(diǎn)時(shí)鐘精度滿足協(xié)議要求。調(diào)試時(shí)最有效的辦法是用示波器看總線波形測(cè)量顯性位寬度是否等于理論位時(shí)間。如果位寬差太多說明波特率配錯(cuò)了如果位寬正確但波形毛刺密集優(yōu)先檢查終端電阻和線纜質(zhì)量??偩€上如果出現(xiàn)連續(xù)錯(cuò)誤幀先用 CAN 分析儀抓包看看錯(cuò)誤幀的類型是填充錯(cuò)誤、CRC 錯(cuò)誤還是應(yīng)答錯(cuò)誤這些信息能快速定位問題方向。5.2 錯(cuò)誤計(jì)數(shù)器和總線離線恢復(fù)機(jī)制CAN 控制器內(nèi)部有發(fā)送錯(cuò)誤計(jì)數(shù)器和接收錯(cuò)誤計(jì)數(shù)器。發(fā)送錯(cuò)誤每錯(cuò)誤一次加 8接收錯(cuò)誤每錯(cuò)誤一次加 1成功發(fā)送或接收一次則減 1。當(dāng)發(fā)送錯(cuò)誤計(jì)數(shù)超過 255節(jié)點(diǎn)進(jìn)入 Bus-Off 狀態(tài)徹底與總線隔離不再發(fā)送任何報(bào)文。在 Bus-Off 狀態(tài)下如果 ABOM 使能硬件會(huì)自動(dòng)等待 128 次總線空閑重新恢復(fù)如果未使能軟件需要手動(dòng)觸發(fā)恢復(fù)流程。錯(cuò)誤狀態(tài)可以通過讀取 ESR 寄存器或調(diào)用庫(kù)函數(shù)狀態(tài)位來獲取。調(diào)試時(shí)我習(xí)慣在主循環(huán)里周期性讀取錯(cuò)誤計(jì)數(shù)一旦發(fā)現(xiàn)持續(xù)增長(zhǎng)就說明鏈路存在系統(tǒng)性問題而不是偶發(fā)干擾。工程現(xiàn)場(chǎng)如果出現(xiàn)間歇性 Bus-Off需要考慮屏蔽線接地、總線拓?fù)浣Y(jié)構(gòu)不要接成星形以及線纜長(zhǎng)度與波特率的匹配關(guān)系。5.3 用測(cè)試模式驗(yàn)證硬件鏈路在查詢具體協(xié)議問題之前先用回環(huán)模式驗(yàn)證 MCU 那一側(cè)是否正常。bxCAN 支持靜默回環(huán)模式Silent Loopback和回環(huán)模式Loopback?;丨h(huán)模式中控制器發(fā)出的報(bào)文會(huì)在內(nèi)部直接被接收不經(jīng)過外部收發(fā)器。初始化時(shí)把 CAN_Mode 設(shè)置為 CAN_Mode_LoopBack然后自發(fā)自收如果接收成功說明控制器和中斷配置沒問題再切到 Normal 模式并連接總線測(cè)試如果失敗問題就鎖定在收發(fā)器、接線或?qū)Χ斯?jié)點(diǎn)。這個(gè)簡(jiǎn)明的排除法能省大量時(shí)間特別是多人協(xié)作調(diào)試時(shí)可以先各自確認(rèn)自發(fā)自收通過再對(duì)接聯(lián)調(diào)避免兩邊互相懷疑浪費(fèi)精力。6. 實(shí)戰(zhàn)經(jīng)驗(yàn)工程中如何設(shè)計(jì)與調(diào)試一套 CAN 節(jié)點(diǎn)6.1 應(yīng)用層協(xié)議設(shè)計(jì)選擇實(shí)際做一個(gè) CAN 項(xiàng)目首先要確定應(yīng)用層協(xié)議。CAN 2.0 只規(guī)定了物理層和數(shù)據(jù)鏈路層的幀格式并沒有定義數(shù)據(jù)內(nèi)容的標(biāo)準(zhǔn)含義所以每個(gè)項(xiàng)目都要自己約定報(bào)文 ID 映射和數(shù)據(jù)字段含義。工業(yè)領(lǐng)域常用 CANopen汽車領(lǐng)域常用 J1939這些協(xié)議棧已經(jīng)幫你規(guī)劃了 ID 分配、對(duì)象字典、周期通信和診斷機(jī)制。如果是簡(jiǎn)單應(yīng)用自己定義一套輕量協(xié)議也完全可行關(guān)鍵要在一開始就明確哪些報(bào)文是周期發(fā)送哪些是事件觸發(fā)哪些是主站請(qǐng)求哪些是從站主動(dòng)上報(bào)。我的習(xí)慣是先整理一張報(bào)文定義表列出每個(gè) ID 的發(fā)送節(jié)點(diǎn)、接收節(jié)點(diǎn)、周期、數(shù)據(jù)長(zhǎng)度、字節(jié)含義和字節(jié)序。后期調(diào)試對(duì)照這張表比翻代碼快得多。尤其是多團(tuán)隊(duì)協(xié)作時(shí)這張表本身就要同步更新不然代碼寫出來各調(diào)各的聯(lián)調(diào)必然一團(tuán)亂。6.2 多節(jié)點(diǎn)組網(wǎng)和線纜規(guī)則CAN 總線拓?fù)渫扑]直線型也就是一根主線上掛所有節(jié)點(diǎn)每個(gè)節(jié)點(diǎn)的連接 STUB 線盡量短。STUB 線超過 1 米時(shí)高速通信很容易出現(xiàn)信號(hào)反射。工業(yè)現(xiàn)場(chǎng)如果節(jié)點(diǎn)分散考慮使用帶 CAN 收發(fā)器的中繼器做分段隔離??偩€供電方式也有講究有些設(shè)備回環(huán)供電但同一網(wǎng)絡(luò)中多個(gè)節(jié)點(diǎn)供電時(shí)要注意地電位差不要太大不然即使有共模抑制也扛不住地面環(huán)路噪音。最好在網(wǎng)絡(luò)兩端之一做單點(diǎn)接地。終端電阻的選擇上標(biāo)準(zhǔn) 120Ω 是常規(guī)配置但低速短距離應(yīng)用有時(shí)會(huì)用 1kΩ 甚至不接也能工作。從長(zhǎng)期可靠性的角度我不建議利用這種無(wú)意中的寬松性還是按規(guī)范來這樣設(shè)備和設(shè)備之間互換時(shí)不會(huì)出莫名其妙的問題。6.3 測(cè)試工具CAN 分析儀和示波器做 CAN 開發(fā)最好有一臺(tái) CAN 分析儀市面上很多 USB 轉(zhuǎn) CAN 的小工具價(jià)格不貴。它可以抓取總線報(bào)文、模擬節(jié)點(diǎn)發(fā)送、統(tǒng)計(jì)錯(cuò)誤幀功能非常實(shí)用。沒有分析儀時(shí)示波器也是必需品至少能觀察波形、測(cè)量位寬度和跳變沿質(zhì)量。示波器探頭要接在 CANH 和 CANL 之間也就是差分測(cè)量不要分別對(duì)地測(cè)量然后口頭比較誤差那樣很容易被噪聲誤導(dǎo)。調(diào)試時(shí)如果發(fā)現(xiàn)總線波形高電平不到 3.5V 或者電壓擺幅偏小檢查收發(fā)器供電電壓是否偏低。波形上升沿明顯傾斜先懷疑線纜電容太大或驅(qū)動(dòng)能力不足必要時(shí)更換更高驅(qū)動(dòng)能力的收發(fā)器。波形上莫名其妙的凹陷和振鈴檢查 STUB 線和終端電阻位置。這些觀察比盲目改代碼高效得多。6.4 高可靠場(chǎng)景的冗余與診斷設(shè)計(jì)在一些穩(wěn)定性要求極高的場(chǎng)景比如電梯控制、車輛通信CAN 網(wǎng)絡(luò)往往采用雙冗余總線或者增加健康狀態(tài)報(bào)文。健康狀態(tài)報(bào)文就是節(jié)點(diǎn)周期性上報(bào)運(yùn)行狀態(tài)、錯(cuò)誤計(jì)數(shù)和供電電壓等信息主控側(cè)一旦超時(shí)未收到某個(gè)節(jié)點(diǎn)的健康報(bào)文就能作出離線判斷。這個(gè)設(shè)計(jì)不復(fù)雜對(duì)定位故障節(jié)點(diǎn)和總線故障非常有幫助。還可以考慮使用 CAN 的遠(yuǎn)程幀RTR機(jī)制讓主站請(qǐng)求從站數(shù)據(jù)。遠(yuǎn)程幀的優(yōu)先級(jí)和數(shù)據(jù)幀一樣靠 ID 仲裁。不過遠(yuǎn)程幀的應(yīng)用沒有想象中那么普遍在很多協(xié)議棧里已經(jīng)弱化了自己設(shè)計(jì)協(xié)議時(shí)按需使用即可不要為了用而用。7. 常見問題速查表與調(diào)試技巧下面整理一份經(jīng)常遇到的 CAN 故障排查表基本都是我在實(shí)際項(xiàng)目中反復(fù)看到的問題現(xiàn)象可能原因排查方向完全收不到報(bào)文終端電阻未接或接錯(cuò)示波器測(cè) CANH/CANL 波形確認(rèn)顯隱性電平變化波特率不一致節(jié)點(diǎn)配置的位時(shí)序不通計(jì)算各節(jié)點(diǎn)實(shí)際波特率比對(duì)采樣點(diǎn)偶發(fā)錯(cuò)誤幀STUB 線過長(zhǎng)、接線松動(dòng)檢查拓?fù)渥ュe(cuò)誤幀類型看 CRC 錯(cuò)誤比例某個(gè)節(jié)點(diǎn)單獨(dú)發(fā)不出過濾器配置錯(cuò)誤先開全匹配掩碼確認(rèn)鏈路總線處于 Bus-Off嚴(yán)重短路或 ID 沖突斷開節(jié)點(diǎn)逐個(gè)排查檢查線纜是否短路發(fā)送超時(shí)且 error 計(jì)數(shù)持續(xù)增長(zhǎng)對(duì)端節(jié)點(diǎn)不存在確認(rèn)節(jié)點(diǎn)是否上電、地址是否匹配、ACK 是否正常還有一個(gè)容易被忽略的問題STM32 復(fù)位期間CAN 引腳是默認(rèn)的浮空輸入狀態(tài)。如果在復(fù)用外設(shè)配置完成后才初始化這段時(shí)間內(nèi)引腳可能被外部總線電平影響導(dǎo)致寄生上電或電平競(jìng)爭(zhēng)問題。板子上若有其他外設(shè)復(fù)用同一引腳務(wù)必檢查引腳沖突這也是排查 CAN 不工作的一個(gè)方向。調(diào)試技巧方面我想強(qiáng)調(diào)日志的重要性。如果固件里開了錯(cuò)誤中斷把錯(cuò)誤狀態(tài)和時(shí)間戳打印到調(diào)試串口存成日志聯(lián)調(diào)時(shí)對(duì)照時(shí)間軸分析總線事件很多時(shí)候能直接定位是哪個(gè)節(jié)點(diǎn)在哪一步觸發(fā)了錯(cuò)誤。我見過不少同事調(diào)試時(shí)不打錯(cuò)誤日志就靠眼睛盯邏輯分析儀效率確實(shí)低很多。再一個(gè)技巧是開發(fā)階段抓包記錄正常發(fā)送報(bào)文的周期和時(shí)序作為后續(xù)回歸測(cè)試的基準(zhǔn)改代碼后如果報(bào)文時(shí)序出現(xiàn)明顯漂移說明代碼引入了額外延遲。8. 從 CAN 到 CAN FD后續(xù)擴(kuò)展思路CAN FDCAN with Flexible Data-rate是經(jīng)典 CAN 的升級(jí)版最大變化是數(shù)據(jù)段可以長(zhǎng)達(dá) 64 字節(jié)并且在數(shù)據(jù)段支持更高的可變速率。它和經(jīng)典 CAN 在幀格式上做了兼容設(shè)計(jì)總線上可以混合運(yùn)行但需要所有節(jié)點(diǎn)都支持才能完全發(fā)揮 FD 特性。新項(xiàng)目中如果確定未來會(huì)有大數(shù)據(jù)量傳輸、固件升級(jí)這類需求直接選支持 CAN FD 的 MCU如 STM32F405 以上系列是更長(zhǎng)遠(yuǎn)的選擇。固件升級(jí)是 CAN 應(yīng)用的經(jīng)典場(chǎng)景通過 CAN 總線把新的固件分包發(fā)給目標(biāo)節(jié)點(diǎn)節(jié)點(diǎn)收到后寫入 Flash 再跳轉(zhuǎn)運(yùn)行。這個(gè)功能對(duì)通信可靠性的要求比普通控制報(bào)文更高通常需要一個(gè)簡(jiǎn)單的傳輸層協(xié)議包括幀序號(hào)、總幀數(shù)、CRC32 校驗(yàn)和重傳機(jī)制。實(shí)現(xiàn)這類功能時(shí)參考 CANopen 的 SDO 分段傳輸思想會(huì)很有啟發(fā)它把大塊數(shù)據(jù)拆成多幀傳輸?shù)臋C(jī)制非常成熟完全可以借鑒。還有多 CAN 網(wǎng)絡(luò)橋接的需求比如車身網(wǎng)絡(luò)和動(dòng)力網(wǎng)絡(luò)之間需要網(wǎng)關(guān)。網(wǎng)關(guān)節(jié)點(diǎn)用兩個(gè) CAN 通道分別接入兩個(gè)網(wǎng)絡(luò)類似 STM32F105 的雙 CAN 特性內(nèi)部做 ID 映射和數(shù)據(jù)轉(zhuǎn)發(fā)。這種場(chǎng)景下過濾器和發(fā)送優(yōu)先級(jí)配置尤為重要否則大量無(wú)關(guān)報(bào)文穿過網(wǎng)關(guān)會(huì)顯著增加負(fù)載。我在實(shí)際使用中對(duì) CAN 最大的感受是它是一種需要從物理層、協(xié)議層到應(yīng)用層全鏈路理解的總線。有時(shí)候問題明明出在應(yīng)用層的 ID 規(guī)劃不合理但排查時(shí)總習(xí)慣先懷疑硬件。反過來也一樣很多人代碼調(diào)了很久最后發(fā)現(xiàn)是時(shí)鐘精度不夠或者終端電阻沒焊。所以我建議新做 CAN 項(xiàng)目的朋友先不要急著上代碼把協(xié)議設(shè)計(jì)、硬件連接和調(diào)試工具都想清楚后面能省下大量時(shí)間。最后再分享一個(gè)小技巧在 STM32 工程里提前寫好一個(gè) CAN 狀態(tài)打印函數(shù)周期性輸出錯(cuò)誤計(jì)數(shù)、發(fā)送失敗次數(shù)、接收 FIFO 剩余空間等數(shù)據(jù)。這個(gè)函數(shù)在開發(fā)階段和現(xiàn)場(chǎng)調(diào)試階段都能用稍微花點(diǎn)時(shí)間回報(bào)率非常高。等以后做復(fù)雜網(wǎng)絡(luò)時(shí)你會(huì)發(fā)現(xiàn)當(dāng)初花在調(diào)試工具上的時(shí)間遠(yuǎn)比想象中值錢。