算與外設(shè)配置)
第一次寫STM32寄存器代碼我記得是從點(diǎn)亮一顆LED開始的。當(dāng)時(shí)庫(kù)函數(shù)封裝得已經(jīng)很好直接調(diào)一行GPIO_WriteBit就能亮燈可我盯著那行封裝看半天硬是想不明白它憑什么能把燈點(diǎn)亮。后來(lái)翻到參考手冊(cè)的寄存器章節(jié)把外設(shè)地址和位域定義一點(diǎn)點(diǎn)對(duì)著代碼看才真正明白庫(kù)函數(shù)背后那些-操作符、宏定義、位運(yùn)算到底在干什么。這種感覺(jué)就像一直開著自動(dòng)擋汽車突然有一天下決心去弄懂離合器和換擋邏輯。這本“STM32寄存器白話手冊(cè)”不是什么權(quán)威手冊(cè)的替代品而是想幫那些剛接觸寄存器、被一堆英文縮寫和十六進(jìn)制地址勸退的朋友把寄存器這層窗戶紙捅破。這篇文章適合兩類人一類是剛開始學(xué)STM32只會(huì)抄庫(kù)函數(shù)但想弄清底層原理的新手另一類是調(diào)庫(kù)函數(shù)調(diào)到頭大遇到詭異問(wèn)題不知道怎么定位的開發(fā)者。我會(huì)用最直白的話解釋寄存器地址、位運(yùn)算、時(shí)鐘使能、GPIO配置再帶你把定時(shí)器和串口的寄存器配置思路走一遍。讀完之后你再去翻官方參考手冊(cè)會(huì)發(fā)現(xiàn)里面的表格其實(shí)沒(méi)那么嚇人。1. 寄存器到底是個(gè)啥先別背把它當(dāng)成帶鎖的郵箱1.1 地址就是“門牌號(hào)”寄存器就是“控制面板”芯片內(nèi)部的處理器和外設(shè)之間靠什么溝通靠地址。每一個(gè)可供CPU訪問(wèn)的存儲(chǔ)單元都有一個(gè)地址而寄存器就是一堆掛到特定地址上的存儲(chǔ)單元只不過(guò)它們里面存的每一個(gè)位都有明確定義讀寫之后會(huì)影響外設(shè)的實(shí)際行為。你可以把寄存器想成“帶鎖的郵箱”郵箱掛在一條叫“總線”的街上每個(gè)郵箱都有門牌號(hào)地址CPU要寫信就往門牌號(hào)里扔要讀信就從門牌號(hào)里取。但郵箱里的信紙不是隨便寫的哪一格寫什么內(nèi)容硬件設(shè)計(jì)者都在手冊(cè)里規(guī)定了。比如某一位代表“使能這個(gè)外設(shè)的時(shí)鐘”另一位代表“把引腳配成輸出模式”。你把這些位拼好寫進(jìn)去外設(shè)就按你的意思工作。我在給新人講的時(shí)候常打一個(gè)比方寄存器有點(diǎn)像家用電器面板上的旋鈕和指示燈。旋鈕是CPU能寫的位指示燈是CPU能讀的位。你不在意面板后面的電路只按標(biāo)注操作就能控制空調(diào)、微波爐。STM32的寄存器也一樣手冊(cè)就是那塊面板上的標(biāo)注說(shuō)明只不過(guò)標(biāo)注用的是ENABLE、MODE、RESET這種詞。1.2 為什么從寄存器入手反而容易理解單片機(jī)很多人會(huì)問(wèn)既然庫(kù)函數(shù)方便為什么還要寫寄存器我的體會(huì)是寄存器不是用來(lái)替代庫(kù)函數(shù)的而是用來(lái)理解單片機(jī)工作邏輯的。庫(kù)函數(shù)本質(zhì)上就是一組操作寄存器的代碼組合。如果你不知道某項(xiàng)功能操作了哪個(gè)寄存器、置了哪個(gè)位遇到配置不生效、引腳沒(méi)反應(yīng)、中斷起不來(lái)的問(wèn)題時(shí)就只能靠猜。而寄存器方式讓你每一步都明確這一步打開時(shí)鐘那一步改模式再一步輸出電平。排查問(wèn)題的時(shí)候可以按地址逐個(gè)檢查比黑盒調(diào)試踏實(shí)得多。另外有些功能用庫(kù)函數(shù)的默認(rèn)配置反而繞比如快速翻轉(zhuǎn)引腳、做位帶操作、低延遲控制通常都要直接訪問(wèn)寄存器。至少我見過(guò)的很多底層驅(qū)動(dòng)程序最后還是直接寫寄存器。這不是為了顯得技術(shù)高深而是效率高、意圖清晰。理解了寄存器再回頭用庫(kù)函數(shù)反而是降維操作。2. 打開手冊(cè)先練“三查”基地址、偏移量、位定義2.1 從哪里開始讀先看時(shí)鐘樹和總線圖我見過(guò)太多人一上來(lái)就找某個(gè)外設(shè)的寄存器列表結(jié)果被幾十個(gè)寄存器砸暈。正確的順序應(yīng)該是先看芯片的“總線圖”和“時(shí)鐘樹”。STM32的內(nèi)部外設(shè)不是全部掛在一根總線上而是分散在AHB、APB1、APB2等不同總線上。不同總線對(duì)應(yīng)不同的基地址范圍訪問(wèn)外設(shè)之前還要確認(rèn)外設(shè)掛在哪條總線因?yàn)檫@直接決定你要使能哪個(gè)時(shí)鐘寄存器。拿一個(gè)很常見的芯片型號(hào)來(lái)說(shuō)GPIOA掛在APB2總線上它的基地址是0x40010800GPIOB掛在APB2基地址是0x40010C00而定時(shí)器TIM2掛在APB1上基地址是0x40000000。這些地址不用死記打開芯片參考手冊(cè)的“內(nèi)存映射”章節(jié)就能查到。我習(xí)慣把這頁(yè)截圖放在項(xiàng)目文件夾里寫代碼的時(shí)候隨時(shí)翻。2.2 寄存器地址怎么算基地址加偏移剛接觸寄存器的人最容易卡在“地址怎么來(lái)”這個(gè)問(wèn)題上。其實(shí)很簡(jiǎn)單外設(shè)的基地址加上每個(gè)寄存器的偏移量就是這個(gè)寄存器的絕對(duì)地址。舉例GPIOB的基地址是0x40010C00手冊(cè)里告訴你CRH寄存器的偏移量是0x04那么GPIOB的CRH寄存器地址就是0x40010C00 0x04 0x40010C10在C語(yǔ)言里你通常會(huì)用一個(gè)指向整型數(shù)的指針來(lái)訪問(wèn)它#define GPIOB_CRH (*(volatile unsigned int *)(0x40010C10))volatile在這里千萬(wàn)不能丟它告訴編譯器這個(gè)地址里的內(nèi)容可能被硬件修改不要做優(yōu)化緩存。這條后面我會(huì)專門講因?yàn)樘嗳嗽栽谶@上面。2.3 手冊(cè)里的“位”怎么讀一列一列講清楚打開官方參考手冊(cè)隨便翻到一個(gè)寄存器會(huì)發(fā)現(xiàn)表格分為好幾列偏移地址、位序號(hào)、位名稱、復(fù)位值、讀/寫屬性、描述。我第一次看這個(gè)表也是一臉懵后來(lái)總結(jié)出一個(gè)“三看”習(xí)慣先看偏移地址確認(rèn)寄存器在整個(gè)外設(shè)基地址上的位置。再看位序號(hào)和位名稱確認(rèn)我要操作的是哪一位。最后看復(fù)位值和描述確認(rèn)上電默認(rèn)是什么狀態(tài)寫入什么值才生效。比如GPIO的CRH寄存器里面按4個(gè)位一組對(duì)引腳8~15進(jìn)行配置每組包含MODE和CNF兩個(gè)字段。要配置某個(gè)引腳你不僅要找到對(duì)應(yīng)的4位位域還要保證寫入的模式值和配置值符合表里的編碼。這種“查表填值”的過(guò)程本身就是寄存器編程的核心動(dòng)作。3. 位運(yùn)算就是寄存器的母語(yǔ)置位清零判斷的底層邏輯3.1 置位用|清零用~翻轉(zhuǎn)用^寄存器的一位通常只改對(duì)應(yīng)的Bit其他位要保持原樣。所以直接賦值reg 0x01是危險(xiǎn)的它會(huì)把整個(gè)寄存器其他位置清零。正確做法是用位運(yùn)算。把某一位置1就用“按位或”reg | (1 4); // 把reg的第4位置1把某一位置0就用“按位與 按位取反”reg ~(1 4); // 把reg的第4位清0需要多位一起操作時(shí)可以組成一個(gè)“位掩碼”。比如要把CRH里面引腳9對(duì)應(yīng)的MODE字段寫成輸出模式有兩種寫法一種是直接清掉再填GPIOB_CRH ~(0xF 20); // 先清這個(gè)4位位域 GPIOB_CRH | (0x1 20); // 再寫入新值另一種是讀出原值、改好后一并寫回。第二種看起來(lái)只寫了一次寄存器但實(shí)際上處理的是軟件里的臨時(shí)值對(duì)硬件而言沒(méi)有本質(zhì)差別反而更容易出錯(cuò)。我一般推薦先清再置邏輯清楚。翻轉(zhuǎn)某一位用“按位異或”reg ^ (1 3); // 每執(zhí)行一次第3位翻轉(zhuǎn)一次想要LED在定時(shí)中斷里來(lái)回翻轉(zhuǎn)這種寫法最常用比先讀ODR再改ODR直觀多了。3.2 讀狀態(tài)時(shí)別忘了屏蔽無(wú)關(guān)位讀寄存器判斷狀態(tài)的時(shí)候新手最容易犯的錯(cuò)誤是直接拿整個(gè)寄存器的值和某個(gè)狀態(tài)比較。比如讀GPIO的IDR想知道引腳5是不是高電平if (GPIOB_IDR (1 5)) // 結(jié)果為非0說(shuō)明bit5為1 { // 引腳是高電平 }這里必須用“與”運(yùn)算把bit5單獨(dú)提取出來(lái)。如果你寫成if (GPIOB_IDR (1 5))那只有引腳5為高且其他引腳全為低時(shí)條件才成立基本等于是個(gè)寫錯(cuò)的判斷。讀外設(shè)狀態(tài)寄存器也是這樣尤其看中斷標(biāo)志位。正確姿勢(shì)永遠(yuǎn)是“先屏蔽再判斷”把關(guān)心的位單獨(dú)拿出來(lái)看。3.3 位帶操作給每一位起一個(gè)小名STM32的部分芯片支持“位帶區(qū)”也就是把每個(gè)位映射到另一個(gè)地址空間里的一個(gè)字。你可以把一個(gè)bit當(dāng)作一個(gè)完整的32位變量來(lái)讀寫代碼會(huì)變得非常簡(jiǎn)潔#define LED_PIN_BIT ((*(volatile unsigned int *)0x42000000 (0x40010C00 0x0C) * 32 5 * 4))這段宏看起來(lái)很復(fù)雜但它的意思就是“GPIOB_ODR的第5位被映射成了一個(gè)變量”。對(duì)它賦值就是置位或清零讀它就等于讀該引腳電平。位帶操作適合用在實(shí)時(shí)性要求比較高的場(chǎng)景省去了讀-改-寫三步變成一條編譯指令。不過(guò)別處處都玩位帶它讓代碼可讀性下降很多定位問(wèn)題的時(shí)候光找地址就夠喝一壺的。4. 點(diǎn)亮一顆LED要?jiǎng)尤齻€(gè)寄存器完整走一遍4.1 RCC時(shí)鐘使能外設(shè)沒(méi)電配置白搭STM32的外設(shè)都有獨(dú)立的時(shí)鐘開關(guān)不用它的時(shí)候默認(rèn)是關(guān)著的。比如GPIOB你要用它的引腳第一步不是配置引腳模式而是先讓GPIOB的時(shí)鐘跑起來(lái)。RCC復(fù)位與時(shí)鐘控制寄存器里有一個(gè)APB2ENR對(duì)應(yīng)APB2總線上的外設(shè)使能。GPIOB在APB2總線上APB2ENR的第4位是它的使能位于是RCC_APB2ENR | (1 4);這一步如果不做后面寫CRH、ODR全都無(wú)效因?yàn)镚PIOB這個(gè)外設(shè)壓根沒(méi)通電。這里也解釋了為什么很多庫(kù)例程里第一行總是開時(shí)鐘——不是為了走流程是硬件設(shè)計(jì)的根本要求。4.2 GPIO模式寄存器輸入輸出模式怎么填假設(shè)LED接在PB5和地之間高電平點(diǎn)亮。GPIOB的引腳5屬于低8位引腳對(duì)應(yīng)的配置寄存器是CRL偏移0x00。我習(xí)慣先定義兩個(gè)干凈的點(diǎn)用宏#define RCC_APB2ENR (*(volatile unsigned int *)(0x40021000 0x18)) #define GPIOB_CRL (*(volatile unsigned int *)(0x40010C00 0x00)) #define GPIOB_ODR (*(volatile unsigned int *)(0x40010C00 0x0C))PB5對(duì)應(yīng)CRL中的位域是bit20~bit23。把它清空后寫入0x01意思是“通用推挽輸出輸出速度10MHz”。GPIOB_CRL ~(0xF 20); GPIOB_CRL | (0x1 20);為什么是10MHz而不是50MHz普通LED用10MHz足夠。如果引腳用來(lái)接I2C高速信號(hào)或者屏幕的像素時(shí)鐘才需要配到50MHz。輸出速度快會(huì)帶來(lái)噪聲和功耗不是越大越好。4.3 輸出電壓寄存器ODR和BSRR的區(qū)別引腳配成輸出模式后控制高低電平有兩組寄存器ODR和BSRR。ODR是輸出數(shù)據(jù)寄存器直接整個(gè)寄存器賦值或讀改寫B(tài)SRR是位設(shè)置/清除寄存器按位寫可以讓指定位置高或拉低而且不需要讀改寫。用ODR點(diǎn)燈GPIOB_ODR | (1 5); // 點(diǎn)亮 GPIOB_ODR ~(1 5); // 熄滅用BSRR點(diǎn)燈GPIOB_BSRR (1 5); // bit0~15是置位寫1則PB5輸出高 GPIOB_BSRR (1 (5 16)); // bit16~31是清零寫1則PB5輸出低BSRR的好處是寫入只對(duì)有影響的位置生效而且不需要先讀再改。在多線程或中斷里操作同一個(gè)引腳時(shí)ODR的讀改寫可能會(huì)被中途打斷BSRR則沒(méi)有這個(gè)問(wèn)題。我寫底層驅(qū)動(dòng)時(shí)輸出引腳優(yōu)先用BSRR。4.4 從LED擴(kuò)展到按鍵輸入模式怎么配點(diǎn)完燈再試按鍵你會(huì)進(jìn)一步理解模式配置。按鍵一端接引腳一端接GND再配上拉電阻。配置原理是把引腳模式設(shè)成輸入上拉方法是在MODC字段寫入0b1000輸入上拉/下拉模式然后給ODR這位寫1表示上拉最后從IDR里讀電平#define GPIOB_IDR (*(volatile unsigned int *)(0x40010C00 0x08)) GPIOB_CRL ~(0xF 12); GPIOB_CRL | (0x8 12); // PB3配置成輸入上拉/下拉 GPIOB_ODR | (1 3); // 上拉 if ((GPIOB_IDR (1 3)) 0) { // 按鍵按下電平被拉低 }這一步做好你對(duì)GPIO的理解基本就到位了先開時(shí)鐘再選模式最后讀寫數(shù)據(jù)。后面再去碰串口和定時(shí)器思路是一模一樣的。5. 定時(shí)器和串口的寄存器配法舉一反三的關(guān)鍵5.1 定時(shí)器配置順序的原則定時(shí)器比GPIO多但核心其實(shí)就幾件事時(shí)鐘源、預(yù)分頻、自動(dòng)重裝、中斷使能、啟動(dòng)計(jì)數(shù)。這些其實(shí)是一連串有先后關(guān)系的工作其中最關(guān)鍵的是理解時(shí)鐘來(lái)源。以大家常用的定時(shí)器為例它掛在APB1總線上。如果系統(tǒng)時(shí)鐘是72MHz而APB1分頻器被配置為2分頻那么APB1總線上的外設(shè)時(shí)鐘是36MHz但定時(shí)器會(huì)自動(dòng)加倍回到72MHz。很多手冊(cè)和教程都強(qiáng)調(diào)過(guò)這個(gè)“×2”規(guī)則但實(shí)際做項(xiàng)目時(shí)還是經(jīng)常有人算錯(cuò)定時(shí)頻率原因就是忘了這一步。正確做法是先看RCC時(shí)鐘樹確定APB1分頻系數(shù)再算定時(shí)器實(shí)際時(shí)鐘最后反推PSC和ARR。順序不能反反了所有時(shí)間都不對(duì)。5.2 一個(gè)定時(shí)中斷的例子假設(shè)要用定時(shí)器產(chǎn)生1秒中斷翻轉(zhuǎn)一顆LED。系統(tǒng)時(shí)鐘72MHzAPB1分頻2定時(shí)器時(shí)鐘72MHz。設(shè)置預(yù)分頻器PSC為7200-1即計(jì)數(shù)頻率變成10kHz自動(dòng)重裝寄存器ARR設(shè)為10000-1那么累加到10000次就是1秒。代碼核心步驟示例#define TIM2_BASE 0x40000000 #define RCC_APB1ENR (*(volatile unsigned int *)(0x40021000 0x1C)) // 開定時(shí)器2時(shí)鐘 RCC_APB1ENR | (1 0); // 配預(yù)分頻和重裝值 *(volatile unsigned int *)(TIM2_BASE 0x28) 7200 - 1; // PSC *(volatile unsigned int *)(TIM2_BASE 0x2C) 10000 - 1; // ARR // 清除更新標(biāo)志使能更新中斷 *(volatile unsigned int *)(TIM2_BASE 0x24) ~(1 0); // SR *(volatile unsigned int *)(TIM2_BASE 0x0C) | (1 0); // DIER // 啟動(dòng)計(jì)數(shù) *(volatile unsigned int *)(TIM2_BASE 0x00) | (1 0); // CR1這里有一個(gè)很多人忽略的細(xì)節(jié)改PSC和ARR之前最好先把計(jì)數(shù)器停掉或者等當(dāng)前周期結(jié)束否則可能出現(xiàn)一次意外跳變。我在調(diào)試時(shí)就遇到過(guò)改完ARR后第一次中斷時(shí)間明顯偏短的情況后來(lái)先清零計(jì)數(shù)器再重載配置才穩(wěn)定。5.3 串口初始化從復(fù)位值開始拼寄存器串口初始化要?jiǎng)拥募拇嫫鞲嗟悸啡匀皇恰安楸硖钗弧?。首先是打開USART所在總線的時(shí)鐘然后配置GPIO復(fù)用功能接下來(lái)設(shè)置波特率寄存器BRR最后使能USART發(fā)送和接收。波特率寄存器是最有意思的部分它不是直接寫波特率數(shù)值而是要算一個(gè)分頻值放進(jìn)BRR。不過(guò)很多芯片支持用USARTDIV的小數(shù)部分從BRR的手冊(cè)公式反推你可以按公式一步步算。初學(xué)者最容易犯錯(cuò)的是忘記BRR的最高位必須保留為0否則可能把寄存器值寫成一個(gè)不存在的分頻。串口初始化里還有一個(gè)小陷阱USART的CR1寄存器復(fù)位值默認(rèn)是不使能的。很多人寫好一堆代碼后發(fā)現(xiàn)根本沒(méi)輸出回頭一查原來(lái)是忘了把UE位置1。這種問(wèn)題不是邏輯復(fù)雜純粹是對(duì)復(fù)位值不夠敏感。所以我的習(xí)慣是每次配置外設(shè)前先看一眼手冊(cè)里的“復(fù)位值”列哪些位默認(rèn)是0哪些位默認(rèn)是1都心里有數(shù)再動(dòng)手。6. 寄存器踩坑記錄volatile、讀-改-寫和手冊(cè)閱讀習(xí)慣6.1 volatile不寫編譯器可能連外設(shè)都“優(yōu)化”掉volatile是我見過(guò)新手寫寄存器代碼時(shí)最容易漏掉的東西。它的作用是告訴編譯器這個(gè)地址的內(nèi)容每次必須重新從內(nèi)存讀取不要用緩存值替代。舉個(gè)例子你寫一個(gè)循環(huán)等待某個(gè)狀態(tài)寄存器變成1while ((flag_reg 0x01) 0);如果flag_reg沒(méi)用volatile修飾編譯器可能認(rèn)為這個(gè)值在循環(huán)體里沒(méi)被修改于是把循環(huán)優(yōu)化成一個(gè)死循環(huán)或者干脆優(yōu)化掉。單片機(jī)的寄存器不僅有CPU寫還有外設(shè)硬件在改編譯器并不知道這一點(diǎn)。所以在定義寄存器指針時(shí)一律用*(volatile unsigned int *)不要嫌難看。這個(gè)表達(dá)是寄存器編程的保命符。6.2 讀-改-寫操作與多任務(wù)競(jìng)爭(zhēng)寄存器操作里最常見的操作方式就是讀、改、寫三步比如GPIOB_ODR | (1 5);這行代碼編譯后實(shí)際上是三條指令讀ODR、修改第5位、再寫回ODR。如果在中斷和主程序里同時(shí)操作同一個(gè)寄存器的不同位就可能出現(xiàn)“丟失寫入”的問(wèn)題。比如主程序剛要清bit0中斷里正好置bit2最后主程序把舊值寫回去中斷置的bit2就沒(méi)了。解決這個(gè)問(wèn)題的辦法有三種一是盡量用BSRR這類硬件置位/清除寄存器二是中斷和主程序操作同一個(gè)引腳時(shí)暫時(shí)關(guān)中斷三是用關(guān)鍵區(qū)的互斥保護(hù)。實(shí)際項(xiàng)目里我基本把“能用BSRR就不碰ODR”寫進(jìn)了自己的驅(qū)動(dòng)規(guī)范。6.3 我的小習(xí)慣按項(xiàng)目整理寄存器速查表官方參考手冊(cè)有幾百頁(yè)每次找寄存器翻大厚書確實(shí)麻煩。我現(xiàn)在每一個(gè)項(xiàng)目都會(huì)建一個(gè)頭文件只寫上這個(gè)項(xiàng)目真正用到的寄存器地址和位掩碼。比如用USART1就把USART1基地址、SR、DR、CR1和BRR的地址全部定義進(jìn)去旁邊加上注釋說(shuō)明哪位是干什么的。注釋別寫成復(fù)制粘貼要按自己的理解寫。比如“bit13串口總開關(guān)必須置1”這種話在調(diào)試時(shí)比英文縮寫有用得多。整理這份速查表的過(guò)程其實(shí)也是重新梳理外設(shè)邏輯的過(guò)程。很多我當(dāng)時(shí)“明明會(huì)用但說(shuō)不清”的功能都是在這時(shí)候想明白的。還有一定要養(yǎng)成“改一個(gè)寄存器就驗(yàn)證一次”的習(xí)慣。不要一口氣寫完幾十行配置再上電測(cè)試那樣出了問(wèn)題根本不知道是哪一行惹的禍。我用串口調(diào)試或者仿真器查看寄存器實(shí)時(shí)值一步一確認(rèn)時(shí)鐘開了沒(méi)引腳模式對(duì)沒(méi)對(duì)計(jì)數(shù)器走沒(méi)走這樣能把問(wèn)題鎖定在小范圍內(nèi)。最后分享一個(gè)對(duì)我?guī)椭艽蟮男〖记伞Wx手冊(cè)寄存器表格時(shí)我習(xí)慣拿一張紙把這個(gè)寄存器從bit0到bit31全部畫成一格一格的然后把復(fù)位值、當(dāng)前值、目標(biāo)值寫上去。這個(gè)看起來(lái)原始的辦法比盯著PDF盯半天效率高得多。寄存器編程說(shuō)到底是跟具體地址和每一位打交道把抽象表格變成自己手里的圖很多糊涂賬自然而然就清楚了。