第8章 自己寫庫—構建庫函數雛形
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載:www.firebbs.cn
野火視頻教程優酷觀看網址:http://i.youku.com/firege
本章參考資料:《STM32F4xx 中文參考手冊》、《STM32F429規格書》
雖然我們上面用寄存器點亮了 LED,乍看一下好像代碼也很簡單,但是我們別僥幸以后就可以一直用寄存器開發。在用寄存器點亮 LED 的時候,我們會發現 STM32 的寄存器都是 32 位的,每次配置的時候都要對照着《STM32F4xx參考手冊》中寄存器的說明,然后根據說明對每個控制的寄存器位寫入特定參數,因此在配置的時候非常容易出錯,而且代碼還很不好理解,不便於維護。所以學習 STM32 最好的方法是用軟件庫,然后在軟件庫的基礎上了解底層,學習遍所有寄存器。
8.1 什么是STM32函數庫
以上所說的軟件庫是指"STM32標准函數庫",它是由ST公司針對STM32提供的函數接口,即API (Application Program Interface),開發者可調用這些函數接口來配置STM32的寄存器,使開發人員得以脫離最底層的寄存器操作,有開發快速,易於閱讀,維護成本低等優點。
當我們調用庫API的時候不需要挖空心思去了解庫底層的寄存器操作,就像當年我們剛開始學習C語言的時候,用prinft()函數時只是學習它的使用格式,並沒有去研究它的源碼實現,但需要深入研究的時候,經過千錘百煉的庫API源碼就是最佳學習范例。
實際上,庫是架設在寄存器與用戶驅動層之間的代碼,向下處理與寄存器直接相關的配置,向上為用戶提供配置寄存器的接口。庫開發方式與直接配置寄存器方式的區別見圖 81。
圖 81 開發方式對比圖
8.2
為什么采用庫來開發及學習?
在以前8位機時代的程序開發中,一般直接配置芯片的寄存器,控制芯片的工作方式,如中斷,定時器等。配置的時候,常常要查閱寄存器表,看用到哪些配置位,為了配置某功能,該置1還是置0。這些都是很瑣碎的、機械的工作,因為8位機的軟件相對來說較簡單,而且資源很有限,所以可以直接配置寄存器的方式來開發。
對於STM32,因為外設資源豐富,帶來的必然是寄存器的數量和復雜度的增加,這時直接配置寄存器方式的缺陷就突顯出來了:
(1) 開發速度慢
(2) 程序可讀性差
(3) 維護復雜
這些缺陷直接影響了開發效率,程序維護成本,交流成本。庫開發方式則正好彌補了這些缺陷。
而堅持采用直接配置寄存器的方式開發的程序員,會列舉以下原因:
(1) 具體參數更直觀
(2) 程序運行占用資源少
相對於庫開發的方式,直接配置寄存器方式生成的代碼量的確會少一點,但因為STM32有充足的資源,權衡庫的優勢與不足,絕大部分時候,我們願意犧牲一點CPU資源,選擇庫開發。一般只有在對代碼運行時間要求極苛刻的地方,才用直接配置寄存器的方式代替,如頻繁調用的中斷服務函數。
對於庫開發與直接配置寄存器的方式,就好比編程是用匯編好還是用 C 好一樣。在STM32F1系列剛推出函數庫時引起程序員的激烈爭論,但是,隨着ST庫的完善與大家對庫的了解,更多的程序員選擇了庫開發。現在STM32F1系列和STM32F4系列各有一套自己的函數庫,但是它們大部分是兼容的,F1和F4之間的程序移植,只需要小修改即可。而如果要移植用寄存器寫的程序,我只想說:"呵呵"。
用庫來進行開發,市場已有定論,用戶群說明了一切,但對於STM32的學習仍然有人認為用寄存器好,而且匯編不是還沒退出大學教材么?認為這種方法直觀,能夠了解到是配置了哪些寄存器,怎樣配置寄存器。事實上,庫函數的底層實現恰恰是直接配置寄存器方式的最佳例子,它代替我們完成了寄存器配置的工作,而想深入了解芯片是如何工作的話,只要直接查看庫函數的最底層實現就能理解,相信你會為它嚴謹、優美的實現方式而陶醉,要想修煉C語言,就從ST的庫開始吧。所以在以后的章節中,使用軟件庫是我們的重點,而且我們通過講解庫API去高效地學習STM32的寄存器,並不至於因為用庫學習,就不會用寄存器控制STM32芯片。
8.3 實驗:構建庫函數雛形
雖然庫的優點多多,但很多人對庫還是很忌憚,因為一開始用庫的時候有很多代碼,很多文件,不知道如何入手。不知道您是否認同這么一句話:一切的恐懼都來源於認知的空缺。我們對庫忌憚那是因為我們不知道什么是庫,不知道庫是怎么實現的。
接下來,我們在寄存器點亮 LED 的代碼上繼續完善,把代碼一層層封裝,實現庫的最初的雛形,相信經過這一步的學習后,您對庫的運用會游刃有余。這里我們只講如何實現GPIO函數庫,其他外設的我們直接參考ST標准庫學習即可,不必自己寫。
下面請打開本章配套例程"構建庫函數雛形"來閱讀理解,該例程是在上一章的基礎上修改得來的。
8.3.1 修改寄存器地址封裝
上一章中我們在操作寄存器的時候,操作的是都寄存器的絕對地址,如果每個外設寄存器都這樣操作,那將非常麻煩。我們考慮到外設寄存器的地址都是基於外設基地址的偏移地址,都是在外設基地址上逐個連續遞增的,每個寄存器占 32 個或者 16 個字節,這種方式跟結構體里面的成員類似。所以我們可以定義一種外設結構體,結構體的地址等於外設的基地址,結構體的成員等於寄存器,成員的排列順序跟寄存器的順序一樣。這樣我們操作寄存器的時候就不用每次都找到絕對地址,只要知道外設的基地址就可以操作外設的全部寄存器,即操作結構體的成員即可。
在工程中的"stm32f4xx.h"文件中,我們使用結構體封裝GPIO及RCC外設的的寄存器,見代碼清單 81。結構體成員的順序按照寄存器的偏移地址從低到高排列,成員類型跟寄存器類型一樣。如不理解C語言對寄存器的封的語法原理,請參考《C語言對寄存器的封裝》小節。
代碼清單 81 封裝寄存器列表
1 //volatile表示易變的變量,防止編譯器優化
2 #define __IO volatile
3 typedef unsigned int uint32_t;
4 typedef unsigned short uint16_t;
5
6 /* GPIO寄存器列表 */
7 typedef struct {
8 __IO uint32_t MODER; /*GPIO模式寄存器地址偏移: 0x00 */
9 __IO uint32_t OTYPER; /*GPIO輸出類型寄存器地址偏移: 0x04 */
10 __IO uint32_t OSPEEDR; /*GPIO輸出速度寄存器地址偏移: 0x08 */
11 __IO uint32_t PUPDR; /*GPIO上拉/下拉寄存器地址偏移: 0x0C */
12 __IO uint32_t IDR; /*GPIO輸入數據寄存器地址偏移: 0x10 */
13 __IO uint32_t ODR; /*GPIO輸出數據寄存器地址偏移: 0x14 */
14 __IO uint16_t BSRRL; /*GPIO置位/復位寄存器低16位部分地址偏移: 0x18 */
15 __IO uint16_t BSRRH; /*GPIO置位/復位寄存器高16位部分地址偏移: 0x1A */
16 __IO uint32_t LCKR; /*GPIO配置鎖定寄存器地址偏移: 0x1C */
17 __IO uint32_t AFR[2]; /*GPIO復用功能配置寄存器地址偏移: 0x20-0x24 */
18 } GPIO_TypeDef;
19
20 /*RCC寄存器列表*/
21 typedef struct {
22 __IO uint32_t CR; /*!< RCC 時鍾控制寄存器,地址偏移: 0x00 */
23 __IO uint32_t PLLCFGR; /*!< RCC PLL配置寄存器,地址偏移: 0x04 */
24 __IO uint32_t CFGR; /*!< RCC 時鍾配置寄存器,地址偏移: 0x08 */
25 __IO uint32_t CIR; /*!< RCC 時鍾中斷寄存器,地址偏移: 0x0C */
26 __IO uint32_t AHB1RSTR; /*!< RCC AHB1 外設復位寄存器,地址偏移: 0x10 */
27 __IO uint32_t AHB2RSTR; /*!< RCC AHB2 外設復位寄存器,地址偏移: 0x14 */
28 __IO uint32_t AHB3RSTR; /*!< RCC AHB3 外設復位寄存器,地址偏移: 0x18 */
29 __IO uint32_t RESERVED0; /*!< 保留, 地址偏移:0x1C */
30 __IO uint32_t APB1RSTR; /*!< RCC APB1 外設復位寄存器,地址偏移: 0x20 */
31 __IO uint32_t APB2RSTR; /*!< RCC APB2 外設復位寄存器,地址偏移: 0x24*/
32 __IO uint32_t RESERVED1[2]; /*!< 保留,地址偏移:0x28-0x2C*/
33 __IO uint32_t AHB1ENR; /*!< RCC AHB1 外設時鍾寄存器,地址偏移: 0x30 */
34 __IO uint32_t AHB2ENR; /*!< RCC AHB2 外設時鍾寄存器,地址偏移: 0x34 */
35 __IO uint32_t AHB3ENR; /*!< RCC AHB3 外設時鍾寄存器,地址偏移: 0x38 */
36 /*RCC后面還有很多寄存器,此處省略*/
37 } RCC_TypeDef;
這段代碼在每個結構體成員前增加了一個"__IO"前綴,它的原型在這段代碼的第一行,代表了C語言中的關鍵字"volatile",在C語言中該關鍵字用於表示變量是易變的,要求編譯器不要優化。這些結構體內的成員,都代表着寄存器,而寄存器很多時候是由外設或STM32芯片狀態修改的,也就是說即使CPU不執行代碼修改這些變量,變量的值也有可能被外設修改、更新,所以每次使用這些變量的時候,我們都要求CPU去該變量的地址重新訪問。若沒有這個關鍵字修飾,在某些情況下,編譯器認為沒有代碼修改該變量,就直接從CPU的某個緩存獲取該變量值,這時可以加快執行速度,但該緩存中的是陳舊數據,與我們要求的寄存器最新狀態可能會有出入。
8.3.2 定義訪問外設的結構體指針
以結構體的形式定義好了外設寄存器后,使用結構體前還需要給結構體的首地址賦值,才能訪問到需要的寄存器。為方便操作,我們給每個外設都定義好指向它地址的結構體指針,見代碼清單 82。
代碼清單 82 指向外設首地址的結構體指針
1 /*定義GPIOA-H 寄存器結構體指針*/
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 /*定義RCC外設寄存器結構體指針*/
12 #define RCC ((RCC_TypeDef *) RCC_BASE)
這些宏通過強制把外設的基地址轉換成GPIO_TypeDef類型的地址,從而得到GPIOA、GPIOB等直接指向對應外設的指針,通過結構體的指針操作,即可訪問對應外設的寄存器。
利用這些指針訪問寄存器,我們把main文件里對應的代碼修改掉,見代碼清單 83。
代碼清單 83 使用結構體指針方式控制LED燈
1 /**
2 * 主函數
3 */
4 int main(void)
5 {
6
7 RCC->AHB1ENR |= (1<<7);
8
9 /* LED 端口初始化 */
10
11 /*GPIOH MODER10清空*/
12 GPIOH->MODER &= ~( 0x03<< (2*10));
13 /*PH10 MODER10 = 01b 輸出模式*/
14 GPIOH->MODER |= (1<<2*10);
15
16 /*GPIOH OTYPER10清空*/
17 GPIOH->OTYPER &= ~(1<<1*10);
18 /*PH10 OTYPER10 = 0b 推挽模式*/
19 GPIOH->OTYPER |= (0<<1*10);
20
21 /*GPIOH OSPEEDR10清空*/
22 GPIOH->OSPEEDR &= ~(0x03<<2*10);
23 /*PH10 OSPEEDR10 = 0b 速率2MHz*/
24 GPIOH->OSPEEDR |= (0<<2*10);
25
26 /*GPIOH PUPDR10清空*/
27 GPIOH->PUPDR &= ~(0x03<<2*10);
28 /*PH10 PUPDR10 = 01b 上拉模式*/
29 GPIOH->PUPDR |= (1<<2*10);
30
31 /*PH10 BSRR寄存器的 BR10置1,使引腳輸出低電平*/
32 GPIOH->BSRRH |= (1<<10);
33
34 /*PH10 BSRR寄存器的 BS10置1,使引腳輸出高電平*/
35 //GPIOH->BSRRL |= (1<<10);
36
37 while (1);
38
39 }
乍一看,除了最后一部分,把BSRR寄存器分成BSRRH和BSRRL兩段,其它部分跟直接用絕對地址訪問只是名字改了而已,用起來跟上一章沒什么區別。這是因為我們現在只實現了庫函數的基礎,還沒有定義庫函數。
打好了地基,下面我們就來建高樓。接下來使用函數來封裝GPIO的基本操作,方便以后應用的時候不需要再查詢寄存器,而是直接通過調用這里定義的函數來實現。我們把針對GPIO外設操作的函數及其宏定義分別存放在"stm32f4xx_gpio.c"和"stm32f4xx_gpio.h"文件中。
定義位操作函數
在"stm32f4xx_gpio.c"文件定義兩個位操作函數,分別用於控制引腳輸出高電平和低電平,見代碼清單 84。
代碼清單 84 GPIO置位函數與復位函數的定義
1 /**
2 *函數功能:設置引腳為高電平
3 *參數說明:GPIOx:該參數為GPIO_TypeDef類型的指針,指向GPIO端口的地址
4 * GPIO_Pin:選擇要設置的GPIO端口引腳,可輸入宏GPIO_Pin_0-15,
5 * 表示GPIOx端口的0-15號引腳。
6 */
7 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
8 {
9 /*設置GPIOx端口BSRRL寄存器的第GPIO_Pin位,使其輸出高電平*/
10 /*因為BSRR寄存器寫0不影響,
11 宏GPIO_Pin只是對應位為1,其它位均為0,所以可以直接賦值*/
12
13 GPIOx->BSRRL = GPIO_Pin;
14 }
15
16 /**
17 *函數功能:設置引腳為低電平
18 *參數說明:GPIOx:該參數為GPIO_TypeDef類型的指針,指向GPIO端口的地址
19 * GPIO_Pin:選擇要設置的GPIO端口引腳,可輸入宏GPIO_Pin_0-15,
20 * 表示GPIOx端口的0-15號引腳。
21 */
22 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
23 {
24 /*設置GPIOx端口BSRRH寄存器的第GPIO_Pin位,使其輸出低電平*/
25 /*因為BSRR寄存器寫0不影響,
26 宏GPIO_Pin只是對應位為1,其它位均為0,所以可以直接賦值*/
27
28 GPIOx->BSRRH = GPIO_Pin;
29 }
這兩個函數體內都是只有一個語句,對GPIOx的BSRRL或BSRRH寄存器賦值,從而設置引腳為高電平或低電平。其中GPIOx是一個指針變量,通過函數的輸入參數我們可以修改它的值,如給它賦予GPIOA、GPIOB、GPIOH等結構體指針值,這個函數就可以控制相應的GPIOA、GPIOB、GPIOH等端口的輸出。
對比我們前面對BSRR寄存器的賦值,都是用"|="操作來防止對其它數據位產生干擾的,為何此函數里的操作卻直接用"="號賦值,這樣不怕干擾其它數據位嗎?見代碼清單 85。
代碼清單 85 賦值方式對比
1 /*使用 "|=" 來賦值*/
2 GPIOH->BSRRH |= (1<<10);
3 /*直接使用 "=" 號賦值*/
4 GPIOx->BSRRH = GPIO_Pin;
根據BSRR寄存器的特性,對它的數據位寫"0",是不會影響輸出的,只有對它的數據位寫"1",才會控制引腳輸出。對低16位寫"1"輸出高電平,對高16位寫"1"輸出低電平。也就是說,假如我們對BSRRH(高16位)直接用"="操作賦二進制值"0000 0000 0000 0001 b",它會控制GPIO的引腳0輸出低電平,賦二進制值"0000 0000 0001 0000 b",它會控制GPIO引腳4輸出低電平,而其它數據位由於是0,所以不會受到干擾。同理,對BSRRL(低16位)直接賦值也是如此,數據位為1的位輸出高電平。代碼清單 86 中的兩種方式賦值,功能相同。
代碼清單 86 BSRR寄存器賦值等效代碼
1 /*使用 "|=" 來賦值*/
2 GPIOH->BSRRH |= (uint16_t)(1<<10);
3 /*直接使用"=" 來賦值,二進制數(0000 0100 0000 0000)*/
4 GPIOH->BSRRH = (uint16_t)(1<<10);
這兩行代碼功能等效,都把BSRRH的bit10設置為1,控制引腳10輸出低電平,且其它引腳狀態不變。但第二個語句操作效率是比較高的,因為"|="號包含了讀寫操作,而"="號只需要一個寫操作。因此在定義位操作函數中我們使用后者。
利用這兩個位操作函數,就可以方便地操作各種GPIO的引腳電平了,控制各種端口引腳的范例見代碼清單 87。
代碼清單 87 位操作函數使用范例
1
2 /*控制GPIOH的引腳10輸出高電平*/
3 GPIO_SetBits(GPIOH,(uint16_t)(1<<10));
4 /*控制GPIOH的引腳10輸出低電平*/
5 GPIO_ResetBits(GPIOH,(uint16_t)(1<<10));
6
7 /*控制GPIOH的引腳10、引腳11輸出高電平,使用"|"同時控制多個引腳*/
8 GPIO_SetBits(GPIOH,(uint16_t)(1<<10)|(uint16_t)(1<<11));
9 /*控制GPIOH的引腳10、引腳11輸出低電平*/
10 GPIO_ResetBits(GPIOH,(uint16_t)(1<<10)|(uint16_t)(1<<10));
11
12 /*控制GPIOA的引腳8輸出高電平*/
13 GPIO_SetBits(GPIOA,(uint16_t)(1<<8));
14 /*控制GPIOB的引腳9輸出低電平*/
15 GPIO_ResetBits(GPIOB,(uint16_t)(1<<9));
使用以上函數輸入參數,設置引腳號時,還是稍感不便,為此我們把表示16個引腳的操作數都定義成宏,見代碼清單 88。
代碼清單 88 選擇引腳參數的宏
1 /*GPIO引腳號定義*/
2 #define GPIO_Pin_0 (uint16_t)0x0001) /*!< 選擇Pin0 (1<<0) */
3 #define GPIO_Pin_1 ((uint16_t)0x0002) /*!< 選擇Pin1 (1<<1)*/
4 #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< 選擇Pin2 (1<<2)*/
5 #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< 選擇Pin3 (1<<3)*/
6 #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< 選擇Pin4 */
7 #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< 選擇Pin5 */
8 #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< 選擇Pin6 */
9 #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< 選擇Pin7 */
10 #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< 選擇Pin8 */
11 #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< 選擇Pin9 */
12 #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< 選擇Pin10 */
13 #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< 選擇Pin11 */
14 #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< 選擇Pin12 */
15 #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< 選擇Pin13 */
16 #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< 選擇Pin14 */
17 #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< 選擇Pin15 */
18 #define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< 選擇全部引腳 */
這些宏代表的參數是某位置"1"其它位置"0"的數值,其中最后一個"GPIO_Pin_ALL"是所有數據位都為"1",所以用它可以一次控制設置整個端口的0-15所有引腳。利用這些宏, GPIO的控制代碼可改為代碼清單 89。
代碼清單 89 使用位操作函數及宏控制GPIO
1
2 /*控制GPIOH的引腳10輸出高電平*/
3 GPIO_SetBits(GPIOH,GPIO_Pin_10);
4 /*控制GPIOH的引腳10輸出低電平*/
5 GPIO_ResetBits(GPIOH,GPIO_Pin_10);
6
7 /*控制GPIOH的引腳10、引腳11輸出高電平,使用"|",同時控制多個引腳*/
8 GPIO_SetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11);
9 /*控制GPIOH的引腳10、引腳11輸出低電平*/
10 GPIO_ResetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11);
11 /*控制GPIOH的所有輸出低電平*/
12 GPIO_ResetBits(GPIOH,GPIO_Pin_ALL);
13
14 /*控制GPIOA的引腳8輸出高電平*/
15 GPIO_SetBits(GPIOA,GPIO_Pin_8);
16 /*控制GPIOB的引腳9輸出低電平*/
17 GPIO_ResetBits(GPIOB,GPIO_Pin_9);
使用以上代碼控制GPIO,我們就不需要再看寄存器了,直接從函數名和輸入參數就可以直觀看出這個語句要實現什么操作。(英文中"Set"表示"置位",即高電平,"Reset"表示"復位",即低電平)
8.3.3 定義初始化結構體GPIO_InitTypeDef
定義位操作函數后,控制GPIO輸出電平的代碼得到了簡化,但在控制GPIO輸出電平前還需要初始化GPIO引腳的各種模式,這部分代碼涉及的寄存器有很多,我們希望初始化GPIO也能以如此簡單的方法去實現。為此,我們先根據GPIO初始化時涉及到的初始化參數以結構體的形式封裝起來,聲明一個名為GPIO_InitTypeDef的結構體類型,見代碼清單 810。
代碼清單 810 定義GPIO初始化結構體
1 typedef uint8_t unsigned char;
2 /**
3 * GPIO初始化結構體類型定義
4 */
5 typedef struct {
6 uint32_t GPIO_Pin; /*!< 選擇要配置的GPIO引腳
7 可輸入 GPIO_Pin_ 定義的宏 */
8
9 uint8_t GPIO_Mode; /*!< 選擇GPIO引腳的工作模式
10 可輸入二進制值: 00 、01、 10、 11
11 表示輸入/輸出/復用/模擬 */
12
13 uint8_t GPIO_Speed; /*!< 選擇GPIO引腳的速率
14 可輸入二進制值: 00 、01、 10、 11
15 表示2/25/50/100MHz */
16
17 uint8_t GPIO_OType; /*!< 選擇GPIO引腳輸出類型
18 可輸入二進制值: 0 、1
19 表示推挽/開漏 */
20
21 uint8_t GPIO_PuPd; /*!<選擇GPIO引腳的上/下拉模式
22 可輸入二進制值: 00 、01、 10
23 表示浮空/上拉/下拉*/
24 } GPIO_InitTypeDef;
這個結構體中包含了初始化GPIO所需要的信息,包括引腳號、工作模式、輸出速率、輸出類型以及上/下拉模式。設計這個結構體的思路是:初始化GPIO前,先定義一個這樣的結構體變量,根據需要配置GPIO的模式,對這個結構體的各個成員進行賦值,然后把這個變量作為"GPIO初始化函數"的輸入參數,該函數能根據這個變量值中的內容去配置寄存器,從而實現初始化GPIO。
8.3.4 定義引腳模式的枚舉類型
上面定義的結構體很直接,美中不足的是在對結構體中各個成員賦值時還需要看具體哪個模式對應哪個數值,如GPIO_Mode成員的"輸入/輸出/復用/模擬"模式對應二進制值"00 、01、 10、 11",我們不希望每次用到都要去查找這些索引值,所以使用C語言中的枚舉語法定義這些參數,見代碼清單 811。
代碼清單 811 GPIO配置參數的枚舉定義
1 /**
2 * GPIO端口配置模式的枚舉定義
3 */
4 typedef enum {
5 GPIO_Mode_IN = 0x00, /*!< 輸入模式 */
6 GPIO_Mode_OUT = 0x01, /*!< 輸出模式 */
7 GPIO_Mode_AF = 0x02, /*!< 復用模式 */
8 GPIO_Mode_AN = 0x03 /*!< 模擬模式 */
9 } GPIOMode_TypeDef;
10
11 /**
12 * GPIO輸出類型枚舉定義
13 */
14 typedef enum {
15 GPIO_OType_PP = 0x00, /*!< 推挽模式 */
16 GPIO_OType_OD = 0x01 /*!< 開漏模式 */
17 } GPIOOType_TypeDef;
18
19 /**
20 * GPIO輸出速率枚舉定義
21 */
22 typedef enum {
23 GPIO_Speed_2MHz = 0x00, /*!< 2MHz */
24 GPIO_Speed_25MHz = 0x01, /*!< 25MHz */
25 GPIO_Speed_50MHz = 0x02, /*!< 50MHz */
26 GPIO_Speed_100MHz = 0x03 /*!<100MHz */
27 } GPIOSpeed_TypeDef;
28
29 /**
30 *GPIO上/下拉配置枚舉定義
31 */
32 typedef enum {
33 GPIO_PuPd_NOPULL = 0x00,/*浮空*/
34 GPIO_PuPd_UP = 0x01, /*上拉*/
35 GPIO_PuPd_DOWN = 0x02 /*下拉*/
36 } GPIOPuPd_TypeDef;
有了這些枚舉定義,我們的GPIO_InitTypeDef結構體也可以使用枚舉類型來限定輸入了,代碼清單 813。
代碼清單 812 使用枚舉類型定義的GPIO_InitTypeDef結構體成員
1 /**
2 * GPIO初始化結構體類型定義
3 */
4 typedef struct {
5 uint32_t GPIO_Pin; /*!< 選擇要配置的GPIO引腳
6 可輸入 GPIO_Pin_ 定義的宏 */
7
8 GPIOMode_TypeDef GPIO_Mode; /*!< 選擇GPIO引腳的工作模式
9 可輸入 GPIOMode_TypeDef 定義的枚舉值*/
10
11 GPIOSpeed_TypeDef GPIO_Speed; /*!< 選擇GPIO引腳的速率
12 可輸入 GPIOSpeed_TypeDef 定義的枚舉值 */
13
14 GPIOOType_TypeDef GPIO_OType; /*!< 選擇GPIO引腳輸出類型
15 可輸入 GPIOOType_TypeDef 定義的枚舉值*/
16
17 GPIOPuPd_TypeDef GPIO_PuPd; /*!<選擇GPIO引腳的上/下拉模式
18 可輸入 GPIOPuPd_TypeDef 定義的枚舉值*/
19 } GPIO_InitTypeDef;
如果不使用枚舉類型,仍使用"uint8_t"類型來定義結構體成員,那么成員值的范圍就是0-255了,而實際上這些成員都只能輸入幾個數值。所以使用枚舉類型可以對結構體成員起到限定輸入的作用,只能輸入相應已定義的枚舉值。
利用這些枚舉定義,給GPIO_InitTypeDef結構體類型賦值配置就非常直觀了,范例見代碼清單 813。
代碼清單 813 給GPIO_InitTypeDef初始化結構體賦值范例
1 GPIO_InitTypeDef InitStruct;
2
3 /* LED 端口初始化 */
4 /*選擇要控制的GPIO引腳*/
5 InitStruct.GPIO_Pin = GPIO_Pin_10;
6 /*設置引腳模式為輸出模式*/
7 InitStruct.GPIO_Mode = GPIO_Mode_OUT;
8 /*設置引腳的輸出類型為推挽輸出*/
9 InitStruct.GPIO_OType = GPIO_OType_PP;
10 /*設置引腳為上拉模式*/
11 InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
12 /*設置引腳速率為2MHz */
13 InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
8.3.5 定義GPIO初始化函數
接着前面的思路,對初始化結構體賦值后,把它輸入到GPIO初始化函數,由它來實現寄存器配置。我們的GPIO初始化函數實現見代碼清單 814,
代碼清單 814 GPIO初始化函數
1
2 /**
3 *函數功能:初始化引腳模式
4 *參數說明:GPIOx,該參數為GPIO_TypeDef類型的指針,指向GPIO端口的地址
5 * GPIO_InitTypeDef:GPIO_InitTypeDef結構體指針,指向初始化變量
6 */
7 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
8 {
9 uint32_t pinpos = 0x00, pos = 0x00 , currentpin = 0x00;
10
11 /*-- GPIO Mode Configuration --*/
12 for (pinpos = 0x00; pinpos < 16; pinpos++) {
13 /*以下運算是為了通過 GPIO_InitStruct->GPIO_Pin 算出引腳號0-15*/
14
15 /*經過運算后pos的pinpos位為1,其余為0,與GPIO_Pin_x宏對應。
16 pinpos變量每次循環加1,*/
17 pos = ((uint32_t)0x01) << pinpos;
18
19 /* pos與GPIO_InitStruct->GPIO_Pin做 & 運算,
20 若運算結果currentpin == pos,
21 則表示GPIO_InitStruct->GPIO_Pin的pinpos位也為1,
22 從而可知pinpos就是GPIO_InitStruct->GPIO_Pin對應的引腳號:0-15*/
23 currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
24
25 /*currentpin == pos時執行初始化*/
26 if (currentpin == pos) {
27 /*GPIOx端口,MODER寄存器的GPIO_InitStruct->GPIO_Pin對應的引腳,
28 MODER位清空*/
29 GPIOx->MODER &= ~(3 << (2 *pinpos));
30
31 /*GPIOx端口,MODER寄存器的GPIO_Pin引腳,
32 MODER位設置"輸入/輸出/復用輸出/模擬"模式*/
33 GPIOx->MODER |= (((uint32_t)GPIO_InitStruct->GPIO_Mode) << (2 *pinpos));
34
35 /*GPIOx端口,PUPDR寄存器的GPIO_Pin引腳,
36 PUPDR位清空*/
37 GPIOx->PUPDR &= ~(3 << ((2 *pinpos)));
38
39 /*GPIOx端口,PUPDR寄存器的GPIO_Pin引腳,
40 PUPDR位設置"上/下拉"模式*/
41 GPIOx->PUPDR |= (((uint32_t)GPIO_InitStruct->GPIO_PuPd) << (2 *pinpos));
42
43 /*若模式為"輸出/復用輸出"模式,則設置速度與輸出類型*/
44 if ((GPIO_InitStruct->GPIO_Mode == GPIO_Mode_OUT) ||
45 (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_AF)) {
46 /*GPIOx端口,OSPEEDR寄存器的GPIO_Pin引腳,
47 OSPEEDR位清空*/
48 GPIOx->OSPEEDR &= ~(3 << (2 *pinpos));
49 /*GPIOx端口,OSPEEDR寄存器的GPIO_Pin引腳,
50 OSPEEDR位設置輸出速度*/
51 GPIOx->OSPEEDR |= ((uint32_t)(GPIO_InitStruct->GPIO_Speed)<<(2 *pinpos));
52
53 /*GPIOx端口,OTYPER寄存器的GPIO_Pin引腳,
54 OTYPER位清空*/
55 GPIOx->OTYPER &= ~(1 << (pinpos)) ;
56 /*GPIOx端口,OTYPER位寄存器的GPIO_Pin引腳,
57 OTYPER位設置"推挽/開漏"輸出類型*/
58 GPIOx->OTYPER |= (uint16_t)(( GPIO_InitStruct->GPIO_OType)<< (pinpos));
59 }
60 }
61 }
62 }
這個函數有GPIOx和GPIO_InitStruct兩個輸入參數,分別是GPIO外設指針和GPIO初始化結構體指針。分別用來指定要初始化的GPIO端口及引腳的工作模式。
函數實現主要分兩個環節:
(1) 利用for循環,根據GPIO_InitStruct的結構體成員GPIO_Pin計算出要初始化的引腳號。這段看起來復雜的運算實際上可以這樣理解:它要通過宏"GPIO_Pin_x"的參數計算出x值(宏的參數值是第x數據位為1,其余為0,參考代碼清單 88),計算得的引腳號結果存儲在pinpos變量中。
(2) 得到引腳號pinpos后,利用初始化結構體各個成員的值,對相應寄存器進行配置,這部分與我們前面直接配置寄存器的操作是類似的,先對引腳號pinpos相應的配置位清空,后根據結構體成員對配置位賦值(GPIO_Mode成員對應MODER寄存器的配置,GPIO_PuPd成員對應PUPDR寄存器的配置等)。區別是這里的寄存器配置值及引腳號都是由變量存儲的。
8.3.6 全新面貌,使用函數點亮LED燈
完成以上的准備后,我們就可以自己定義的函數來點亮LED燈了,見代碼清單 815。
代碼清單 815 使用函數點亮LED燈
1
2 /*
3 使用寄存器的方法點亮LED燈
4 */
5 #include "stm32f4xx_gpio.h"
6
7 void Delay( uint32_t nCount);
8
9 /**
10 * 主函數,使用封裝好的函數來控制LED燈
11 */
12 int main(void)
13 {
14 GPIO_InitTypeDef InitStruct;
15
16 /*開啟 GPIOH 時鍾,使用外設時都要先開啟它的時鍾*/
17 RCC->AHB1ENR |= (1<<7);
18
19 /* LED 端口初始化 */
20
21 /*初始化PH10引腳*/
22 /*選擇要控制的GPIO引腳*/
23 InitStruct.GPIO_Pin = GPIO_Pin_10;
24 /*設置引腳模式為輸出模式*/
25 InitStruct.GPIO_Mode = GPIO_Mode_OUT;
26 /*設置引腳的輸出類型為推挽輸出*/
27 InitStruct.GPIO_OType = GPIO_OType_PP;
28 /*設置引腳為上拉模式*/
29 InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
30 /*設置引腳速率為2MHz */
31 InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
32 /*調用庫函數,使用上面配置的GPIO_InitStructure初始化GPIO*/
33 GPIO_Init(GPIOH, &InitStruct);
34
35 /*使引腳輸出低電平,點亮LED1*/
36 GPIO_ResetBits(GPIOH,GPIO_Pin_10);
37
38 /*延時一段時間*/
39 Delay(0xFFFFFF);
40
41 /*使引腳輸出高電平,關閉LED1*/
42 GPIO_SetBits(GPIOH,GPIO_Pin_10);
43
44 /*初始化PH11引腳*/
45 InitStruct.GPIO_Pin = GPIO_Pin_11;
46 GPIO_Init(GPIOH,&InitStruct);
47
48 /*使引腳輸出低電平,點亮LED2*/
49 GPIO_ResetBits(GPIOH,GPIO_Pin_11);
50
51 while (1);
52
53 }
54
55 //簡單的延時函數,讓cpu執行無意義指令,消耗時間
56 //具體延時時間難以計算,以后我們可使用定時器精確延時
57 void Delay( uint32_t nCount)
58 {
59 for (; nCount != 0; nCount--);
60 }
61
62 // 函數為空,目的是為了騙過編譯器不報錯
63 void SystemInit(void)
64 {
65 }
現在看起來,使用函數來控制LED燈與之前直接控制寄存器已經有了很大的區別:main函數中先定義了一個初始化結構體變量InitStruct,然后對該變量的各個成員按點亮LED燈所需要的GPIO配置模式進行賦值,賦值后,調用GPIO_Init函數,讓它根據結構體成員值對GPIO寄存器寫入控制參數,完成GPIO引腳初始化。控制電平時,直接使用GPIO_SetBits和GPIO_Resetbits函數控制輸出。如若對其它引腳進行不同模式的初始化,只要修改初始化結構體InitStruct的成員值,把新的參數值輸入到GPIO_Init函數再調用即可。
代碼中新增的Delay函數,主要功能是延時,讓我們可以看清楚實驗現象(不延時的話指令執行太快,肉眼看不出來),它的實現原理是讓CPU執行無意義的指令,消耗時間,在此不要糾結它的延時時間,寫一個大概輸入參數值,下載到實驗板實測,覺得太久了就把參數值改小,短了就改大即可。需要精確延時的時候我們會用STM32的定時器外設進行精確延時的。
8.3.7 下載驗證
把編譯好的程序下載到開發板並復位,可看到板子上的燈先亮紅色(LED1),后亮綠色(LED2)。
8.3.8 總結
什么是ST標准軟件庫?這就是。
我們從寄存器映像開始,把內存跟寄存器建立起一一對應的關系,然后操作寄存器點亮 LED,再把寄存器操作封裝成一個個函數。一步一步走來,我們實現了庫最簡單的雛形,如果我們不斷地增加操作外設的函數,並且把所有的外設都寫完,一個完整的庫就實現了。
本章中的GPIO相關庫函數及結構體定義,實際上都是從ST標准庫搬過來的。這樣分析它純粹是為了滿足自己的求知欲,學習其編程的方式、思想,這對提高我們的編程水平是很有好處的,順便感受一下ST庫設計的嚴謹性,我認為這樣的代碼不僅嚴謹且華麗優美,不知您是否也有這樣的感受。
與直接配置寄存器相比,從執行效率上看會有額外的消耗:初始化變量賦值的過程、庫函數在被調用的時候要耗費調用時間;在函數內部,對輸入參數轉換所需要的額外運算也消耗一些時間(如GPIO中運算求出引腳號時)。而其它的宏、枚舉等解釋操作是作編譯過程完成的,這部分並不消耗內核的時間。那么函數庫的優點呢?是我們可以快速上手STM32控制器;配置外設狀態時,不需要再糾結要向寄存器寫入什么數值;交流方便,查錯簡單。這就是我們選擇庫的原因。
現在的處理器的主頻是越來越高,我們不需要擔心CPU耗費那么多時間來干活會不會被累倒,庫主要應用是在初始化過程,而初始化過程一般是芯片剛上電或在核心運算之前的執行的,這段時間的等待是0.02us還是0.01us在很多時候並沒有什么區別。相對來說,我們還是擔心一下如果都用寄存器操作,每行代碼都要查《STM32F4xx規格書》中的說明,自己會不會被累倒吧。
在以后開發的工程中,一般不會去分析ST的庫函數的實現了。因為外設的庫函數是很類似的,庫外設都包含初始化結構體,以及特定的宏或枚舉標識符,這些封裝被庫函數這些轉化成相應的值,寫入到寄存器之中,函數內部的具體實現是十分枯燥和機械的工作。如果您有興趣,在您掌握了如何使用外設的庫函數之后,可以查看一下它的源碼實現。
通常我們只需要通過了解每種外設的"初始化結構體"就能夠通過它去了解STM32的外設功能及控制了。
8.4 每課一問
1. 閱讀《STM32F4xx參考手冊》及《STM32F4xx規格書》中關於USART外設(通用同步異步收發器)的寄存器說明及地址映射,參考"GPIO_TypeDef"的結構體聲明,封裝USART的寄存器成一個USART_TypeDef類型,並定義USART1、USART2、USART3外設的結構體訪問指針。
2. 參考GPIO_SetBits的函數實現,定義一個能讀取GPIO引腳狀態的函數,函數聲明:"uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)",要求能夠返回輸入參數GPIOx端口的 GPIO_Pin引腳的電平狀態,高電平時返回1,低電平返回0。