iT邦幫忙

2021 iThome 鐵人賽

DAY 11
1
Arm Platforms

STM32 基礎入門教學系列 第 11

【Day11】:庫函數包裝—對於底層暫存器的操縱(下)

C語言對暫存器的封裝

  1. 封裝匯流排和外設基地址
    為了方便使用者理解和記憶,我們把匯流排基地址和外設基地址都以define的方式來定義。
    在stm32f429xx.h當中可以看到
/* 外設基地址 */
#define PERIPH_BASE           0x40000000UL

/* 總線基地址 */
#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000UL)
#define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000UL)

/* GPIO 外設基地址 */
#define GPIOA_BASE            (AHB1PERIPH_BASE + 0x0000UL)
#define GPIOB_BASE            (AHB1PERIPH_BASE + 0x0400UL)
#define GPIOC_BASE            (AHB1PERIPH_BASE + 0x0800UL)
#define GPIOD_BASE            (AHB1PERIPH_BASE + 0x0C00UL)
#define GPIOE_BASE            (AHB1PERIPH_BASE + 0x1000UL)
#define GPIOF_BASE            (AHB1PERIPH_BASE + 0x1400UL)
#define GPIOG_BASE            (AHB1PERIPH_BASE + 0x1800UL)
#define GPIOH_BASE            (AHB1PERIPH_BASE + 0x1C00UL)
#define GPIOI_BASE            (AHB1PERIPH_BASE + 0x2000UL)
#define GPIOJ_BASE            (AHB1PERIPH_BASE + 0x2400UL)
#define GPIOK_BASE            (AHB1PERIPH_BASE + 0x2800UL)

數字後面的"UL"是後綴,代表unsigned longlong
這些地址在前幾天都已經介紹過了,現在看來應該熟悉多了,相同類型的地址都是以一個基地址去做偏移。

  1. 封裝暫存器列表
    學過C語言結構的應該自然而然會想到,既然GPIOA~GPIOK功能都是相似的,底下又有那麼多暫存器,那我們應該用struct的方式來對這些資料做包裝啊,於是就有了底下的定義:
typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO 模式暫存器           Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO 輸出類型暫存器       Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO 輸出速度暫存器       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO 上拉/下拉暫存器      Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO 輸入數據暫存器       Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO 輸出數據暫存器       Address offset: 0x14      */
  __IO uint32_t BSRR;     /*!< GPIO 置位/復位暫存器      Address offset: 0x18      */
  __IO uint32_t LCKR;     /*!< GPIO 配置鎖定暫存器       Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO 富用功能配置暫存器   Address offset: 0x20-0x24 */
} GPIO_TypeDef;

這樣我們就可以以結構體的方式來使用暫存器,增加可讀性。
最後再來簡化一下名稱,以GPIOx的方式直接獲取GPIOx的基位址。

#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)
#define GPIOH               ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI               ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ               ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK               ((GPIO_TypeDef *) GPIOK_BASE)

HAL_GPIO_WritePin()做了哪些事?

stm32幾乎都把所有參數都用define的方式來定義增加可讀性,我們再附上GPIO_Pin,以及GPIO_PinState的定義吧

#define GPIO_PIN_0                 ((uint16_t)0x0001)  /* Pin 0 selected    */
#define GPIO_PIN_1                 ((uint16_t)0x0002)  /* Pin 1 selected    */
#define GPIO_PIN_2                 ((uint16_t)0x0004)  /* Pin 2 selected    */
#define GPIO_PIN_3                 ((uint16_t)0x0008)  /* Pin 3 selected    */
#define GPIO_PIN_4                 ((uint16_t)0x0010)  /* Pin 4 selected    */
#define GPIO_PIN_5                 ((uint16_t)0x0020)  /* Pin 5 selected    */
#define GPIO_PIN_6                 ((uint16_t)0x0040)  /* Pin 6 selected    */
#define GPIO_PIN_7                 ((uint16_t)0x0080)  /* Pin 7 selected    */
#define GPIO_PIN_8                 ((uint16_t)0x0100)  /* Pin 8 selected    */
#define GPIO_PIN_9                 ((uint16_t)0x0200)  /* Pin 9 selected    */
#define GPIO_PIN_10                ((uint16_t)0x0400)  /* Pin 10 selected   */
#define GPIO_PIN_11                ((uint16_t)0x0800)  /* Pin 11 selected   */
#define GPIO_PIN_12                ((uint16_t)0x1000)  /* Pin 12 selected   */
#define GPIO_PIN_13                ((uint16_t)0x2000)  /* Pin 13 selected   */
#define GPIO_PIN_14                ((uint16_t)0x4000)  /* Pin 14 selected   */
#define GPIO_PIN_15                ((uint16_t)0x8000)  /* Pin 15 selected   */
#define GPIO_PIN_All               ((uint16_t)0xFFFF)  /* All pins selected */

可以發現GPIO_PIN_0所對應到的數字為0x0001轉換為2進位的話就是0b 0000 0000 0000 0001,我們再來做幾個轉換,觀察一下
GPIO_PIN_1 對應到0x0002 = 0b 0000 0000 0000 0010
GPIO_PIN_2 對應到0x0004 = 0b 0000 0000 0000 0100
GPIO_PIN_3 對應到0x0008 = 0b 0000 0000 0000 1000
GPIO_PIN_4 對應到0x0010 = 0b 0000 0000 0001 0000
這樣應該很清楚了吧,一次就是只有一個bit會是1,這樣就可以用每個位元來代表pin0~15剛好16個位元!
而最後一個GPIO_PIN_ALL 對應到0xffff轉換成二進位就是每個位元都是1喔
接著是GPIO_PinState的部分:

typedef enum
{
  GPIO_PIN_RESET = 0,
  GPIO_PIN_SET
}GPIO_PinState;

這裡比較特別,是用C語言列舉的語法(enum)事實上他與define沒什麼不同啦,如果不太認識的可以再翻一下C語言的相關書籍。總之GPIO_PIN_RESET代表0,而GPIO_PIN_SET代表1(enum的會照著順序定義下去,可以省略不寫)

之前在使用GPIO輸出高電位的時候曾經見過這個函式,但當時我們沒有對這個函式的API做進一步的講解,經過我們好幾天對於暫存器的介紹,現在來好好地看看這個函式吧

void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  assert_param(IS_GPIO_PIN_ACTION(PinState));

  if(PinState != GPIO_PIN_RESET)
  {
    GPIOx->BSRR = GPIO_Pin;
  }
  else
  {
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16U;
  }
}

而當我們要使用這個函式的時候會打

HAL_GPIO_WritePIn(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);

雖然一開始要記得這些變數名稱不是很容易,也會覺得相比Arudino的digitatWrite()的語法難上許多,但是這可是stm32經過暫存器封裝後的結果!要是不這麼打,我們就要自己去操縱那些暫存器,並且還要記得許多暫存器的基位址與偏移位址,因此這樣的打法現在看來是不是更清楚了呢。
這個函式一開始的assert_param只是要確定傳進來的參數是符合的我們要求的,可以先忽略,底下if-else才是真正實際操縱暫存器的部分,至於實際上的運作原理就交給大家思考囉~(可以再回去看一下昨天對於BSRR暫存器的介紹)

參考資料

  1. 劉火良、楊森(2017)。《STM32庫開發時戰指南-基於STM32F4》。北京:機械工業出版社。

上一篇
【Day10】:庫函數包裝—對於底層暫存器的操縱(上)
下一篇
【Day12】:NVIC中斷概要
系列文
STM32 基礎入門教學30

尚未有邦友留言

立即登入留言