stm32之GPIO库函数点灯分析

stm32官方为了方便开发者,利用CubeMX 生成HAL库有关的C代码。HAL库就是硬件抽象层(hardware abstraction layer),生成一系列的函数帮助我们快速生成工程,脱离复杂的寄存器配置。stm32相对于51来功能强大,但是寄存器的数量也不是一个量级,单靠配置寄存器来做项目的话,进度会非常缓慢。但是在学习阶段还是有必要研究一个寄存器配置或者说研究HAL是如何操作寄存器的。

一、利用CubeMX 生成代码

具体怎么操作的这里就不讲解了,网上一大堆。实验用的开发板用的led,一端接到vcc,一端接到PB9(GPIO_B的第9个引脚),当PB9输出低电平时,led就会亮,反之则灭。

生成的工程中,main函数内调用了MX_GPIO_Init函数,这个是GPIO_B_9引脚的初始化。

函数示例如下:

cpp 复制代码
void MX_GPIO_Init(void)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
  /*Configure GPIO pin : PB9 */
  GPIO_InitStruct.Pin = GPIO_PIN_9;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

}

整个过程就是

  1. 配置**__HAL_RCC_GPIOB_CLK_ENABLE**时钟,
  2. 配置GPIO_InitTypeDef变量,
  3. GPIO_PIN_9进行reset操作,也是给引脚设置成0
  4. HAL_GPIO_Init进行初始化。

1.1、 GPIO_InitTypeDef类型定义

cpp 复制代码
typedef struct
{
  uint32_t Pin;       /*!< Specifies the GPIO pins to be configured.
                           This parameter can be any value of @ref GPIO_pins_define */

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

  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_define */

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

结构体内部有四个成员变量

  • Pin: 要配置的引脚
  • Mode: 要配置的模式,输入或输出
  • Pull: 配置上拉、下拉或者两者都不
  • Speed:这里的speed暂时有三种模式,low(2MHZ), medium(10MHZ), high(50MHZ),后续会有说明

引脚有两种状态,0或1,定义如下

cpp 复制代码
typedef enum
{
  GPIO_PIN_RESET = 0u,
  GPIO_PIN_SET
} GPIO_PinState;

1.2、给GPIOB开启时钟

在stm32中每个片上外设都有对应的时钟,所以在使用时都需要开启。GPIOB时钟开启的代码如下所示

cpp 复制代码
#define __HAL_RCC_GPIOB_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg; \
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);\
                                        /* Delay after an RCC peripheral clock enabling */\
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);\
                                        UNUSED(tmpreg); \
                                      } while(0U)

代码里面就是搞了一个宏进行设置的。其中__IO 也是一个宏,它的类型是volatile,意为"直接存取原始内存地址"。

1.2.1、 SET_BIT是一个宏,作用就是将寄存器的某一个设置成1。
cpp 复制代码
#define SET_BIT(REG, BIT)     ((REG) |= (BIT))

另外所有GPIO是挂载到APB2总线上的,

SET_BIT的第一个参数是APB2ENR,这里就涉及到RCC_APB2ENR寄存器。

所有外设都需要时钟驱动,这里用到的是GPIO_B, 所以要把RCC_APB2ENR中的第3位置1。

SET_BIT的第二个参数是一个宏

cpp 复制代码
#define RCC_APB2ENR_IOPBEN_Pos               (3U)                              
#define RCC_APB2ENR_IOPBEN_Msk               (0x1UL << RCC_APB2ENR_IOPBEN_Pos)  /*!< 0x00000008 */
#define RCC_APB2ENR_IOPBEN                   RCC_APB2ENR_IOPBEN_Msk            /*!< I/O port B clock enable */

它代表的意思是通过移位将第3位(从0开始数)变成1,其它位都是0。

这样一来 **SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN)**代表的意思就是将RCC_APB2ENR寄存器中的第三位置1,也即使能GPIOB。

另外一点是RCC_APB2ENR_IOPBEN 寄存器的获取是通过RCC->APB2ENR这种形式。这里又涉及到RCC的问题。

1.2.2、RCC相关寄存器

RCC内部包括一系列和时钟相关的寄存器,当然也包括APB2ENR。RCC是一个宏,我把相关代码集中到一块如下所示

cpp 复制代码
// 所有外设的基址
#define PERIPH_BASE           0x40000000UL /*!< Peripheral base address in the alias region */

// 这里AHB的基址
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x00020000UL)

// RCC 是挂载到AHB上的
define RCC_BASE              (AHBPERIPH_BASE + 0x00001000UL)

// RCC 包括众多寄存器,这里通过C语言的结构体和指针进行操作,将RCC的地址绑定到RCC_BASE
#define RCC                 ((RCC_TypeDef *)RCC_BASE)

typedef struct
{
  __IO uint32_t CR;
  __IO uint32_t CFGR;
  __IO uint32_t CIR;
  __IO uint32_t APB2RSTR;
  __IO uint32_t APB1RSTR;
  __IO uint32_t AHBENR;
  __IO uint32_t APB2ENR;
  __IO uint32_t APB1ENR;
  __IO uint32_t BDCR;
  __IO uint32_t CSR;


} RCC_TypeDef

通过上面的操作,就可以利用RCC这个指针来操作各个寄存器,RCC内的变量都是按照寄存器的顺序排列的。这里再强制一点,RCC是挂载到AHB总线上的,所以它的地址是相对AHB总线进行偏移的。

1.2.3、READ_BIT

知道了SET_BIT, 那么READ_BIT也清楚了,采取的操作都差不多,作用就是取出指定的位。

将要取出的位置1,其它位全部是0,这样就取出了指定位。

cpp 复制代码
#define READ_BIT(REG, BIT)    ((REG) & (BIT))

1.3、HAL_GPIO_WritePin

这个函数的定义如下:

cpp 复制代码
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;
  }
}

这里可以看到主要是设置寄存器BSRR。这个函数传入的第一个参数是GPIOB,GPIOB的配置和上面说的RCC采用相似的策略

cpp 复制代码
// 所有外设的基址
#define PERIPH_BASE           0x40000000UL /*!< Peripheral base address in the alias region */

// APB2总线基址
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)
// GPIOB总线基址,GPIO是挂载到APB2上的
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x00000C00UL)
// GPIOB 包括众多寄存器,这里通过C语言的结构体和指针进行操作,将GPIOB的地址绑定到GPIOB_BASE
#define GPIOB               ((GPIO_TypeDef *)GPIOB_BASE)

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

BSRR寄存器定义如下:

ODR(端口输出数据寄存器)定义如下:

上面简单来讲

  • 给BSy设置1,ODR对应的位会输出1
  • 给BRy设置1,ODR对应的位会输出0

所以设置将引脚设置成0时需要在BSRR高16位进行操作,输出1 时在BSRR低16位进行操作。

1.4、HAL_GPIO_Init

这里涉及主要的初始化配置,是重中之重。涉及的函数比较多,已经在里面添加了注释。

cpp 复制代码
void HAL_GPIO_Init(GPIO_TypeDef  *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
	//通过这个变量遍历所有的引脚
  uint32_t position = 0x00u;
  uint32_t ioposition;
  uint32_t iocurrent;
  uint32_t temp;
  uint32_t config = 0x00u;
	// 
  __IO uint32_t *configregister; /* Store the address of CRL or CRH register based on pin number */
	// 引脚的偏移量(每个引脚由CNF和MODE总共4位组成)
  uint32_t registeroffset;       /* offset used during computation of CNF and MODE bits placement inside CRL or CRH register */

  /* Check the parameters */
  assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Init->Pin));
  assert_param(IS_GPIO_MODE(GPIO_Init->Mode));

	// 判断后续是否还有引脚设置了1
  while (((GPIO_Init->Pin) >> position) != 0x00u)
  {
    /* 获取引脚的位置 */
    ioposition = (0x01uL << position);

    /* 再次能过Pin获取当前要设置的引脚 */
    iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;

		// 两种方式求得的引脚比较如果相同才进行处理
    if (iocurrent == ioposition)
    {
      /* Check the Alternate function parameters */
      assert_param(IS_GPIO_AF_INSTANCE(GPIOx));

      /* Based on the required mode, filling config variable with MODEy[1:0] and CNFy[3:2] corresponding bits */
			// 根据模式来配置config
      switch (GPIO_Init->Mode)
      {
        /* 推挽输出模式 */
        case GPIO_MODE_OUTPUT_PP:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP;
          break;

        /* 开漏输出模式 */
        case GPIO_MODE_OUTPUT_OD:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_OD;
          break;

        /* 复用推挽模式 */
        case GPIO_MODE_AF_PP:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_PP;
          break;

        /* 复用开漏输出模式  */
        case GPIO_MODE_AF_OD:
          /* Check the GPIO speed parameter */
          assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
          config = GPIO_Init->Speed + GPIO_CR_CNF_AF_OUTPUT_OD;
          break;

        /* 下面是输入,要么事件输入要么中断输入 */
				// 输入悬空
        case GPIO_MODE_INPUT:
				// 中断上升沿
        case GPIO_MODE_IT_RISING:
				// 中断下降沿
        case GPIO_MODE_IT_FALLING:
				// 中断上长或下降沿
        case GPIO_MODE_IT_RISING_FALLING:
				// 事件上长沿
        case GPIO_MODE_EVT_RISING:
				// 事件下降沿
        case GPIO_MODE_EVT_FALLING:
				// 事件上长或下降沿
        case GPIO_MODE_EVT_RISING_FALLING:
          /* Check the GPIO pull parameter */
          assert_param(IS_GPIO_PULL(GPIO_Init->Pull));
          if (GPIO_Init->Pull == GPIO_NOPULL)
          {
            config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_FLOATING;
          }
          else if (GPIO_Init->Pull == GPIO_PULLUP)
          {
            config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD;

            /* Set the corresponding ODR bit */
            GPIOx->BSRR = ioposition;
          }
          else /* GPIO_PULLDOWN */
          {
            config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_INPUT_PU_PD;

            /* Reset the corresponding ODR bit */
            GPIOx->BRR = ioposition;
          }
          break;

        /* 模拟输入 */
        case GPIO_MODE_ANALOG:
          config = GPIO_CR_MODE_INPUT + GPIO_CR_CNF_ANALOG;
          break;

        /* Parameters are checked with assert_param */
        default:
          break;
      }

      /* Check if the current bit belongs to first half or last half of the pin count number
       in order to address CRH or CRL register*/
			/*
				点灯目前没有用到输入模式,只用到输出模式
			1、如果当前的引脚是低8位,那就是么对CRL的操作,否则就是对CRH的操作
			2、如果当前的引脚是低8位,将Postion向左偏移四位,操作的就是当前引脚的MODE和CNF.
				如果是高8位,将(position - 8u)向左偏移四位,操作的就是当前引脚的MODE和CNF.
			*/
      configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL     : &GPIOx->CRH;
      registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);

      /* 清除当前引脚的配置,并设置新配置 */
      MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));

      //  由于没有中断这里的操作是可以删除的
      /*--------------------- EXTI Mode Configuration ------------------------*/
      /* Configure the External Interrupt or event for the current IO */
      if ((GPIO_Init->Mode & EXTI_MODE) == EXTI_MODE)
      {
        /* Enable AFIO Clock */
        __HAL_RCC_AFIO_CLK_ENABLE();
        temp = AFIO->EXTICR[position >> 2u];
        CLEAR_BIT(temp, (0x0Fu) << (4u * (position & 0x03u)));
        SET_BIT(temp, (GPIO_GET_INDEX(GPIOx)) << (4u * (position & 0x03u)));
        AFIO->EXTICR[position >> 2u] = temp;


        /* Enable or disable the rising trigger */
        if ((GPIO_Init->Mode & RISING_EDGE) == RISING_EDGE)
        {
          SET_BIT(EXTI->RTSR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->RTSR, iocurrent);
        }

        /* Enable or disable the falling trigger */
        if ((GPIO_Init->Mode & FALLING_EDGE) == FALLING_EDGE)
        {
          SET_BIT(EXTI->FTSR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->FTSR, iocurrent);
        }

        /* Configure the event mask */
        if ((GPIO_Init->Mode & GPIO_MODE_EVT) == GPIO_MODE_EVT)
        {
          SET_BIT(EXTI->EMR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->EMR, iocurrent);
        }

        /* Configure the interrupt mask */
        if ((GPIO_Init->Mode & GPIO_MODE_IT) == GPIO_MODE_IT)
        {
          SET_BIT(EXTI->IMR, iocurrent);
        }
        else
        {
          CLEAR_BIT(EXTI->IMR, iocurrent);
        }
      }
    }

	position++;
  }
}

代码相关的解释已经写到注释里了,这里有一个比较有意思的点。

开始是GPIO_Init->Speed设置的是GPIO_SPEED_FREQ_LOW,它其实是一个有关MODE的宏,就是和CNY对应的MODE的宏。

cpp 复制代码
#define GPIO_CRL_MODE0_Pos                   (0U)   
                           
// 这个就是50MHZ的输出模式
#define GPIO_CRL_MODE0_Msk                   (0x3UL << GPIO_CRL_MODE0_Pos)      /*!< 0x00000003 */
// 这个就是50MHZ的输出模式
#define GPIO_CRL_MODE0                       GPIO_CRL_MODE0_Msk                /*!< MODE0[1:0] bits (Port x mode bits, pin 0) */
// 这个就是1的输出模式
#define GPIO_CRL_MODE0_0                     (0x1UL << GPIO_CRL_MODE0_Pos)      /*!< 0x00000001 */
// 这个就是2MHZ的输出模式
#define GPIO_CRL_MODE0_1                     (0x2UL << GPIO_CRL_MODE0_Pos)      /*!< 0x00000002 */


#define  GPIO_SPEED_FREQ_LOW              (GPIO_CRL_MODE0_1) /*!< Low speed */
#define  GPIO_SPEED_FREQ_MEDIUM           (GPIO_CRL_MODE0_0) /*!< Medium speed */
#define  GPIO_SPEED_FREQ_HIGH             (GPIO_CRL_MODE0)   /*!< High speed */

所以初始化的Speed其实就是设置了MODE。而对于每个输出模式,在HAL_GPIO_Init函数里都用到这样的操作

cpp 复制代码
// 低二位都是0,为了配合Mode
#define  GPIO_CR_CNF_GP_OUTPUT_PP   0x00000000u /*!< 00: General purpose output push-pull  */
// 低二位都是0,为了配合Mode
#define  GPIO_CR_CNF_GP_OUTPUT_OD   0x00000004u /*!< 01: General purpose output Open-drain  */
// 低二位都是0,为了配合Mode
#define  GPIO_CR_CNF_AF_OUTPUT_PP   0x00000008u /*!< 10: Alternate function output Push-pull  */
// 低二位都是0,为了配合Mode
#define  GPIO_CR_CNF_AF_OUTPUT_OD   0x0000000Cu /*!< 11: Alternate function output Open-drain  */

// 这里就是配置CNF+Mode
config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_PP;

其实就是将MODE和CNF结合到一块共同控制一个引脚。

二、在main()自定义操作

知道了库函数的操作,其实我们自己也可以将Led灯进行点亮或者熄灭,比如

cpp 复制代码
// 熄灭
GPIOB->BSRR = GPIO_PIN_9;
// 点亮
GPIOB->BSRR = GPIO_PIN_9 << 16;

其实最终就是操作的寄存器(不研究也知道操作的寄存器,哈哈哈)。

点灯对寄存器的操作如下:

  1. 配置APB2ENR 开启GPIOB时钟
  2. 配置CRLCRH ,来操作引脚的CNF 和**MODE,**让其作为输入或输出
  3. 配置BSRR 使ODR对应的位输出1或0

通过对stm32内存映射和GPIO寄存器的分析,大致了解了HAL的一些细节,确实单独操作寄存器难度比较大,但是通过对HAL函数的研究,加深了自己对HAL操作的了解以及寄存器细节的了解。

相关推荐
嗯嗯=1 小时前
STM32单片机学习篇9
stm32·单片机·学习
小范馆5 小时前
ESP各模组的引脚图-小智接线图
stm32
松涛和鸣5 小时前
DAY63 IMX6ULL ADC Driver Development
linux·运维·arm开发·单片机·嵌入式硬件·ubuntu
想放学的刺客9 小时前
单片机嵌入式试题(第23期)嵌入式系统电源管理策略设计、嵌入式系统通信协议栈实现要点两个全新主题。
c语言·stm32·单片机·嵌入式硬件·物联网
猫猫的小茶馆9 小时前
【Linux 驱动开发】五. 设备树
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu·硬件工程
YouEmbedded9 小时前
解码内部集成电路(IIC)与OLED屏
stm32·0.96寸oled·硬件iic·软件模拟iic·图片取模·汉字取模
jghhh0110 小时前
基于上海钜泉科技HT7017单相计量芯片的参考例程实现
科技·单片机·嵌入式硬件
恶魔泡泡糖10 小时前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
意法半导体STM3211 小时前
【官方原创】如何基于DevelopPackage开启安全启动(MP15x) LAT6036
javascript·stm32·单片机·嵌入式硬件·mcu·安全·stm32开发
v_for_van11 小时前
STM32低频函数信号发生器(四通道纯软件生成)
驱动开发·vscode·stm32·单片机·嵌入式硬件·mcu·硬件工程