
簡介本資源是一套基于STM32F103系列單片機的CAN總線雙機通信實戰例程面向嵌入式初學者與物聯網項目開發者聚焦工業現場常見的多節點數據交互場景幫助用戶快速掌握HAL庫下CAN外設配置、消息發送/接收、錯誤處理及雙機協同調試等核心能力。壓縮包共195個文件含107個頭文件定義寄存器映射與接口、69個C源文件涵蓋HAL驅動、主邏輯與CAN協議棧實現、14個匯編啟動文件及KEIL工程配置文件uvprojx/uvoptx整體大小為1.76MB結構完整、模塊清晰便于逐層理解與移植。已有200人學習下載配套代碼注釋詳盡關鍵引腳定義、時鐘配置與CAN濾波器設置均在源碼中標明附帶重置編譯環境的bat腳本顯著降低IDE適配門檻內容預覽顯示工程已集成UART、SPI、I2C、ADC等多外設HAL驅動具備向復雜傳感器融合系統擴展的基礎支撐能力。1. 項目背景與CAN總線通訊的價值最近在整理一個老項目的代碼翻出來一個基于STM32F103的CAN雙機通訊例程用的是HAL庫。這個項目當時是為了實現兩個設備間的實時數據交換比如傳感器數據采集和運動控制指令下發。CAN總線在工業控制、汽車電子這些領域用得非常多它的核心優勢在于多主、高可靠、抗干擾。簡單來說它不像I2C或者UART那樣有明確的主從之分總線上任何一個節點都可以主動發消息而且通過一套復雜的錯誤檢測和仲裁機制保證了即使在惡劣的電氣環境下通訊也能穩定進行。對于STM32F103這類經典的Cortex-M3內核單片機其內置的bxCAN控制器功能相當完整配合ST提供的HAL庫能讓我們把更多精力放在應用邏輯上而不是底層的寄存器操作。這個例程雖然基礎但涵蓋了從CubeMX配置、過濾器設置、中斷處理到數據收發的完整鏈路是理解CAN通訊一個很好的切入點。2. 硬件連接與CubeMX工程配置詳解搞CAN通訊第一步永遠是硬件。STM32F103的CAN接口通常映射到特定的GPIO上最常見的是PA11(CAN_RX)和PA12(CAN_TX)。你需要確保這兩個引腳通過一個CAN收發器比如經典的TJA1050連接到CAN總線上。總線兩端必須各接一個120歐姆的終端電阻這個電阻對于消除信號反射、保證信號完整性至關重要很多通訊不穩定的問題都出在這里。軟件配置從STM32CubeMX開始。新建一個STM32F103C8Tx或其他對應型號工程后關鍵步驟如下2.1 時鐘與引腳配置首先在Pinout Configuration標簽頁下找到Connectivity-CAN1。將模式Mode設置為Normal。這時PA11和PA12會自動被配置為CAN1_RX和CAN1_TX。接著去RCC復位和時鐘控制設置里將High Speed Clock (HSE)選為Crystal/Ceramic Resonator因為CAN對時鐘精度有一定要求通常使用外部晶振更可靠。2.2 CAN參數配置點擊進入CAN1的配置頁面這里有幾個核心參數需要關注Bit Timing Parameters位時序參數這是CAN配置的靈魂直接關系到通訊速率和穩定性。它由Prescaler、Time Quanta in Bit Segment 1、Time Quanta in Bit Segment 2和ReSynchronization Jump Width共同決定。Prescaler預分頻器決定了時間單元Time Quantum, tq的長度。tq (Prescaler) / (APB1總線時鐘頻率)。APB1時鐘在STM32F103上最高為36MHz。Bit Segment 1 (BS1)包含傳播段和相位緩沖段1用于補償網絡上的物理延遲。Bit Segment 2 (BS2)相位緩沖段2。ReSynchronization Jump Width (SJW)重新同步跳轉寬度決定了在一次重新同步中位時間可以被縮短或延長多少個tq通常設置為1或2。一個常見的1Mbps125kbps更常用抗干擾性更好配置示例如下假設APB1時鐘為36MHz目標波特率1Mbps。總的時間單元數Time Quanta (tq) 1 / (波特率 * tq時間)。但更直觀的方法是使用CubeMX的自動計算功能或者手動計算總tq數 BS1 BS2 1同步段。為了穩定性通常總tq數在8-25之間。例如設置Prescaler3BS113 tqBS22 tqSJW1 tq。則tq 3 / 36MHz ≈ 83.33ns。位時間 (1132)*83.33ns ≈ 1.33us對應波特率約751kbps。需要反復調整Prescaler和段長度直到接近目標值。對于初學者可以先從125kbpsPrescaler12,BS113 tq,BS22 tq開始這是工業上非常常見的速率。Operating Mode操作模式選擇Normal即可。Loopback和Silent模式用于自測試和監聽非常有用。功能使能務必勾選CAN Interrupts下的FIFO0 message pending interrupt或FIFO1 message pending interrupt或兩者都勾選這樣收到消息時才能觸發中斷。通常使用FIFO0就夠了。配置完成后生成代碼。CubeMX會幫我們初始化好GPIO、時鐘和CAN外設的基本參數并生成中斷服務函數CAN1_RX0_IRQHandler的框架。3. CAN過濾器配置精準接收的關鍵CAN總線是廣播式的總線上所有消息所有節點都能“聽到”。過濾器Filter的作用就是設置一個“關卡”只讓符合特定規則的消息進入單片機的接收FIFO從而減輕CPU負擔。這是CAN應用開發中必須理解的一環。STM32的bxCAN提供了兩種基本的過濾模式標識符列表模式和標識符掩碼模式。列表模式過濾器像一個嚴格的名單只接收ID完全等于預設值的報文。掩碼模式過濾器像一個模糊匹配的規則。你設置一個ID值和一個掩碼Mask。掩碼位為1表示必須嚴格匹配ID對應位為0表示不關心。例如ID0x123 Mask0x7F0。那么所有ID的高8位0x12必須匹配低4位任意。這常用于接收一組ID連續的報文。在HAL庫中我們通過HAL_CAN_ConfigFilter函數來配置。配置通常在CAN啟動前進行。一個典型的配置掩碼模式過濾器的代碼如下CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank 0; // 使用第0組過濾器F103有14組 sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; // 掩碼模式 sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; // 32位寬模式 sFilterConfig.FilterIdHigh 0x0000; // 期望的ID高16位 sFilterConfig.FilterIdLow 0x0000; // 期望的ID低16位 sFilterConfig.FilterMaskIdHigh 0x0000; // 掩碼高16位 sFilterConfig.FilterMaskIdLow 0x0000; // 掩碼低16位 sFilterConfig.FilterFIFOAssignment CAN_RX_FIFO0; // 匹配到的報文存到FIFO0 sFilterConfig.FilterActivation ENABLE; // 使能該過濾器 sFilterConfig.SlaveStartFilterBank 14; // 對于單CAN設備此參數無效 if (HAL_CAN_ConfigFilter(hcan1, sFilterConfig) ! HAL_OK) { Error_Handler(); }這段代碼將過濾器0配置為32位掩碼模式并且ID和掩碼都設為0。這意味著不進行任何過濾接收所有報文。這在開發調試初期非常有用可以確保能收到數據。在實際應用中你需要根據通訊協議規劃設置具體的ID和掩碼。例如如果你只想接收標準ID為0x100到0x10F的報文可以設置期望ID為0x100掩碼為0x7F0二進制11111110000這樣低4位不關心高7位標準ID共11位必須匹配0x100的高7位。注意過濾器的配置必須在CAN處于初始化模式HAL_CAN_Init之后HAL_CAN_Start之前下進行。一旦CAN啟動過濾器配置就被鎖定了除非重新進入初始化模式否則無法修改。4. 中斷服務函數與報文接收處理流程配置好過濾器并啟動CANHAL_CAN_Start后當有匹配的報文到達就會觸發我們之前使能的中斷。中斷服務函數CAN1_RX0_IRQHandler中會自動調用HAL庫的回調函數HAL_CAN_RxFifo0MsgPendingCallback。我們的核心接收邏輯就寫在這個回調函數里。這個回調函數的工作流程是標準化的檢查中斷來源回調函數被調用意味著FIFO0有消息掛起。讀取報文使用HAL_CAN_GetRxMessage函數從指定的FIFO這里是FIFO0中把報文數據拷貝到一個CAN_RxHeaderTypeDef存放報文頭信息如ID、類型、長度DLC和一個數據數組中。處理數據根據報文頭中的ID解析數據數組中的內容執行相應的應用邏輯如更新變量、設置標志位、轉發數據等。釋放FIFO處理完成后HAL庫在HAL_CAN_GetRxMessage內部通常會管理FIFO的出隊我們無需手動操作。一個典型的接收回調函數實現如下// 定義全局或靜態變量用于接收 CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; // CAN一幀最多8字節 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { if(hcan-Instance CAN1) // 判斷是哪個CAN實例觸發 { // 1. 讀取報文 if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, RxHeader, RxData) HAL_OK) { // 2. 處理數據 uint32_t receivedId RxHeader.StdId; // 獲取標準ID uint8_t dataLength RxHeader.DLC; // 獲取數據長度 // 示例如果ID是0x101則將數據作為32位整數處理 if (receivedId 0x101 dataLength 4) { uint32_t sensorValue (RxData[3] 24) | (RxData[2] 16) | (RxData[1] 8) | RxData[0]; // ... 使用sensorValue ... } // 可以根據不同的ID添加更多的處理分支 } } }這里的關鍵是HAL_CAN_GetRxMessage函數它一次性完成了報文的提取。RxHeader結構體包含了幀的所有元信息RxData數組存放了有效數據載荷。5. 報文發送流程與HAL_CAN_AddTxMessage的使用發送報文比接收要主動一些。核心函數是HAL_CAN_AddTxMessage。發送前你需要填充一個CAN_TxHeaderTypeDef結構體來描述要發送的報文并準備好數據緩沖區。發送流程通常如下準備報文頭設置ID標準或擴展、幀類型數據幀或遠程幀通常用數據幀CAN_RTR_DATA、數據長度DLC0-8。準備數據將待發送的數據填入一個uint8_t數組。選擇郵箱并發送CAN控制器有3個發送郵箱。調用HAL_CAN_AddTxMessage函數會找一個空閑的郵箱將報文裝載進去并自動啟動發送。你可以指定一個uint32_t變量來獲取實際使用的郵箱號。檢查發送完成可以通過輪詢方式檢查HAL_CAN_GetTxMailboxesFreeLevel或者使用發送完成中斷HAL_CAN_TxMailbox0CompleteCallback來確認發送成功。一個阻塞式輪詢等待的發送函數示例uint8_t sendCANMessage(uint32_t id, uint8_t* data, uint8_t len) { CAN_TxHeaderTypeDef TxHeader; uint32_t TxMailbox; // 1. 填充報文頭 TxHeader.StdId id; // 標準ID TxHeader.ExtId 0; // 擴展ID標準幀時設為0 TxHeader.IDE CAN_ID_STD; // 標識符類型標準幀 TxHeader.RTR CAN_RTR_DATA; // 幀類型數據幀 TxHeader.DLC len; // 數據長度 TxHeader.TransmitGlobalTime DISABLE; // 2. 發送報文 if (HAL_CAN_AddTxMessage(hcan1, TxHeader, data, TxMailbox) ! HAL_OK) { return 0; // 發送失敗 } // 3. 可選簡單輪詢等待發送完成超時處理 uint32_t tickstart HAL_GetTick(); while(HAL_CAN_GetTxMailboxesFreeLevel(hcan1) ! 3) // 等待3個郵箱都空閑 { if((HAL_GetTick() - tickstart) 100) // 超時100ms { return 0; } } return 1; // 發送成功 }在實際應用中更推薦使用非阻塞的中斷方式進行發送。在CubeMX中使能CAN1_TX中斷然后在發送函數中不進行輪詢等待。發送請求提交后函數立即返回。當硬件真正發送完成時會觸發CAN1_TX_IRQHandler進而調用對應的回調函數如HAL_CAN_TxMailbox0CompleteCallback你在回調函數里處理發送成功后的邏輯如釋放資源、觸發下一個發送等這樣不會阻塞主程序。6. 雙機通訊例程的完整框架與主循環設計一個完整的雙機通訊例程需要兩個STM32節點。它們的硬件連接相同軟件配置也基本相同波特率、位時序必須完全一致。區別可能在于節點地址/ID規劃每個節點應有自己唯一的發送ID以及需要監聽的其他節點的ID。例如節點A發送ID為0x101接收ID為0x102節點B則相反。過濾器配置根據上述ID規劃配置各自的接收過濾器。主程序的設計通常是一個超級循環while(1)里面包含狀態機或定時任務。一個典型的結構是初始化HAL_Init,SystemClock_Config,MX_GPIO_Init,MX_CAN_Init,MX_USART1_Init用于調試打印等。配置過濾器并啟動CAN同時使能接收中斷HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)。進入主循環定時發送使用HAL_Delay或硬件定時器周期性地如每100ms組織數據并調用發送函數。處理接收數據接收數據的處理在中斷回調函數中完成主循環可以通過檢查回調函數中設置的全局標志位來執行一些非實時或復雜的后續邏輯。狀態指示與調試根據通訊狀態控制LED閃爍或者通過串口打印關鍵信息如發送/接收計數、錯誤狀態這對于調試至關重要。這里有一個關鍵點中斷回調函數里做的事情要快。只做最必要的拷貝和設置標志位把耗時的處理如復雜的計算、格式化字符串打印放到主循環中基于標志位去執行。否則可能因為中斷處理時間過長導致丟失后續報文或系統響應變慢。7. 調試技巧與常見問題排查CAN通訊調試光看代碼不行必須借助工具。最常用的就是USB-CAN適配器如周立功、創芯科技等品牌的產品配合上位機軟件如CANTest、CANPro、PCAN-View等。它能讓你直觀地看到總線上流動的所有報文包括ID、數據、幀類型這是定位問題最快的方法。以下是幾個最常見的坑和排查思路問題一根本收不到任何報文。檢查硬件這是第一步也是最容易出錯的一步。用萬用表測量CAN_H和CAN_L之間的電阻在總線兩端都連接的情況下應該是60歐姆左右兩個120歐并聯。如果電阻是120歐或無窮大說明終端電阻沒接或只接了一端。再檢查TJA1050的電源、STM32與TJA1050之間的連接是否正確。檢查波特率兩個節點的波特率配置必須一字不差。用示波器測量CAN_H和CAN_L的差分信號計算位寬是最直接的方法。或者將兩個節點都配置為環回模式CAN_MODE_LOOPBACK自己發自己收如果能通則說明軟件配置和驅動電路基本沒問題問題很可能出在總線連接或另一個節點上。檢查過濾器確認是否因為過濾器設置過于嚴格把想要的報文過濾掉了。調試初期可以先將過濾器配置為接收所有報文掩碼全0確保物理層和數據鏈路層是通的。檢查中斷是否使能確認HAL_CAN_ActivateNotification被調用且對應的中斷向量表配置正確CubeMX通常已處理好。問題二能收到部分報文但時通時斷或者出現錯誤幀。檢查總線負載如果報文發送太頻繁可能導致總線擁堵。CAN控制器有錯誤計數和自動離線管理功能錯誤過多會進入離線狀態。可以通過HAL_CAN_GetError函數獲取錯誤狀態。檢查電源與地線強烈的共模干擾會導致通訊錯誤。確保各個節點的電源干凈并且共地良好。在干擾大的環境可以考慮使用帶隔離的CAN收發器模塊。查看錯誤計數器HAL庫提供了HAL_CAN_GetError函數可以讀取接收錯誤計數器REC和發送錯誤計數器TEC。如果它們持續增長說明總線存在物理問題或嚴重的仲裁失敗。問題三數據內容不對。檢查字節序這是最經典的坑。CAN報文數據場是8字節的數組對于多字節數據如int32_t, float發送方和接收方必須約定好字節序大端還是小端。通常的做法是約定小端模式即低字節在前。在上一節的接收示例中sensorValue的拼接方式就是小端。檢查DLC確保發送方設置的DLC和實際拷貝的數據長度一致并且接收方按照DLC來解析數據。使用調試工具用USB-CAN適配器抓取總線上的原始報文對比發送的數據和接收的數據一眼就能看出是發送端數據組織錯了還是接收端解析錯了。8. 從基礎例程到實際項目的進階思考這個雙機通訊例程跑通只是萬里長征第一步。在實際項目中我們還需要考慮更多1. 協議設計裸的CAN幀只有ID和數據場你需要在上層定義一套應用層協議。例如如何區分命令幀、數據幀、應答幀如何實現多幀傳輸數據大于8字節如何加入校驗如CRC常見的簡易做法是利用ID的高位作為“幀類型”數據場的第一個字節作為“命令字”或“數據索引”。2. 錯誤處理與恢復增加對HAL_CAN_ErrorCallback回調函數的處理監控總線錯誤、被動錯誤、離線等狀態。當檢測到總線關閉時可以嘗試執行HAL_CAN_ResetError然后重新初始化CAN外設實現自動恢復。3. 使用FreeRTOS等RTOS在復雜的系統中CAN通訊最好放在一個獨立的RTOS任務中。接收中斷通過隊列Queue或信號量Semaphore將報文傳遞給處理任務發送也通過隊列進行緩沖。這樣可以避免在中斷服務函數中處理復雜邏輯也使得發送非阻塞化系統架構更清晰。4. 性能優化對于高頻發送使用三個發送郵箱并配合DMA如果支持可以顯著提高效率。合理規劃過濾器組減少軟件過濾的開銷。如果接收數據量很大確保接收FIFO的溢出中斷被使能并妥善處理防止數據丟失。5. 代碼封裝將CAN的初始化、發送、接收回調處理封裝成獨立的模塊.c/.h文件。對外提供清晰的接口如CAN_Init(),CAN_SendMsg(msg_t* msg),CAN_RegisterRxCallback(callback_func)。這樣主程序代碼會更簡潔模塊也更容易移植到其他項目。最后這個基于HAL庫的例程最大的好處是跨STM32系列芯片的兼容性比較好。當你從F103切換到F4、F7甚至H7系列時CAN外設的基本操作和HAL API是一致的主要差異可能在于時鐘配置和部分高級特性。掌握這個基礎框架再去看ST官方更復雜的例程如CAN FD、CANopen等就會容易得多。本文還有配套的精品資源點擊獲取