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