【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》。北京:机械工业出版社。

<<:  [Day24]创建Table及捞取资料

>>:  【从实作学习ASP.NET Core】Day13 | 後台 | 编辑与删除

Promise 方法

今天继续认识四种 Promise 可以使用的方法,基础的用法可以先参考昨天的文章 Promise.a...

Day 6 网路宝石:AWS VPC 架构 Routes & Security (下)

NACL vs SG 的安全设定介绍 当请求想进出在 Private Subnet 内的 EC2 ...

Unity自主学习(十四):认识Unity介面(5)

昨天我们了解了游戏执行区以及场景编辑区中"Main Camera"物件对其的影响...

【Day 10】C 语言的位元运算子

今天,我们来学位元运算子以及赋值运算子吧! 位元运算子 位元运算子作用於位元,并逐位执行。"&...

Day3 自订电脑开机讯息

上一回,我提到 CC: Tweaked 的 Computer 方块有许多基础指令 但我不打算逐一介绍...