自學STM32 - 深入理解GPIO

2024年2月6日 23点热度 0人点赞

作者: junziyang


GPIO是General Purpose Input / Output的簡稱,意為通用輸入/輸出。GPIO端口是MCU芯片上負責信息(數據和控制信號)輸入/輸出的引腳的統稱,也稱為I/O引腳。之所以被稱為“通用”,是因為每個引腳的功能不是唯一的,而是可以通過配置相關寄存器進行設置的。初始化GPIO配置往往是開發MCU驅動程序的第一步。初始化GPIO配置的方法是配置相關的寄存器。STM32F1系列芯片的每組GPIO,都設有7個寄存器,包括:

  • 2個32位配置寄存器(GPIOx_CRL, GPIOx_CRH)
  • 2個32位數據寄存器(GPIOx_IDR, GPIOx_ODR)
  • 1個32位設置/復位寄存器(GPIOx_BSRR)
  • 1個16位復位寄存器(GPIOx_BRR)
  • 1個32位鎖定寄存器(GPIOx_LCKR)

寄存器的每個位可理解為一個MOS開關,配置為1開關閉合/斷開,配置為0開關斷開/閉合。

註:如非特別聲明,以下筆記內容均針對STM32F103ZET6而言。不同型號,細節可能存在差別。

一、 GPIO配置寄存器(Configuration Register)

GPIO配置寄存器是用來設置I/O引腳工作模式的寄存器。STM32的每組GPIO都有16個引腳,從Px0到Px15編號,例如GPIOA對應的引腳編號為PA0到PA15。每個引腳的配置需要4個位,其中2位配置引腳的功能,2位配置引腳的模式。這樣每組GPIO的16個引腳就需要2個32位的CR寄存器。其中,用於配置編號Px0-Px7引腳的寄存器,稱為GPIOx_CRL;用於配置編號Px8-Px15引腳的寄存器,稱為GPIOx_CRH。

1.1 工作原理

無論歸屬哪個CR寄存器配置,GPIO引腳的配置方法是相同的,隻是不同引腳對應的配置位在寄存器中的位置不同。以GPIOx_CRL寄存器為例,各引腳占位分配如圖1所示。

圖1. GPIOx_CRL寄存器引腳分配

每個引腳占用連續的4個位,其中高位的2個位為功能配置位(CNF: CoNFigure)),低位的2個位為工作模式設置位(MODE)。圖1的表格中,CNFy和MODEy中的y即為引腳編號,下方的rw表示該位可read和write。端口模式配位表如圖2所示:

圖2. 端口模式配位表

簡言之,當MODEy等於00時,y引腳為輸入模式,此時配置CNFy可以得到4種輸入模式:

  • 模擬輸入(Analog mode)
  • 浮空輸入(Floating input)
  • 輸入上拉(Input with pull-up)
  • 輸入下拉(Input with pull-down)

當MODEy不等於00時,y引腳為輸出模式,此時配置CNFy可以得到4種輸出模式:

  • 通用開漏輸出(General purpose output push-pull)
  • 通用推挽輸出(General purpose output open-drain)
  • 復用開漏輸出(Alternate function output push-pull)
  • 復用推挽輸出(Alternate function output open-drain)

每種輸出模式又可通過MODEy配置3種速度:

  • 01 最高輸出速度10 MHz
  • 10 最高輸出速度 2 MHz
  • 11 最高輸出速度50 MHz

綜上,每個GPIO引腳有4種輸入模式和4種輸出模式,每種輸出模式又有3種速度。可見,通過配置CR寄存器的4個位,每個GPIO引腳可實現多達16種配置,“通用”名副其實!具體到連接某個外設時I/O口該配置成哪種模式,需要根據所連接的外設和外設的工作狀態確定,可以查閱芯片的《Reference manual》中GPIO configurations for device peripherals章節與外設相關的表格。

說明:

  • 輸入下拉和輸入上拉的CNFy和MODEy配置是一樣的,為了區分兩種模式,還需要配置引腳的ODR寄存器,ODRy為0則為下拉,反之則為上拉。
  • GPIO的CR寄存器的復位值為0x44444444,即復位後引腳的默認狀態為輸入浮空
  • 在程序初始化階段,建議將所有引腳復位為模擬輸入,即先將所有CR寄存器位設置為0,然後按需配置引腳模式。這樣可以關閉閑置引腳上的TTL Schmitt trigger,達到省電的目的。

1.2 操作方法

CR寄存器的設置既可直接對寄存器進行位操作,也可以通過調用庫函數進行操作。

1. 直接操作寄存器

為了便於對寄存器進行位操作,在頭文件stm32f10x.h(以F103ZET6標準庫為例,HAL庫中對應的頭文件為stm32f103xe.h)中定義了大量的宏、枚舉類和結構體,來實現與寄存器地址的映射。以GPIOA為例,從上到下的映射關系為:

#define PERIPH_BASE       ((uint32_t)0x40000000)         /* 外設總線地址 */
#define APB2PERIPH_BASE   (PERIPH_BASE   0x10000)        /* APB2總線地址 0x40010000 */
#define GPIOA_BASE        (APB2PERIPH_BASE   0x0800)     /* GPIOA地址 0x40010800 */
#define GPIOA             ((GPIO_TypeDef *) GPIOA_BASE)  /* GPIOA宏定義 0x40010800 */
typedef struct
{
  __IO uint32_t CRL;  //0x40010800
  __IO uint32_t CRH;  //0x40010804
  __IO uint32_t IDR;  //0x40010808
  __IO uint32_t ODR;  //0x4001080C
  __IO uint32_t BSRR; //0x40010810
  __IO uint32_t BRR;  //0x40010814
  __IO uint32_t LCKR; //0x40010818
} GPIO_TypeDef; 

(註:地址以字節為單位,每個地址存放1個字節,即8個bit,每個寄存器占32bit ,所以地址偏移0x04)

從下往上看,GPIO_TypeDef結構體中為每組GPIO的7個寄存器都定義了一個別名,指向相應寄存器的首地址;為每組GPIO也定義了別名,GPIOx(x=A....)是指向GPIO_TypeDef結構體對象的首地址的指針,GPIOx掛載在APB2外設總線上,APB2又掛載在APB總線上(APB-Advanced Peripheral Bus,高級外設總線,分APB1和APB2)。從上往下看,是先定義一個基地址,然後通過地址偏移(Address offset)來逐級分配地址。

給這些地址定義別名,可以提高程序的可讀性,便於在程序中設置寄存器的相關位。為了在設置寄存器的功能位時,不影響其他位,操作分為兩步:

  • 用一個Reset mask(復位掩碼),與寄存器現有的值作&運算,定位清零。
  • 再用一個Set mask(設置掩碼),與寄存器的現有值作|運算,寫入新設置。

比如,將PA5引腳設為50MHz的推挽輸出:

GPIOA->CRL&=0XFF0FFFFF; //先清空。與操作,將PA5對應的4個位設為0,其餘位不受影響
GPIOA->CRL|=0X00300000; //後賦值。或操作,將PA5對應的位設為0011b,對應16進制的3。

這種直接賦值的方法雖然簡單直接,但影響程序的可讀性和通用性。單片機開發中更常用的方法是移位操作。例如,用移位操作上述代碼可改寫為:

GPIOA->CRL&= ~(((uint32_t)0x0F)<<5); //移位得到0x00F00000,再取反即為0XFF0FFFFF
GPIOA->CRL|=  (0x03<<5); //0x03左移5次即為0x00300000

為了進一步提高寄存器操作代碼的可讀性和通用性,頭文件stm32f10x.h中為所有寄存器的功能位定義了宏名稱,以PA5對應的位為例:

#define  GPIO_CRL_MODE5     ((uint32_t)0x00300000)  /*!< MODE5[1:0]=11 */
#define  GPIO_CRL_MODE5_0   ((uint32_t)0x00100000)  /*!< MODE5[1:0]=01 */
#define  GPIO_CRL_MODE5_1   ((uint32_t)0x00200000)  /*!< MODE5[1:0]=10 */
#define  GPIO_CRL_CNF5      ((uint32_t)0x00C00000)  /*!< CNF5[1:0]=11 */
#define  GPIO_CRL_CNF5_0    ((uint32_t)0x00400000)  /*!< CNF5[1:0]=01 */
#define  GPIO_CRL_CNF5_1    ((uint32_t)0x00800000)  /*!< CNF5[1:0]=10 */

用上述宏定義,前述設置等效為:

tempRV = GPIOA->CRL; //定義臨時變量,減少讀寫寄存器次數
tempRV &= ~(GPIO_CRL_CNF5|GPIO_CRL_MODE5);//1111取反,定位清零
tempRV |= (~GPIO_CRL_CNF5)|GPIO_CRL_MODE5;//0011,寫入新設置
GPIOA->CRL = tempRV; //寫入寄存器

說明:

  • GPIO相關的寄存器不支持按字節(byte,8bit)或半字(half-word, 16bit)操作,隻能通過全字(word,32-bit)方式進行訪問。
  • 對輸入上拉或下拉還要配置同引腳的ODR的對應位。可以直接設置ODR的位,也可通過操作BRR或BSRR寄存器來實現。詳見下文BSRR和BRR寄存器。
  • 移位操作可提高代碼可讀性和重用性,也是庫函數中常用的方法。要熟悉和掌握。
  • 定義臨時變量存儲寄存器值,可將原來的2次讀取2次寫入簡化為1次讀取1次寫入。
  • 這裡僅以CRL寄存器的操作為例,掌握了基本原理,其餘寄存器的操作可觸類旁通。

2. 調用標準庫函數

標準庫(STD庫)中,與GPIO相關的庫函數及相關參數在頭文件stm32f10x_gpio.h中聲明,在stm32f10x_gpio.c中實現。頭文件中GPIO各Pin定義如下:

#define GPIO_Pin_0    ((uint16_t)0x0001)  //0b0000000000000001
#define GPIO_Pin_1    ((uint16_t)0x0002)  //0b0000000000000010
#define GPIO_Pin_2    ((uint16_t)0x0004)  //0b0000000000000100
#define GPIO_Pin_3    ((uint16_t)0x0008)  //0b0000000000001000
#define GPIO_Pin_4    ((uint16_t)0x0010)  //0b0000000000010000
#define GPIO_Pin_5    ((uint16_t)0x0020)  //0b0000000000100000
#define GPIO_Pin_6    ((uint16_t)0x0040)  //0b0000000001000000
#define GPIO_Pin_7    ((uint16_t)0x0080)  //0b0000000010000000
#define GPIO_Pin_8    ((uint16_t)0x0100)  //0b0000000100000000
#define GPIO_Pin_9    ((uint16_t)0x0200)  //0b0000001000000000
#define GPIO_Pin_10  ((uint16_t)0x0400)  //0b0000010000000000
#define GPIO_Pin_11  ((uint16_t)0x0800)  //0b0000100000000000
#define GPIO_Pin_12  ((uint16_t)0x1000)  //0b0001000000000000
#define GPIO_Pin_13  ((uint16_t)0x2000)  //0b0010000000000000
#define GPIO_Pin_14  ((uint16_t)0x4000)  //0b0100000000000000
#define GPIO_Pin_15  ((uint16_t)0x8000)  //0b1000000000000000
#define GPIO_Pin_All ((uint16_t)0xFFFF)   //0b1111111111111111

可見,為每個Pin設定了一個別名,對應一個16位的無符號整數。註釋部分是該整數對應的二進制數,可以看到其中隻有一個位是1且位置與引腳編號相關。在GPIO_Init()函數中,正是通過對0x01進行循環左移操作,通過搜索與輸入引腳匹配時移位的次數來判斷引腳編號的。(疑問1:為何用循環移位,不直接用switch…case呢?)

頭文件中還定義了兩個枚舉類來分別管理GPIO的模式(GPIO_Mode_TypeDef)和輸出模式的速度(GPIO_Speed_TypeDef),並定義了一個結構體(GPIO_InitTypeDef)來便於向初始化函數傳遞參數。

GPIO_Mode_TypeDef枚舉類定義如下:

typedef enum
{ GPIO_Mode_AIN = 0x0,                    //0b00000000
  GPIO_Mode_IN_FLOATING = 0x04,  //0b00000100
  GPIO_Mode_IPD = 0x28,                  //0b00101000
  GPIO_Mode_IPU = 0x48,                  //0b01001000
  GPIO_Mode_Out_OD = 0x14,          //0b00010100
  GPIO_Mode_Out_PP = 0x10,          //0b00010000
  GPIO_Mode_AF_OD = 0x1C,          //0b00011100
  GPIO_Mode_AF_PP = 0x18            //0b00011000
}GPIOMode_TypeDef;

可見,在該枚舉類中為GPIO的8種模式分別定義了一個名稱,每個名稱被賦值一個十六進制數,在GPIO_Init()函數中會基於這個十六進制數來判斷GPIO的模式。判斷的方法也是通過位操作來實現的:位5為1則為輸出,反之為輸入;低4位用來設置CR寄存器,其中高2位對應CNFy,低2位與下述GPIOSpeed作&運算後的結果對應MODEy。

GPIOSpeed_TypeDef枚舉類定義如下:

typedef enum
{ 
  GPIO_Speed_10MHz = 1,  //MODE[1:0]=01
  GPIO_Speed_2MHz,          //MODE[1:0]=10
  GPIO_Speed_50MHz         //MODE[1:0]=11
}GPIOSpeed_TypeDef;

可見,每個模式速度也被定義了一個名稱,並被賦值了一個十進制數,對應的二進制碼即為MODEy的邏輯碼。

GPIO_InitTypeDef結構體用來設置GPIO的初始化參數,其定義如下:

typedef struct
{
  uint16_t GPIO_Pin;                            /*!< 指定管腳 */
  GPIOSpeed_TypeDef GPIO_Speed;  /*!< 指定管腳的速度 */
  GPIOMode_TypeDef GPIO_Mode;    /*!< 指定管腳的模式*/
}GPIO_InitTypeDef;

可見,這個結構體匯總了前述管腳編號、速度、模式,以結構體作為入口參數,可以簡化函數的接口。最終,GPIO端口的配置通過調用初始化函數GPIO_Init()完成,其接口如下:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

可見,函數共需要傳入兩個參數,第一個參數指定GPIO引腳所在的分組,第二個參數是個GPIO_InitTypeDef結構體,傳入引腳、模式、速度的配置參數。例如:將PA5配置為推挽輸出,速度50MHz。

GPIO_InitTypeDef  GPIO_InitStruct;  //先定義一個GPIO_InitTypeDef結構體變量
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5;  //引腳號
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;  //模式
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //速度
GPIO_Init(GPIOA, &GPIO_InitStruct); //調用GPIO_Int函數完成初始化

GPIO_Init()函數的定義可在stm32f10xx_gpio.c中查看。其基本步驟和原理總結如下:

  • 確定CR寄存器的位:取出GPIO_Mode(&0x0F)的低四位;根據GPIO_MODE判斷是輸入還是輸出(&0x10);若為輸出則將前面取出的四個位與GPIO_Speed作或操作,確定CR寄存器的4個位的值。
  • 判斷寄存器及對應位:根據GPIO_Pin編號判斷該配置CRL寄存器還是CRH寄存器及該修改的位置(即CNFy和MODEy中的y)。
  • 設置寄存器:與前述直接操作寄存器的設置方法是一致的,即通過與操作先清除,然後通過或操作再把新的設置寫入。
  • 處理輸入上/下拉:如果GPIO_Mode是IPD或IPU,GPIO_Int()函數中還會通過設置BRR或BSRR寄存器,將引腳ODR對應位設為0或1。

3. 調用HAL庫函數

標準庫函數一般隻適用於某個具體芯片(如F10x),而HAL(HARDWARE Abstraction Layer,硬件抽象層)庫通過進一步的抽象和封裝,大大提高了代碼的可移植性,其目標是實現對STM32全系列的兼容。就像標準庫是對寄存器操作的封裝一樣,HAL庫可以理解為是對不同系列芯片庫函數的進一步封裝。當然,不會是簡單的打包,也有架構的調整。

具體到這裡討論的GPIO的配置,HAL庫的頭文件stm32f1xx_hal_gpio.h中,除了引腳的別名定義基本未變(變為全部大寫),其餘部分都重寫了。

#define  GPIO_MODE_INPUT      0x00000000u   /*!<輸入浮空*/
#define  GPIO_MODE_OUTPUT_PP  0x00000001u   /*!<輸出推挽*/
#define  GPIO_MODE_AF_PP      0x00000002u   /*!<復用推挽*/
#define  GPIO_MODE_ANALOG     0x00000003u   /*!<模擬模式*/
#define  GPIO_MODE_OUTPUT_OD  0x00000011u   /*!<輸出開漏*/
#define  GPIO_MODE_AF_OD      0x00000012u   /*!<復用開漏*/
#define  GPIO_MODE_AF_INPUT   GPIO_MODE_INPUT  /*!<復用輸入*/
#define  GPIO_MODE_IT_RISING          0x10110000u   /*!< 外部中斷模式上升沿觸發*/
#define  GPIO_MODE_IT_FALLING         0x10210000u   /*!< 外部中斷模式下降沿觸發*/
#define  GPIO_MODE_IT_RISING_FALLING  0x10310000u   /*!< 外部中斷模式上下沿觸發*/
#define  GPIO_MODE_EVT_RISING         0x10120000u   /*!< 事件模式上升沿觸發*/
#define  GPIO_MODE_EVT_FALLING        0x10220000u   /*!< 事件模式下降沿觸發*/
#define  GPIO_MODE_EVT_RISING_FALLING 0x10320000u   /*!< 事件模式上下沿觸發*/
#define  GPIO_NOPULL        0x00000000u   /*!<無上下拉*/
#define  GPIO_PULLUP        0x00000001u   /*!<上拉*/
#define  GPIO_PULLDOWN      0x00000002u   /*!<下拉*/
#define  GPIO_SPEED_FREQ_LOW     (GPIO_CRL_MODE0_1) /*!< 低速*/
#define  GPIO_SPEED_FREQ_MEDIUM  (GPIO_CRL_MODE0_0) /*!< 中速*/
#define  GPIO_SPEED_FREQ_HIGH    (GPIO_CRL_MODE0)   /*!< 高速*/

可見,MODE和SPEED的設置都不再用枚舉類定義,而是直接用#define定義了別名;上下拉被分離了出來,可獨立設置;MODE擴充明顯,外部中斷和事件模式也被整合了進來,不同的芯片可能支持的模式會有所不同;速度模式采用高、中、低來定義,不再是具體的xxMHz,目的顯然是為了兼容不同芯片的速度。

typedef struct
{
  uint32_t Pin;   /*!< 指定管腳 */
  uint32_t Mode;  /*!< 指定模式 */
  uint32_t Pull;  /*!< 指定PULL方式 */
  uint32_t Speed; /*!< 指定速度 */
} GPIO_InitTypeDef;

GPIO_InitTypeDef仍用結構體定義,但字段名發生了變化,更簡潔了,增加了專門的Pull字段。雖然HAL庫內容變化很大,但用戶代碼方面的差別不是太大。例如:將PA5配置為推挽輸出,速度50MHz,HAL庫函數版本的代碼如下:

GPIO_InitTypeDef  GPIO_InitStruct = {0};  //先定義一個GPIO_InitTypeDef結構體變量
GPIO_InitStruct.Pin = GPIO_PIN_5;  //引腳號,HAL庫中全大寫
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  //模式
GPIO_InitStruct.Pull = GPIO_NOPULL; //無上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; //速度
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); //調用GPIO_Int函數完成初始化

HAL庫的函數開頭一般以HAL_開頭,相應的.h和.c文件中也加_hal_以示區別。例如與STD庫中stm32f10x_gpio.h對應的頭文件在HAL庫中名為stm32f1xx_hal_gpio.h。除了名稱的變化,函數的代碼也是重寫過的。比如,HAL_GPIO_Init()函數中,是用switch...case語句來區別處理的各種MODE;通過直接比較管腳的編號大小來確定的CRL/CRH寄存器;定義了一個專門的MODIFY_REG宏函數來完成清除和設置寄存器;......HAL庫中開始出現了越來越多軟件工程的思想。但萬變不離其宗,無論是HAL庫還是STD庫,各種函數最終實現的其實還是對寄存器的操作。由此可見,熟悉底層硬件和學會直接操作寄存器的必要性,寄存器操作是庫函數的基石。

二、 GPIO數據寄存器(Data Register)

每個GPIO引腳都有兩個DR寄存器與之相連:GPIOx_IDR輸入數據寄存器(Input Data Register)和GPIOx_ODR輸出數據寄存器(Output Data Register)。

2.1 工作原理

兩個寄存器都是32bit寄存器,但都隻用到其中的低16位,每位對應GPIOx中的一個引腳。引腳分配如圖3所示:

圖3. IDR(上)和ODR(下)寄存器的引腳分配圖

GPIOx_IDR寄存器是隻讀的,用於查詢I/O引腳的電平狀態,借以判斷與之相連的外設的狀態,例如按鍵是否按下。在I/O端口被配置為模擬輸入模式時,該寄存器被強制置零,這種情況下查詢返回的值始終為0。

GPIOx_ODR寄存器可讀可寫,在I/O端口被配置位輸出模式時,通過寫該寄存器可以控制引腳的電平。而在輸入模式配置下,該寄存器與外部引腳之間被斷開,此時它被用來配合CR寄存器,來實現輸入上拉(ODRy置1)或下拉(ODRy置0)模式的配置。

2.2 操作方法

雖然不同的寄存器功能不同,但操作方法都是相似的。有了前面GPIO CR寄存器操作方法的詳細介紹,DR寄存器的操作可觸類旁通,毋庸贅言。我們分別用不同的方法來實現:讀取PA5引腳的電平;先設置PA6引腳輸出高電平,然後讀取PA6引腳的電平。

1. 直接操作寄存器

PA5in = GPIOA->IDR&(0x01<<5);  //移位操作提高代碼可讀性
GPIOA->ODR &=0xFFFFFF4F;       //ODR可寫
PA6out = GPIOA->ODR&(0x01<<6); //ODR可讀

說明:

  • 雖然兩個寄存器都隻用到了低16位,但訪問時必須以32bit的word模式訪問,十六進制數前導0可省略。
  • 移位操作可提高代碼可讀性,也省卻了換算的麻煩。如0x01<<5,即為0x020,即第6位置1,對應PA5引腳。

2. 調用標準庫函數

STD庫中操作GPIO的IDR和ODR寄存器的庫函數有5個。在stm32f10x_gpio.h中聲明、stm32f10x_gpio.c中實現。函數聲明如下:

uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);    //整體讀取IDR
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);   //整體讀取ODR
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);  //定Pin讀取IDR
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //定Pin讀取ODR
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); //整體寫入ODR,16個引腳一起寫
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal); //定Pin寫入IDR

用STD庫函數來實現,本節例子的代碼如下:

PA5in =  GPIO_ReadInputData(GPIOA) & GPIO_Pin_5; //整體讀取,作與過濾
PA5in = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5); //定Pin讀取IDR
GPIO_WriteBit(GPIOA,GPIO_Pin_6,Bit_RESET); //GPIO_Write無法單獨改變一個引腳
PA6out = GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_6); //定Pin讀取ODR

說明:

  • 根據引腳宏定義,GPIO_Pin_5對應的就是0x0020,可以用引腳別名與GPIO_ReadInputData讀取的整個IDR的數據作&過濾出引腳對應的位的電平。由此可見,STD庫中引腳宏定義的設計也是深思熟慮的結果。
  • GPIO_Write函數無法單獨寫1個位,寫一個位要用GPIO_WriteBit函數。但GPIO_WriteBit函數不是操作的ODR寄存器,而是BRR和BSRR寄存器。BitAction是為配合BRR和BSRR寄存器操作而定義的枚舉類:
typedef enum
{ Bit_RESET = 0,
  Bit_SET
}BitAction;

3. 調用HAL庫函數

HAL庫中讀取和修改GPIO引腳電平的函數精簡到了3個。在stm32f1xx_hal_gpio.h中聲明、stm32f1xx_hal_gpio.c中定義。接口如下:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); //定pin讀取IDR
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); //定Pin寫入ODR
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); //反轉指定Pin的狀態

讀一下HAL_GPIO_ReadPin函數的源碼會發現,這個函數是通過讀取GPIOx->IDR來實現的。嚴格來說,HAL庫中沒有直接操作GPIOx_ODR的函數,HAL_GPIO_WritePin和HAL_GPIO_TogglePin這兩個函數中是通過設置BSRR和BSR寄存器來修改引腳狀態的,原因在BSRR和BSR寄存器部分解釋。STD庫中有分別讀取IDR和ODR狀態的兩個函數,這其實是不必要的,因為IDRy和ODRy連接到的都是y引腳,同一個引腳不可能同時有兩個電平狀態。HAL庫中合二為一,隻定義了一個HAL_GPIO_ReadPin函數。

HAL庫中為引腳狀態專門定義了一個GPIO_PinState枚舉類:

typedef enum
{
  GPIO_PIN_RESET = 0u,
  GPIO_PIN_SET
} GPIO_PinState;

可見,PIN_RESET是低電平狀態,PIN_SET為高電平狀態。用HAL庫函數來實現,本節例子的代碼如下:

PA5in = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5); //返回參數類型是GPIO_PinState
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET);
PA6in = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6);

三、BSRR和BSR寄存器

從前述ODR寄存器的設置過程可以看出,為了在設置一個引腳的電平時不影響其它引腳的電平,實際上是分三步實現的:先讀取ODR寄存器的值,與一個合適的32位的數(mask)作&運算後,再將運算結果寫回ODR寄存器。GPIO的BSRR寄存器和BRR寄存器也是用來設置引腳電平的,但隻需一步。用這兩個寄存器來操作引腳電平,不僅邏輯簡潔,而且可以有效避免ODR分步操作過程中因高優先級中斷介入而造成的ODR狀態設置錯誤。

3.1 工作原理

BSRR(Bit Set/Reset Registor)和BRR(Bit Reset Register) 均為32位的隻寫寄存器。BSRR寄存器的引腳分配如圖4所示。

圖4. BSRR寄存器的引腳分配

BSRR寄存器的低16位BSy(y=1...15)用於設置y引腳電平(置1),高16位BRy(y=0...15)用於復位y引腳的電平(置0)。換句話說,當BSy位被賦值為1時,y引腳被設為高電平1,而當BSy被賦值為0時,則不改變y引腳的當前電平;與此相反,當BRy位被賦值為1時,y引腳被強制復位,設為低電平0,而當BRy被賦值為0時,則不改變y引腳的當前電平。

BRR寄存器的引腳分配如圖5所示。

圖5. BRR寄存器的引腳分配

可以看出,BRR雖然也是32bit隻寫寄存器,但隻用了低16位。該寄存器的作用與BSRR寄存器的高16位完全相同,不再贅述。

BSRR、BSR、ODR三個寄存器都可以設置引腳的電平。但由於用ODR設置引腳電平需要三步,如果期間有高優先級的中斷發生,當中斷執行完畢返回繼續執行時,原來的設置有可能會與中斷期間的設置相沖突,造成ODR寄存器設置錯誤。因此,一般不用ODR寄存器來設置引腳電平,而是用BSRR的低16位來作位設置(置1),而用BRR來作位清除(置0)。如前所述,在輸出模式下,ODR與IDR寄存器是同步的,一般用IDR來讀取引腳的電平狀態

3.2 操作方法

1. 直接操作寄存器

GPIOB->BRR  = 0x01<<5; //將PB5設為0
GPIOE->BSRR = 0x01<<5;//將PE5設為1

2. 調用標準庫函數

STD庫中設置和清除位的兩個對應函數在頭文件stm32f10x_gpio.h中的聲明如下:

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

查看stm32f10x_gpio.c中的函數定義可以發現,兩個函數中是分別通過BSRR和BRR寄存器來設置和清除位的。另外,前述的GPIO_WriteBit函數中,也是基於操作這兩個寄存器來實現位寫入的,寫0時調用了BRR,寫1時調用了BSRR。

GPIO_ReSetBits(GPIOB,GPIO_Pin_5); //將PB5設為0
GPIO_SetBits(GPIOB,GPIO_Pin_5);     //將PB5設為1
GPIO_WriteBit(GPIOB,GPIO_Pin_5,Bit_RESET); //將PB5設為0
GPIO_WriteBit(GPIOB,GPIO_Pin_5,Bit_SET);     //將PB5設為1

3. 調用HAL庫函數

HAL庫中修改引腳電平的函數隻有兩個,即前面介紹過的HAL_GPIO_WritePin和HAL_GPIO_TogglePin。

HAL_GPIO_WritePin(GPIOB,GPIO_PIN_5,PIN_RESET); //將PB5設為0
HAL_Delay(1000); //延時1s
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_5); //反轉PB5的電平

四、端口配置鎖定寄存器(Configuration Lock Registor)

如前所述,通過配置CR寄存器的4個位,每個GPIO端口理論上來說有多達16種配置。有時候希望MCU工作過程中能夠鎖定某個I/O引腳的配置,避免在傳輸重要數據時受到幹擾。GPIOx_LCKR寄存器就是為這一目的而設計的。執行端口上鎖操作後,與端口對應的CR寄存器中的4個配置位將被鎖定,在下次系統復位前不可修改。

4.1 工作原理

LCKR(LoCK Register)是一個32bit寄存器,引腳分配如圖6所示。LCKR的低16位各自負責GPIOx的一個引腳,位16為上鎖位。上鎖後,若LCKy=1,則y端口的配置被鎖定,即y端口對應的CR寄存器中的4位不可修改。

圖6. 端口配置鎖定寄存器引腳分配

為了避免誤鎖,上鎖過程需要按順序對LCKR寄存器執行一套操作,稱為密鑰寫入序列(Key writing sequence)。密鑰輸入過程包括3次寫入,2次讀取。3次寫入中在保證鎖定位寫入1的情況下,LCKK位要依次寫入1-0-1。具體實現細節,請參考下面的直接操作寄存器。

4.2 操作方法

1. 直接操作寄存器

比如要鎖定PB6,加鎖過程如下:

uint32_t tmp = 0x00010000; //定義一個32位的臨時參數,LCKK位為1,其餘位為0
uint32_t pin = (0x1<<6);   //LCK6位為1。 也可直接用引腳別名GPIO_Pin_6
tmp |= pin; //或操作後,LCKK=1,LCK6=1
GPIOB->LCKR = tmp; //第1次寫入,LCKK=1,LCK6=1
GPIOB->LCKR = pin;  //第2次寫入,LCKK=0,LCK6=1
GPIOB->LCKR = tmp; //第3次寫入,LCKK=1,LCK6=1
tmp = GPIOB->LCKR; //第1次讀取
tmp = GPIOB->LCKR; //第2次讀取。可省略,也可用這次讀取結果去判斷是否上鎖成功。

2. 調用標準庫函數

STD庫中的GPIO_PinLockConfig函數,可以便捷地鎖定指定引腳的配置。該函數聲明如下:

void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

例如,

GPIO_PinLockConfig(GPIOB,GPIO_Pin_6); //鎖定PB6的配置

GPIO_PinLockConfig的代碼可以在stm32f10x_gpio.c中查看,與上面直接配置寄存器的代碼基本一致。此例中,庫函數的優勢盡顯。

3. 調用HAL庫函數

HAL庫中鎖定引腳配置的函數名為HAL_GPIO_LockPin,聲明如下:

HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

除了函數名跟STD庫中不同外,HAL庫中的函數還返回一個狀態參數,可以根據該參數判斷是否上鎖成功。例如:

if (HAL_GPIO_LockPin(GPIOB, GPIO_PIN_6) != HAL_OK) //鎖定PB6的配置,如果返回不是HAL_OK
{
  Error_Handler(); //調用容錯處理函數
}

HAL_GPIO_LockPin與STD庫中的函數源代碼基本一致,都是對上鎖過程的一組寄存器操作進行了封裝。

五、總結

以GPIO相關的7個寄存器為主線,學習了GPIO寄存器工作原理,分別采用直接操作寄存器、調用STD庫函數、調用HAL庫函數進行了GPIO設置,並對不同的方法進行了比較。簡單總結幾點體會:

  • 庫函數把對寄存器的操作進行了封裝,一定程度上降低了代碼對硬件參數的依賴,為程序編寫、修改維護和移植提供了便利。
  • 直接操作寄存器的優點是:代碼簡潔,效率高,編譯後文件體積小(省內存);缺點是:代碼可讀性差,可移植性也較差,需要熟悉底層硬件及原理,編程過程中需要經常翻閱芯片的用戶手冊。
  • 庫函數的優點是:代碼可讀性好,有一定的可移植性,容易上手;缺點是:代碼稍顯冗長,效率稍差,編譯後文件體積較大。由於函數要有一定的通用性,其中的循環和條件判斷無疑會增加程序的運行開銷,特別是在編譯階段。當然,如果編譯器優化的到位,最終寫到芯片裡的代碼或許性能損失並不大。(如何定量比較?有沒有剖析軟件?)
  • 無論喜歡用哪種方式編程,多多研讀庫函數的代碼大有裨益。特別對初學者,尤為必要,有助於提高編程技巧和對硬件本身的了解。
  • 庫函數中的運算基本都是通過位運算(移位/按位邏輯運算)來實現的,這是最貼近硬件的編程方法。初學者剛開始可能會不適應,仔細研究幾個庫函數,堅持一下就欲罷不能啦。
  • 目前ST已有STD庫、HAL庫、LL庫。用庫函數可以提高開發效率,提高程序的穩定性,而且易於維護和共同開發。但操作寄存器是所有庫函數的基石,要想將嵌入式系統開發作為一技之長,理解庫函數背後的寄存器操作很重要。
  • 庫函數和直接操作寄存器隻是實現同一目的的不同方式,各有優點,可以混用,並不矛盾。不要因為會用c語言操作寄存器就自視甚高,匯編語言其實更底層。打個比方,用打火機和火柴都可以點煙,鉆木取火也可以。

發現庫函數中還有些關於GPIO引腳重映射、復用、外部中斷響應的函數。留待以後學習吧。革命尚未成功,同志仍需努力。STM32要是有MATLAB那樣強大的幫助系統就好啦。

附錄:庫函數

1. 標準庫中GPIO相關函數 (見stm32f10x_gpio.h)

void GPIO_DeInit(GPIO_TypeDef* GPIOx); 
void GPIO_AFIODeInit(void);
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); //GPIO初始化
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct); //初始化GPIO結構體為默認值
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//定Pin讀取IDR
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);//整體讀取IDR
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//定Pin讀取ODR
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);//整體讀取ODR
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //引腳設置(1)
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //引腳復位(0)
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//整體寫入ODR,16個引腳一起寫
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //鎖定引腳配置
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);

2. HAL庫中GPIO相關函數(見stm32f1xx_hal_gpio.h)

/* Initialization and de-initialization functions *****************************/
void  HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init);//GPIO初始化
void  HAL_GPIO_DeInit(GPIO_TypeDef  *GPIOx, uint32_t GPIO_Pin);
/* IO operation functions *****************************************************/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); //定位讀取IDR
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); //設置引腳電平
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); //反轉引腳電平
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); //鎖定引腳配置
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin);
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);

參考文獻

[1] RM008 Reference manual Stm32F101/2/3/5/7xx advanced ARM-based 32-bit MCUs.

#精品長文創作季##文章首發挑戰賽#