議底層原理與STM32實戰(zhàn):從SPI/I2C到DMA工程實踐)
1. 從第一性原理重新理解嵌入式系統(tǒng)1.1 為什么“會用”和“懂”之間隔著一道鴻溝很多人學嵌入式是從“點燈”開始的。打開CubeMX配置一個GPIO輸出生成代碼在while循環(huán)里翻轉(zhuǎn)電平LED閃爍成就感拉滿。然后學UART學I2C學SPI每個外設(shè)都跑一遍例程看起來什么都會了。但一旦項目需求稍微偏離例程——比如要求SPI用DMA接收不定長數(shù)據(jù)、I2C總線上掛多個同地址設(shè)備、DMA傳輸一半要動態(tài)切換緩沖區(qū)——立刻就卡住了。這個現(xiàn)象背后的原因很直接大多數(shù)教程教的是“怎么配置”而不是“為什么這樣配置”。配置步驟是死的但工程問題是活的。你背下了I2C需要開漏輸出加上拉電阻但不知道為什么所以當上拉電阻從4.7k換成10k導致通信失敗時你只能靠猜。你照抄了SPI的DMA配置代碼但不知道DMA的傳輸完成中斷和空閑中斷有什么區(qū)別所以面對不定長數(shù)據(jù)幀時無從下手。第一性原理的思路就是回到協(xié)議本身要解決什么問題回到硬件層面信號是怎么走的回到寄存器層面每個bit控制了什么。理解了這些配置代碼只是表達你理解的一種方式而不是需要死記硬背的咒語。1.2 嵌入式系統(tǒng)的本質(zhì)在約束中做權(quán)衡嵌入式系統(tǒng)最核心的特征不是“小”而是“約束”。CPU主頻有限、RAM有限、Flash有限、功耗有限、成本有限。所有的技術(shù)選型本質(zhì)上都是在這些約束下做權(quán)衡。舉個例子為什么STM32F103的SPI最高只能到18MHzAPB2時鐘72MHz的4分頻而有些專用芯片能到50MHz以上因為通用MCU的SPI外設(shè)要兼顧靈活性內(nèi)部邏輯更復雜信號路徑更長時序余量更小。而專用芯片只需要做好一件事可以把電路優(yōu)化到極致。再比如為什么I2C要用開漏輸出加上拉電阻而不是像SPI那樣推挽輸出因為I2C是總線結(jié)構(gòu)允許多個設(shè)備掛在同一組信號線上。推挽輸出意味著每個設(shè)備都強行把線拉高或拉低如果兩個設(shè)備同時輸出不同電平就會短路。開漏輸出只能拉低或者釋放高阻態(tài)釋放時由上拉電阻把線拉高這樣就不會有沖突。代價是上升沿變緩因為上拉電阻給總線電容充電需要時間。這些“為什么”才是嵌入式系統(tǒng)核心知識的骨架。具體的外設(shè)配置、寄存器操作、代碼實現(xiàn)都是在這個骨架上長出來的血肉。1.3 這份講義適合誰能解決什么問題這份內(nèi)容面向的是已經(jīng)跑過例程、但想真正搞懂底層原理的開發(fā)者。如果你還在糾結(jié)怎么安裝Keil、怎么新建工程那可能需要先補一補基礎(chǔ)操作。但如果你已經(jīng)能用STM32完成一些基本功能卻在以下場景中感到吃力那這份講義就是為你準備的SPI通信時好時壞示波器上看波形總覺得哪里不對但又說不清楚I2C總線上掛了三四個設(shè)備偶爾某個設(shè)備不響應(yīng)重啟又好了用DMA傳輸數(shù)據(jù)不知道什么時候該用普通模式、什么時候該用循環(huán)模式看到別人的代碼里DMA加空閑中斷處理串口數(shù)據(jù)想搞懂原理但找不到系統(tǒng)講解想從STM32F103過渡到更復雜的項目但感覺底層知識不夠扎實接下來的內(nèi)容會從協(xié)議原理講到硬件電路從寄存器操作講到HAL庫封裝從單個外設(shè)講到多外設(shè)協(xié)同。每個環(huán)節(jié)都會解釋“為什么這樣做”并給出實際項目中驗證過的配置和代碼。2. 通信協(xié)議的第一性原理SPI與I2C到底在解決什么問題2.1 同步串行通信的基本模型SPI和I2C都屬于同步串行通信。拆開來看同步意味著通信雙方共享一個時鐘信號數(shù)據(jù)在時鐘的節(jié)拍下逐位傳輸串行意味著數(shù)據(jù)一位一位地走而不是像并行總線那樣多位同時傳輸。為什么嵌入式系統(tǒng)偏愛串行通信因為引腳資源寶貴。一個8位并行總線需要8根數(shù)據(jù)線加若干控制線而SPI只需要4根SCK、MOSI、MISO、CSI2C只需要2根SCL、SDA。引腳少意味著PCB布線簡單、芯片封裝可以更小、成本更低。代價是傳輸速率——同樣時鐘頻率下串行比并行慢。但在大多數(shù)嵌入式場景中傳感器數(shù)據(jù)量不大串行完全夠用。同步通信和異步通信比如UART的核心區(qū)別在于同步通信有時鐘線接收方知道每一位數(shù)據(jù)的準確時刻異步通信沒有時鐘線接收方靠波特率約定來采樣對時鐘精度的要求更高。SPI和I2C有時鐘線所以對雙方時鐘精度的容忍度更高可以實現(xiàn)更高的速率。2.2 SPI簡單粗暴的高速通道SPI的設(shè)計哲學是“簡單”。一個主機、一個或多個從機主機產(chǎn)生時鐘數(shù)據(jù)在時鐘邊沿移入移出。沒有地址、沒有應(yīng)答、沒有仲裁就是純粹的移位寄存器對移位寄存器。SPI有四種模式由CPOL時鐘極性和CPHA時鐘相位組合決定。CPOL決定空閑時SCK是高還是低CPHA決定數(shù)據(jù)在第一個邊沿還是第二個邊沿采樣。很多初學者在這里卡住其實用一個簡單的方法就能記住CPHA0時數(shù)據(jù)在第一個邊沿采樣第二個邊沿移出CPHA1時數(shù)據(jù)在第一個邊沿移出第二個邊沿采樣。具體用哪種模式取決于從機芯片的數(shù)據(jù)手冊沒有商量余地。SPI的片選信號CS有兩種實現(xiàn)方式硬件片選和軟件片選。硬件片選由SPI外設(shè)自動控制傳輸時拉低傳輸完拉高時序精準。軟件片選就是普通GPIO代碼里手動拉低拉高。硬件片選的問題是STM32的SPI外設(shè)通常只有一到兩個硬件片選引腳掛多個從機時不夠用。軟件片選更靈活但要注意在拉低CS和開始傳輸之間不能有太長的延遲否則某些從機會超時復位。實操心得用軟件片選時拉低CS之后最好插入幾個NOP或者短暫延時再開始SPI傳輸。我遇到過從機芯片要求CS建立時間至少50ns而STM32的GPIO翻轉(zhuǎn)太快直接傳輸導致第一個bit丟失。后來在CS拉低后加了一個__NOP()就穩(wěn)定了。SPI的速率可以很高。STM32F103的SPI1掛在APB2上最高72MHz經(jīng)過2分頻后36MHz但實際能否跑到36MHz取決于從機芯片和PCB走線質(zhì)量。一般傳感器芯片SPI速率在1MHz到10MHz之間SPI Flash可以到50MHz以上。速率越高對PCB走線的要求越高——線要短、要等長、要遠離干擾源。2.3 I2C兩根線上的多設(shè)備舞蹈I2C的設(shè)計哲學是“用最少的線連接最多的設(shè)備”。兩根線SCL和SDA可以掛128個設(shè)備7位地址。每個設(shè)備有唯一的地址主機通過地址來選擇通信對象。I2C的時序比SPI復雜得多。起始條件、地址幀、應(yīng)答位、數(shù)據(jù)幀、停止條件每個環(huán)節(jié)都有嚴格的時序要求。起始條件是SCL高時SDA從高變低停止條件是SCL高時SDA從低變高。數(shù)據(jù)在SCL低時變化在SCL高時采樣。應(yīng)答位是接收方在第9個時鐘周期把SDA拉低。為什么I2C要用開漏輸出加上拉電阻前面已經(jīng)解釋過核心原因是總線仲裁和防止沖突。但這里還有一個實際問題上拉電阻的阻值怎么選阻值太小功耗大且可能超過設(shè)備的灌電流能力阻值太大上升沿太慢高速通信時波形還沒恢復到高電平就被下一個時鐘拉低了。計算上拉電阻的公式是R tr / (0.8473 × C)其中tr是上升時間要求C是總線電容。標準模式100kHz要求上升時間小于1000ns快速模式400kHz要求小于300ns。假設(shè)總線電容200pF快速模式下R 300ns / (0.8473 × 200pF) ≈ 1.77kΩ。所以快速模式常用1.8k到2.2k的上拉電阻標準模式常用4.7k到10k。注意I2C總線上掛的設(shè)備越多總線電容越大上拉電阻就要越小。但總電容不能超過400pF否則需要加I2C緩沖器或中繼器。我見過一個項目掛了8個I2C設(shè)備總線電容超過600pF400kHz通信時波形完全變形降到100kHz才能勉強工作。后來加了I2C多路復用器才解決問題。I2C還有一個容易踩的坑地址沖突。有些芯片的I2C地址是固定的比如某些EEPROM的地址是0xA0如果掛兩個同型號的EEPROM就會沖突。解決辦法是用I2C多路復用器如TCA9548A或者選擇地址可配置的芯片。另外有些芯片的地址引腳可以配置但配置方式不同——有的接高電平表示1有的接低電平表示1看數(shù)據(jù)手冊時要仔細。2.4 SPI與I2C的選型邏輯什么時候用SPI什么時候用I2C這不是一個非此即彼的問題而是根據(jù)需求權(quán)衡。對比維度SPII2C引腳數(shù)4根多從機加CS2根最高速率幾十MHz幾MHz受總線電容限制多設(shè)備支持每個設(shè)備一根CS地址尋址兩根線掛多個協(xié)議開銷無純數(shù)據(jù)移位起始、地址、應(yīng)答、停止硬件復雜度簡單需要開漏和上拉抗干擾能力推挽輸出較強開漏輸出較弱典型應(yīng)用Flash、顯示屏、高速ADC傳感器、EEPROM、RTC選型的核心邏輯是如果數(shù)據(jù)量大、速率要求高、設(shè)備數(shù)量少選SPI如果設(shè)備多、引腳緊張、速率要求不高選I2C。有些場景兩者都可以比如驅(qū)動一個OLED屏SPI和I2C版本都有SPI刷新率更高但占引腳多I2C省引腳但刷新率低。還有一個實際考慮PCB布線。SPI的時鐘線是高速信號走線要短、要遠離模擬信號。I2C的兩根線雖然速率低但因為是開漏輸出上升沿緩容易受干擾走線也要注意。如果板上同時有SPI和I2C盡量讓它們走不同的層或者保持足夠間距。3. STM32外設(shè)配置的底層邏輯從寄存器到HAL庫3.1 GPIO的八種模式與選擇依據(jù)STM32的GPIO有八種模式輸入浮空、輸入上拉、輸入下拉、模擬輸入、開漏輸出、推挽輸出、開漏復用、推挽復用。每種模式對應(yīng)不同的硬件結(jié)構(gòu)適用于不同的場景。輸入浮空引腳懸空時電平不確定適合外部已經(jīng)有上拉或下拉的場合。比如I2C的SDA和SCL外部有上拉電阻MCU端配置為復用開漏即可不需要內(nèi)部上拉。輸入上拉/下拉內(nèi)部有30k到50k的弱上拉或下拉適合按鍵輸入等簡單場景。但內(nèi)部上拉電阻較大驅(qū)動能力弱如果外部有強干擾還是需要外部上拉。模擬輸入引腳直接連到ADC或DAC不經(jīng)過施密特觸發(fā)器減少數(shù)字電路對模擬信號的干擾。配置ADC引腳時必須用這個模式。推挽輸出可以輸出高電平和低電平驅(qū)動能力強適合驅(qū)動LED、控制使能引腳等。但兩個推挽輸出不能直接連在一起否則會短路。開漏輸出只能拉低或釋放釋放時靠外部上拉電阻拉高。適合I2C、多設(shè)備共享信號線、電平轉(zhuǎn)換等場景。復用推挽/開漏當引腳被SPI、UART、I2C等外設(shè)使用時需要配置為復用模式。SPI的SCK、MOSI用復用推挽I2C的SCL、SDA用復用開漏。實操心得配置GPIO時最容易犯的錯誤是忘記開時鐘。STM32的每個外設(shè)都有獨立的時鐘門控不使能時鐘寄存器寫不進去引腳也不工作。我見過有人調(diào)試了半天I2C不通信最后發(fā)現(xiàn)是GPIOB的時鐘沒開。現(xiàn)在養(yǎng)成的習慣是任何外設(shè)初始化之前先確認對應(yīng)的RCC時鐘已經(jīng)使能。3.2 SPI外設(shè)的配置細節(jié)STM32的SPI外設(shè)配置涉及幾個關(guān)鍵參數(shù)波特率預分頻、時鐘極性、時鐘相位、數(shù)據(jù)幀格式、片選管理。波特率預分頻決定了SPI的時鐘頻率。SPI1掛在APB2上SPI2和SPI3掛在APB1上。APB2最高72MHzAPB1最高36MHz。預分頻系數(shù)可以是2、4、8、16、32、64、128、256。實際SPI時鐘 APB時鐘 / 預分頻系數(shù)。比如SPI1用8分頻就是72/89MHz。時鐘極性和相位要和從機匹配。大多數(shù)SPI Flash用模式0CPOL0CPHA0或模式3CPOL1CPHA1。傳感器芯片的模式各不相同必須查數(shù)據(jù)手冊。數(shù)據(jù)幀格式通常是8位但有些芯片支持16位。STM32的SPI支持8位和16位通過DFF位配置。如果用DMA傳輸數(shù)據(jù)寬度要和DMA配置匹配。片選管理有兩種硬件NSS和軟件NSS。硬件NSS由SPI外設(shè)自動控制但STM32的硬件NSS有時會有問題——比如在多主機場景下NSS引腳被拉低時SPI會自動進入從模式。所以大多數(shù)項目用軟件NSS即普通GPIO手動控制。// SPI1初始化示例軟件NSS模式08位數(shù)據(jù)9MHz void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_HandleTypeDef hspi1 {0}; // 使能時鐘 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5SCK, PA6MISO, PA7MOSI GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA4CS普通推挽輸出 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS默認高 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1); }這段代碼里有個細節(jié)GPIO速度配置為HIGH。SPI時鐘9MHz時GPIO翻轉(zhuǎn)速度要足夠快否則波形會變形。STM32的GPIO速度等級有LOW、MEDIUM、HIGH、VERY_HIGH對應(yīng)不同的壓擺率。SPI的SCK和MOSI建議用HIGH或VERY_HIGH。3.3 I2C外設(shè)的配置與常見問題STM32的I2C外設(shè)配置比SPI復雜因為I2C協(xié)議本身有更多狀態(tài)。關(guān)鍵參數(shù)包括時鐘頻率、占空比、自身地址、應(yīng)答使能、地址模式。時鐘頻率配置為100kHz或400kHz對應(yīng)標準模式和快速模式。占空比在快速模式下可以配置為2:1或16:9影響SCL高電平和低電平的時間比例。STM32F103的I2C外設(shè)有一個著名的問題在特定條件下會鎖死。具體表現(xiàn)是I2C通信中途卡住SCL被拉低不放總線死鎖。原因是STM32的I2C狀態(tài)機在某些錯誤條件下無法自動恢復。解決辦法是在檢測到總線錯誤時手動切換SCL和SDA為普通GPIO模擬幾個時鐘脈沖把從機狀態(tài)復位然后重新初始化I2C。// I2C總線恢復函數(shù) void I2C_Bus_Recovery(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; uint8_t i; // 切換SCL和SDA為普通開漏輸出 GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // SDA拉高SCL發(fā)9個脈沖 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); for(i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } // 發(fā)送停止條件SCL高時SDA從低變高 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); // 重新初始化為I2C復用開漏 GPIO_InitStruct.Mode GPIO_MODE_AF_OD; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }注意這個恢復函數(shù)要在I2C初始化之前調(diào)用或者在檢測到通信超時后調(diào)用。我一般會在I2C讀寫函數(shù)里加超時檢測超時后調(diào)用恢復函數(shù)然后重試一次。實測下來90%以上的I2C死鎖都能恢復。I2C的另一個常見問題是上拉電阻。前面算過快速模式400kHz時如果總線電容200pF上拉電阻要小于1.8k。但很多開發(fā)板用的是4.7k或10k跑400kHz時波形上升沿太緩通信不穩(wěn)定。如果遇到I2C通信時好時壞先用示波器看SCL和SDA的上升沿如果上升時間超過300ns就要減小上拉電阻。3.4 DMA讓數(shù)據(jù)搬運不占用CPUDMA是嵌入式系統(tǒng)里提升效率的關(guān)鍵外設(shè)。沒有DMA時串口每收一個字節(jié)都要進中斷CPU頻繁被打斷效率低。有了DMA數(shù)據(jù)可以在外設(shè)和內(nèi)存之間自動搬運CPU只需要在傳輸完成后處理一次。STM32的DMA有多個通道每個通道可以配置源地址、目標地址、傳輸方向、數(shù)據(jù)寬度、傳輸模式。傳輸模式有普通模式和循環(huán)模式。普通模式傳輸完設(shè)定數(shù)量后停止循環(huán)模式傳輸完后自動重新開始。DMA的傳輸完成中斷和半傳輸中斷是兩個重要概念。傳輸完成中斷在全部數(shù)據(jù)傳輸完后觸發(fā)半傳輸中斷在傳輸了一半時觸發(fā)。半傳輸中斷常用于雙緩沖——一半數(shù)據(jù)在傳輸時CPU處理另一半。串口DMA接收不定長數(shù)據(jù)是一個經(jīng)典場景。串口數(shù)據(jù)長度不固定DMA配置為循環(huán)模式配合串口空閑中斷IDLE當總線空閑時觸發(fā)中斷此時DMA已經(jīng)接收了若干字節(jié)CPU讀取當前DMA剩余計數(shù)就能算出接收了多少數(shù)據(jù)。// 串口DMA加空閑中斷接收不定長數(shù)據(jù) #define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint16_t rx_len 0; volatile uint8_t rx_flag 0; void UART_DMA_Init(void) { // 串口和DMA初始化省略關(guān)鍵是DMA配置為循環(huán)模式 // 并使能串口空閑中斷 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUFFER_SIZE); } // 串口中斷處理 void USART1_IRQHandler(void) { if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_DMAStop(huart1); rx_len RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); rx_flag 1; HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUFFER_SIZE); } HAL_UART_IRQHandler(huart1); }這段代碼的核心邏輯是DMA循環(huán)接收空閑中斷觸發(fā)時計算已接收字節(jié)數(shù)置標志位重新啟動DMA。主循環(huán)檢測到標志位后處理數(shù)據(jù)。實操心得DMA加空閑中斷處理串口數(shù)據(jù)時有一個坑是DMA的循環(huán)模式和普通模式選擇。如果用普通模式每次接收完要重新配置DMA比較麻煩。用循環(huán)模式DMA自動回繞配合空閑中斷讀取數(shù)據(jù)效率最高。但要注意如果數(shù)據(jù)量超過緩沖區(qū)大小循環(huán)模式會覆蓋舊數(shù)據(jù)。所以緩沖區(qū)要足夠大或者用雙緩沖。SPI通過DMA讀取芯片數(shù)據(jù)也是類似思路。配置SPI的DMA接收通道啟動DMA傳輸傳輸完成后中斷處理。STM32F103的SPI1有專用的DMA通道DMA1通道2和3配置時要注意通道映射。// SPI1 DMA接收配置 void SPI1_DMA_Init(void) { // SPI1_RX DMA1_Channel2, SPI1_TX DMA1_Channel3 __HAL_RCC_DMA1_CLK_ENABLE(); hdma_spi1_rx.Instance DMA1_Channel2; hdma_spi1_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_spi1_rx.Init.PeriphInc DMA_PINC_DISABLE; hdma_spi1_rx.Init.MemInc DMA_MINC_ENABLE; hdma_spi1_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_spi1_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_spi1_rx.Init.Mode DMA_NORMAL; hdma_spi1_rx.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_spi1_rx); __HAL_LINKDMA(hspi1, hdmarx, hdma_spi1_rx); // 使能DMA傳輸完成中斷 HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn); }SPI DMA接收時要注意SPI是全雙工發(fā)送和接收同時進行。如果只想接收需要發(fā)送 dummy 數(shù)據(jù)來產(chǎn)生時鐘。用HAL庫的HAL_SPI_Receive_DMA函數(shù)時庫會自動處理發(fā)送 dummy 數(shù)據(jù)。但如果用寄存器操作要手動往DR寄存器寫數(shù)據(jù)來產(chǎn)生時鐘。4. 工程實踐從原理圖到代碼的完整鏈路4.1 硬件設(shè)計中的信號完整性考量嵌入式項目從原理圖開始。原理圖階段的一個小疏忽可能在PCB階段變成大問題在調(diào)試階段變成噩夢。SPI信號線要盡量短尤其是SCK。SCK是時鐘信號頻率高走線長了會產(chǎn)生反射和振鈴。如果SCK走線超過5cm建議在源端串一個22Ω到33Ω的電阻做阻抗匹配。MISO和MOSI相對不那么敏感但也要盡量短。I2C的上拉電阻位置有講究。上拉電阻應(yīng)該放在總線的一端而不是每個設(shè)備旁邊都放。如果每個設(shè)備都放上拉電阻并聯(lián)后阻值變小功耗增大且可能超過設(shè)備的灌電流能力。一般整條總線只放一組上拉電阻放在靠近主機的位置。電源去耦是另一個關(guān)鍵點。每個芯片的電源引腳旁邊都要放一個100nF的陶瓷電容越近越好。如果芯片有多個電源引腳每個都要放。大容量電容10uF到100uF放在電源入口處用于濾低頻噪聲。我見過一個項目因為STM32的VDDA引腳沒放去耦電容ADC采樣值跳動很大加了電容后立刻穩(wěn)定。注意STM32F103的VDDA和VSSA是模擬電源引腳即使不用ADC也要接。VDDA通過一個磁珠或0Ω電阻連接到VDDVSSA直接連到GND。VDDA旁邊要放1個100nF和1個1uF的電容。4.2 從CubeMX到Keil的工程搭建CubeMX是ST官方的圖形化配置工具可以生成初始化代碼。但CubeMX生成的代碼只是起點實際項目要在上面做大量修改。CubeMX配置的步驟選擇芯片型號、配置時鐘樹、配置外設(shè)、配置中斷優(yōu)先級、生成代碼。時鐘樹配置是關(guān)鍵STM32F103最高72MHz外部晶振一般是8MHz經(jīng)過PLL倍頻到72MHz。如果外部晶振是12MHzPLL配置不同。時鐘樹配錯了串口波特率會不對SPI速率也會不對。中斷優(yōu)先級配置要合理。NVIC的優(yōu)先級分組有5種常用的是分組22位搶占優(yōu)先級2位響應(yīng)優(yōu)先級。搶占優(yōu)先級高的中斷可以打斷搶占優(yōu)先級低的中斷。DMA傳輸完成中斷、串口空閑中斷、定時器中斷的優(yōu)先級要根據(jù)實時性要求排列。一般串口和DMA中斷優(yōu)先級較高定時器中斷較低。Keil5安裝時要注意芯片包。STM32F1系列的芯片包是Keil.STM32F1xx_DFP安裝后才能在Keil里選擇STM32F103芯片。如果同時用C51和STM32Keil5可以兼容但要注意安裝路徑不要有中文和空格。# Keil5芯片包安裝路徑 # 默認在 C:\Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP # 如果芯片包沒裝Keil里找不到STM32F1034.3 調(diào)試工具與手段調(diào)試嵌入式系統(tǒng)光靠printf是不夠的。示波器、邏輯分析儀、調(diào)試器是三大法寶。示波器看模擬波形比如I2C的上升沿、SPI的時鐘質(zhì)量、電源紋波。邏輯分析儀看數(shù)字協(xié)議比如SPI的時序、I2C的地址幀、UART的數(shù)據(jù)幀。調(diào)試器ST-Link、J-Link用于單步調(diào)試、查看寄存器、設(shè)置斷點。邏輯分析儀配合協(xié)議解碼軟件如Saleae Logic、PulseView可以自動解析SPI和I2C協(xié)議直接看到地址、數(shù)據(jù)、應(yīng)答位。調(diào)試I2C時如果設(shè)備不響應(yīng)先用邏輯分析儀抓波形看地址幀發(fā)出去后有沒有應(yīng)答。沒有應(yīng)答可能是地址錯了、設(shè)備沒供電、上拉電阻不對。ST-Link調(diào)試時可以查看外設(shè)寄存器的值。比如SPI不工作時查看SPI_CR1、SPI_SR寄存器看使能位有沒有置位、TXE和RXNE標志有沒有變化。這比盲目改代碼高效得多。實操心得我習慣在項目初期就引出調(diào)試串口用printf輸出關(guān)鍵信息。但printf會占用CPU時間在高速通信時可能影響時序。所以調(diào)試完成后要把printf去掉或者改成條件編譯。另外SWD調(diào)試接口的SWCLK和SWDIO要引出排針方便連接調(diào)試器。4.4 一個完整的SPI DMA讀取案例假設(shè)有一個SPI接口的傳感器芯片需要周期性讀取數(shù)據(jù)。傳感器數(shù)據(jù)通過SPI輸出數(shù)據(jù)長度不固定以某個字節(jié)作為幀尾。硬件連接STM32F103的SPI1PA5SCKPA6MISOPA7MOSIPA4作為CS傳感器還有一個數(shù)據(jù)就緒引腳接PA0外部中斷。軟件流程傳感器數(shù)據(jù)就緒時觸發(fā)外部中斷在中斷中拉低CS啟動SPI DMA接收DMA接收完成后拉高CS處理數(shù)據(jù)。// 外部中斷處理 void EXTI0_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); // 拉低CS HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 啟動SPI DMA接收 HAL_SPI_Receive_DMA(hspi1, spi_rx_buffer, SPI_RX_SIZE); } } // DMA傳輸完成回調(diào) void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi-Instance SPI1) { // 拉高CS HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 置標志位主循環(huán)處理數(shù)據(jù) spi_data_ready 1; } }這個案例的關(guān)鍵點CS的拉低和拉高時機。CS拉低后要盡快啟動SPI傳輸否則傳感器可能超時。CS拉高要在DMA傳輸完成后不能在數(shù)據(jù)還沒收完就拉高。用DMA傳輸完成回調(diào)來拉高CS是最穩(wěn)妥的。如果傳感器數(shù)據(jù)長度不固定可以用DMA加空閑檢測。但SPI沒有空閑中斷所以要么用固定長度接收要么用定時器配合——啟動DMA后開一個定時器定時器超時表示一幀數(shù)據(jù)接收完畢。5. 常見問題與排查技巧實錄5.1 SPI通信失敗排查清單SPI通信失敗是最常見的問題之一。排查時按以下順序檢查排查項檢查方法常見問題時鐘使能查看RCC寄存器GPIO或SPI時鐘沒開GPIO模式查看GPIOx_CRL/CRHSCK/MOSI沒配復用推挽片選信號示波器看CS引腳CS沒拉低或拉低太晚時鐘極性相位對比從機數(shù)據(jù)手冊CPOL/CPHA配錯波特率示波器測SCK頻率分頻系數(shù)太大或太小數(shù)據(jù)順序查看從機要求MSB/LSB配錯硬件連接萬用表測通斷虛焊或接錯線我遇到最多的是CPOL/CPHA配錯。有一次調(diào)試一個SPI Flash讀ID一直是0xFF查了半天發(fā)現(xiàn)是模式配成了模式0而Flash要求模式3。改成模式3后立刻正常。所以調(diào)試SPI的第一步應(yīng)該是讀從機的ID寄存器確認基本通信正常再讀寫數(shù)據(jù)。5.2 I2C死鎖與恢復I2C死鎖的表現(xiàn)是SCL被從機拉低不放主機無法產(chǎn)生時鐘。原因通常是從機在發(fā)送數(shù)據(jù)時被復位狀態(tài)機停在某個中間狀態(tài)一直等待時鐘?;謴头椒ㄇ懊嬉呀?jīng)給了代碼。核心思路是把SCL和SDA切換為普通GPIO主機發(fā)送9個時鐘脈沖讓從機把剩余的數(shù)據(jù)位發(fā)完然后發(fā)送停止條件從機狀態(tài)機復位。預防I2C死鎖的措施在I2C讀寫函數(shù)里加超時檢測超時后調(diào)用恢復函數(shù)。另外I2C的時鐘頻率不要超過從機支持的最高頻率。有些從機標稱支持400kHz但實際在400kHz下容易出錯降到100kHz就穩(wěn)定。注意I2C總線上如果有一個設(shè)備一直拉低SDA整個總線就癱瘓了。所以I2C設(shè)備最好有獨立的電源控制引腳出問題時可以斷電重啟?;蛘哂肐2C多路復用器把設(shè)備分組一組出問題不影響其他組。5.3 DMA傳輸?shù)某R娍覦MA傳輸有幾個容易踩的坑第一個坑是數(shù)據(jù)寬度不匹配。源地址和目的地址的數(shù)據(jù)寬度要一致否則傳輸?shù)臄?shù)據(jù)會錯位。比如SPI數(shù)據(jù)寄存器是8位DMA內(nèi)存地址也應(yīng)該是8位。如果配成16位傳輸?shù)臄?shù)據(jù)會變成兩個字節(jié)合并。第二個坑是DMA通道映射錯誤。STM32F103的DMA1有7個通道每個通道對應(yīng)固定的外設(shè)請求。SPI1_RX是DMA1_Channel2SPI1_TX是DMA1_Channel3USART1_RX是DMA1_Channel5USART1_TX是DMA1_Channel4。配錯通道DMA不工作。第三個坑是DMA傳輸完成中斷標志沒清除。在中斷處理函數(shù)里要清除對應(yīng)的標志位否則中斷會反復觸發(fā)。HAL庫的HAL_DMA_IRQHandler會自動清除標志但如果用寄存器操作要手動清除。第四個坑是DMA和CPU同時訪問同一塊內(nèi)存。DMA在傳輸時CPU如果修改了源數(shù)據(jù)或讀取了目標數(shù)據(jù)可能導致數(shù)據(jù)不一致。解決辦法是用雙緩沖或者DMA傳輸完成后再讓CPU訪問。5.4 嵌入式系統(tǒng)設(shè)計師的實戰(zhàn)建議如果你在準備嵌入式系統(tǒng)設(shè)計師考試或者想系統(tǒng)提升嵌入式能力以下幾點建議來自實際項目經(jīng)驗第一不要只跑例程。例程是別人嚼過的饃跑通了不代表你懂了。試著改參數(shù)、改配置看現(xiàn)象怎么變化。比如把SPI波特率預分頻從8改成2看通信是否還正常用示波器看波形變化。第二學會看數(shù)據(jù)手冊和參考手冊。數(shù)據(jù)手冊Datasheet講引腳定義、電氣特性、封裝信息。參考手冊Reference Manual講外設(shè)的寄存器、工作模式、時序參數(shù)。中文資料雖然方便但翻譯可能有誤關(guān)鍵參數(shù)要以英文原版為準。第三積累調(diào)試經(jīng)驗。每次遇到問題記錄現(xiàn)象、排查過程、解決方法。時間長了你會形成自己的排查套路。比如SPI不通信先查時鐘、再查GPIO、再查片選、再查模式按順序來不遺漏。第四理解比記憶重要。STM32的寄存器有幾百個不可能全記住。但理解了外設(shè)的工作原理需要時查手冊就能找到對應(yīng)的寄存器。比如知道了SPI的傳輸流程是“寫DR啟動傳輸讀DR獲取數(shù)據(jù)”就能在手冊里找到DR寄存器的地址和位定義。第五動手做完整項目。從原理圖設(shè)計、PCB布線、焊接、調(diào)試到代碼編寫完整走一遍。哪怕是一個簡單的溫濕度計也能讓你學到很多——電源怎么設(shè)計、I2C上拉電阻怎么選、PCB怎么布局、代碼怎么分層。我在實際項目中體會最深的一點是嵌入式系統(tǒng)的核心能力不是寫代碼而是定位問題。代碼寫錯了可以改但問題定位不準改代碼就是瞎猜。而定位問題的能力來自于對底層原理的理解和對調(diào)試工具的熟練使用。示波器、邏輯分析儀、調(diào)試器這三個工具用好了80%的問題都能快速定位。剩下的20%靠經(jīng)驗和直覺。