GPIO概念

GPIO为通用输入输出

输入输出的本质就是上图看到的输入或输出高低电平,达到获取外设信息和控制外设的目的。

通用的含义就是,GPIO可以被多种外设来使用。

APB1和APB2下面连接的就是片上外设

GPIO外部引脚与IO

其中红色框住的部分,就是GPIO所对应的针脚,从A-G一共七组,每组从0-15一共16个针脚。那么我们已经知道了这些针脚,那这些针脚是如何和外部进行关联的呢。就是通过输入和输出来联系的。输入比如按钮,当按钮按下,输入低电平,输出比如LED灯,当输出低电平LED灯亮。

这是按键的例子

这是LED灯的例子

到这里我们已经基本了解了GPIO和针脚之间的关系。简单总结就是,有七组GPIO,每组包含16个针脚,针脚连接外部设备,使用GPIO来和MCU内部进行通讯,下来我们来介绍一个针脚下的电路原理图

这个电路图我们可以大概的拆为两部分,一部分为上面的输入,一部分为下面的输出,我们会分别来进行介绍。

这里一定注意,我们现在研究的是GPIO中的一个针脚,一个GPIO中有16个这样的电路图。

I/O端口保护

这一部分就是对端口进行保护,当输入电压过大,与VDD形成较大电压差,则电流会从上面二极管流走,当电压过小时,则下面二极管导通,从而不让电压输入MCU内部。

输出

到这里我们只剩下输入部分和输出部分没有讲解,接下来我们先来讲解输出。

首先我们要知道输出一共包含四种模式,分为通用推挽输出,通用开漏输出,复用推挽输出,复用开漏输出。通用就是指由CPU直接进行输出,复用指由其他片上外设进行输出。我们下来把上图简化一下

其中由于我们现在只研究一个针脚,所以输出寄存器只有一位。复用器的作用是从多组输入中,选择一个作为有效输入。即在输出过程中,是选择ARM-CPU的进行输出还是其他外设进行输出,就是它说了算。输出控制器就是驱动MOS管的栅极,从而将信号转换为高低电平输出出去。

P-MOS和N-MOS

想要理解输出电路,前面我们解释的东西都非常简单,最核心的就在这里。

这是两个MOS管的电路图,G就是上面我们说的输出控制器驱动的栅极。S为源极,D为漏极,这里我建议大家当作目的地来记也可以。P-MOS管的P为positive,当栅极施加电压,电流的流向为S->D,N-MOS管的N为negative,当栅极释放电压,电流的留向为D->S。

在我们的电路当中,是这样的

我们把整体的图就可以改为

通用推挽输出

当P-MOS管连通,N-MOS管断开,相当于输出高电平。当N-MOS管连通,P-MOS管断开,相当于输出低电平。

如果两个同时连通呢?

如果同时连通,就会造成短路的情况。

推挽输出流程

首先,ARM-CPU执行用户代码指令,用户要求向寄存器中写入1,CPU执行用户代码,写入1,复用器选择信号输入源是寄存器,输出控制读取到1,将P-MOS打开,N-MOS不进行操作,输出1。

通用开漏输出

通用开漏输出就是将P-MOS永久关闭,只使用N-MOS管,当N-MOS管闭合输出0,当N-MOS管断开,输出高阻抗,我们一般会在外面接一个上拉电阻,使其输出1。

这里为什么要使用上拉电阻呢?

当N-MOS管闭合时,如果没有上拉电阻,就会造成短路。

复用推挽输出和复用开漏输出,就是把输出源改为了其他片上外设。

输入

我们还是提取出来输入部分

再进一步简化

输入部分分为了上拉输入,下拉输入,浮空输入和模拟输入。

这里的上拉电阻,下拉电阻和上面的一样,为特定情况提供高低电平。TTL施密特触发器,由于输入的电平是模拟信号,就用它让电平变得规整。

阈值内不改变0/1值。

上拉输入

即上拉电阻闭合,下拉电阻断开

当输入高电平时,没有电压差,故直接输入高电平,核心在于下面讲的输入低电平。

为什么这里不会把低电平拉高呢?这是由于上拉电阻的值很大,会把压分完。

当引脚不接设备时,由于上拉电阻的存在,会输入1

下拉输入

此时上拉电阻断开,下拉电阻闭合

当输入高电平时,为什么不会拉为低电平呢,和上面是同样的道理。

输入低电平,同样没有电压差。

这里当引脚不接东西时,由于下拉电阻存在,默认输入0

浮空输入

当我们不使用上拉电阻,也不使用下拉电阻,这就叫浮空输入。这里输入1/0大家应该都明白。主要在引脚不接设备时,输入值是不确定的。

模拟输入

模拟输入就是不经过TTL,直接把模拟信号直接接收。

GPIO寄存器

首先明确,我们上面讲解的是一个针脚,接下来的寄存器是一个GPIO中,即16个针脚共用。

这两个寄存器是对针脚进行的最基本的信息配置,也就是我们上面讲的,输入输出,和输入/输出模式进行配置。

CNF

MODE

其中CRL配置的是针脚0-7,CRH是针脚8-15,每一个针脚对应四位。

代码与源码

我们先来一个最简单的程序,把LED灯点亮

这是我们板子中LED灯对应的针脚。我们使用CubeMX进行快速配置,为开漏模式,默认为高电平(熄灭)。

HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, 0); 
uint16_t gleds[] = {GPIO_PIN_8,GPIO_PIN_9,GPIO_PIN_10,GPIO_PIN_11};
const uint16_t gleds_num = sizeof(gleds)/sizeof(gleds[0]);
while(1)
{
    for(int i = 0; i < gleds_num; i++)
    {
      HAL_GPIO_TogglePin(GPIOF,gleds[i]);
      HAL_Delay(150);
      HAL_GPIO_TogglePin(GPIOF,gleds[i]);
    }
    for(int i = gleds_num-1; i >=0; i--)
    {
      HAL_GPIO_TogglePin(GPIOF,gleds[i]);
      HAL_Delay(150);
      HAL_GPIO_TogglePin(GPIOF,gleds[i]);
    }
}

这里面的GPIOF其实就是一个地址,GPIO_PIN_x这个对应的是16个针脚,它的位数就代表他是几号针脚,这个设计非常巧妙,我们后面介绍。

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;
  }
}

这个代码是设置输出的一个GPIO接口,首先三个参数为第一个是对应哪个GPIO的地址,第二个是我上面说的针脚,第三个是设置为0/1,开始两行代码是检查合法的。接下来是if判断,如果要输出的值为1,则把BSRR寄存器改为这个针脚,否则,把BSRR寄存器高16位改为对应针脚。这里我们来认识一下BSRR寄存器。

这个寄存器靠硬件实现对ODR寄存器的赋值,ODR寄存器就是我们要进行输出的寄存器。

HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, 0); 

比如在这句代码中,我们要对GPIOF8输出改为0,代码会把GPIOF的BSRR高16位改为0000000100000000,然后同步到ODR中,把pin8对应的寄存器改为0

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;

我们对GPIO的操作其实就是对寄存器进行操作。

#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)

我刚才说GPIOx本质就是一个地址,然后转为了结构体类型,寄存器连续进行存放。

void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
  uint32_t odr;

  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  /* get current Output Data Register value */
  odr = GPIOx->ODR;

  /* Set selected pins that were at low level, and reset ones that were high */
  GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin);
}

这段源码本质就是对状态进行反转。使用本来的状态&引脚号,得到该引脚当前的状态,随后左移16位,若本来为1,就会改为0,若本来为0,就用到了后面的逻辑,得到当前当前状态的相反值,放在低16位,把上面ODR会设为1,这里我一直说这个就是一个地址,为什么?

因为这里所有的设备,都占着空间所以我们一切的操作都是在地址之上完成的。

接下来我们讲解GPIO的初始化源码

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* USER CODE BEGIN MX_GPIO_Init_1 */

  /* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11, GPIO_PIN_SET);

  /*Configure GPIO pins : PF8 PF9 PF10 PF11 */
  GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);

  /* USER CODE BEGIN MX_GPIO_Init_2 */

  /* USER CODE END MX_GPIO_Init_2 */
}

首先第一步定义一个结构体,我们在后面把配置的数据写入寄存器就要使用这个结构体,再往后两行RCC我们先不关注,再往下是设置开始的引脚状态,我们上面已经讲解,再往下是给结构体的赋值,最后,也是最关键的一步,把初始化数据写入寄存器。

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;

这是结构体的内容,第一个就是哪个引脚,第二个是输入还是输出,第三个是有无上下拉,第四个是输出速度。

这个代码我先讲解核心,第一个参数是哪个GPIO,第二个参数是刚才的结构体。我们直接来看while循环,判断语句用来判断后面还有没有要设置的针脚,进来ioposition用来表明当下设置的是哪个针脚,iocurrent表示传入数据中这个针脚用不用设计,相等则需要配置,进入条件,最后position++逻辑上实现对pin整体右移。这里我们也理解了,为什么我们在配置时可以用|来处理相同配置的针脚。

if里面主要有这几行,首先使用的什么模式,然后得到config,这里的config刚好就是要写入的数据,这和Speed,模式宏定义的数字有关。得到config后,先configregister来确定往哪个寄存器进行配置,registeroffset来确定往这个寄存器的哪几位去配置。

#define MODIFY_REG(REG, CLEARMASK, SETMASK)  WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))

随后进行写入,这里第二个参数对要写入位置全部为1,第三个得到要写入位置实际写入值。随后在下面代码中,二和三结合起来,首先把要写入位置全部置0,然后|实际值,把值写入寄存器。

GPIO按键输入

首先在我们的电路中每一个按键都有一个电容,它的作用就是进行消抖,因为我们在操作中,会存在抖动问题。

当然我们也可以通过软件来完成消抖,我们在后面进行介绍。

我们使用的是浮空输入

typedef enum
{
  LED1 = 1,
  LED2,
  LED3,
  LED4
}LED_t;

typedef enum
{
  KEY1 = 1,
  KEY2,
  KEY3,
  KEY4
}KEY_t;
void BSP_LED_Control(LED_t led, bool flag);
void BSP_Led_On(LED_t led);
void BSP_Led_Off(LED_t led);
GPIO_PinState BSP_KEY_Control_V1(KEY_t key);
bool BSP_KEY_Control_V2(KEY_t key);
while
{
    BSP_KEY_Control_V2(KEY1) ? BSP_Led_On(LED1) : BSP_Led_Off(LED1);
    BSP_KEY_Control_V2(KEY2) ? BSP_Led_On(LED2) : BSP_Led_Off(LED2);
    BSP_KEY_Control_V2(KEY3) ? BSP_Led_On(LED3) : BSP_Led_Off(LED3);
    BSP_KEY_Control_V2(KEY4) ? BSP_Led_On(LED4) : BSP_Led_Off(LED4);
}
//关于KEY

bool BSP_KEY_Control_V2(KEY_t key)
{
  bool ret = false;
  switch(key)
  {
    case KEY1:
    {
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_10) == GPIO_PIN_RESET)
    {
      HAL_Delay(20);
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_10) == GPIO_PIN_RESET)
    {
      ret = true;
    }
    }
    }
    break;
    case KEY2:
    {
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_6) == GPIO_PIN_RESET)
    {
      HAL_Delay(20);
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_6) == GPIO_PIN_RESET)
    {
      ret = true;
    }
    }
    }
    break;
    case KEY3:
    {
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_8) == GPIO_PIN_RESET)
    {
      HAL_Delay(20);
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_8) == GPIO_PIN_RESET)
    {
      ret = true;
    }
    }
    }
    break;
    case KEY4:
    {
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_7) == GPIO_PIN_RESET)
    {
      HAL_Delay(20);
    if(HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_7) == GPIO_PIN_RESET)
    {
      ret = true;
    }
    }
    }
    break;
    default:
    break;
  }
  return ret;
}
//关于LED
void BSP_LED_Control(LED_t led, bool flag)
{
  GPIO_PinState state = flag ? GPIO_PIN_RESET : GPIO_PIN_SET;
  switch(led)
  {
    case LED1:
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, state);
    break;
    case LED2:
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, state);
    break;
    case LED3:
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, state);
    break;
    case LED4:
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_11, state);

    break;
    default:
    break;
  }
}
void BSP_Led_On(LED_t led)
{
  BSP_LED_Control(led,true);
}
void BSP_Led_Off(LED_t led)
{
  BSP_LED_Control(led,false);
}

这里是通过HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_10)来获取状态的,在根据状态对LED灯做出响应。

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
  GPIO_PinState bitstatus;

  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)GPIO_PIN_RESET)
  {
    bitstatus = GPIO_PIN_SET;
  }
  else
  {
    bitstatus = GPIO_PIN_RESET;
  }
  return bitstatus;
}

我们讲解过之前的代码,这里就很简单了,获取到输入寄存器对应位数的值,并进行返回。

Logo

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

更多推荐