STM32入門系列-使用C語言封裝暫存器
前面文章介紹了儲存器對映、暫存器和暫存器對映,這些都是為了介紹使用 C語言封裝暫存器做鋪墊。這裡我們通過一個例項來對 C 語言封裝暫存器進行介紹。
具體例項:控制 GPIOC 埠的第 0 管腳輸出一個低電平。首先我們需要知道GPIOC 埠外設是掛接在哪個總線上的,然後根據匯流排基地址和本身的偏移地址得到 GPIOC 外設基地址,最後通過這個外設基地址得到裡面各種暫存器基地址。
匯流排和外設基地址封裝
根據暫存器的概念,我們可以使用 C 語言中的巨集定義對暫存器進行定義。具體程式碼如下:
1 //定義外設基地址 2 3 #define PERIPH_BASE ((unsigned int)0x40000000) 1) 45 //定義 APB2 匯流排基地址 6 7 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000) 2) 8 9 //定義 GPIOC 外設基地址 10 11 #define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800) 3) 12 13 //定義暫存器基地址 這裡以 GPIOC 為例 14 15 #define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00) 4) 16 17 #define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04) 1819 #define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08) 20 21 #define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C) 22 23 #define GPIOC_BSRR *(unsigned int*)(GPIOC_BASE+0x10) 24 25 #define GPIOC_BRR *(unsigned int*)(GPIOC_BASE+0x14) 26 27 #define GPIOC_LCKR *(unsigned int*)(GPIOC_BASE+0x18)
上述程式碼中我們在後面備註了數字,下面對其進行簡單介紹下其功能:
-
定義外設的基地址,這個地址也是 Block2 的基地址。
-
定義 APB2 匯流排基地址,因為 Block2 的第一個匯流排是 APB1,而 APB2 匯流排地址只需要加上對應的地址偏移量即可。
-
定義 GPIO 外設基地址,因為 GPIOC 是掛接在 APB2 總線上的,所以找到對應的埠地址偏移量即可知道 GPIOC 埠基地址。
-
定義 GPIO 外設暫存器基地址,這裡以 GPIOC 埠為例,因GPIOC_CRL是 GPIOC 外設的第一個暫存器,所以基地址就是 GPIOC 地址,其他暫存器地址只需要在 GPIOC 基地址上加上相應的偏移量即可。
我們得到了暫存器具體的地址,那麼就可以使用 C 語言指標來操作讀寫。例如我們需要 GPIOC0 輸出一個低電平或者高電平,可以使用下面語句來操作。
1 //控制 GPIOC 第 0 管腳輸出一個低電平 2 3 GPIOC_BSRR = (0x01<<(16+0)); 4 5 //控制 GPIOC 第 0 管腳輸出一個高電平 6 7 GPIOC_BSRR = (0x01<<0);
我們知道 GPIOC_BSRR 的值是這個暫存器的地址,但是編譯器不知道它是地址,而是把它當做立即數,所以我們必須要強制轉換為(unsigned int *)指標型別才可以對其操作,這一點特別要注意。然後再在前面加上一個“*”作取指標操作,表示對該地址內內容進行寫,讀操作也同樣使用“*”取指標操作。如下:
1 unsigned int temp; 2 temp =GPIOC_IDR;
將暫存器內的資料儲存在變數 temp 中,使用到變數時一定要進行定義。
暫存器封裝
通過前面講解,我們已經可以對暫存器進行操作,但是還稍有不足,因為STM32的GPIO比較多, 我們不可能每使用一個GPIO都做前面一樣的一大堆定義。根據GPIO暫存器的特點,我們知道不論GPIOA還是GPIOB等都擁有一組功能相同的暫存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR等等,它們只是地址不一樣。
為了更方便地訪問暫存器,我們引入C語言中的結構體對暫存器進行封裝,具體程式碼如下:
1 typedef unsigned int uint32_t; /*無符號 32 位變數*/ 2 3 typedef unsigned short int uint16_t; /*無符號 16 位變數*/ 4 5 /* GPIO 暫存器列表 */ 6 7 typedef struct 8 9 { 10 11 uint32_t CRL; /*GPIO 埠配置低暫存器 地址偏移: 0x00 */ 12 13 uint32_t CRH; /*GPIO 埠配置高暫存器 地址偏移: 0x04 */ 14 15 uint32_t IDR; /*GPIO 資料輸入暫存器 地址偏移: 0x08 */ 16 17 uint32_t ODR; /*GPIO 資料輸出暫存器 地址偏移: 0x0C */ 18 19 uint32_t BSRR; /*GPIO 位設定/清除暫存器 地址偏移: 0x10 */ 20 21 uint32_t BRR; /*GPIO 埠位清除暫存器 地址偏移: 0x14 */ 22 23 uint16_t LCKR; /*GPIO 埠配置鎖定暫存器 地址偏移: 0x18 */ 24 25 }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輸出低電平,具體程式碼如下:
1 GPIO_TypeDef * GPIOx; //定義一個GPIO_TypeDef型結構體指標GPIOx 2 3 GPIOx = GPIOC_BASE; //把指標地址設定為巨集 GPIOC_BASE 地址 4 5 GPIOx->BSRR =(1<<(16+0)); //通過指標訪問並修改 GPIOC_BSRR 暫存器
這段程式碼先用GPIO_TypeDef型別定義一個結構體指標GPIOx,並讓指標指向GPIOC基地址GPIOC_BASE,地址確定下來,然後根據C語言訪問結構體的內容,用GPIOx->BSRR寫暫存器。為了操作更簡便靈活,我們直接使用巨集定義好GPIO_TypeDef型別的指標,而且指標指向各個GPIO埠的首地址,使用時我們直接用該巨集訪問暫存器即可。具體程式碼如下:
1 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 2 3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) 4 5 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) 6 7 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) 8 9 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) 10 11 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) 12 13 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE) 14 15 GPIOC->BSRR = (1<<(16+0));
我們這裡僅僅以GPIO這個外設為例,給大家講解了如何使用C語言對暫存器封裝,對於其他的外設也是使用同樣方法。其實到了後面的實驗程式的編寫,我們都是使用ST公司提供的韌體庫,他們把STM32所有外設都已經封裝好了,我們只需要呼叫即可。 我們這裡分析這個封裝過程只是想讓大家更加清楚理解如何使用C來封裝暫存器的。