avatar🌌
零醇零醇の小屋

愿你有一天,能与你最重要的人重逢。

摸鱼摸鱼

STM32 HAL库入门教程(二):按键点灯与 GPIO 模式详解

上期说了 单片机点灯的教程,现在我们来学习 GPIO 的特性。

GPIO 就是单片机的 IO 口,有四种模式:数字输入输出、模拟输入输出(部分芯片可能只有模拟输入,这个以后会写( )。

数字模式下的 IO 分为:推挽输出、开漏输出、浮空输入,还可以选择使用 GPIO 内部自带的上拉电阻或者下拉电阻。

听得一脸懵对吧?没事!


1. 推挽输出 vs 开漏输出 ​

简单来说:

  • 推挽输出的时候,IO 口有驱动能力,可以带一些电流不大的负载,可以输出 0 或者 1。
  • 开漏输出的时候,IO 口没有驱动能力,只能输出 0,不能输出 1。

肯定有人问:不能输出 1,只能输出 0,那这输出和接地有什么区别???

看图: 开漏

开漏时:假设 PA0 要输出 1,那么只需要让三极管不导通,电压会被电阻拉上去;假设要输出 0,只需要三极管导通,电压会被三极管拉到地。

推挽时: 推挽

假设要输出 1,只要上半导通;输出 0,只要下半导通即可。而且相比于电阻,三极管的驱动能力更强。 (当然,此处画的三极管只是示意,实际电路应该是 MOS 管,不过这不重要(划去)

因此对于点灯来说,我们需要驱动能力,选推挽输出。


2. 浮空输入与上下拉电阻 ​

那对于按键呢?按键是输入,浮空输入下引脚电平是不确定的,这个时候就需要上下拉电阻了:

上拉下拉

电阻可以把电压在没按下时保持在某个电平;按下时类似三极管导通,拉到另一个电平,从而实现判定。 因此,这里按键我们选择 浮空输入 + 上拉电阻,按钮接在 IO 口和地(GND)之间。


3. CubeMX 工程配置 ​

打开我们熟悉的 CubeMX,选择我们的 STM32F103C8T6,配置好时钟来源、时钟树和调试口:

  • 配置时钟来源(RCC): 图2

  • 配置时钟树(Clock Configuration): 图1

  • 配置调试口(SYS): 图3

接着配置 PC13 为 LED(GPIO_Output),PA0 为按键(GPIO_Input),还有别忘记把 PA0 配置为上拉模式(Pull-up)哦! 图4图5图6图7

配置好工程信息之后,点击生成代码: 图8图9图10

打开生成的 Keil 工程: 图11


4. 认识几个常用的 HAL 库函数 ​

写代码前,我们先认识几个函数:

① 读取引脚电平:HAL_GPIO_ReadPin ​

c
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

这个就是输入模式下往 GPIO 读电平的函数。乍看一下似乎很复杂,别紧张,我们逐步拆解!

返回值是 GPIO_PinState,这是个什么东西呢?

c
/**
  * @brief  GPIO Bit SET and Bit RESET enumeration
  */
typedef enum
{
  GPIO_PIN_RESET = 0u,
  GPIO_PIN_SET
} GPIO_PinState;

是一个枚举类型(星星眼)!里面把 0 定义成 GPIO_PIN_RESET,1 定义成 GPIO_PIN_SET,也就是说你可以认为返回值是 0 或者 1。

第一个参数是 GPIO_TypeDef *GPIOx,这是什么呢?我们看看板子上的 PAx、PBx、PCx、PDx、PEx……也就是说,对应到板子上就是:PAx = GPIOA,PBx = GPIOB……

那第二个参数 uint16_t GPIO_Pin 呢? 我们来看看定义:

c
#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 脚的下标。 合在一起举个例子:PA0 就是 GPIOA, GPIO_PIN_0,PB12 就是 GPIOB, GPIO_PIN_12。 理解了吗,喵~

② 写入引脚电平:HAL_GPIO_WritePin ​

c
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

发现了什么?和上面的参数几乎一致!这个就是输出模式下往 GPIO 写电平的函数。

③ 毫秒级延时:HAL_Delay ​

c
__weak void HAL_Delay(uint32_t Delay);

前面的 __weak 我们暂时不解释,把它看作不存在,即 void HAL_Delay(uint32_t Delay);。 uint32_t Delay 是你需要延时的毫秒数。这个函数在调用之后,会让 CPU 什么都不做,空转 Delay 个 ms 时间,然后再开始干活。


5. 编写按键控制代码与按键消抖 ​

好了,现在开始写代码吧!我们预期:按一下按钮,LED 状态反转一下。 因此,在 main.c 的 while 循环里,我们来写这么几句代码:

c
  /* USER CODE BEGIN 2 */
  uint8_t led_state = 0;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0){
      if(led_state) HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, 0);
      else HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, 1);
      led_state = !led_state;
    }
  }
  /* USER CODE END 3 */
图12

下载、烧录一下! 按按钮,发现确实可以控制灯泡了。但是——这个按一下怎么会闪呢?或者说,按一下怎么状态会切换好几次呢?

别紧张,让我们来看看按钮按下去时候发生了什么。我们来看这张图: 图13

图片展示了按钮按下去时发生的过程:按下去之后,由于按钮的机械抖动,会导致多次判别;而且在手指还没松开期间,while(1) 循环跑得飞快,也会重复进入判断。

怎么解决呢?

  1. 第一种办法(硬件消抖):在按钮旁边加一个小电容,电容吸收抖动。
  2. 第二种方法(软件消抖 + 松手检测):我们让第一次检测到按下后,等待 20ms 左右丢弃掉抖动的这部分时间,并用 while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0); 等待手指松开后再执行状态切换。

显然方案二很经济,就是比较掉头发喵(

那么代码改成:

c
  /* USER CODE BEGIN 2 */
  uint8_t led_state = 0;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0){
      HAL_Delay(20);
      while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0);
      if(led_state) HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, 0);
      else HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, 1);
      led_state = !led_state;
    }
  }
  /* USER CODE END 3 */

现在就没有这个问题了!

完结撒花!(=^・ω・^=) ​

你已经学会了 GPIO 的操作了!

STM32 HAL库入门教程(一):从零开始做点灯大师