前言

  本文旨在以最简单的GPIO为例,从基地址开始,结合STM32框架,从底层到前端的用户层,去理解HAL库是如何操作底层寄存器、如何将结构体映射到具体的寄存器上、最后解析GPIO的初始化函数HAL_UART_Init。


一、Hal库如何与寄存器映射

1、获取外设的基地址

  首先要明白,使用库去操作寄存器时采用的是基地址+偏移地址的形式实现的,在STM32中,每一个外设的寄存器都有自己固定的地址,我们如果用寄存器编程,那完全可以直接对寄存器进行操作,但这样的话就会变得很死板,你去操作每一个外设都得从寄存器开始配置,这样开发的效率就会非常低,所以出现了库函数。底层逻辑是这样的,STM32的同一种外设都是挂在一种总线上这就是基地址,那我要操作哪个外设的寄存器,就采用基地址+偏移地址的方式,这种方式就为我灵活操作寄存器奠定了基础结构。
  大家学习用的基本是STM32 F1或者F4系列的,本文为了学以致用,采用的是STM32G030C8T6,但原理以及操作方式是一样的。
  我们先来举个例子。如图Figure 1,是 STM32G030C8T6 的系统结构图,比如我要操作GPIOC,那我最底层是去操作GPIOC对应的寄存器。首先GPIOX(X代表A,B,C,D,E,F)的总线是挂载在图中的IOPORT上,然后我再加上GPIOC相对于IOPORT的偏移地址那我的GPIOC的基地址就得到了。
在这里插入图片描述  Figure1是我在参考手册上找到的结构图。
在这里插入图片描述  Figure2是我在参考手册上找到的外设地址图。明显可以看到IOPORT的地址是0x50000000
在这里插入图片描述
  查阅数据手册GPIOC的起始地址是0x50000800,那他的偏移地址就是0x00000800,这样就定义出GPIOC的基地址。下面的代码是从HAL库中复制来的。

#define IOPORT_BASE           (0x50000000UL)  /*!< IOPORT base address */
#define GPIOC_BASE            (IOPORT_BASE + 0x00000800UL)
#define GPIOC                 ((GPIO_TypeDef *) GPIOC_BASE)

  我们现在有了基地址,就差对应到相关的寄存器上。

2、对应寄存器的方法

  寄存器在STM32里是实实在在有对应的地址和空间。HAL库是这样做的,先定义一个结构体,包含了外设对应的所有寄存器,如Table 30所示,MODER寄存器的偏移地址是0,占用了4个字节,也就是32位(bit),那OTYPER寄存器紧跟着MODER寄存器,因为他们是连续存储的,那他的偏移地址就是4,同理,OSPEEDR寄存器的偏移地址就是8,那现在按照各个寄存器的大小创建出来的结构体是不是就对应上寄存器具体的空间大小,但到这一步,还没有对应上地址,只是把空间大小对应上。

typedef struct
{
  __IO uint32_t MODER;       /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;      /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;     /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;       /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;         /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;         /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;        /*!< GPIO port bit set/reset  register,     Address offset: 0x18      */
  __IO uint32_t LCKR;        /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];      /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
  __IO uint32_t BRR;         /*!< GPIO Bit Reset register,               Address offset: 0x28      */
} GPIO_TypeDef;

在这里插入图片描述

  HAL库是采用一下代码中的方法映射上的,意思是,告诉编译器:“把 0x5000 0800(GPIOC基地址) 这个地址当作一个 GPIO_TypeDef 结构体的起始地址来看待”,然后要操作具体寄存器的时候采用的是结构体指针的方式“->”,而不是“GPIOC.MODER = temp;”。
  解释一下“GPIOC->MODER = temp”:代码A(看代码区的注释)说明了GPIOC是一个存着GPIOC_BASE的一个结构体指针,也就是说GPIOC指向了0x5000 0800这块地址,然后你调用代码B,并且指向的是一个结构体,这个GPIOC这个指针实际的访问地址就是“基地址+MODER寄存器的偏移地址”,箭头是自动帮你进行解引用,他就相当于(*GPIOC).MODER = temp;这种做法就完美的将自己创建的结构体与实际的寄存器映射起来。

#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)//代码A
GPIOC->MODER = temp;	//代码B

二、用户层代码

1.用户初始化函数

  其实这个初始化函数的本质是去调用HAL_GPIO_Init(GPIOC, &GPIO_InitStruct)函数,在调用这个函数之前,你需要定义一个结构体,用来存放引脚号、输入输出模式、速率等信息,然后你再去调用初始化函数,HAL库就可以实现你要的功能,对于使用者来说非常方便。但具体为什么这样做就能行,他到底是怎么实现的,这就需要去阅读HAL_GPIO_Init函数,在此之前我们先看看在MX_GPIO_Init里往结构体中存的都是什么值。

typedef struct
{
  uint32_t Pin;        /*!< Specifies the GPIO pins to be configured.
                           This parameter can be any value of @ref GPIO_pins */

  uint32_t Mode;       /*!< Specifies the operating mode for the selected pins.
                           This parameter can be a value of @ref GPIO_mode */

  uint32_t Pull;       /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
                           This parameter can be a value of @ref GPIO_pull */

  uint32_t Speed;      /*!< Specifies the speed for the selected pins.
                           This parameter can be a value of @ref GPIO_speed */

  uint32_t Alternate;  /*!< Peripheral to be connected to the selected pins
                            This parameter can be a value of @ref GPIOEx_Alternate_function_selection */
} GPIO_InitTypeDef;

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};


  __HAL_RCC_GPIOC_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
  /*Configure GPIO pin : PC13 */
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}

2.具体参数的二进制表达

  这个GPIO_PIN_13 对应的二进制是0010 0000 0000 0000,因为我操作的是13引脚,因此他就在第14的地方为1(初始是从0开始,13对应的就是14位),如果我操作的是pin2,那就是0x0003。
  看这个推挽输出写的是什么,注意看代码的注释,HAL 库使用32位的字段同时存储:基本工作模式(输入/输出/复用/模拟)、输出类型(推挽/开漏)、外部中断配置、触发方式,这个代码中注释标注的什么掩码,这种宏定义是在初始化函数中进行判断状态的,他的二进制代码都是在相应模式下的全1,比如,GPIO_MODE=0000 0000 0000 0000 0000 0000 0000 0011,其他的也是类似,只是占用的不同位,然后用户需要什么模式,或起来就行,这里有一点注意,uL是unsigned long的缩写,这个是32位的,所以你或出来就是32位。剩下的都简单了,本文就不讲了。

#define GPIO_PIN_13                     ((uint16_t)0x2000)  /* Pin 13 selected   */ 
#define GPIO_MODE_OUTPUT_PP             (MODE_OUTPUT | OUTPUT_PP)                          
/* 基本模式 - 占用 bit[1:0] */
#define GPIO_MODE_Pos         0u  // 模式字段起始位
#define GPIO_MODE            (0x3uL << GPIO_MODE_Pos)  // 模式位掩码 (0x00000003)
#define MODE_INPUT           (0x0uL << GPIO_MODE_Pos)  // 00: 输入模式
#define MODE_OUTPUT          (0x1uL << GPIO_MODE_Pos)  // 01: 输出模式
#define MODE_AF              (0x2uL << GPIO_MODE_Pos)  // 10: 复用模式
#define MODE_ANALOG          (0x3uL << GPIO_MODE_Pos)  // 11: 模拟模式

/* 输出类型 - 占用 bit[4] */
#define OUTPUT_TYPE_Pos      4u  // 输出类型起始位
#define OUTPUT_TYPE          (0x1uL << OUTPUT_TYPE_Pos) // 输出类型掩码 (0x00000010)
#define OUTPUT_PP            (0x0uL << OUTPUT_TYPE_Pos) // 0: 推挽输出
#define OUTPUT_OD            (0x1uL << OUTPUT_TYPE_Pos) // 1: 开漏输出

/* 外部中断模式 - 占用 bit[17:16] */
#define EXTI_MODE_Pos        16u // EXTI 模式起始位
#define EXTI_MODE            (0x3uL << EXTI_MODE_Pos)  // EXTI 模式掩码 (0x00030000)
#define EXTI_IT              (0x1uL << EXTI_MODE_Pos)  // 01: 使能中断
#define EXTI_EVT             (0x2uL << EXTI_MODE_Pos)  // 10: 使能事件

/* 触发方式 - 占用 bit[22:20] */
#define TRIGGER_MODE_Pos     20u // 触发方式起始位
#define TRIGGER_MODE         (0x7uL << TRIGGER_MODE_Pos) // 触发模式掩码 (0x00700000)
#define TRIGGER_RISING       (0x1uL << TRIGGER_MODE_Pos) // 001: 上升沿触发
#define TRIGGER_FALLING      (0x2uL << TRIGGER_MODE_Pos) // 010: 下降沿触发

三、中间层代码

  这个HAL_GPIO_Init是连接用户和底层寄存器的函数,具体详细解释一下这个函数都在干什么,首先position是用来确定是操作哪个引脚,举例,我们操作13引脚,传入的是0010 0000 0000 0000,看while循环,把传入的0010 0000 0000 0000向右移位,不为0时进入循环,iocurrent来获取当前位置,下面if判断,只有不为0才能进入,那就意味着position=13、iocurrent=0010 0000 0000 0000,才能进入下面程序。
  我们看GPIO配置模式,先判断,在上文2中,我们分析到GPIO_MODE是0x00000003,MODE_OUTPUT 是 0x00000001 ,那这个if判断就判断我们写的GPIO_Init->Mode是不是推挽输出模式,接下来就是配置速度寄存器,我发现这基本上都是通用的方法,先读取出来当前寄存器里的值(第一行代码),然后清除相应的位(第二行代码),写入新的值(第三行代码),最后再写回寄存器中(第四行代码)。
  直接举例子:第一行没啥说的,第二行就是把11(二进制)移动到你对应引脚的寄存器下面,比如我操作pin13,那我就得移动26位,也就是position的两倍,不明白的话对着寄存器去数,然后取反,那你操作的地方就是0,其余地方是1,你再与一下,就只把你要操作的地方变为0,其余不变。
  第三行,和第二行的操作一样,把要传入的速度代码,移动到对应的寄存器下面,然后或,那就只改变了对应的寄存器,第四行没啥说的,用的是前文解释过的"->"操作手法。其余的下面代码中我已经把注释标注了,就不再过多赘述。

temp = GPIOx->OSPEEDR;  // 读取当前速度寄存器值
 // 清除目标引脚的速度配置位 (每引脚占2位)
 // GPIO_OSPEEDR_OSPEED0 = 0x3 (掩码)
 temp &= ~(GPIO_OSPEEDR_OSPEED0 << (position * 2u));
 // 设置新的速度值 (GPIO_SPEED_FREQ_LOW/MEDIUM/HIGH/VERY_HIGH)
 temp |= (GPIO_Init->Speed << (position * 2u));
 GPIOx->OSPEEDR = temp;  // 写回寄存器

#define GPIO_OSPEEDR_OSPEED0_Pos       (0U)
#define GPIO_OSPEEDR_OSPEED0_Msk       (0x3UL << GPIO_OSPEEDR_OSPEED0_Pos)      /*!< 0x00000003 */
#define GPIO_OSPEEDR_OSPEED0           GPIO_OSPEEDR_OSPEED0_Msk

  解释一下assert_param,这个是断言,比如assert_param(IS_GPIO_ALL_INSTANCE(GPIOx)),这个断言专门用于验证传入的GPIOx参数是否是一个有效的GPIO端口指针。具体检查:
  a、是否指向STM32芯片上实际存在的GPIO端口(如GPIOA, GPIOB等)
  b、是否在芯片支持的GPIO端口范围内
  c、是否不是空指针(NULL)
  这个在出厂时已经把断言禁止了,你进入assert_param这个函数会得到下面的宏定义,这个已经被禁止了,运行时,这个时不会被运行的,零开销。但如果打开,具体是如何获取问题标志,我在做项目时没用到过,所以也没查过,感兴趣的小伙伴可以自己去查。

#define assert_param(expr) ((void)0U)//禁止断言
#define IS_GPIO_ALL_INSTANCE(INSTANCE) (((INSTANCE) == GPIOA) || \
                                        ((INSTANCE) == GPIOB) || \
                                        ((INSTANCE) == GPIOC) || \
                                        ((INSTANCE) == GPIOD) || \
                                        ((INSTANCE) == GPIOF))
assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));          // 验证GPIO端口是否有效
assert_param(IS_GPIO_PIN(GPIO_Init->Pin));          // 验证引脚掩码是否有效
assert_param(IS_GPIO_MODE(GPIO_Init->Mode));        // 验证模式是否有效

  下面时完整的初始化函数,上面的代码都是从下面的代码中复制出来的,不用怀疑一致性。

void HAL_GPIO_DeInit(GPIO_TypeDef  *GPIOx, uint32_t GPIO_Pin)
{
  uint32_t position = 0x00u;
  uint32_t iocurrent;
  uint32_t tmp;

/* 参数检查 */
  assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));          // 验证GPIO端口是否有效
  assert_param(IS_GPIO_PIN(GPIO_Init->Pin));          // 验证引脚掩码是否有效
  assert_param(IS_GPIO_MODE(GPIO_Init->Mode));        // 验证模式是否有效

  /* 遍历所有引脚 (从0到15) */
  while (((GPIO_Init->Pin) >> position) != 0x00u)
  {
    /* 获取当前引脚位置对应的位掩码 */
    iocurrent = (GPIO_Init->Pin) & (1uL << position);

    /* 如果当前引脚需要配置 */
    if (iocurrent != 0x00u)
    {
      /*--------------------- GPIO模式配置 ------------------------*/
      /* 输出模式或复用功能模式 */
      if (((GPIO_Init->Mode & GPIO_MODE) == MODE_OUTPUT) || 
          ((GPIO_Init->Mode & GPIO_MODE) == MODE_AF))
      {
        /* 检查速度参数有效性 */
        assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));

        /* 配置输出速度 (OSPEEDR寄存器) */
        temp = GPIOx->OSPEEDR;  // 读取当前速度寄存器值
        // 清除目标引脚的速度配置位 (每引脚占2位)
        // GPIO_OSPEEDR_OSPEED0 = 0x3 (掩码)
        temp &= ~(GPIO_OSPEEDR_OSPEED0 << (position * 2u));
        // 设置新的速度值 (GPIO_SPEED_FREQ_LOW/MEDIUM/HIGH/VERY_HIGH)
        temp |= (GPIO_Init->Speed << (position * 2u));
        GPIOx->OSPEEDR = temp;  // 写回寄存器
        
        /* 配置输出类型 (OTYPER寄存器) */
        temp = GPIOx->OTYPER;   // 读取当前输出类型寄存器值
        // 清除目标引脚的输出类型位 (每引脚占1位)
        // GPIO_OTYPER_OT0 = 0x1 (掩码)
        temp &= ~(GPIO_OTYPER_OT0 << position);
        // 从Mode参数中提取输出类型 (OUTPUT_TYPE在bit4)
        // 右移OUTPUT_TYPE_Pos(4)位得到0或1
        temp |= (((GPIO_Init->Mode & OUTPUT_TYPE) >> OUTPUT_TYPE_Pos) << position);
        GPIOx->OTYPER = temp;   // 写回寄存器
      }

      /* 非模拟模式需要配置上拉/下拉 */
      if ((GPIO_Init->Mode & GPIO_MODE) != MODE_ANALOG)
      {
        /* 检查上拉/下拉参数有效性 */
        assert_param(IS_GPIO_PULL(GPIO_Init->Pull));

        /* 配置上拉/下拉电阻 (PUPDR寄存器) */
        temp = GPIOx->PUPDR;  // 读取当前上拉/下拉寄存器值
        // 清除目标引脚的上拉/下拉配置 (每引脚占2位)
        // GPIO_PUPDR_PUPD0 = 0x3 (掩码)
        temp &= ~(GPIO_PUPDR_PUPD0 << (position * 2u));
        // 设置新的上拉/下拉配置
        temp |= ((GPIO_Init->Pull) << (position * 2u));
        GPIOx->PUPDR = temp;  // 写回寄存器
      }

      /* 复用功能模式 */
      if ((GPIO_Init->Mode & GPIO_MODE) == MODE_AF)
      {
        /* 检查复用功能参数有效性 */
        assert_param(IS_GPIO_AF_INSTANCE(GPIOx));
        assert_param(IS_GPIO_AF(GPIO_Init->Alternate));

        /* 配置复用功能 (AFR寄存器) */
        // 选择AFR[0](引脚0-7)或AFR[1](引脚8-15)
        temp = GPIOx->AFR[position >> 3u];
        // 计算在AFR寄存器内的位偏移 (每引脚占4位)
        uint32_t shift = (position & 0x07u) * 4u;
        // 清除目标引脚的复用功能配置
        temp &= ~(0xFu << shift);
        // 设置新的复用功能编号 (AF0-AF15)
        temp |= ((GPIO_Init->Alternate) << shift);
        GPIOx->AFR[position >> 3u] = temp;  // 写回寄存器
      }

      /* 配置基本方向模式 (MODER寄存器) */
      temp = GPIOx->MODER;  // 读取当前模式寄存器值
      // 清除目标引脚的模式配置 (每引脚占2位)
      // GPIO_MODER_MODE0 = 0x3 (掩码)
      temp &= ~(GPIO_MODER_MODE0 << (position * 2u));
      // 设置新的模式 (输入/输出/复用/模拟)
      // 只取低2位(GPIO_MODE掩码)
      temp |= ((GPIO_Init->Mode & GPIO_MODE) << (position * 2u));
      GPIOx->MODER = temp;  // 写回寄存器

      /*--------------------- EXTI模式配置 ------------------------*/
      /* 如果启用了外部中断/事件 */
      if ((GPIO_Init->Mode & EXTI_MODE) != 0x00u)
      {
        /* 配置EXTI源选择 (EXTICR寄存器) */
        // 选择EXTICR寄存器 (每4个引脚一组)
        temp = EXTI->EXTICR[position >> 2u];
        // 计算在EXTICR内的位偏移 (每4位控制一个引脚源)
        uint32_t shift = 8u * (position & 0x03u);
        // 清除当前配置
        temp &= ~(0x0FuL << shift);
        // 设置GPIO端口索引 (如GPIOA=0, GPIOB=1等)
        temp |= (GPIO_GET_INDEX(GPIOx) << shift);
        EXTI->EXTICR[position >> 2u] = temp;  // 写回寄存器

        /* 配置上升沿触发 (RTSR1寄存器) */
        temp = EXTI->RTSR1;  // 读取当前上升沿触发寄存器
        temp &= ~(iocurrent);  // 清除当前引脚位
        if ((GPIO_Init->Mode & TRIGGER_RISING) != 0x00u)
        {
          temp |= iocurrent;  // 设置上升沿触发
        }
        EXTI->RTSR1 = temp;  // 写回寄存器

        /* 配置下降沿触发 (FTSR1寄存器) */
        temp = EXTI->FTSR1;  // 读取当前下降沿触发寄存器
        temp &= ~(iocurrent);  // 清除当前引脚位
        if ((GPIO_Init->Mode & TRIGGER_FALLING) != 0x00u)
        {
          temp |= iocurrent;  // 设置下降沿触发
        }
        EXTI->FTSR1 = temp;  // 写回寄存器

        /* 配置事件屏蔽 (EMR1寄存器) */
        temp = EXTI->EMR1;  // 读取当前事件屏蔽寄存器
        temp &= ~(iocurrent);  // 清除当前引脚位
        if ((GPIO_Init->Mode & EXTI_EVT) != 0x00u)
        {
          temp |= iocurrent;  // 使能事件
        }
        EXTI->EMR1 = temp;  // 写回寄存器

        /* 配置中断屏蔽 (IMR1寄存器) */
        temp = EXTI->IMR1;  // 读取当前中断屏蔽寄存器
        temp &= ~(iocurrent);  // 清除当前引脚位
        if ((GPIO_Init->Mode & EXTI_IT) != 0x00u)
        {
          temp |= iocurrent;  // 使能中断
        }
        EXTI->IMR1 = temp;  // 写回寄存器
      }
    }

    position++;  // 检查下一个引脚位置
  }
}



总结

  本文介绍了stm32的基本框架“基地址+偏移量”、分析了HAL是如何将代码与硬件电路中的寄存器对应起来、从应用层分析了用户配置的模式是如何转变成二进制写入到寄存器中,本文也是我突发奇想,想要去了解一下,这些东西是怎么联系起来的,写下文章用于日后自己复习,如有错误,欢迎大家评论留言,一起进步!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐