STM32官方固件库代码解读–GPIO



最近闲得无聊,又把 stm32 拿了出来。之前学的时候是看的库函数版本,现在和寄存器版本的一起看感觉比一开始接触的时候看得顺畅多了,详细了解了底层寄存器的功能。之前用 stm32 只是调用函数,看了寄存器版本后对那些实现具体细节的库函数代码产生了兴趣,所以稍微看了看库函数的代码,同时把一些难理解的地方也记录下来。

我用的是 ALIENTEK 的战舰版,看的也是他们的开发指南,这里我就写对库函数代码的理解,寄存器种类和功能等指南里面有的我基本不写了。

一开始是 GPIO,学任何单片机一开始都是流水灯。stm32 的每组 GPIO 有 7 个寄存器,功能这里就不介绍了,网上很多。官方库里关于 GPIO 有 18 个函数,看了几个常用的,如下

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

这些函数除了第一个初始化函数以外其他都很简单,了解一下寄存器的功能就能看明白代码了,这里只写一下初始化函数。
代码如下:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
  uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;
  uint32_t tmpreg = 0x00, pinmask = 0x00;
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
  assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));  

/*---------------------------- GPIO Mode Configuration -----------------------*/
  currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
  if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
  { 
    /* Check the parameters */
    assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
    /* Output mode */
    currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
  }
/*---------------------------- GPIO CRL Configuration ------------------------*/
  /* Configure the eight low port pins */
  if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
  {
    tmpreg = GPIOx->CRL;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = ((uint32_t)0x01) << pinpos;
      /* Get the port pins position */
      currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding low control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos);
        /* Reset the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << pinpos);
        }
        else
        {
          /* Set the corresponding ODR bit */
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
            GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
          }
        }
      }
    }
    GPIOx->CRL = tmpreg;
  }
/*---------------------------- GPIO CRH Configuration ------------------------*/
  /* Configure the eight high port pins */
  if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
  {
    tmpreg = GPIOx->CRH;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = (((uint32_t)0x01) << (pinpos + 0x08));
      /* Get the port pins position */
      currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding high control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos);
        /* Reset the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
        }
        /* Set the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
        {
          GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
        }
      }
    }
    GPIOx->CRH = tmpreg;
  }
}

每个 GPIO 端口有四个配置为,分别是 CNF1,CNF0,MODE1,MODE0,初始化主要就是配置这四位,如下表

这里写图片描述

可以看到输入模式中上拉输入与下拉输入 CNF1 和 CNF0 是一样的,那这两种模式在配置时是怎么区分的呢?其实这是通过配置输出数据寄存器 ODR 来区分的,对应位置 1 为上拉,对应位置 0 为下拉。那这在库函数里又是怎么实现的呢?

先介绍固件库 GPIO 头文件里的两个枚举类型,如下

typedef enum
{ 
  GPIO_Speed_10MHz = 1,
  GPIO_Speed_2MHz, 
  GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

typedef enum
{ GPIO_Mode_AIN = 0x0,
  GPIO_Mode_IN_FLOATING = 0x04,
  GPIO_Mode_IPD = 0x28,
  GPIO_Mode_IPU = 0x48,
  GPIO_Mode_Out_OD = 0x14,
  GPIO_Mode_Out_PP = 0x10,
  GPIO_Mode_AF_OD = 0x1C,
  GPIO_Mode_AF_PP = 0x18
}GPIOMode_TypeDef;

这两个是初始化函数的第二个结构体参数里的两个参数,用于速率和输入输出模式的初始化。第一个速率还好理解,01、10、11 分别对应上表中代表不同的输出速率。第二个就不太看得懂了,模式配置明明只要 4 位数据,怎么有 8 位,这个 8 位数据又到底是什么涵义?我也在网上看到有人问这个问题,他说对着寄存器的配置怎么也看不懂,其实对着寄存器配置是肯定看不懂的,因为这个要对着库函数的具体实现方法来看。

初始化函数里先定义了几个参数,如下

uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00; uint32_t tmpreg = 0x00, pinmask = 0x00;

看变量名就知道大概是用来干嘛的,就不专门解释了,讲具体用法再说。

下面一段用来模式配置

currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
{ 
  /* Check the parameters */
  assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
  /* Output mode */
  currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}

这里用到了之前看不懂的那个枚举量,现在就可以看它的值到底是什么意思了。先把这个 8 位数保留低 4 位,高四位置 0 ,保存到 currentmode 这个变量,然后判断这个 8 位数的第 4 位是不是 1,如果是 1,把 currentmode 和速率按位或之后保存到 currentmode 这个变量中。乍一看这段代码根本不知道是什么意思,只知道现在 currentmode 大概保存的是要配置的模式参数。其实这要和之前那个枚举量结合起来看。

既然检测了第四位是否为 1,那看之前的枚举类型中的数值,四种输出模式对应的值为 0x14、0x10、0x1C、0x18,四种输入模式对应的值为 0x0、0x04、0x28、0x48。可以发现输出模式的第四位都为 1,输入模式的第四位都不为 1,所以只有输出模式会进入 if 语句,因为只有输出模式要配置速率,所以再将 currentmode 和速率按位或就得到了输出模式的配置参数。以 0x14 为例,高四位 0001 是用来检测是否为输出模式,第 2 位和第 3 位是输出模式参数,第 0 位和第一位为 0,在 if 语句里和速率参数拼成一个完整的参数。至于输入模式的参数,它不用进入 if 语句,它的低四位就是它的模式参数,同时下拉和上拉输入枚举值高四位的不同也起到区分的作用,因为之前说过它们的配置参数是一样的。

下面是 CRL 寄存器的配置,对应 GPIO_x的 0~7,每个口四位参数。代码如下:

if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
  {
    tmpreg = GPIOx->CRL;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = ((uint32_t)0x01) << pinpos;
      /* Get the port pins position */
      currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding low control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos);
        /* Reset the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << pinpos);
        }
        else
        {
          /* Set the corresponding ODR bit */
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
            GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
          }
        }
      }
    }
    GPIOx->CRL = tmpreg;
  }

先判断是不是低 8 位的端口,若是就进入继续判断。pinpos 控制判断次数,因为有 8 个口,所以要循环 8 次。pos 的值表示当前判断的是哪一个端口,只有 1 位为 1。接着 pos 和输入参数 GPIO_Pin 按位与赋值给 currentpin,如果 currentpin 的值和 pos 相等,说明要配置当前这个端口,进入 if 语句进行配置。下面这句pos = pinpos << 2,就是把 pinpos 乘上 4 赋给 pos,pos 之后控制移位次数,因为一个口的配置参数有 4 位,所以计数参数要乘 4。下面两句pinmask =((uint32_t)0x0F) << pos;tmpreg &= ~pinmask,是把要配置的这个口对应的四位清零。然后一句tmpreg |= (currentmode << pos),大功告成,要配置的那个端口对应的四位参数设置好了。下面还有两个判断语句是用来判断是否为上拉或下拉输入模式,若是则设置相应的 ODR 位。

下面还有一段是设置 CRH 寄存器的,和设置 CRL 差不多,只是它是端口 8~15。

OK,对 GPIO 的库函数先写这么多吧。