:時鐘、引腳與中斷配置避坑指南)
簡介這份資源面向使用STM32F407進行多串口開發(fā)的嵌入式工程師與學生提供UART1至UART6六路串口同時工作的完整工程代碼解決多串口并行通信、不定長度數(shù)據(jù)接收等常見開發(fā)難題。工程基于CubeMx生成六個串口均已啟用DMA與空閑中斷可高效接收變長數(shù)據(jù)幀并支持通過CubeMx裁剪串口數(shù)量裁剪后僅需刪除少量與硬件資源相關(guān)的代碼即可移植默認使用片外8M晶振也可在CubeMx中修改。壓縮包共67個文件約581KB以41個h頭文件與19個c源文件為主體涵蓋HAL庫配置、中斷服務(wù)、MSP初始化與主程序邏輯另含ioc工程文件、MDK-ARM工程文件及啟動匯編文件方便直接編譯與二次開發(fā)。目前已有7802人學習下載適合需要快速搭建多串口通信框架、研究DMA加空閑中斷接收機制的開發(fā)者參考借鑒。1. 六路串口一起上STM32F407 串口1~6 同時使用的環(huán)境底賬很多人第一次在 STM32F407 上把 USART1 到 UART6 全部打開都會經(jīng)歷一個反直覺的瞬間代碼編譯通過、下載成功但串口助手里只有一兩個口有輸出其余全是空白。這不是玄學是時鐘、引腳、中斷優(yōu)先級三件事沒對齊。STM32F407 的六個串口分布在 APB1 和 APB2 兩條總線上USART1 和 USART6 掛 APB2USART2/3 和 UART4/5 掛 APB1總線頻率不同波特率寄存器算錯一位就全盤亂碼。這套環(huán)境適合正在做多路數(shù)據(jù)采集、工控網(wǎng)關(guān)、多傳感器匯聚的開發(fā)者尤其是需要同時接 GPS、藍牙、上位機、調(diào)試口、RS485 和預(yù)留通道的場景。裝不好不會報錯只會讓你在示波器前懷疑人生。2. 環(huán)境準備與版本選擇時鐘、引腳、庫的三條硬線2.1 芯片型號與封裝確認STM32F407 家族里不是每個型號都引出全部六個串口。LQFP100 封裝的 F407VET6、F407VGT6 通常能完整引出 USART1~3 和 UART4/5USART6 要看具體引腳復(fù)用表。LQFP64 的 F407ZE 之類UART5 和 USART6 可能被其他功能占用。我一般先翻對應(yīng)型號的 datasheet 第 3 章引腳定義表把六個串口的 TX/RX 候選引腳列出來再對照自己板子的原理圖確認哪些腳已經(jīng)接了晶振、SWD、USB 或外部 Flash。常見做法是優(yōu)先選默認復(fù)用引腳避免重映射帶來的額外配置。比如 USART1 默認 PA9/PA10USART2 默認 PA2/PA3USART3 默認 PB10/PB11UART4 默認 PC10/PC11UART5 默認 PC12/PD2USART6 默認 PC6/PC7。如果板子已經(jīng)把這些腳占用了再考慮部分重映射或完全重映射但重映射會牽扯 AFIO 時鐘和重映射寄存器多一層出錯點。2.2 標準外設(shè)庫還是 HAL 庫網(wǎng)上關(guān)于 STM32F407 串口配置的代碼標準外設(shè)庫和 HAL 庫各占一半。標準外設(shè)庫直接操作寄存器代碼量小、執(zhí)行效率高但 ST 已經(jīng)停止維護新項目用得少。HAL 庫配合 CubeMX 生成初始化代碼六個串口的配置可以圖形化勾選適合快速搭建。我自己的取舍是如果項目周期緊、團隊里有人不熟悉寄存器用 HAL 庫加 CubeMX如果對中斷響應(yīng)時間和代碼體積敏感用標準外設(shè)庫手動配。版本上標準外設(shè)庫選 V1.8.0 或 V1.9.0 都行差別不大。HAL 庫建議用 STM32CubeF4 包里較新的版本比如 V1.27.x 之后的早期版本在 UART 中斷接收上有已知的溢出標志清除問題。注意HAL 庫版本和 CubeMX 版本要匹配否則生成的代碼可能編譯不過。2.3 時鐘樹與總線頻率這是六串口同時使用最容易翻車的地方。STM32F407 最高主頻 168MHzAPB1 最高 42MHzAPB2 最高 84MHz。USART1 和 USART6 在 APB2 上USART2/3 和 UART4/5 在 APB1 上。如果 APB1 分頻系數(shù)設(shè)成 4APB1 時鐘就是 42MHzAPB2 分頻系數(shù)設(shè)成 2APB2 時鐘就是 84MHz。波特率計算公式是USARTDIV fCK / (16 × 波特率)。以 115200 為例APB2 上 USART1 的 USARTDIV 84000000 / (16 × 115200) ≈ 45.5729整數(shù)部分 45小數(shù)部分 0.5729 × 16 ≈ 9.17取 9。APB1 上 USART2 的 USARTDIV 42000000 / (16 × 115200) ≈ 22.7865整數(shù)部分 22小數(shù)部分 0.7865 × 16 ≈ 12.58取 13。如果兩個口用了同一個波特率寄存器值必然有一個是亂碼。提示用 CubeMX 配置時鐘樹時把 APB1 和 APB2 的分頻系數(shù)截圖保存后面手動算波特率或者排查亂碼時直接對照。3. 六串口初始化從 GPIO 到 NVIC 的完整配置3.1 GPIO 與復(fù)用功能配置六個串口同時使用第一步是把所有 TX/RX 引腳配成復(fù)用推挽輸出和浮空輸入或上拉輸入。標準外設(shè)庫的寫法如下// 使能 GPIO 和 USART 時鐘 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_USART6, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_USART3 | RCC_APB1Periph_UART4 | RCC_APB1Periph_UART5, ENABLE); // USART1 PA9 TX, PA10 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // USART2 PA2 TX, PA3 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2); // USART3 PB10 TX, PB11 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3); GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3); // UART4 PC10 TX, PC11 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_UART4); GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_UART4); // UART5 PC12 TX, PD2 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_UART5); GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_Init(GPIOD, GPIO_InitStructure); GPIO_PinAFConfig(GPIOD, GPIO_PinSource2, GPIO_AF_UART5); // USART6 PC6 TX, PC7 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_USART6); GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_USART6);這段代碼的關(guān)鍵點有三個。第一時鐘使能要分總線APB1 和 APB2 分開寫漏一個對應(yīng)的串口就完全沒反應(yīng)。第二GPIO_PinAFConfig 的復(fù)用功能號必須和引腳對應(yīng)USART1 到 USART6 的 AF 編號不全是同一個F407 上 USART1/2/3 是 AF7UART4/5 和 USART6 也是 AF8 或 AF7具體查數(shù)據(jù)手冊的復(fù)用功能映射表。第三UART5 的 TX 在 PC12RX 在 PD2跨了兩個 GPIO 端口初始化時別只配一個。3.2 串口參數(shù)與中斷優(yōu)先級GPIO 配好后逐個初始化串口參數(shù)。六個串口的波特率可以不同但同一個串口的 TX 和 RX 必須一致。下面以 USART1 和 USART2 為例// USART1 初始化波特率 115200 USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); // USART2 初始化波特率 9600 USART_InitStructure.USART_BaudRate 9600; USART_Init(USART2, USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); // 其余 USART3、UART4、UART5、USART6 按同樣結(jié)構(gòu)逐個初始化參數(shù)說明USART_WordLength_8b 是數(shù)據(jù)位 8 位USART_StopBits_1 是停止位 1 位USART_Parity_No 是無校驗。如果某個口接的是 Modbus RTU 設(shè)備通常用 8 數(shù)據(jù)位、1 停止位、無校驗或偶校驗按從機手冊改。USART_Mode 要同時開 Rx 和 Tx只開一個的話另一個方向沒數(shù)據(jù)。中斷優(yōu)先級是六串口同時使用時最容易被忽略的。NVIC 里每個串口的中斷通道不同USART1 是 USART1_IRQnUSART2 是 USART2_IRQn依此類推。如果六個口都開接收中斷優(yōu)先級分組和搶占優(yōu)先級要提前規(guī)劃。我一般把調(diào)試口 USART1 設(shè)成最高搶占優(yōu)先級數(shù)據(jù)采集口設(shè)成中等預(yù)留口設(shè)成最低。用 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2) 分組2 位搶占優(yōu)先級、2 位子優(yōu)先級夠用。NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_Init(NVIC_InitStructure); // 其余串口依次降低搶占優(yōu)先級3.3 中斷服務(wù)函數(shù)與數(shù)據(jù)緩沖六個串口的中斷服務(wù)函數(shù)名是固定的寫錯一個字母就進不了中斷。標準外設(shè)庫下分別是 USART1_IRQHandler、USART2_IRQHandler、USART3_IRQHandler、UART4_IRQHandler、UART5_IRQHandler、USART6_IRQHandler。每個函數(shù)里先判斷 RXNE 標志再讀數(shù)據(jù)寄存器最后清標志。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 存入 USART1 接收緩沖區(qū) USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } void UART4_IRQHandler(void) { if (USART_GetITStatus(UART4, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(UART4); // 存入 UART4 接收緩沖區(qū) USART_ClearITPendingBit(UART4, USART_IT_RXNE); } }注意 UART4 和 UART5 的中斷函數(shù)名里沒有 S是 UART4_IRQHandler 而不是 USART4_IRQHandler這是很多人編譯報錯的直接原因。另外如果開了多個中斷源比如同時開 RXNE 和 IDLE判斷順序要先 RXNE 后 IDLE否則可能丟數(shù)據(jù)。4. 避坑與排查六串口同時使用的五條血淚記錄4.1 現(xiàn)象只有 USART1 有輸出其余全靜默原因APB1 時鐘沒使能或者使能了但波特率算錯。USART2/3 和 UART4/5 掛 APB1如果 RCC_APB1PeriphClockCmd 里漏了某個串口對應(yīng)口完全不工作。另一種可能是 GPIO 復(fù)用功能號寫錯引腳沒有真正切換到串口模式。解決用調(diào)試器讀 RCC_APB1ENR 寄存器確認對應(yīng)位是 1。再用萬用表測 TX 引腳空閑時應(yīng)該是高電平如果一直是低電平說明 GPIO 沒配成復(fù)用推挽。逐個口單獨測試先保證一個口能收發(fā)再疊加下一個。4.2 現(xiàn)象數(shù)據(jù)能發(fā)但全是亂碼原因波特率不匹配。APB1 和 APB2 頻率不同如果兩個口用了同一個 USARTDIV 值掛在不同總線上的口必然有一個亂碼。另外如果外部晶振頻率和代碼里 HSE_VALUE 定義不一致比如板子是 8MHz 但代碼寫 25MHz系統(tǒng)時鐘整體偏移所有串口波特率都會偏。解決用示波器測 TX 引腳的一個字節(jié)波形算實際波特率?;蛘哂么谥种饌€口試不同波特率能正常收發(fā)的就是實際值。檢查 system_stm32f4xx.c 里的 HSE_VALUE 和 PLL 配置確保和板子晶振一致。4.3 現(xiàn)象同時收發(fā)時丟數(shù)據(jù)或死機原因中斷優(yōu)先級沖突或緩沖區(qū)溢出。六個口都開接收中斷如果某個口數(shù)據(jù)量大、中斷服務(wù)函數(shù)里處理時間過長高優(yōu)先級中斷會打斷低優(yōu)先級導致低優(yōu)先級口的數(shù)據(jù)寄存器溢出。另外如果中斷函數(shù)里調(diào)用了 printf 之類的阻塞函數(shù)直接卡死。解決中斷服務(wù)函數(shù)里只做最少的操作把數(shù)據(jù)存入環(huán)形緩沖區(qū)主循環(huán)里再處理。提高數(shù)據(jù)量大的串口搶占優(yōu)先級或者用 DMA 接收。檢查 ORE 溢出標志如果置位了先讀 SR 再讀 DR 清除。4.4 現(xiàn)象UART5 完全沒反應(yīng)原因UART5 的 TX 在 PC12RX 在 PD2跨了 GPIOC 和 GPIOD 兩個端口。如果只使能了 GPIOC 時鐘PD2 就沒法配成復(fù)用功能。另外UART5 沒有重映射選項引腳是固定的。解決確認 RCC_AHB1Periph_GPIOD 時鐘已使能PD2 配成復(fù)用功能并選對 AF 號。用示波器直接測 PC12如果發(fā)送時沒有波形檢查 UART5 的時鐘使能和 USART_Cmd 是否調(diào)用。4.5 現(xiàn)象CubeMX 生成的代碼編譯報錯提示重復(fù)定義原因CubeMX 在生成代碼時如果手動改過 main.c 或 stm32f4xx_it.c重新生成會覆蓋或產(chǎn)生重復(fù)的中斷服務(wù)函數(shù)。另外HAL 庫和標準外設(shè)庫混用時函數(shù)名沖突。解決在 CubeMX 里把用戶代碼寫在 /* USER CODE BEGIN/ 和 /USER CODE END */ 之間重新生成不會丟。如果已經(jīng)混用了兩個庫把標準外設(shè)庫的初始化代碼全部移到 HAL 之外或者干脆統(tǒng)一用一個庫。5. 裝完怎么驗證六串口回環(huán)與壓力測試六個串口全部初始化完成后不要急著接外部設(shè)備先做回環(huán)測試。把每個串口的 TX 和 RX 用杜邦線短接發(fā)送一個字節(jié)看能否原樣收到。這一步能排除 GPIO、波特率、中斷配置的大部分問題。// 六串口回環(huán)測試逐個發(fā)送并等待接收 uint8_t test_data 0x5A; uint8_t recv_data 0; // USART1 回環(huán) USART_SendData(USART1, test_data); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); recv_data USART_ReceiveData(USART1); if (recv_data ! test_data) { /* 記錄 USART1 失敗 */ } // 其余五個口按同樣邏輯逐個測試回環(huán)通過后再做并發(fā)壓力測試。用一個定時器每 10ms 向六個口同時發(fā)送遞增數(shù)據(jù)接收端用串口助手分別打開六個 COM 口觀察是否有丟包或亂碼。我一般會連續(xù)跑 10 分鐘統(tǒng)計每個口的接收字節(jié)數(shù)和錯誤數(shù)。如果某個口錯誤率明顯偏高優(yōu)先查該口的時鐘使能和波特率寄存器值。驗證時還有一個容易忽略的點六個串口同時工作時電源電流會上升。如果板子供電不足可能出現(xiàn)某個口時好時壞。用萬用表測 3.3V 軌電壓發(fā)送時如果跌到 3.0V 以下就要檢查 LDO 或電源走線。從那以后我每次配多串口都強制先跑一遍六口回環(huán)再跑十分鐘并發(fā)壓力測試確認每個口的錯誤計數(shù)為零才接外部設(shè)備。希望幫到你。本文還有配套的精品資源點擊獲取