嵌入式C語言開發---存儲器與寄存器
概述:
講述如何使用C語言來對底層寄存器進行封裝
內容:
- 存儲器映射
- 寄存器與寄存器映射
- C語言訪問寄存器
- 存儲器映射
程序存儲器、數據存儲器、寄存器和I/O 端口排列在同一個順序的4 GB 地
址空間內
存儲器映射:
存儲器本身不具有地址信息,它的地址是由芯片廠商或用戶分配,給存儲器
分配地址的過程稱為存儲器映射,如果再分配一個地址就叫重映射。
存儲器區域劃分
ARM 將這4GB 的存儲器空間,平均分成了8 塊區
域,每塊區域的大小是512MB,這個容量是非常大的,因此芯片廠商就在每塊容
量範圍內設計各自特色的外設,要註意一點每塊區域容量占用越大,芯片成本就
越高,所以說我們使用的STM32 芯片都是只用了其中一部分。
在這8 個Block 裏面,Block0、Block1 和Block2 這3 個塊是我們最為關
心的。因為它包含了STM32 芯片的內部Flash、RAM 和片上外設。
Block0 內部又劃分了好多個功能塊,我們按地址從低到高順序依次
介紹。
0x0000 0000-0x0007 FFFF:取決於BOOT 引腳,為FLASH、系統存儲器、
SRAM 的別名。
0x0008 0000-0x07FF FFFF:預留。
0x0800 0000-0x0807 FFFF:片內FLASH,我們編寫的程序就放在這一區域
(512KB)。
0x0808 0000-0x1FFF EFFF:預留。
0x1FFF F000-0x1FFF F7FF:系統存儲器,裏面存放的是ST 出廠時燒寫好的
isp 自舉程序,用戶無法改動。使用串口下載的時候需要用到這部分程序。
0x1FFF F800-0x1FFF F80F:選項字節,用於配置讀寫保護、
BOR 級別、軟件/硬件看門狗以及器件處於待機或停止模式下的復位。當芯片不
小心被鎖住之後,我們可以從RAM 裏面啟動來修改這部分相應的寄存器位。
0x1FFF F810-0x1FFF FFFF:預留。
(2)Block1 內部區域功能劃分
Block1 用於設計片內的SRAM,我們使用的STM32F103ZET6 的SRAM 是64KB。
從圖5.1.1 中可以看到Block1 內部又劃分了幾個功能塊,我們按地址從低到高
順序依次介紹。
0x2000 0000-0x2000 FFFF:SRAM,容量為64KB。
0x2001 0000-0x3FFF FFFF:預留。
(3)Block2 內部區域功能劃分
Block2 用於設計片內外設,根據外設總線速度的不同,Block2 被劃分為AHB
和APB 兩部分,APB 又被分成APB1 和APB2 總線。這些都可以在圖5.1.1 中看到,
我們按地址從低到高順序依次介紹。
0x4000 0000-0x4000 77FF:APB1 總線外設。
0x4000 7800-0x4000 FFFF:預留。
0x4001 0000-0x4001 3FFF:APB2 總線外設。
0x4001 4000-0x4001 7FFF:預留。
0x4001 8000-0x4002 33FF:AHB 總線外設。
0x4002 4400-0x5FFF FFFF:預留。
在Block3/4/5 中還包含了FSMC 擴展區域,這3 個塊可用於擴展外部存儲器,
比如SRAM,NORFLASH 和NANDFLASH 等。
寄存器和寄存器映射
lock2 這片區域是用來設計片上外設的,
由於Cortex-M3 內核是32 位的,所以存儲器內部是以四個字節為一個單元,每
一個單元對應不同的功能,當我們控制這些單元時也就可以控制外設。每一個單
元還對應一個地址,我們要操作這些單元,也就是通過對應的地址來訪問。由於
STM32 外設非常多而且復雜,如果每操作一個外設就要寫一大串對應的存儲單元
地址,顯然是非常麻煩的而且還極容易出錯。因此我們就把每個單元的功能作為
名,給這個內存取一個別名,這個別名就是我們經常說的寄存器。
然後通過C
語言指針來操作這些寄存器即可。那什麽是寄存器映射呢?給已經分配好地址的
有特定功能的內存單元取別名的過程就叫寄存器映射。
比方說我們找到0x4001 1010 這個單元地址,那麽可以通過查閱相關資料了
解到此單元具有GPIOC 端口置位/復位功能(至於此地址如何查找這個功能我們
後面會具體介紹)。因此為了更好區分此單元的功能和方便後續的程序開發,可
以給這個單元取一個別名GPIOC_BSRR,那麽這個GPIOC_BSRR 就是寄存器,並且
這個寄存器地址就是0x4001 1010。這個過程就是寄存器映射。
如何訪問STM32 寄存器內容:
我們知道寄存器就是一些有特定功能的內存單元,所以要訪問STM32 寄存器
也就是操作STM32 的內存單元,根據C 語言指針的特點,可以使用指針來操作
STM32 的內存單元。假如我們要讓STM32 的GPIOC 的第0 管腳輸出低電平,我們
怎麽使用C 語言來處理?
片上外設區分為四條總線,根據外設速度的不同,不同總線掛載著不同的外
設, APB1 掛載低速外設,APB2 和AHB 掛載高速外設。相應總線的最低地址我
們稱為該總線的基地址,總線基地址也是掛載在該總線上的首個外設的地址。
APB1 總線的地址最低,因此片上外設就從這這個地址開始,也稱外設基地址。
外設基地址
每條總線上都會掛接著很多的外設,這些外設也會有自己的地址範圍,
XXX 外設的首個地址即最低地址就是XXX 外設的基地址,也稱作XXX 邊界地
址。有關STM32F1xx 外設的具體邊界地址可以參考《STM32F1xx 中文參考手
冊》P28 頁,裏面有詳細的介紹。這裏我們就以GPIO 外設來講解外設基地址。
其他的外設也是同樣分析
外設GPIOx 都是掛接在APB2 總線上,屬於高速的外
設,而APB2 總線的基地址是0x4001 0000,故GPIOA 的相對APB2 總線的地址偏
移是800。
(3)外設寄存器地址
XXX 外設的寄存器就分布在其對應的外設地址範圍內。這裏我們以GPIO 外
設為例,GPIO 是通用輸入輸出端口的簡稱,可以通過軟件來控制其輸入和輸出。
GPIO 有很多個寄存器,每一個都有特定的功能。每個寄存器為32bit,占四個
字節,這些寄存器都是按順序依次排列在外設的基地址上。寄存器的位置都以相
普中STM32F1xx 開發攻略
www.prechin.cn
39
對該外設基地址的偏移地址來描述。這裏我們以GPIOC 端口為例,來說明GPIO
都有哪些寄存器,
這裏我們就以GPIOC_BSRR 寄存器來教大家如何看《STM32F1xx 中文參考手
冊》內寄存器的說明。大家如果想要了解更多的寄存器內容,可以參考《STM32F1xx
中文參考手冊》相應寄存器外設部分。
首先我們需要打開STM32 中文參考手冊,然後找到GPIO 外設章節,裏面會
有一個GPIO 寄存器,只要找到我們所要查找的寄存器即可
A.紅色框4 表示的我們所查找寄存器的名稱,寄存器GPIOx_BSRR 內的x 表
示的是STM32GPIO 端口,範圍是A-E,也就是說在GPIOA、GPIOB 等端口中都有
這個寄存器。
B.紅色框5 表示的是相對GPIOx 地址的偏移值,比如現在我們使用的是
GPIOC 外設,其基地址是0x4001 1000,那麽本寄存器GPIOx_BSRR 地址=0x4001
1000+0x10=0x4001 1010。對於其他的GPIO 外設也是一個原理。
C.紅色框6 和7 表示的是寄存器的位表。其中6 表示寄存器編號,因為一個
寄存器是32bit,所以範圍是0-31。7 表示的是相應位的權限,w:只寫,r:只
讀,rw:可讀可寫。本寄存器位權限是w,所以只能寫,如果試圖讀本寄存器,
是無法保證讀取到它真正內容的。而有的寄存器位權限為只讀,一般是用於表示
STM32 外設的某種工作狀態的,由STM32 硬件自動更改,通過讀取那些寄存器位
來判斷外設的工作狀態。
D.紅色框8 是寄存器位功能說明。這個也是寄存器說明中最重要的部分,它
詳細介紹了寄存器每一個位的功能。例如本寄存器中有兩種寄存器位,分別為
BRy 及BSy,其中的y 數值表示的是管腳號,可以是0-15。如BR0、BS0 用於
控制GPIOx 的第0 個引腳,若x 表示GPIOC,那就是控制GPIOC 的第0 引腳,
而BR1、BS1 就是控制GPIOC 第1 個引腳。
其中BRy 引腳的說明是“ 0:不會對相應的ODRx 位執行任何操作; 1:
對相應ODRx 位進行復位”。這裏的“復位”是將該位設置為0 的意思,而“置
位”表示將該位設置為1;說明中的ODRx 是另一個寄存器的寄存器位,我們只
需要知道ODRx 位為1 的時候,對應的引腳x 輸出高電平,為0 的時候對應的
引腳輸出低電平即可(感興趣的讀者可以查詢該寄存器GPIOx_ODR 的說明了
解)。所以,如果對BR0 寫入“ 1”的話,那麽GPIOx 的第0 個引腳就會輸出
“低電平”,但是對BR0 寫入“ 0”的話,卻不會影響ODR0 位,所以引腳電
平不會改變。要想該引腳輸出“高電平”,就需要對“ BS0”位寫入“ 1”,寄
存器位BSy 與BRy 是相反的操作。
使用C 語言封裝寄存器:
實例1:控制GPIOC 端口的第0 管腳輸出一個低電平。首先我們需要知道
GPIOC 端口外設是掛接在哪個總線上的,然後根據總線基地址和本身的偏移地址
得到GPIOC 外設基地址,最後通過這個外設基地址得到裏面各種寄存器基地址。
(1)總線和外設基地址封裝
普中STM32F1xx 開發攻略
www.prechin.cn
41
根據寄存器的概念,我們可以使用C 語言中的宏定義對寄存器進行定義。具
體代碼如下:
//定義外設基地址
#define PERIPH_BASE ((unsigned int)0x40000000) 1)
//定義APB2 總線基地址
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000) 2)
//定義GPIOC 外設基地址
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800) 3)
//定義寄存器基地址這裏以GPIOC 為例
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00) 4)
#define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04)
#define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08)
#define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C)
#define GPIOC_BSRR *(unsigned int*)(GPIOC_BASE+0x10)
#define GPIOC_BRR *(unsigned int*)(GPIOC_BASE+0x14)
#define GPIOC_LCKR *(unsigned int*)(GPIOC_BASE+0x18)
上述代碼中我們在後面備註了數字,下面對其進行簡單介紹下其功能:
1)定義外設的基地址,這個地址也是Block2 的基地址。
2)定義APB2 總線基地址,因為Block2 的第一個總線是APB1,而APB2 總
線地址只需要加上對應的地址偏移量即可。
3)定義GPIO 外設基地址,因為GPIOC 是掛接在APB2 總線上的,所以找到
對應的端口地址偏移量即可知道GPIOC 端口基地址。
4)定義GPIO 外設寄存器基地址,這裏以GPIOC 端口為例,因為GPIOC_CRL
是GPIOC 外設的第一個寄存器,所以基地址就是GPIOC 地址,其他寄存器地址只
需要在GPIOC 基地址上加上相應的偏移量即可。
我們得到了寄存器具體的地址,那麽就可以使用C 語言指針來操作讀寫。例
如我們需要GPIOC0 輸出一個低電平或者高電平,可以使用下面語句來操作。
//控制GPIOC 第0 管腳輸出一個低電平
GPIOC_BSRR = (0x01<<(16+0));
//控制GPIOC 第0 管腳輸出一個高電平
GPIOC_BSRR = (0x01<<0);
我們知道GPIOC_BSRR 的值是這個寄存器的地址,但是編譯器不知道它是地
址,而是把它當做立即數,所以我們必須要強制轉換為(unsigned int *)指針
類型才可以對其操作,這一點特別要註意。然後再在前面加上一個“*”作取指
針操作,表示對該地址內內容進行寫,讀操作也同樣使用“*”取指針操作。如
下:
unsigned int temp;
temp =GPIOC_IDR;
將寄存器內的數據保存在變量temp 中,使用到變量時一定要進行定義。
寄存器封裝
通過前面講解,我們已經可以對寄存器進行操作,但是還稍有不足,因為
STM32 的GPIO 比較多,我們不可能每使用一個GPIO 都做前面一樣的一大堆定義。
根據GPIO 寄存器的特點,我們知道不論GPIOA 還是GPIOB 等都擁有一組功能相
同的寄存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它們只是地址不一樣。
為了更方便地訪問寄存器,我們引入C 語言中的結構體對寄存器進行封裝,具
體代碼如下:
typedef unsigned int uint32_t; /*無符號32 位變量*/
typedef unsigned short int uint16_t; /*無符號16 位變量*/
/* GPIO 寄存器列表*/
typedef struct
{
uint32_t CRL; /*GPIO 端口配置低寄存器地址偏移: 0x00 */
uint32_t CRH; /*GPIO 端口配置高寄存器地址偏移: 0x04 */
uint32_t IDR; /*GPIO 數據輸入寄存器地址偏移: 0x08 */
uint32_t ODR; /*GPIO 數據輸出寄存器地址偏移: 0x0C */
uint32_t BSRR; /*GPIO 位設置/清除寄存器地址偏移: 0x10 */
uint32_t BRR; /*GPIO 端口位清除寄存器地址偏移: 0x14 */
普中STM32F1xx 開發攻略
www.prechin.cn
43
uint16_t LCKR; /*GPIO 端口配置鎖定寄存器地址偏移: 0x18 */
}GPIO_TypeDef;
這段代碼用typedef 關鍵字聲明了名為GPIO_TypeDef 的結構體類型,結
構體內有7 個成員變量,變量名正好對應寄存器的名字。C 語言的語法規定,
結構體內變量的存儲空間是連續的,其中32 位的變量占用4 個字節,16 位的變
量占用2 個字節。
也就是說,我們定義的這個GPIO_TypeDef ,假如這個結構體的首地址為
0x4001 1000(這也是第一個成員變量CRL 的地址),那麽結構體中第二個成員
變量CRH 的地址即為0x4001 1000 +0x04 ,加上的這個0x04 ,正是代表CRH
所占用的4 個字節地址的偏移量,其它成員變量相對於結構體首地址的偏移,
在上述代碼右側註釋已給出。
這樣的地址偏移與STM32 GPIO 外設定義的寄存器地址偏移一一對應,只要
給結構體設置好首地址,就能把結構體內成員的地址確定下來,然後就能以結構
體的形式訪問寄存器了,比如我們還是將GPIOC0 輸出低電平,具體代碼如下:
GPIO_TypeDef * GPIOx; //定義一個GPIO_TypeDef 型結構體指針GPIOx
GPIOx = GPIOC_BASE; //把指針地址設置為宏GPIOC_BASE 地址
GPIOx->BSRR =(1<<(16+0)); //通過指針訪問並修改GPIOC_BSRR 寄存器
這段代碼先用GPIO_TypeDef 類型定義一個結構體指針GPIOx,並讓指針指
向GPIOC 基地址GPIOC_BASE,地址確定下來,然後根據C 語言訪問結構體的內
容,用GPIOx->BSRR 寫寄存器。為了操作更簡便靈活,我們直接使用宏定義好
GPIO_TypeDef 類型的指針,而且指針指向各個GPIO 端口的首地址,使用時我
們直接用該宏訪問寄存器即可。具體代碼如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
GPIOC->BSRR = (1<<(16+0));
我們這裏僅僅以GPIO 這個外設為例,給大家講解了如何使用C 語言對寄存
器封裝,對於其他的外設也是使用同樣方法。其實到了後面的實驗程序的編寫,
我們都是使用ST 公司提供的固件庫,他們把STM32 所有外設都已經封裝好了,
我們只需要調用即可。我們這裏分析這個封裝過程只是想讓大家更加清楚理解如
何使用C 來封裝寄存器的。
嵌入式C語言開發---存儲器與寄存器