戰(zhàn):從GPIO到DMA的底層開發(fā)指南)
1. 為什么還要折騰寄存器1.1 從一個(gè)真實(shí)的困惑說起剛接觸STM32那會(huì)兒我和大多數(shù)人一樣從庫(kù)函數(shù)入手。新建工程、調(diào)用HAL_GPIO_Init()、編譯下載LED亮了覺得單片機(jī)不過如此。直到有一次做一個(gè)WS2812燈帶的驅(qū)動(dòng)需要精確到零點(diǎn)幾微秒的時(shí)序控制庫(kù)函數(shù)那層層封裝帶來的延遲讓我怎么調(diào)都調(diào)不準(zhǔn)。翻遍論壇有人甩了一句“直接操作寄存器”我盯著參考手冊(cè)里那一長(zhǎng)串GPIOB-BSRR、TIM2-CCR1完全不知道從哪下手。后來花了大概兩周時(shí)間把GPIO、TIM、USART、SPI、I2C這幾個(gè)常用外設(shè)的寄存器從頭到尾啃了一遍才發(fā)現(xiàn)寄存器操作根本沒有想象中那么可怕。它就像學(xué)開車庫(kù)函數(shù)是自動(dòng)擋踩油門就走寄存器是手動(dòng)擋你得知道離合、檔位、轉(zhuǎn)速之間的關(guān)系但一旦掌握了什么路況你都能應(yīng)付。這份手冊(cè)就是把我踩過的坑、總結(jié)的方法用最直白的話講清楚。它適合已經(jīng)會(huì)用庫(kù)函數(shù)但想深入理解底層的人也適合剛?cè)腴T想少走彎路的初學(xué)者。核心目標(biāo)只有一個(gè)讓你看到寄存器名字時(shí)不再發(fā)怵能對(duì)著參考手冊(cè)直接寫出可運(yùn)行的代碼。1.2 寄存器操作到底在操作什么很多人被“寄存器”三個(gè)字嚇住其實(shí)它的本質(zhì)極其簡(jiǎn)單。STM32內(nèi)部有成千上萬個(gè)存儲(chǔ)單元每個(gè)單元有固定的地址。所謂寄存器就是這些地址被賦予了特定功能的存儲(chǔ)單元。你往某個(gè)地址寫一個(gè)值硬件就根據(jù)這個(gè)值改變引腳電平、啟動(dòng)定時(shí)器、發(fā)送數(shù)據(jù)。舉個(gè)例子GPIOB端口的輸出數(shù)據(jù)寄存器GPIOB_ODR它的地址是0x40010C0C。你往這個(gè)地址寫0x00000001GPIOB的第0號(hào)引腳就輸出高電平。就這么直接。那為什么庫(kù)函數(shù)要封裝那么多層因?yàn)橹苯硬僮鞯刂啡菀壮鲥e(cuò)而且不同型號(hào)的STM32地址可能不一樣。庫(kù)函數(shù)用結(jié)構(gòu)體指針把地址包裝起來用GPIOB-ODR 0x01代替*(volatile uint32_t*)0x40010C0C 0x01既好記又不容易寫錯(cuò)。但封裝帶來的代價(jià)就是執(zhí)行效率降低和代碼體積增大。我個(gè)人的經(jīng)驗(yàn)是常規(guī)邏輯控制用庫(kù)函數(shù)對(duì)時(shí)序敏感或需要極致優(yōu)化的場(chǎng)景用寄存器。兩者不是對(duì)立的在同一個(gè)工程里混用完全沒問題。1.3 這份手冊(cè)的閱讀方式我不會(huì)按參考手冊(cè)的順序逐個(gè)寄存器講那樣太枯燥。我會(huì)按“實(shí)際使用場(chǎng)景”來組織點(diǎn)亮一個(gè)LED需要操作哪些寄存器、配置一個(gè)定時(shí)器中斷需要幾步、串口發(fā)送一個(gè)字節(jié)背后發(fā)生了什么。每個(gè)場(chǎng)景都從需求出發(fā)先講清楚要做什么再講需要配置哪些寄存器最后給出可以直接復(fù)制運(yùn)行的代碼。閱讀時(shí)建議手邊開著對(duì)應(yīng)型號(hào)的參考手冊(cè)遇到不認(rèn)識(shí)的位域隨時(shí)查閱。我寫的每個(gè)寄存器操作都會(huì)標(biāo)注它在參考手冊(cè)中的章節(jié)號(hào)方便你對(duì)照。另外所有代碼基于STM32F103系列但原理通用換到F4、H7系列只是寄存器地址和個(gè)別位定義不同思路完全一致。注意操作寄存器時(shí)一定要用volatile關(guān)鍵字修飾指針否則編譯器優(yōu)化可能把看似“重復(fù)”的寫操作優(yōu)化掉導(dǎo)致硬件行為異常。這是新手最容易忽略的坑。2. 動(dòng)手前的必修課地址映射與位操作2.1 存儲(chǔ)器映射找到寄存器的門牌號(hào)STM32的4GB地址空間被劃分成幾個(gè)大區(qū)域其中外設(shè)寄存器的映射區(qū)從0x40000000開始。不同外設(shè)掛在不同總線上地址范圍也不同。比如GPIO掛在APB2總線上起始地址是0x40010000TIM2掛在APB1總線上起始地址是0x40000000。每個(gè)外設(shè)內(nèi)部寄存器按功能順序排列每個(gè)寄存器占4字節(jié)。以GPIOB為例它的寄存器地址如下表所示寄存器名稱偏移地址絕對(duì)地址功能CRL0x000x40010C00端口配置低寄存器CRH0x040x40010C04端口配置高寄存器IDR0x080x40010C08端口輸入數(shù)據(jù)寄存器ODR0x0C0x40010C0C端口輸出數(shù)據(jù)寄存器BSRR0x100x40010C10端口位設(shè)置/清除寄存器BRR0x140x40010C14端口位清除寄存器LCKR0x180x40010C18端口配置鎖定寄存器知道這張表你就能直接操作GPIOB的任何寄存器。比如讓PB0輸出高電平寫*(volatile uint32_t*)0x40010C0C | 0x01;即可。但每次都算地址太麻煩所以標(biāo)準(zhǔn)做法是用結(jié)構(gòu)體把外設(shè)的所有寄存器組織起來。在CMSIS頭文件里GPIO的結(jié)構(gòu)體定義大致是這樣的typedef struct { volatile uint32_t CRL; volatile uint32_t CRH; volatile uint32_t IDR; volatile uint32_t ODR; volatile uint32_t BSRR; volatile uint32_t BRR; volatile uint32_t LCKR; } GPIO_TypeDef;然后定義宏#define GPIOB ((GPIO_TypeDef *)0x40010C00)這樣就能用GPIOB-ODR來訪問。結(jié)構(gòu)體成員的偏移量正好對(duì)應(yīng)寄存器的偏移地址編譯器會(huì)自動(dòng)計(jì)算絕對(duì)地址。這種寫法既直觀又高效編譯后生成的指令和直接寫地址完全一樣。2.2 位操作精準(zhǔn)控制每一個(gè)比特寄存器操作的核心是位操作。一個(gè)32位寄存器每一位可能控制不同的功能。你經(jīng)常需要只修改某幾位而不影響其他位這時(shí)候就要用到C語言的位運(yùn)算。最常用的三個(gè)操作置位REG | (1 n)把第n位置1其他位不變。清零REG ~(1 n)把第n位清0其他位不變。取反REG ^ (1 n)把第n位翻轉(zhuǎn)。如果要同時(shí)操作多位比如把第4到第7位設(shè)置為1010可以這樣寫REG ~(0x0F 4); // 先清零這4位 REG | (0x0A 4); // 再寫入新值STM32的GPIO還提供了BSRR寄存器可以原子性地置位或清零。寫B(tài)SRR的低16位對(duì)應(yīng)引腳置1寫高16位對(duì)應(yīng)引腳清0。比如同時(shí)讓PB0置1、PB1清0GPIOB-BSRR (1 0) | (1 (16 1));這種操作不需要讀-改-寫一條指令完成在中斷環(huán)境下特別安全。實(shí)操心得我習(xí)慣在頭文件里定義一組宏比如#define SET_BIT(REG, BIT) ((REG) | (BIT))、#define CLEAR_BIT(REG, BIT) ((REG) ~(BIT))寫代碼時(shí)直接調(diào)用既簡(jiǎn)潔又不容易出錯(cuò)。2.3 時(shí)鐘使能打開外設(shè)的電源開關(guān)STM32為了省電每個(gè)外設(shè)的時(shí)鐘默認(rèn)是關(guān)閉的。你配置了半天寄存器卻沒反應(yīng)十有八九是忘了開時(shí)鐘。時(shí)鐘使能寄存器在RCC模塊里APB1和APB2各有一個(gè)。以GPIOB為例它掛在APB2總線上對(duì)應(yīng)的時(shí)鐘使能位是RCC_APB2ENR的第3位。開啟時(shí)鐘的代碼是RCC-APB2ENR | (1 3); // 使能GPIOB時(shí)鐘APB1上的外設(shè)如TIM2、USART2則操作RCC_APB1ENR。我建議把常用外設(shè)的時(shí)鐘使能位做成宏定義比如#define RCC_APB2ENR_GPIOBEN (1 3) #define RCC_APB1ENR_TIM2EN (1 0)這樣代碼可讀性會(huì)好很多。另外注意開啟時(shí)鐘后最好加幾個(gè)空指令或短暫延時(shí)等時(shí)鐘穩(wěn)定后再配置外設(shè)雖然大多數(shù)情況下不需要但在低功耗場(chǎng)景下這個(gè)細(xì)節(jié)很重要。3. GPIO寄存器實(shí)戰(zhàn)從點(diǎn)亮LED到按鍵消抖3.1 輸出模式配置CRL和CRH的位域解析GPIO的配置寄存器分CRL和CRH兩個(gè)CRL管第0到7引腳CRH管第8到15引腳。每個(gè)引腳占4個(gè)位低2位是MODE高2位是CNF。MODE位定義輸出速度00輸入模式01輸出模式最大10MHz10輸出模式最大2MHz11輸出模式最大50MHzCNF位在輸出模式下的定義00通用推挽輸出01通用開漏輸出10復(fù)用推挽輸出11復(fù)用開漏輸出在輸入模式下的定義00模擬輸入01浮空輸入10上拉/下拉輸入11保留假設(shè)要讓PB0作為推挽輸出速度50MHz操作如下GPIOB-CRL ~(0x0F 0); // 清零PB0的配置位 GPIOB-CRL | (0x03 0); // MODE11, CNF00如果PB8也要配置成同樣模式因?yàn)樗鼘儆贑RH管操作變成GPIOB-CRH ~(0x0F 0); // 注意PB8在CRH中占第0到3位 GPIOB-CRH | (0x03 0);這里有個(gè)容易搞混的地方CRH中第0到3位對(duì)應(yīng)的是第8號(hào)引腳不是第0號(hào)。計(jì)算偏移量時(shí)要用(pin - 8) * 4。3.2 輸出電平控制ODR與BSRR的選擇配置好模式后控制電平有兩種方式。ODR寄存器直接寫整個(gè)端口的值比如GPIOB-ODR 0x0001會(huì)把PB0置1、其他引腳清0。這種方式適合一次性設(shè)置多個(gè)引腳但在中斷里操作有風(fēng)險(xiǎn)因?yàn)樽x-改-寫不是原子操作。BSRR寄存器則安全得多。低16位寫1置位高16位寫1清零寫0無效。比如GPIOB-BSRR (1 0); // PB0輸出高 GPIOB-BSRR (1 (16 0)); // PB0輸出低我實(shí)測(cè)過用BSRR翻轉(zhuǎn)引腳在72MHz主頻下能產(chǎn)生接近18MHz的方波而用ODR加異或操作只能到8MHz左右。所以對(duì)速度有要求時(shí)BSRR是首選。3.3 輸入讀取與按鍵消抖的寄存器實(shí)現(xiàn)讀取引腳電平用IDR寄存器。比如判斷PB0是否為低電平if ((GPIOB-IDR (1 0)) 0) { // 按鍵按下 }但機(jī)械按鍵有抖動(dòng)直接讀會(huì)誤判。硬件消抖加電容當(dāng)然好但大多數(shù)時(shí)候我們用軟件消抖。常見的做法是延時(shí)10ms再讀一次但這樣會(huì)阻塞CPU。我的做法是用定時(shí)器中斷每隔5ms采樣一次連續(xù)3次相同才確認(rèn)狀態(tài)變化。用寄存器實(shí)現(xiàn)的狀態(tài)機(jī)大概是這樣typedef struct { uint8_t current; uint8_t count; uint8_t stable; } KeyState; void KeyScan(KeyState *key, uint8_t pinState) { if (pinState ! key-current) { key-current pinState; key-count 0; } else { if (key-count 3) { key-count; } else { key-stable key-current; } } }這個(gè)邏輯放在定時(shí)器中斷里主循環(huán)只讀key-stable完全不阻塞。實(shí)測(cè)下來比HAL_Delay(10)那種寫法響應(yīng)快得多而且不浪費(fèi)CPU時(shí)間。注意事項(xiàng)配置輸入引腳時(shí)如果外部沒有上拉電阻一定要把CNF設(shè)成上拉輸入CNF10否則引腳浮空會(huì)導(dǎo)致讀數(shù)隨機(jī)跳動(dòng)。我見過太多人在這里卡住以為是代碼問題其實(shí)是硬件配置沒做對(duì)。4. 定時(shí)器寄存器精確計(jì)時(shí)的核心4.1 時(shí)基單元PSC、ARR與CNT的配合定時(shí)器的核心是三個(gè)寄存器預(yù)分頻器PSC、自動(dòng)重裝載寄存器ARR、計(jì)數(shù)器CNT。它們的關(guān)系可以用一個(gè)公式概括定時(shí)器溢出頻率 時(shí)鐘源頻率 / ((PSC 1) * (ARR 1))假設(shè)TIM2的時(shí)鐘是72MHz我想要1kHz的中斷頻率也就是每1ms觸發(fā)一次。計(jì)算過程先定PSC。為了獲得較好的分辨率PSC不要太大。取PSC71則分頻后頻率為72MHz / 72 1MHz。再算ARR。1MHz / 1kHz 1000所以ARR999。配置代碼RCC-APB1ENR | (1 0); // 使能TIM2時(shí)鐘 TIM2-PSC 71; // 預(yù)分頻值 TIM2-ARR 999; // 自動(dòng)重裝載值 TIM2-CNT 0; // 清零計(jì)數(shù)器 TIM2-CR1 | (1 0); // 使能定時(shí)器這里有個(gè)細(xì)節(jié)PSC和ARR都是16位寄存器最大值65535。如果需要的分頻比超過這個(gè)范圍可以級(jí)聯(lián)兩個(gè)定時(shí)器或者用主從模式。我在做超聲波測(cè)距時(shí)遇到過這個(gè)問題后來用TIM3給TIM2提供時(shí)鐘輕松解決了大分頻比的需求。4.2 中斷配置DIER與NVIC的聯(lián)動(dòng)定時(shí)器溢出后要產(chǎn)生中斷需要兩步使能定時(shí)器的更新中斷再在NVIC中使能對(duì)應(yīng)的中斷通道。TIM2-DIER | (1 0); // 使能更新中斷 NVIC_EnableIRQ(TIM2_IRQn); // 使能NVIC通道 NVIC_SetPriority(TIM2_IRQn, 1); // 設(shè)置優(yōu)先級(jí)然后在中斷服務(wù)函數(shù)里清除標(biāo)志位void TIM2_IRQHandler(void) { if (TIM2-SR (1 0)) { TIM2-SR ~(1 0); // 清除更新標(biāo)志 // 用戶代碼 } }注意SR寄存器是“讀后自動(dòng)清除”還是“寫0清除”不同系列可能不同。F1系列是寫0清除F4系列是讀后自動(dòng)清除。這個(gè)差異坑過我一次在F4上寫了清除代碼反而導(dǎo)致中斷丟失后來查手冊(cè)才發(fā)現(xiàn)。4.3 PWM輸出CCR與CCMR的配置邏輯PWM是定時(shí)器最常用的功能之一。以TIM3的通道1對(duì)應(yīng)PA6為例配置步驟配置PA6為復(fù)用推挽輸出CNF10MODE11。使能TIM3時(shí)鐘和GPIOA時(shí)鐘。設(shè)置PSC和ARR決定PWM頻率。配置通道1為PWM模式1TIM3-CCMR1 ~(0x7 4); // 清除OC1M位 TIM3-CCMR1 | (0x6 4); // PWM模式1 TIM3-CCMR1 | (1 3); // 使能預(yù)裝載 TIM3-CCER | (1 0); // 使能通道1輸出 TIM3-CCR1 500; // 占空比50%ARR999時(shí) TIM3-CR1 | (1 7); // 使能自動(dòng)重裝載預(yù)裝載 TIM3-CR1 | (1 0); // 啟動(dòng)定時(shí)器占空比的計(jì)算公式是CCR / (ARR 1)。ARR999時(shí)CCR500對(duì)應(yīng)50%CCR250對(duì)應(yīng)25%。改變CCR就能實(shí)時(shí)調(diào)整占空比不需要停止定時(shí)器。實(shí)操心得做電機(jī)調(diào)速時(shí)PWM頻率一般選15kHz到20kHz超出人耳聽覺范圍電機(jī)不會(huì)發(fā)出嘯叫。我試過8kHz電機(jī)聲音很明顯調(diào)到16kHz后就安靜了。另外死區(qū)時(shí)間用BDTR寄存器配置做H橋驅(qū)動(dòng)時(shí)必不可少否則上下管直通會(huì)燒管子。5. 串口通信寄存器從發(fā)送一個(gè)字節(jié)到DMA搬運(yùn)5.1 波特率計(jì)算BRR寄存器的分頻值USART的波特率由BRR寄存器決定。計(jì)算公式是波特率 fCK / (16 * USARTDIV)其中USARTDIV是一個(gè)定點(diǎn)數(shù)整數(shù)部分占12位小數(shù)部分占4位。假設(shè)系統(tǒng)時(shí)鐘72MHz要得到115200波特率USARTDIV 72000000 / (16 * 115200) 39.0625整數(shù)部分39小數(shù)部分0.0625 * 16 1所以BRR (39 4) | 1 0x271。USART1-BRR 0x271;實(shí)際寫代碼時(shí)不用手算用宏#define BRR_VALUE(fCK, baud) ((fCK baud/2) / baud)可以自動(dòng)計(jì)算。但理解這個(gè)公式很重要因?yàn)橛袝r(shí)候你需要非標(biāo)準(zhǔn)波特率比如某些傳感器用9600但時(shí)鐘不是標(biāo)準(zhǔn)值就得手動(dòng)調(diào)整。5.2 發(fā)送與接收SR、DR的握手流程發(fā)送一個(gè)字節(jié)的流程等待TXE標(biāo)志置位發(fā)送數(shù)據(jù)寄存器空。寫DR寄存器。等待TC標(biāo)志置位發(fā)送完成。while (!(USART1-SR (1 7))); // 等待TXE USART1-DR data; while (!(USART1-SR (1 6))); // 等待TC接收則更復(fù)雜一些因?yàn)橐幚砀鞣N錯(cuò)誤標(biāo)志?;玖鞒痰却齊XNE標(biāo)志置位。讀DR寄存器。檢查ORE、NE、FE、PE標(biāo)志有錯(cuò)誤則處理。if (USART1-SR (1 5)) { // RXNE uint8_t data USART1-DR; if (USART1-SR (1 3)) { // ORE溢出錯(cuò)誤 // 處理溢出 } }注意讀SR和讀DR的順序有講究。先讀SR再讀DR可以清除某些錯(cuò)誤標(biāo)志反過來則不行。這個(gè)細(xì)節(jié)在參考手冊(cè)里有說明但很容易被忽略。5.3 中斷與DMA解放CPU的兩種方式輪詢方式發(fā)送大量數(shù)據(jù)會(huì)占滿CPU。用中斷可以緩解但每個(gè)字節(jié)一次中斷115200波特率下每秒上萬次中斷CPU還是吃不消。DMA才是終極方案。配置USART1的TX使用DMA1通道4RCC-AHBENR | (1 0); // 使能DMA1時(shí)鐘 DMA1_Channel4-CPAR (uint32_t)USART1-DR; DMA1_Channel4-CMAR (uint32_t)buffer; DMA1_Channel4-CNDTR length; DMA1_Channel4-CCR | (1 4); // 從存儲(chǔ)器讀 DMA1_Channel4-CCR | (1 7); // 存儲(chǔ)器地址增量 DMA1_Channel4-CCR | (1 0); // 使能通道 USART1-CR3 | (1 7); // 使能USART DMA發(fā)送配置完成后往buffer里填數(shù)據(jù)DMA自動(dòng)搬運(yùn)CPU完全不用管。我做過測(cè)試用DMA發(fā)送1KB數(shù)據(jù)CPU占用率不到1%而輪詢方式接近100%。常見問題DMA傳輸完成后需要清除標(biāo)志位并重新配置CNDTR才能開始下一次傳輸。我習(xí)慣在DMA傳輸完成中斷里做這件事形成雙緩沖機(jī)制實(shí)現(xiàn)連續(xù)不間斷的數(shù)據(jù)流。6. 常見問題與排查技巧實(shí)錄6.1 寄存器操作失效的五大原因現(xiàn)象可能原因排查方法配置后無反應(yīng)外設(shè)時(shí)鐘未使能檢查RCC相關(guān)使能位引腳電平不變GPIO模式配置錯(cuò)誤讀CRL/CRH確認(rèn)位域中斷不觸發(fā)NVIC未使能或優(yōu)先級(jí)錯(cuò)誤檢查NVIC_ISER和IPR定時(shí)器不計(jì)數(shù)CEN位未置1讀CR1確認(rèn)bit0串口無輸出波特率計(jì)算錯(cuò)誤用示波器測(cè)實(shí)際波特率我遇到最多的是第一個(gè)。有一次調(diào)試SPI折騰了兩個(gè)小時(shí)最后發(fā)現(xiàn)是SPI1的時(shí)鐘沒開。從那以后我養(yǎng)成了一個(gè)習(xí)慣每配置一個(gè)外設(shè)第一件事就是開時(shí)鐘并且寫注釋標(biāo)記。6.2 讀-改-寫陷阱與原子操作前面提過REG | (1 n)這種操作在中斷環(huán)境下有風(fēng)險(xiǎn)。假設(shè)主循環(huán)正在執(zhí)行GPIOB-ODR | 0x01剛讀完ODR中斷來了中斷里修改了ODR的bit1返回后主循環(huán)寫回舊值bit1的修改就丟了。解決方案有三個(gè)用BSRR/BRR代替ODR硬件保證原子性。在操作前關(guān)中斷操作后開中斷。用位帶操作Bit-Banding把每個(gè)位映射到獨(dú)立地址讀寫都是原子的。位帶操作是Cortex-M3/M4的特色功能把外設(shè)區(qū)的每個(gè)位映射到別名區(qū)的一個(gè)32位字。比如GPIOB_ODR的bit0別名地址是0x42000000 (0x40010C0C - 0x40000000) * 32 0 * 4。寫這個(gè)地址就等于原子性地操作bit0。雖然計(jì)算復(fù)雜但可以用宏封裝#define BITBAND(addr, bit) ((volatile uint32_t*)(0x42000000 ((addr - 0x40000000) * 32) (bit * 4))) #define GPIOB_ODR_BIT0 BITBAND(0x40010C0C, 0)然后*GPIOB_ODR_BIT0 1;就是原子置位。這個(gè)技巧在實(shí)時(shí)性要求高的場(chǎng)合特別有用。6.3 寄存器調(diào)試的實(shí)用技巧調(diào)試寄存器問題時(shí)我常用的手段有這幾個(gè)在線調(diào)試看寄存器窗口Keil和IAR都有外設(shè)寄存器查看功能可以實(shí)時(shí)看到每個(gè)位的值。比打印printf快得多。用內(nèi)存窗口直接改寄存器調(diào)試時(shí)暫停程序在內(nèi)存窗口找到寄存器地址直接修改值觀察硬件反應(yīng)。這個(gè)方法用來驗(yàn)證配置是否正確特別有效。示波器/邏輯分析儀懷疑時(shí)序問題時(shí)示波器是終極武器。我調(diào)WS2812時(shí)就是靠邏輯分析儀抓波形發(fā)現(xiàn)高電平時(shí)間差了0.3微秒調(diào)整NOP數(shù)量后解決。二分法定位如果一段配置代碼不工作把它拆成兩半分別測(cè)試逐步縮小范圍。比從頭讀代碼快得多。避坑技巧修改寄存器時(shí)盡量用“讀-改-寫”的宏但要注意編譯器優(yōu)化。我習(xí)慣在關(guān)鍵操作前后加__DSB()和__ISB()屏障指令確保指令順序執(zhí)行。雖然大多數(shù)時(shí)候不需要但在操作時(shí)鐘、電源等關(guān)鍵寄存器時(shí)加上更保險(xiǎn)。7. 從寄存器到庫(kù)函數(shù)的自由切換7.1 什么時(shí)候該用寄存器不是所有場(chǎng)景都適合寄存器操作。我的判斷標(biāo)準(zhǔn)很簡(jiǎn)單需要極致性能中斷服務(wù)函數(shù)、高頻PWM、DMA配置這些用寄存器。需要精確時(shí)序單總線協(xié)議、WS2812、紅外編解碼必須寄存器。調(diào)試底層問題庫(kù)函數(shù)行為異常時(shí)直接看寄存器能快速定位。學(xué)習(xí)原理想真正理解單片機(jī)怎么工作寄存器是必經(jīng)之路。其他情況比如初始化一堆外設(shè)、寫業(yè)務(wù)邏輯用庫(kù)函數(shù)效率更高。我現(xiàn)在的習(xí)慣是初始化用庫(kù)函數(shù)關(guān)鍵路徑用寄存器。兩者混用完全沒問題編譯器會(huì)處理好。7.2 混用時(shí)的注意事項(xiàng)混用時(shí)最大的坑是庫(kù)函數(shù)可能覆蓋你的寄存器配置。比如你先用寄存器配置了GPIO然后調(diào)用HAL_GPIO_Init()后者會(huì)重新寫入CRL/CRH把你的配置沖掉。解決辦法是要么全用寄存器要么全用庫(kù)函數(shù)要么確保庫(kù)函數(shù)配置的參數(shù)和你的寄存器配置一致。另一個(gè)坑是中斷優(yōu)先級(jí)分組。庫(kù)函數(shù)用HAL_NVIC_SetPriority()設(shè)置優(yōu)先級(jí)底層操作的是NVIC的IPR寄存器。如果你直接用寄存器設(shè)置要注意優(yōu)先級(jí)分組設(shè)置SCB-AIRCR的PRIGROUP位否則優(yōu)先級(jí)可能不符合預(yù)期。我一般會(huì)在工程里定義一個(gè)宏USE_HAL_DRIVER條件編譯決定用哪套代碼。這樣切換起來方便也避免了混用沖突。7.3 進(jìn)階方向從寄存器到RTOS和底層驅(qū)動(dòng)掌握寄存器操作后你會(huì)發(fā)現(xiàn)很多以前覺得神秘的東西變得清晰。比如RTOS的任務(wù)切換本質(zhì)就是操作PendSV異常和堆棧指針比如DMA傳輸就是配置幾個(gè)寄存器然后讓硬件自動(dòng)搬運(yùn)。下一步可以嘗試寫一個(gè)自己的GPIO驅(qū)動(dòng)庫(kù)封裝成gpio_set()、gpio_toggle()等函數(shù)理解庫(kù)函數(shù)的設(shè)計(jì)思路。研究啟動(dòng)文件看匯編代碼怎么初始化堆棧、怎么跳轉(zhuǎn)到main函數(shù)。移植RTOS自己寫PendSV_Handler和SysTick_Handler理解任務(wù)調(diào)度的底層機(jī)制。優(yōu)化關(guān)鍵代碼用寄存器重寫耗時(shí)函數(shù)對(duì)比性能提升。我個(gè)人的體會(huì)是寄存器就像武功的內(nèi)功心法庫(kù)函數(shù)是招式。只練招式遇到強(qiáng)敵就束手無策內(nèi)外兼修才能游刃有余。剛開始可能覺得枯燥但堅(jiān)持啃完幾個(gè)外設(shè)后你會(huì)發(fā)現(xiàn)看任何單片機(jī)的手冊(cè)都不再發(fā)怵這種能力才是真正值錢的。