第5章 什么是寄存器


本章參考資料:《STM32F76xxx參考手冊》、《STM32F76xxx數據手冊》、學習本章時,配合《STM32F76xxx參考手冊》“存儲器和總線架構”、“嵌入式FLASH接口”及“通用I/O(GPIO)”章節一起閱讀,效果會更佳,特別是涉及到寄存器說明的部分。

5.1  什么是寄存器

我們經常說寄存器,那么什么是寄存器?這是我們本章需要講解的內容,在學習的過程中,大家帶着這個疑問好好思考下,到最后看看大家能否用一句話給寄存器下一個定義。

5.2  STM32長啥樣

我們開發板中使用的芯片是176pinSTM32F767IGT6,具體見 51。這個就是我們接下來要學習的STM32,它講帶領我們進入嵌入式的殿堂。

芯片正面是絲印,ARM應該是表示該芯片使用的是ARM的內核,STM32F767IGT6是芯片型號,后面的字應該是跟生產批次相關,最下面的是STLOGO

芯片四周是引腳,左下角的小圓點表示1腳,然后從1腳起按照逆時針的順序排列(所有芯片的引腳順序都是逆時針排列的)。開發板中把芯片的引腳引出來,連接到各種傳感器上,然后在STM32上編程(實際就是通過程序控制這些引腳輸出高電平或者低電平)來控制各種傳感器工作,通過做實驗的方式來學習STM32芯片的各個資源。開發板是一種評估板,板載資源非常豐富,引腳復用比較多,力求在一個板子上驗證芯片的全部功能。

 

5-1 STM32F767IGT6 實物圖

 

5-2 STM32F767IGT6正面引腳圖 

5.3  芯片里面有什么

我們看到的STM32芯片已經是已經封裝好的成品,主要由內核和片上外設組成。若與電腦類比,內核與外設就如同電腦上的CPU與主板、內存、顯卡、硬盤的關系。

STM32F767采用的是Cortex-M7內核,內核即CPU,由ARM公司設計。ARM公司並不生產芯片,而是出售其芯片技術授權。芯片生產廠商(SOC)STTIFreescale,負責在內核之外設計部件並生產整個芯片,這些內核之外的部件被稱為核外外設或片上外設。如GPIOUSART(串口)、I2CSPI等都叫做片上外設。具體見 53。

 

5-3 STM32芯片架構簡圖

芯片主系統架構基於兩個子系統,一個是AXI轉多層AHB橋,多層AHB總線矩陣。

AXI轉多層AHB橋,從AXI4協議轉成AHB-Lite協議,其中包含3AXI32-bit AHB橋通過32-bitAHB總線矩陣連接到外部存儲器FMC接口、外部存儲器Quad SPI接口、內部SRAM(SRAM1 and SRAM2)。還包含一個AXI64-bit AHB橋通過64-bit總線矩陣連接到內部FLASH

多層AHB總線矩陣,其中32-bit多層AHB總線矩陣互聯11個主設備和8個從設備,64-bit多層AHB總線矩陣則是CPU通過AXIAHB橋通過這個64-bit多層AHB總線矩陣連接到內部FlashDMA主設備通過32-bit AHB總線矩陣通過這個64-bit多層AHB總線矩陣連接到內部Flash。具體見 54。主控總線通過一個總線矩陣來連接被控總線,總線矩陣用於主控總線之間的訪問仲裁管理,仲裁采用循環調度算法。總線之間交叉的時候如果有個圓圈則表示可以通信,沒有圓圈則表示不可以通信。

 

5-4 STM32F76xxx STM32F77xxx 器件的總線接口

5.4  存儲器映射

54中,連接被控總線的是FLASHRAM和片上外設,這些功能部件共同排列在一個4GB的地址空間內。我們在編程的時候,操作的也正是這些功能部件。

5.4.1  存儲器映射

存儲器本身不具有地址信息,它的地址是由芯片廠商或用戶分配,給存儲器分配地址的過程就稱為存儲器映射,具體見 55。如果給存儲器再分配一個地址就叫存儲器重映射。

 

 

5-5 存儲器映射

1. 存儲器區域功能划分

在這4GB的地址空間中,ARM已經粗線條的平均分成了8個塊,每塊512MB,每個塊也都規定了用途,具體分類見表格 51。每個塊的大小都有512MB,顯然這是非常大的,芯片廠商在每個塊的范圍內設計各具特色的外設時並不一定都用得完,都是只用了其中的一部分而已。

表格 5-1 存儲器功能分類

序號

用途

地址范圍

Block 0

SRAM

0x0000 0000 ~ 0x1FFF FFFF(512MB)

Block 1

SRAM

0x2000 0000 ~ 0x3FFF FFFF(512MB)

Block 2

片上外設

0x4000 0000 ~ 0x5FFF FFFF(512MB)

Block 3

FMCbank1 ~ bank2

0x6000 0000 ~ 0x7FFF FFFF(512MB)

Block 4

FMCbank3 ~ bank4

0x8000 0000 ~ 0x9FFF FFFF(512MB)

Block 5

FMC

0xA000 0000 ~ 0xCFFF FFFF(512MB)

Block 6

FMC

0xD000 0000 ~ 0xDFFF FFFF(512MB)

Block 7

Cortex-M4內部外設

0xE000 0000 ~ 0xFFFF FFFF(512MB)

在這8Block里面,有3個塊非常重要,也是我們最關心的三個塊。Boock0用來設計成內部FLASHBlock1用來設計成內部RAMBlock2用來設計成片上的外設,下面我們簡單的介紹下這三個Block里面的具體區域的功能划分。

存儲器Block0內部區域功能划分

Block0主要用於設計片內的FLASHF429系列片內部FLASH最大是2MB,我們使用的STM32F767IGT6FLASH1MB。要在芯片內部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片內集成的FLASH都不會太大,ST能在追求性價比的同時做到1MB以上,實乃良心之舉。Block內部區域的功能划分具體見表格 52。

表格 52 存儲器Block0 內部區域功能划分

用途說明

地址范圍

Block0

預留

0x1FFF 0020 ~ 0x1FFF FFFF

16個字節用於鎖定對應的OTP數據塊。

0x1FFF 0000 ~ 0x1FFF 001F

預留

0x0820 0000 ~ 0x1FFE FFFF

FLASH:我們的程序就放在這里。

0x0800 0000 ~ 0x081F FFFF

預留

0x0030 0000 ~ 0x07FF FFFF

FLASH存儲基於ITCM總線接口,不支持寫操作,即只讀。 

0x0020 0000 ~ 0x003F FFFF

預留

0x0011 0000 ~ 0x001F FFFF

系統存儲器:里面存的是ST出廠時燒寫好的ISP自舉程序,用戶無法改動。串口下載的時候需要用到這部分程序。

0x0010 0000 ~ 0x0010 EDBF

預留

0x0000 4000 ~ 0x000F FFFF

ITCM RAM,只能被CPU訪問,不用經過總線矩陣,屬於高速的RAM

0x0000 0000 ~ 0x0000 3FFF

儲存器Block1內部區域功能划分

Block1用於設計片內的SRAMF767   內部SRAM的大小為512KB,分SRAM1  368KBSRAM2  16KBDTCM  128KBBlock內部區域的功能划分具體見表格 5-3。

表格 5-3 存儲器Block1 內部區域功能划分

用途說明

地址范圍

Block1

預留

0x2008 0000 ~ 0x3FFF FFFF

SRAM2   16KB

0x2007 C000 ~ 0x2007 FFFF

SRAM1   368KB

0x2002 0000 ~ 0x2007 BFFF

DTCM     128KB

0x2000 0000 ~ 0x2001 FFFF

儲存器Block2內部區域功能划分

Block2用於設計片內的外設,根據外設的總線速度不同,Block被分成了APBAHB兩部分,其中APB又被分為APB1APB2AHB分為AHB1AHB2,具體見表格 5-4。還有一個AHB3包含了Block3/4/5/6,這四個Block用於擴展外部存儲器,如SDRAMNORFLASHNANDFLASH等。

表格 5-4 存儲器Block2 內部區域功能划分

用途說明

地址范圍

Block2

APB1 總線外設

0x4000 0000 ~ 0x4000 7FFF

預留

0x4000 8000 ~ 0x4000 FFFF

APB2 總線外設

0x4001 0000 ~ 0x4001 6BFF

預留

0x4001 6C00 ~ 0x4001 FFFF

AHB1 總線外設

0x4002 0000 ~ 0x4007 FFFF

預留

0x4008 0000 ~ 0x4FFF FFFF

AHB2總線外設

0x5000 0000 ~ 0x5006 0BFF

預留

0x5006 0C00 ~ 0x5FFF FFFF

5.5  寄存器映射

我們知道,存儲器本身沒有地址,給存儲器分配地址的過程叫存儲器映射,那什么叫寄存器映射?寄存器到底是什么?

在存儲器Block2這塊區域,設計的是片上外設,它們以四個字節為一個單元,共32bit,每一個單元對應不同的功能,當我們控制這些單元時就可以驅動外設工作。我們可以找到每個單元的起始地址,然后通過C語言指針的操作方式來訪問這些單元,如果每次都是通過這種地址的方式來訪問,不僅不好記憶還容易出錯,這時我們可以根據每個單元功能的不同,以功能為名給這個內存單元取一個別名,這個別名就是我們經常說的寄存器,這個給已經分配好地址的有特定功能的內存單元取別名的過程就叫寄存器映射。

比如,我們找到GPIOH端口的輸出數據寄存器ODR的地址是0x4002 1C14(至於這個地址如何找到可以先跳過,后面我們會有詳細的講解),ODR寄存器是32bit,低16bit有效,對應着16個外部IO,寫0/1對應的的IO則輸出低/高電平。現在我們通過C語言指針的操作方式,讓GPIOH16IO都輸出高電平,具體見代碼 51。

代碼 51 通過絕對地址訪問內存單元

1 // GPIOH 端口全部輸出 高電平

2 *(unsigned int*)(0x4002 1C14) = 0xFFFF;

0x4002 1C14在我們看來是GPIOH端口ODR的地址,但是在編譯器看來,這只是一個普通的變量,是一個立即數,要想讓編譯器也認為是指針,我們得進行強制類型轉換,把它轉換成指針,即(unsigned int *)0x4002 1C14,然后再對這個指針進行 * 操作。

剛剛我們說了,通過絕對地址訪問內存單元不好記憶且容易出錯,我們可以通過寄存器的方式來操作,具體見代碼 52。

代碼 52 通過寄存器別名方式訪問內存單元

1 // GPIOH 端口全部輸出 高電平

2 #define GPIOH_ODR                   (unsigned int*)(GPIOH_BASE+0x14)

3 * GPIOH_ODR = 0xFF;

為了方便操作,我們干脆把指針操作*”也定義到寄存器別名里面,具體見代碼 53。

代碼 53 通過寄存器別名訪問內存單元

1 // GPIOH 端口全部輸出 高電平

2 #define GPIOH_ODR                   *(unsigned int*)(GPIOH_BASE+0x14)

3 GPIOH_ODR = 0xFF;

5.5.1  STM32的外設地址映射

片上外設區分為四條總線,根據外設速度的不同,不同總線掛載着不同的外設,APB掛載低速外設,AHB掛載高速外設。相應總線的最低地址我們稱為該總線的基地址,總線基地址也是掛載在該總線上的首個外設的地址。其中APB1總線的地址最低,片上外設從這里開始,也叫外設基地址。

1. 總線基地址

表格 5-5 總線基地址

總線名稱

總線基地址

相對外設基地址的偏移

APB1

0x4000 0000

0x0

APB2

0x4001 0000

0x0001 0000

AHB1

0x4002 0000

0x0002 0000

AHB2

0x5000 0000

0x1000 0000

AHB3

0x6000 0000

已不屬於片上外設

表格 5-5“相對外設基地址偏移”即該總線地址與“片上外設”基地址0x4000 0000的差值。關於地址的偏移我們后面還會講到。

2. 外設基地址

總線上掛載着各種外設,這些外設也有自己的地址范圍,特定外設的首個地址稱為XX外設基地址”,也叫XX外設的邊界地址。具體有關STM32F4xx外設的邊界地址請參考《STM32F76xx數據手冊》的第4章節的存儲器映射的表Table 13. STM32F765xx, STM32F767xx, STM32F768Ax and STM32F769xx register boundary addresses。

這里面我們以GPIO這個外設來講解外設的基地址,具體見表格 5-6。

表格 5-6 外設GPIO基地址

外設名稱

外設基地址

相對AHB1總線的地址偏移

GPIOA

0x4002 0000

0x0

GPIOB

0x4002 0400

0x0000 0400

GPIOC

0x4002 0800

0x0000 0800

GPIOD

0x4002 0C00

0x0000 0C00

GPIOE

0x4002 1000

0x0000 1000

GPIOF

0x4002 1400

0x0000 1400

GPIOG

0x4002 1800

0x0000 1800

GPIOH

0x4002 1C00

0x0000 1C00

表格 5-6看到,GPIOA的基址相對於AHB1總線的地址偏移為0,我們應該就可以猜到,AHB1總線的第一個外設就是GPIOA

3. 外設寄存器

XX外設的地址范圍內,分布着的就是該外設的寄存器。以GPIO外設為例,GPIO是通用輸入輸出端口的簡稱,簡單來說就是STM32可控制的引腳,基本功能是控制引腳輸出高電平或者低電平。最簡單的應用就是把GPIO的引腳連接到LED燈的陰極,LED燈的陽極接電源,然后通過STM32控制該引腳的電平,從而實現控制LED燈的亮滅。

GPIO有很多個寄存器,每一個都有特定的功能。每個寄存器為32bit,占四個字節,在該外設的基地址上按照順序排列,寄存器的位置都以相對該外設基地址的偏移地址來描述。這里我們以GPIOH端口為例,來說明GPIO都有哪些寄存器,具體見表格 5-7。

表格 5-7 GPIOH端口的 寄存器地址列表

寄存器名稱

寄存器地址

相對GPIOH基址的偏移

GPIOH_MODER

0x4002 1C00

0x00

GPIOH_OTYPER

0x4002 1C04

0x04

GPIOH_OSPEEDR

0x4002 1C08

0x08

GPIOH_PUPDR

0x4002 1C0C

0x0C

GPIOH_IDR

0x4002 1C10

0x10

GPIOH_ODR

0x4002 1C14

0x14

GPIOH_BSRR

0x4002 1C18

0x18

GPIOH_LCKR

0x4002 1C1C

0x1C

GPIOH_AFRL

0x4002 1C20

0x20

GPIOH_AFRH

0x4002 1C24

0x24

有關外設的寄存器說明可參考《STM32F76xxx參考手冊》中具體章節的寄存器描述部分,在編程的時候我們需要反復的查閱外設的寄存器說明。

這里我們以GPIO端口置位/復位寄存器”為例,教大家如何理解寄存器的說明,具體見 5-6。 

 

5-6 GPIO端口置位/復位寄存器說明

q ①名稱

寄存器說明中首先列出了該寄存器中的名稱,(GPIOx_BSRR)(x=A…I)”這段的意思是該寄存器名為“GPIOx_BSRR”其中的“x”可以為A-I,也就是說這個寄存器說明適用於GPIOAGPIOBGPIOI,這些GPIO端口都有這樣的一個寄存器。

q ②偏移地址

偏移地址是指本寄存器相對於這個外設的基地址的偏移。本寄存器的偏移地址是0x18,從參考手冊中我們可以查到GPIOA外設的基地址為0x4002 0000 ,我們就可以算出GPIOA的這個GPIOA_BSRR寄存器的地址為:0x4002 0000+0x18 ;同理,由於GPIOB的外設基地址為0x4002 0400,可算出GPIOB_BSRR寄存器的地址為:0x4002 0400+0x18 。其他GPIO端口以此類推即可。

q ③寄存器位表

緊接着的是本寄存器的位表,表中列出它的0-31位的名稱及權限。表上方的數字為位編號,中間為位名稱,最下方為讀寫權限,其中w表示只寫,r表示只讀,rw表示可讀寫。本寄存器中的位權限都是w,所以只能寫,如果讀本寄存器,是無法保證讀取到它真正內容的。而有的寄存器位只讀,一般是用於表示STM32外設的某種工作狀態的,由STM32硬件自動更改,程序通過讀取那些寄存器位來判斷外設的工作狀態。

q ④位功能說明

位功能是寄存器說明中最重要的部分,它詳細介紹了寄存器每一個位的功能。例如本寄存器中有兩種寄存器位,分別為BRyBSy,其中的y數值可以是0-15,這里的0-15表示端口的引腳號,如BR0BS0用於控制GPIOx的第0個引腳,若x表示GPIOA,那就是控制GPIOA的第0引腳,而BR1BS1就是控制GPIOA1個引腳。

其中BRy引腳的說明是“0:不會對相應的ODRx位執行任何操作;1:對相應ODRx位進行復位”。這里的“復位”是將該位設置為0的意思,而“置位”表示將該位設置為1;說明中的ODRx是另一個寄存器的寄存器位,我們只需要知道ODRx位為1的時候,對應的引腳x輸出高電平,為0的時候對應的引腳輸出低電平即可(感興趣的讀者可以查詢該寄存器GPIOx_ODR的說明了解)。所以,如果對BR0寫入“1”的話,那么GPIOx的第0個引腳就會輸出“低電平”,但是對BR0寫入“0”的話,卻不會影響ODR0位,所以引腳電平不會改變。要想該引腳輸出“高電平”,就需要對“BS0”位寫入“1”,寄存器位BSyBRy是相反的操作。

5.5.2  C語言對寄存器的封裝

以上所有的關於存儲器映射的內容,最終都是為大家更好地理解如何用C語言控制讀寫外設寄存器做准備,此處是本章的重點內容。

1. 封裝總線和外設基地址

在編程上為了方便理解和記憶,我們把總線基地址和外設基地址都以相應的宏定義起來,總線或者外設都以他們的名字作為宏名,具體見代碼 54。

代碼 54 總線和外設基址宏定義

1 /* 外設基地址 */

 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外設基地址 */

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首先定義了 “片上外設”基地址PERIPH_BASE,接着在PERIPH_BASE上加入各個總線的地址偏移,得到APB1APB2等總線的地址APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外設地址的偏移,得到GPIOAGPIOH的外設地址,最后在外設地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具體地址,就可以用指針操作讀寫了,具體見代碼 55。

代碼 55 使用指針控制BSRR寄存器

1 /* 控制GPIOH 引腳10輸出低電平(BSRR寄存器的BR100) */

2 *(unsigned int *)GPIOH_BSRR = (0x01<<(16+10));

3

4 /* 控制GPIOH 引腳10輸出高電平(BSRR寄存器的BS101) */

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宏的數值強制轉換成了地址,然后再用*”號做取指針操作,對該地址的賦值,從而實現了寫寄存器的功能。同樣,讀寄存器也是用取指針操作,把寄存器中的數據取到變量里,從而獲取STM32外設的狀態。

2. 封裝寄存器列表

用上面的方法去定義地址,還是稍顯繁瑣,例如GPIOA-GPIOH都各有一組功能相同的寄存器,如GPIOA_MODER/GPIOB_MODER/GPIOC_MODER等等,它們只是地址不一樣,但卻要為每個寄存器都定義它的地址。為了更方便地訪問寄存器,我們引入C語言中的結構體語法對寄存器進行封裝,具體見代碼 56。

代碼 56 使用結構體對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輸入數據寄存器          地址偏移: 0x10      */

11     uint32_t ODR;      /*GPIO輸出數據寄存器          地址偏移: 0x14      */

12     uint16_t BSRR;     /*GPIO置位/復位寄存器 地址偏移: 0x18     */

13     uint32_t LCKR;     /*GPIO配置鎖定寄存器          地址偏移: 0x1C       */

14     uint32_t AFR[2];   /*GPIO復用功能配置寄存器       地址偏移: 0x20-0x24  */

15 } GPIO_TypeDef; 

這段代碼用typedef  關鍵字聲明了名為GPIO_TypeDef的結構體類型,結構體內有8個  成員變量,變量名正好對應寄存器的名字。C語言的語法規定,結構體內變量的存儲空間是連續的,其中32位的變量占用4個字節,16位的變量占用2個字節,具體見 5-7。

 

5-7 GPIO_TypeDef結構體成員的地址偏移

也就是說,我們定義的這個GPIO_TypeDef ,假如這個結構體的首地址為0x4002 1C00(這也是第一個成員變量MODER的地址), 那么結構體中第二個成員變量OTYPER的地址即為0x4002 1C00 +0x04 ,加上的這個0x04 ,正是代表MODER所占用的4個字節地址的偏移量,其它成員變量相對於結構體首地址的偏移,在上述代碼右側注釋已給出。

這樣的地址偏移與STM32 GPIO外設定義的寄存器地址偏移一一對應,只要給結構體設置好首地址,就能把結構體內成員的地址確定下來,然后就能以結構體的形式訪問寄存器了,具體見代碼 57。

代碼 57 通過結構體指針訪問寄存器

1 GPIO_TypeDef * GPIOx;        //定義一個GPIO_TypeDef型結構體指針GPIOx

2 GPIOx = GPIOH_BASE;          //把指針地址設置為宏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類型定義一個結構體指針GPIOx,並讓指針指向地址GPIOH_BASE(0x4002 1C00),使用地址確定下來,然后根據C語言訪問結構體的語法,用GPIOx->BSRR、GPIOx->MODER及GPIOx->IDR等方式讀寫寄存器。

最后,我們更進一步,直接使用宏定義好GPIO_TypeDef類型的指針,而且指針指向各個GPIO端口的首地址,使用時我們直接用該宏訪問寄存器即可,具體代碼 58。

代碼 58 定義好GPIO端口首地址址針

1 /*使用GPIO_TypeDef把地址強制轉換成指針*/

 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這個外設為例,給大家講解了C語言對寄存器的封裝。以此類推,其他外設也同樣可以用這種方法來封裝。好消息是,這部分工作都由固件庫幫我們完成了,這里我們只是分析了下這個封裝的過程,讓大家知其然,也只其所以然。

5.5.3  修改寄存器的位操作方法

使用C語言對寄存器賦值時,我們常常要求只修改該寄存器的某幾位的值,且其它的寄存器位不變,這個時候我們就需要用到C語言的位操作方法了。

1. 把變量的某位清零

此處我們以變量a代表寄存器,並假設寄存器中本來已有數值,此時我們需要把變量a的某一位清零,且其它位不變,方法見代碼清單 51。

代碼清單 51 對某位清零

1 //定義一個變量a = 1001 1111 b (二進制數)

 2 unsigned char a = 0x9f;

 3

 4 //bit2 清零

 5

 6 a &= ~(1<<2);

 7

 8 //括號中的1左移兩位,(1<<2)得二進制數:0000 0100 b

 9 //按位取反,~(1<<2)1111 1011 b

10 //假如a中原來的值為二進制數: a = 1001 1111 b

11 //所得的數與a位與&”運算,a = (1001 1111 b)&(1111 1011 b),

12 //經過運算后,a的值 a=1001 1011 b

13 // abit2 位被被零,而其它位不變。

2. 把變量的某幾個連續位清零

由於寄存器中有時會有連續幾個寄存器位用於控制某個功能,現假設我們需要把寄存器的某幾個連續位清零,且其它位不變,方法見代碼清單 52。

代碼清單 52 對某幾個連續位清零

1

 2 //若把a中的二進制位分成2個一組

 3 //bit0bit1為第0組,bit2bit3為第1組,

 4 //  bit4bit5為第2組,bit6bit7為第3

 5 //要對第1組的bit2bit3清零

 6

 7 a &= ~(3<<2*1);

 8

 9 //括號中的3左移兩位,(3<<2*1)得二進制數:0000 1100 b

10 //按位取反,~(3<<2*1)1111 0011 b

11 //假如a中原來的值為二進制數: a = 1001 1111 b

12 //所得的數與a位與&”運算,a = (1001 1111 b)&(1111 0011 b),

13 //經過運算后,a的值 a=1001 0011 b

14 // a的第1組的bit2bit3被清零,而其它位不變。

15

16 //上述(~(3<<2*1))中的(1)即為組編號;如清零第3bit6bit7此處應為3

17 //括號中的(2)為每組的位數,每組有2個二進制位;若分成4個一組,此處即為4

18 //括號中的(3)是組內所有位都為1時的值;若分成4個一組,此處即為二進制數“1111 b”

19

20 //例如對第2bit4bit5清零

21 a &= ~(3<<2*2);

3. 對變量的某幾位進行賦值。

寄存器位經過上面的清零操作后,接下來就可以方便地對某幾位寫入所需要的數值了,且其它位不變,方法見代碼清單 53,這時候寫入的數值一般就是需要設置寄存器的位參數。

代碼清單 53 對某幾位進行賦值

1 //a = 1000 0011 b

2 //此時對清零后的第2bit4bit5設置成二進制數“01 b ”

3

4 a |= (1<<2*2);

5 //a = 1001 0011 b,成功設置了第2組的值,其它組不變

4. 對變量的某位取反

某些情況下,我們需要對寄存器的某個位進行取反操作,即 10 01,這可以直接用如下操作,其它位不變,見代碼清單 54。

代碼清單 54 對某位進行取反操作

1 //a = 1001 0011 b

2 //bit6取反,其它位不變

3

4 a ^=(1<<6);

5 //a = 1101 0011 b

關於修改寄存器位的這些操作,在下一章中有應用實例代碼,可配合閱讀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM