5.1 什么是寄存器
我們經(jīng)常說寄存器,那么什么是寄存器?這是我們本章需要講解的內(nèi)容,在學(xué)習(xí)的過程中,大家?guī)е@個疑問好好思考下,到最后看看大家能否用一句話給寄存器下一個定義。
5.2 STM32長啥樣
我們開發(fā)板中使用的芯片是176pin的STM32F767IGT6,具體見圖 51。這個就是我們接下來要學(xué)習(xí)的STM32,它講帶領(lǐng)我們進(jìn)入嵌入式的殿堂。
芯片正面是絲印,ARM應(yīng)該是表示該芯片使用的是ARM的內(nèi)核,STM32F767IGT6是芯片型號,后面的字應(yīng)該是跟生產(chǎn)批次相關(guān),最下面的是ST的LOGO。
芯片四周是引腳,左下角的小圓點表示1腳,然后從1腳起按照逆時針的順序排列(所有芯片的引腳順序都是逆時針排列的)。開發(fā)板中把芯片的引腳引出來,連接到各種傳感器上,然后在STM32上編程(實際就是通過程序控制這些引腳輸出高電平或者低電平)來控制各種傳感器工作,通過做實驗的方式來學(xué)習(xí)STM32芯片的各個資源。開發(fā)板是一種評估板,板載資源非常豐富,引腳復(fù)用比較多,力求在一個板子上驗證芯片的全部功能。
圖 5-1 STM32F767IGT6 實物圖
圖 5-2 STM32F767IGT6正面引腳圖
5.3 芯片里面有什么
我們看到的STM32芯片已經(jīng)是已經(jīng)封裝好的成品,主要由內(nèi)核和片上外設(shè)組成。若與電腦類比,內(nèi)核與外設(shè)就如同電腦上的CPU與主板、內(nèi)存、顯卡、硬盤的關(guān)系。
STM32F767采用的是Cortex-M7內(nèi)核,內(nèi)核即CPU,由ARM公司設(shè)計。ARM公司并不生產(chǎn)芯片,而是出售其芯片技術(shù)授權(quán)。芯片生產(chǎn)廠商(SOC)如ST、TI、Freescale,負(fù)責(zé)在內(nèi)核之外設(shè)計部件并生產(chǎn)整個芯片,這些內(nèi)核之外的部件被稱為核外外設(shè)或片上外設(shè)。如GPIO、USART(串口)、I2C、SPI等都叫做片上外設(shè)。具體見圖 53。
圖 5-3 STM32芯片架構(gòu)簡圖
芯片主系統(tǒng)架構(gòu)基于兩個子系統(tǒng),一個是AXI轉(zhuǎn)多層AHB橋,多層AHB總線矩陣。
AXI轉(zhuǎn)多層AHB橋,從AXI4協(xié)議轉(zhuǎn)成AHB-Lite協(xié)議,其中包含3個AXI轉(zhuǎn)32-bit AHB橋通過32-bit的AHB總線矩陣連接到外部存儲器FMC接口、外部存儲器Quad SPI接口、內(nèi)部SRAM(SRAM1 and SRAM2)。還包含一個AXI轉(zhuǎn)64-bit AHB橋通過64-bit總線矩陣連接到內(nèi)部FLASH。
多層AHB總線矩陣,其中32-bit多層AHB總線矩陣互聯(lián)11個主設(shè)備和8個從設(shè)備,64-bit多層AHB總線矩陣則是CPU通過AXI轉(zhuǎn)AHB橋通過這個64-bit多層AHB總線矩陣連接到內(nèi)部Flash。DMA主設(shè)備通過32-bit AHB總線矩陣通過這個64-bit多層AHB總線矩陣連接到內(nèi)部Flash。具體見圖 54。主控總線通過一個總線矩陣來連接被控總線,總線矩陣用于主控總線之間的訪問仲裁管理,仲裁采用循環(huán)調(diào)度算法??偩€之間交叉的時候如果有個圓圈則表示可以通信,沒有圓圈則表示不可以通信。
圖 5-4 STM32F76xxx 和 STM32F77xxx 器件的總線接口
5.4 存儲器映射
在圖 54中,連接被控總線的是FLASH,RAM和片上外設(shè),這些功能部件共同排列在一個4GB的地址空間內(nèi)。我們在編程的時候,操作的也正是這些功能部件。
5.4.1 存儲器映射
存儲器本身不具有地址信息,它的地址是由芯片廠商或用戶分配,給存儲器分配地址的過程就稱為存儲器映射,具體見圖 55。如果給存儲器再分配一個地址就叫存儲器重映射。
圖 5-5 存儲器映射
1. 存儲器區(qū)域功能劃分
在這4GB的地址空間中,ARM已經(jīng)粗線條的平均分成了8個塊,每塊512MB,每個塊也都規(guī)定了用途,具體分類見表格 51。每個塊的大小都有512MB,顯然這是非常大的,芯片廠商在每個塊的范圍內(nèi)設(shè)計各具特色的外設(shè)時并不一定都用得完,都是只用了其中的一部分而已。
表格 5-1 存儲器功能分類
序號用途地址范圍
Block 0SRAM0x0000 0000 ~ 0x1FFF FFFF(512MB)
Block 1SRAM0x2000 0000 ~ 0x3FFF FFFF(512MB)
Block 2片上外設(shè)0x4000 0000 ~ 0x5FFF FFFF(512MB)
Block 3FMC的bank1 ~ bank20x6000 0000 ~ 0x7FFF FFFF(512MB)
Block 4FMC的bank3 ~ bank40x8000 0000 ~ 0x9FFF FFFF(512MB)
Block 5FMC0xA000 0000 ~ 0xCFFF FFFF(512MB)
Block 6FMC0xD000 0000 ~ 0xDFFF FFFF(512MB)
Block 7Cortex-M4內(nèi)部外設(shè)0xE000 0000 ~ 0xFFFF FFFF(512MB)
在這8個Block里面,有3個塊非常重要,也是我們最關(guān)心的三個塊。Boock0用來設(shè)計成內(nèi)部FLASH,Block1用來設(shè)計成內(nèi)部RAM,Block2用來設(shè)計成片上的外設(shè),下面我們簡單的介紹下這三個Block里面的具體區(qū)域的功能劃分。
存儲器Block0內(nèi)部區(qū)域功能劃分
Block0主要用于設(shè)計片內(nèi)的FLASH, F429系列片內(nèi)部FLASH最大是2MB,我們使用的STM32F767IGT6的FLASH是1MB。要在芯片內(nèi)部集成更大的FLASH或者SRAM都意味著芯片成本的增加,往往片內(nèi)集成的FLASH都不會太大,ST能在追求性價比的同時做到1MB以上,實乃良心之舉。Block內(nèi)部區(qū)域的功能劃分具體見表格 52。
表格 52 存儲器Block0 內(nèi)部區(qū)域功能劃分
塊用途說明地址范圍
Block0預(yù)留0x1FFF 0020 ~ 0x1FFF FFFF
16個字節(jié)用于鎖定對應(yīng)的OTP數(shù)據(jù)塊。0x1FFF 0000 ~ 0x1FFF 001F
預(yù)留0x0820 0000 ~ 0x1FFE FFFF
FLASH:我們的程序就放在這里。0x0800 0000 ~ 0x081F FFFF
預(yù)留0x0030 0000 ~ 0x07FF FFFF
FLASH存儲基于ITCM總線接口,不支持寫操作,即只讀。 0x0020 0000 ~ 0x003F FFFF
預(yù)留0x0011 0000 ~ 0x001F FFFF
系統(tǒng)存儲器:里面存的是ST出廠時燒寫好的ISP自舉程序,用戶無法改動。串口下載的時候需要用到這部分程序。0x0010 0000 ~ 0x0010 EDBF
預(yù)留0x0000 4000 ~ 0x000F FFFF
ITCM RAM,只能被CPU訪問,不用經(jīng)過總線矩陣,屬于高速的RAM。0x0000 0000 ~ 0x0000 3FFF
儲存器Block1內(nèi)部區(qū)域功能劃分
Block1用于設(shè)計片內(nèi)的SRAM。F767 內(nèi)部SRAM的大小為512KB,分SRAM1 368KB,SRAM2 16KB,DTCM 128KB,Block內(nèi)部區(qū)域的功能劃分具體見表格 5-3。
表格 5-3 存儲器Block1 內(nèi)部區(qū)域功能劃分
塊用途說明地址范圍
Block1預(yù)留0x2008 0000 ~ 0x3FFF FFFF
SRAM2 16KB0x2007 C000 ~ 0x2007 FFFF
SRAM1 368KB0x2002 0000 ~ 0x2007 BFFF
DTCM 128KB0x2000 0000 ~ 0x2001 FFFF
儲存器Block2內(nèi)部區(qū)域功能劃分
Block2用于設(shè)計片內(nèi)的外設(shè),根據(jù)外設(shè)的總線速度不同,Block被分成了APB和AHB兩部分,其中APB又被分為APB1和APB2,AHB分為AHB1和AHB2,具體見表格 5-4。還有一個AHB3包含了Block3/4/5/6,這四個Block用于擴展外部存儲器,如SDRAM,NORFLASH和NANDFLASH等。
表格 5-4 存儲器Block2 內(nèi)部區(qū)域功能劃分
塊用途說明地址范圍
Block2APB1 總線外設(shè)0x4000 0000 ~ 0x4000 7FFF
預(yù)留0x4000 8000 ~ 0x4000 FFFF
APB2 總線外設(shè)0x4001 0000 ~ 0x4001 6BFF
預(yù)留0x4001 6C00 ~ 0x4001 FFFF
AHB1 總線外設(shè)0x4002 0000 ~ 0x4007 FFFF
預(yù)留0x4008 0000 ~ 0x4FFF FFFF
AHB2總線外設(shè)0x5000 0000 ~ 0x5006 0BFF
預(yù)留0x5006 0C00 ~ 0x5FFF FFFF
5.5 寄存器映射
我們知道,存儲器本身沒有地址,給存儲器分配地址的過程叫存儲器映射,那什么叫寄存器映射?寄存器到底是什么?
在存儲器Block2這塊區(qū)域,設(shè)計的是片上外設(shè),它們以四個字節(jié)為一個單元,共32bit,每一個單元對應(yīng)不同的功能,當(dāng)我們控制這些單元時就可以驅(qū)動外設(shè)工作。我們可以找到每個單元的起始地址,然后通過C語言指針的操作方式來訪問這些單元,如果每次都是通過這種地址的方式來訪問,不僅不好記憶還容易出錯,這時我們可以根據(jù)每個單元功能的不同,以功能為名給這個內(nèi)存單元取一個別名,這個別名就是我們經(jīng)常說的寄存器,這個給已經(jīng)分配好地址的有特定功能的內(nèi)存單元取別名的過程就叫寄存器映射。
比如,我們找到GPIOH端口的輸出數(shù)據(jù)寄存器ODR的地址是0x4002 1C14(至于這個地址如何找到可以先跳過,后面我們會有詳細(xì)的講解),ODR寄存器是32bit,低16bit有效,對應(yīng)著16個外部IO,寫0/1對應(yīng)的的IO則輸出低/高電平?,F(xiàn)在我們通過C語言指針的操作方式,讓GPIOH的16個IO都輸出高電平,具體見代碼 51。
代碼 51 通過絕對地址訪問內(nèi)存單元
1 // GPIOH 端口全部輸出 高電平
2 *(unsigned int*)(0x4002 1C14) = 0xFFFF;
0x4002 1C14在我們看來是GPIOH端口ODR的地址,但是在編譯器看來,這只是一個普通的變量,是一個立即數(shù),要想讓編譯器也認(rèn)為是指針,我們得進(jìn)行強制類型轉(zhuǎn)換,把它轉(zhuǎn)換成指針,即(unsigned int *)0x4002 1C14,然后再對這個指針進(jìn)行 * 操作。
剛剛我們說了,通過絕對地址訪問內(nèi)存單元不好記憶且容易出錯,我們可以通過寄存器的方式來操作,具體見代碼 52。
代碼 52 通過寄存器別名方式訪問內(nèi)存單元
1 // GPIOH 端口全部輸出 高電平
2 #define GPIOH_ODR (unsigned int*)(GPIOH_BASE+0x14)
3 * GPIOH_ODR = 0xFF;
為了方便操作,我們干脆把指針操作“*”也定義到寄存器別名里面,具體見代碼 53。
代碼 53 通過寄存器別名訪問內(nèi)存單元
1 // GPIOH 端口全部輸出 高電平
2 #define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14)
3 GPIOH_ODR = 0xFF;
5.5.1 STM32的外設(shè)地址映射
片上外設(shè)區(qū)分為四條總線,根據(jù)外設(shè)速度的不同,不同總線掛載著不同的外設(shè),APB掛載低速外設(shè),AHB掛載高速外設(shè)。相應(yīng)總線的最低地址我們稱為該總線的基地址,總線基地址也是掛載在該總線上的首個外設(shè)的地址。其中APB1總線的地址最低,片上外設(shè)從這里開始,也叫外設(shè)基地址。
1. 總線基地址
表格 5-5 總線基地址
總線名稱總線基地址相對外設(shè)基地址的偏移
APB10x4000 00000x0
APB20x4001 00000x0001 0000
AHB10x4002 00000x0002 0000
AHB20x5000 00000x1000 0000
AHB30x6000 0000已不屬于片上外設(shè)
表格 5-5的“相對外設(shè)基地址偏移”即該總線地址與“片上外設(shè)”基地址0x4000 0000的差值。關(guān)于地址的偏移我們后面還會講到。
2. 外設(shè)基地址
總線上掛載著各種外設(shè),這些外設(shè)也有自己的地址范圍,特定外設(shè)的首個地址稱為“XX外設(shè)基地址”,也叫XX外設(shè)的邊界地址。具體有關(guān)STM32F4xx外設(shè)的邊界地址請參考《STM32F76xx數(shù)據(jù)手冊》的第4章節(jié)的存儲器映射的表Table 13. STM32F765xx, STM32F767xx, STM32F768Ax and STM32F769xx register boundary addresses。
這里面我們以GPIO這個外設(shè)來講解外設(shè)的基地址,具體見表格 5-6。
表格 5-6 外設(shè)GPIO基地址
外設(shè)名稱外設(shè)基地址相對AHB1總線的地址偏移
GPIOA0x4002 00000x0
GPIOB0x4002 04000x0000 0400
GPIOC0x4002 08000x0000 0800
GPIOD0x4002 0C000x0000 0C00
GPIOE0x4002 10000x0000 1000
GPIOF0x4002 14000x0000 1400
GPIOG0x4002 18000x0000 1800
GPIOH0x4002 1C000x0000 1C00
從表格 5-6看到,GPIOA的基址相對于AHB1總線的地址偏移為0,我們應(yīng)該就可以猜到,AHB1總線的第一個外設(shè)就是GPIOA。
3. 外設(shè)寄存器
在XX外設(shè)的地址范圍內(nèi),分布著的就是該外設(shè)的寄存器。以GPIO外設(shè)為例,GPIO是通用輸入輸出端口的簡稱,簡單來說就是STM32可控制的引腳,基本功能是控制引腳輸出高電平或者低電平。最簡單的應(yīng)用就是把GPIO的引腳連接到LED燈的陰極,LED燈的陽極接電源,然后通過STM32控制該引腳的電平,從而實現(xiàn)控制LED燈的亮滅。
GPIO有很多個寄存器,每一個都有特定的功能。每個寄存器為32bit,占四個字節(jié),在該外設(shè)的基地址上按照順序排列,寄存器的位置都以相對該外設(shè)基地址的偏移地址來描述。這里我們以GPIOH端口為例,來說明GPIO都有哪些寄存器,具體見表格 5-7。
表格 5-7 GPIOH端口的 寄存器地址列表
寄存器名稱寄存器地址相對GPIOH基址的偏移
GPIOH_MODER0x4002 1C000x00
GPIOH_OTYPER0x4002 1C040x04
GPIOH_OSPEEDR0x4002 1C080x08
GPIOH_PUPDR0x4002 1C0C0x0C
GPIOH_IDR0x4002 1C100x10
GPIOH_ODR0x4002 1C140x14
GPIOH_BSRR0x4002 1C180x18
GPIOH_LCKR0x4002 1C1C0x1C
GPIOH_AFRL0x4002 1C200x20
GPIOH_AFRH0x4002 1C240x24
有關(guān)外設(shè)的寄存器說明可參考《STM32F76xxx參考手冊》中具體章節(jié)的寄存器描述部分,在編程的時候我們需要反復(fù)的查閱外設(shè)的寄存器說明。
這里我們以“GPIO端口置位/復(fù)位寄存器”為例,教大家如何理解寄存器的說明,具體見圖 5-6。
圖 5-6 GPIO端口置位/復(fù)位寄存器說明
q ①名稱
寄存器說明中首先列出了該寄存器中的名稱,“(GPIOx_BSRR)(x=A…I)”這段的意思是該寄存器名為“GPIOx_BSRR”其中的“x”可以為A-I,也就是說這個寄存器說明適用于GPIOA、GPIOB至GPIOI,這些GPIO端口都有這樣的一個寄存器。
q ②偏移地址
偏移地址是指本寄存器相對于這個外設(shè)的基地址的偏移。本寄存器的偏移地址是0x18,從參考手冊中我們可以查到GPIOA外設(shè)的基地址為0x4002 0000 ,我們就可以算出GPIOA的這個GPIOA_BSRR寄存器的地址為:0x4002 0000+0x18 ;同理,由于GPIOB的外設(shè)基地址為0x4002 0400,可算出GPIOB_BSRR寄存器的地址為:0x4002 0400+0x18 。其他GPIO端口以此類推即可。
q ③寄存器位表
緊接著的是本寄存器的位表,表中列出它的0-31位的名稱及權(quán)限。表上方的數(shù)字為位編號,中間為位名稱,最下方為讀寫權(quán)限,其中w表示只寫,r表示只讀,rw表示可讀寫。本寄存器中的位權(quán)限都是w,所以只能寫,如果讀本寄存器,是無法保證讀取到它真正內(nèi)容的。而有的寄存器位只讀,一般是用于表示STM32外設(shè)的某種工作狀態(tài)的,由STM32硬件自動更改,程序通過讀取那些寄存器位來判斷外設(shè)的工作狀態(tài)。
q ④位功能說明
位功能是寄存器說明中最重要的部分,它詳細(xì)介紹了寄存器每一個位的功能。例如本寄存器中有兩種寄存器位,分別為BRy及BSy,其中的y數(shù)值可以是0-15,這里的0-15表示端口的引腳號,如BR0、BS0用于控制GPIOx的第0個引腳,若x表示GPIOA,那就是控制GPIOA的第0引腳,而BR1、BS1就是控制GPIOA第1個引腳。
其中BRy引腳的說明是“0:不會對相應(yīng)的ODRx位執(zhí)行任何操作;1:對相應(yīng)ODRx位進(jìn)行復(fù)位”。這里的“復(fù)位”是將該位設(shè)置為0的意思,而“置位”表示將該位設(shè)置為1;說明中的ODRx是另一個寄存器的寄存器位,我們只需要知道ODRx位為1的時候,對應(yīng)的引腳x輸出高電平,為0的時候?qū)?yīng)的引腳輸出低電平即可(感興趣的讀者可以查詢該寄存器GPIOx_ODR的說明了解)。所以,如果對BR0寫入“1”的話,那么GPIOx的第0個引腳就會輸出“低電平”,但是對BR0寫入“0”的話,卻不會影響ODR0位,所以引腳電平不會改變。要想該引腳輸出“高電平”,就需要對“BS0”位寫入“1”,寄存器位BSy與BRy是相反的操作。
5.5.2 C語言對寄存器的封裝
以上所有的關(guān)于存儲器映射的內(nèi)容,最終都是為大家更好地理解如何用C語言控制讀寫外設(shè)寄存器做準(zhǔn)備,此處是本章的重點內(nèi)容。
1. 封裝總線和外設(shè)基地址
在編程上為了方便理解和記憶,我們把總線基地址和外設(shè)基地址都以相應(yīng)的宏定義起來,總線或者外設(shè)都以他們的名字作為宏名,具體見代碼 54。
代碼 54 總線和外設(shè)基址宏定義
1 /* 外設(shè)基地址 */
2 #define PERIPH_BASE ((unsigned int)0x40000000)
3
4 /* 總線基地址 */
5 #define APB1PERIPH_BASE PERIPH_BASE
6 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
7 #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
8 #define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000)
9
10 /* GPIO外設(shè)基地址 */
11 #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
12 #define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
13 #define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
14 #define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
15 #define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
16 #define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
17 #define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
18 #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)
19
20 /* 寄存器基地址,以GPIOH為例 */
21 #define GPIOH_MODER (GPIOH_BASE+0x00)
22 #define GPIOH_OTYPER (GPIOH_BASE+0x04)
23 #define GPIOH_OSPEEDR (GPIOH_BASE+0x08)
24 #define GPIOH_PUPDR (GPIOH_BASE+0x0C)
25 #define GPIOH_IDR (GPIOH_BASE+0x10)
26 #define GPIOH_ODR (GPIOH_BASE+0x14)
27 #define GPIOH_BSRR (GPIOH_BASE+0x18)
28 #define GPIOH_LCKR (GPIOH_BASE+0x1C)
29 #define GPIOH_AFRL (GPIOH_BASE+0x20)
30 #define GPIOH_AFRH (GPIOH_BASE+0x24)
代碼 54首先定義了 “片上外設(shè)”基地址PERIPH_BASE,接著在PERIPH_BASE上加入各個總線的地址偏移,得到APB1、APB2等總線的地址APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外設(shè)地址的偏移,得到GPIOA、GPIOH的外設(shè)地址,最后在外設(shè)地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具體地址,就可以用指針操作讀寫了,具體見代碼 55。
代碼 55 使用指針控制BSRR寄存器
1 /* 控制GPIOH 引腳10輸出低電平(BSRR寄存器的BR10置0) */
2 *(unsigned int *)GPIOH_BSRR = (0x01<<(16+10));
3
4 /* 控制GPIOH 引腳10輸出高電平(BSRR寄存器的BS10置1) */
5 *(unsigned int *)GPIOH_BSRR = 0x01<<10;
6
7 unsigned int temp;
8 /* 控制GPIOH 端口所有引腳的電平(讀IDR寄存器) */
9 temp = *(unsigned int *)GPIOH_IDR;
該代碼使用 (unsigned int *) 把GPIOH_BSRR宏的數(shù)值強制轉(zhuǎn)換成了地址,然后再用“*”號做取指針操作,對該地址的賦值,從而實現(xiàn)了寫寄存器的功能。同樣,讀寄存器也是用取指針操作,把寄存器中的數(shù)據(jù)取到變量里,從而獲取STM32外設(shè)的狀態(tài)。
2. 封裝寄存器列表
用上面的方法去定義地址,還是稍顯繁瑣,例如GPIOA-GPIOH都各有一組功能相同的寄存器,如GPIOA_MODER/GPIOB_MODER/GPIOC_MODER等等,它們只是地址不一樣,但卻要為每個寄存器都定義它的地址。為了更方便地訪問寄存器,我們引入C語言中的結(jié)構(gòu)體語法對寄存器進(jìn)行封裝,具體見代碼 56。
代碼 56 使用結(jié)構(gòu)體對GPIO寄存器組的封裝
1 typedef unsigned int uint32_t; /*無符號32位變量*/
2 typedef unsigned short int uint16_t; /*無符號16位變量*/
3
4 /* GPIO寄存器列表 */
5 typedef struct {
6 uint32_t MODER; /*GPIO模式寄存器 地址偏移: 0x00 */
7 uint32_t OTYPER; /*GPIO輸出類型寄存器 地址偏移: 0x04 */
8 uint32_t OSPEEDR; /*GPIO輸出速度寄存器 地址偏移: 0x08 */
9 uint32_t PUPDR; /*GPIO上拉/下拉寄存器 地址偏移: 0x0C */
10 uint32_t IDR; /*GPIO輸入數(shù)據(jù)寄存器 地址偏移: 0x10 */
11 uint32_t ODR; /*GPIO輸出數(shù)據(jù)寄存器 地址偏移: 0x14 */
12 uint16_t BSRR; /*GPIO置位/復(fù)位寄存器 地址偏移: 0x18 */
13 uint32_t LCKR; /*GPIO配置鎖定寄存器 地址偏移: 0x1C */
14 uint32_t AFR[2]; /*GPIO復(fù)用功能配置寄存器 地址偏移: 0x20-0x24 */
15 } GPIO_TypeDef;
這段代碼用typedef 關(guān)鍵字聲明了名為GPIO_TypeDef的結(jié)構(gòu)體類型,結(jié)構(gòu)體內(nèi)有8個 成員變量,變量名正好對應(yīng)寄存器的名字。C語言的語法規(guī)定,結(jié)構(gòu)體內(nèi)變量的存儲空間是連續(xù)的,其中32位的變量占用4個字節(jié),16位的變量占用2個字節(jié),具體見圖 5-7。
圖 5-7 GPIO_TypeDef結(jié)構(gòu)體成員的地址偏移
也就是說,我們定義的這個GPIO_TypeDef ,假如這個結(jié)構(gòu)體的首地址為0x4002 1C00(這也是第一個成員變量MODER的地址), 那么結(jié)構(gòu)體中第二個成員變量OTYPER的地址即為0x4002 1C00 +0x04 ,加上的這個0x04 ,正是代表MODER所占用的4個字節(jié)地址的偏移量,其它成員變量相對于結(jié)構(gòu)體首地址的偏移,在上述代碼右側(cè)注釋已給出。
這樣的地址偏移與STM32 GPIO外設(shè)定義的寄存器地址偏移一一對應(yīng),只要給結(jié)構(gòu)體設(shè)置好首地址,就能把結(jié)構(gòu)體內(nèi)成員的地址確定下來,然后就能以結(jié)構(gòu)體的形式訪問寄存器了,具體見代碼 57。
代碼 57 通過結(jié)構(gòu)體指針訪問寄存器
1 GPIO_TypeDef * GPIOx; //定義一個GPIO_TypeDef型結(jié)構(gòu)體指針GPIOx
2 GPIOx = GPIOH_BASE; //把指針地址設(shè)置為宏GPIOH_BASE地址
3 GPIOx->BSRR = 0x0000FFFF; //通過指針訪問并修改GPIOH_BSRR寄存器
4 GPIOx->MODER = 0xFFFFFFFF; //修改GPIOH_MODER寄存器
5 GPIOx->OTYPER =0xFFFFFFFF; //修改GPIOH_OTYPER寄存器
6
7 uint32_t temp;
8 temp = GPIOx->IDR; //讀取GPIOH_IDR寄存器的值到變量temp中
這段代碼先用GPIO_TypeDef類型定義一個結(jié)構(gòu)體指針GPIOx,并讓指針指向地址GPIOH_BASE(0x4002 1C00),使用地址確定下來,然后根據(jù)C語言訪問結(jié)構(gòu)體的語法,用GPIOx->BSRR、GPIOx->MODER及GPIOx->IDR等方式讀寫寄存器。
最后,我們更進(jìn)一步,直接使用宏定義好GPIO_TypeDef類型的指針,而且指針指向各個GPIO端口的首地址,使用時我們直接用該宏訪問寄存器即可,具體代碼 58。
代碼 58 定義好GPIO端口首地址址針
1 /*使用GPIO_TypeDef把地址強制轉(zhuǎn)換成指針*/
2 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
4 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
5 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
6 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
7 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
8 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
9 #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
10
11
12
13 /*使用定義好的宏直接訪問*/
14 /*訪問GPIOH端口的寄存器*/
15 GPIOH->BSRR = 0xFFFF; //通過指針訪問并修改GPIOH_BSRR寄存器
16 GPIOH->MODER = 0xFFFFFFF; //修改GPIOH_MODER寄存器
17 GPIOH->OTYPER =0xFFFFFFF; //修改GPIOH_OTYPER寄存器
18
19 uint32_t temp;
20 temp = GPIOH->IDR; //讀取GPIOH_IDR寄存器的值到變量temp中
21
22 /*訪問GPIOA端口的寄存器*/
23 GPIOA->BSRR = 0xFFFF; //通過指針訪問并修改GPIOA_BSRR寄存器
24 GPIOA->MODER = 0xFFFFFFF; //修改GPIOA_MODER寄存器
25 GPIOA->OTYPER =0xFFFFFFF; //修改GPIOA_OTYPER寄存器
26
27 uint32_t temp;
28 temp = GPIOA->IDR; //讀取GPIOA_IDR寄存器的值到變量temp中
這里我們僅是以GPIO這個外設(shè)為例,給大家講解了C語言對寄存器的封裝。以此類推,其他外設(shè)也同樣可以用這種方法來封裝。好消息是,這部分工作都由固件庫幫我們完成了,這里我們只是分析了下這個封裝的過程,讓大家知其然,也只其所以然。
5.5.3 修改寄存器的位操作方法
使用C語言對寄存器賦值時,我們常常要求只修改該寄存器的某幾位的值,且其它的寄存器位不變,這個時候我們就需要用到C語言的位操作方法了。
1. 把變量的某位清零
此處我們以變量a代表寄存器,并假設(shè)寄存器中本來已有數(shù)值,此時我們需要把變量a的某一位清零,且其它位不變,方法見代碼清單 51。
代碼清單 51 對某位清零
1 //定義一個變量a = 1001 1111 b (二進(jìn)制數(shù))
2 unsigned char a = 0x9f;
3
4 //對bit2 清零
5
6 a &= ~(1<<2);
7
8 //括號中的1左移兩位,(1<<2)得二進(jìn)制數(shù):0000 0100 b
9 //按位取反,~(1<<2)得1111 1011 b
10 //假如a中原來的值為二進(jìn)制數(shù): a = 1001 1111 b
11 //所得的數(shù)與a作”位與&”運算,a = (1001 1111 b)&(1111 1011 b),
12 //經(jīng)過運算后,a的值 a=1001 1011 b
13 // a的bit2 位被被零,而其它位不變。
2. 把變量的某幾個連續(xù)位清零
由于寄存器中有時會有連續(xù)幾個寄存器位用于控制某個功能,現(xiàn)假設(shè)我們需要把寄存器的某幾個連續(xù)位清零,且其它位不變,方法見代碼清單 52。
代碼清單 52 對某幾個連續(xù)位清零
1
2 //若把a中的二進(jìn)制位分成2個一組
3 //即bit0、bit1為第0組,bit2、bit3為第1組,
4 // bit4、bit5為第2組,bit6、bit7為第3組
5 //要對第1組的bit2、bit3清零
6
7 a &= ~(3<<2*1);
8
9 //括號中的3左移兩位,(3<<2*1)得二進(jìn)制數(shù):0000 1100 b
10 //按位取反,~(3<<2*1)得1111 0011 b
11 //假如a中原來的值為二進(jìn)制數(shù): a = 1001 1111 b
12 //所得的數(shù)與a作”位與&”運算,a = (1001 1111 b)&(1111 0011 b),
13 //經(jīng)過運算后,a的值 a=1001 0011 b
14 // a的第1組的bit2、bit3被清零,而其它位不變。
15
16 //上述(~(3<<2*1))中的(1)即為組編號;如清零第3組bit6、bit7此處應(yīng)為3
17 //括號中的(2)為每組的位數(shù),每組有2個二進(jìn)制位;若分成4個一組,此處即為4
18 //括號中的(3)是組內(nèi)所有位都為1時的值;若分成4個一組,此處即為二進(jìn)制數(shù)“1111 b”
19
20 //例如對第2組bit4、bit5清零
21 a &= ~(3<<2*2);
3. 對變量的某幾位進(jìn)行賦值。
寄存器位經(jīng)過上面的清零操作后,接下來就可以方便地對某幾位寫入所需要的數(shù)值了,且其它位不變,方法見代碼清單 53,這時候?qū)懭氲臄?shù)值一般就是需要設(shè)置寄存器的位參數(shù)。
代碼清單 53 對某幾位進(jìn)行賦值
1 //a = 1000 0011 b
2 //此時對清零后的第2組bit4、bit5設(shè)置成二進(jìn)制數(shù)“01 b ”
3
4 a |= (1<<2*2);
5 //a = 1001 0011 b,成功設(shè)置了第2組的值,其它組不變
4. 對變量的某位取反
某些情況下,我們需要對寄存器的某個位進(jìn)行取反操作,即 1變0 ,0變1,這可以直接用如下操作,其它位不變,見代碼清單 54。
代碼清單 54 對某位進(jìn)行取反操作
1 //a = 1001 0011 b
2 //把bit6取反,其它位不變
3
4 a ^=(1<<6);
5 //a = 1101 0011 b