3. HAL库GPIO驱动开发:从入门到实战

各位同学好,我是老周。今天咱们聊聊GPIO——这玩意儿可以说是STM32最基础、最常用的外设了。你想想看,不管做什么项目,总得跟外部打交道吧?要么控制个LED,要么读个按键状态,这些都离不开GPIO。

我个人习惯把GPIO比作芯片的「手脚」——它负责输出信号去控制外部设备,也负责读取外部信号进来。HAL库把底层的寄存器操作都封装好了,我们直接用API就行。但说实话,光会用API还不够,你得知道背后发生了什么,才能写出靠谱的代码。

本章核心内容:

  • HAL_GPIO_Init 配置详解——每个参数到底干啥用
  • HAL_GPIO_WritePin 与 ReadPin——输出和输入的正确姿势
  • 按键输入检测与消抖——别让抖动坑了你
  • 实战:按键控制LED——把知识串起来
GPIO驱动开发 HAL_GPIO_Init 配置 WritePin / ReadPin 按键检测与消抖 Mode / Pull / Speed / Alternate GPIO_PIN_SET / GPIO_PIN_RESET 硬件消抖 vs 软件消抖 实战:按键控制LED

3.1 HAL_GPIO_Init 配置详解

先看这个函数原型:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

第一个参数指定哪组GPIO,比如GPIOA、GPIOB。第二个参数是个结构体指针,里面装着配置信息。说白了,你告诉HAL库:「我要用哪个引脚,干什么用,怎么用」,它帮你把寄存器设好。

来看看这个结构体长什么样:

typedef struct {
  uint32_t Pin;        // 引脚号,可以是 GPIO_PIN_0 ~ GPIO_PIN_15
  uint32_t Mode;       // 模式:输入、输出、复用、模拟
  uint32_t Pull;       // 上下拉:无、上拉、下拉
  uint32_t Speed;      // 速度:低速、中速、高速、极速
  uint32_t Alternate;  // 复用功能编号,仅复用模式有效
} GPIO_InitTypeDef;

我的小经验:Pin字段可以用或运算组合多个引脚,比如 GPIO_PIN_0 | GPIO_PIN_1。但要注意,这些引脚的模式必须一样。如果不一样,就得分开调用HAL_GPIO_Init。

3.2 Mode模式详解

Mode有几种取值,我列个表方便你对照:

宏定义 说明 典型用途
GPIO_MODE_INPUT 0x00 浮空输入 读取按键、传感器信号
GPIO_MODE_OUTPUT_PP 0x01 推挽输出 控制LED、继电器
GPIO_MODE_OUTPUT_OD 0x02 开漏输出 I2C总线、电平转换
GPIO_MODE_AF_PP 0x03 复用推挽 USART_TX、SPI_MOSI
GPIO_MODE_AF_OD 0x04 复用开漏 I2C_SCL、I2C_SDA
GPIO_MODE_ANALOG 0x05 模拟模式 ADC输入、DAC输出
GPIO_MODE_IT_RISING 0x06 上升沿中断 按键中断
GPIO_MODE_IT_FALLING 0x07 下降沿中断 按键中断
GPIO_MODE_IT_RISING_FALLING 0x08 双边沿中断 编码器输入

嗯,这里要注意:中断模式虽然也在Mode里,但它还涉及NVIC配置,我们后面专门讲中断时会细说。今天先聚焦在普通输入输出上。

3.3 Pull和Speed参数

Pull(上下拉):这个参数很关键。我遇到过不少新手,按键没接上拉电阻,又把Pull设成NOPULL,结果引脚电平飘忽不定,读出来的数据时高时低。你想想看,浮空输入状态下,引脚就像一根悬空的天线,外界一点点干扰都能让它跳变。

所以我的建议是:

  • 按键接GND,内部上拉(GPIO_PULLUP)——按下读低电平
  • 按键接VCC,内部下拉(GPIO_PULLDOWN)——按下读高电平
  • 如果外部已经有上拉/下拉电阻,设成NOPULL就行

Speed(速度):这个参数只对输出模式有效。速度越高,信号边沿越陡,但功耗和EMI也越大。我一般这样选:

  • 控制LED、继电器等低速设备:GPIO_SPEED_FREQ_LOW
  • SPI、USART等通信:GPIO_SPEED_FREQ_HIGH
  • 实在拿不准:GPIO_SPEED_FREQ_MEDIUM,万金油选择

避坑指南:我曾经在一个项目里把SPI的SCLK引脚设成了LOW速度,结果波形上升沿太缓,从设备死活不认。查了两天才发现是速度设置的问题。从那以后,通信引脚我至少用MEDIUM。

3.4 HAL_GPIO_WritePin 与 ReadPin

这两个函数简单,但用法上有讲究。

// 输出函数
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

// 读取函数
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

PinState有两个取值:GPIO_PIN_SET(高电平)和GPIO_PIN_RESET(低电平)。

举个例子,控制LED:

// 点亮LED(假设低电平点亮)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);

// 熄灭LED
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);

// 读取按键状态
GPIO_PinState keyState = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1);
if(keyState == GPIO_PIN_RESET) {
  // 按键按下
}

还有个常用的技巧——翻转电平:

// 用异或实现翻转
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 
  (GPIO_PinState)!HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0));

不过HAL库没有直接提供翻转函数,我习惯自己封装一个:

void GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) {
  HAL_GPIO_WritePin(GPIOx, GPIO_Pin, 
    (GPIO_PinState)(!HAL_GPIO_ReadPin(GPIOx, GPIO_Pin)));
}

3.5 按键输入检测与消抖

按键检测看起来简单,但坑不少。最典型的问题就是——抖动。

你按下按键时,机械触点不会一下子稳定接触,而是会弹跳几次。这个过程大概持续5-20ms。如果不做处理,一次按下可能会被误判成多次。

消抖有两种方式:

硬件消抖:在按键引脚上加RC滤波电路。简单粗暴,但增加BOM成本。

软件消抖:检测到电平变化后,延时一段时间再读一次。如果两次结果一致,才认为是有效动作。这是最常用的方法。

我一般用这个消抖函数:

uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) {
  // 第一次读取
  if(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) {
    // 延时消抖,10ms足够
    HAL_Delay(10);
    // 第二次读取
    if(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) {
      // 等待按键释放,避免重复触发
      while(HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET);
      return 1;  // 有效按键
    }
  }
  return 0;  // 无效
}

注意:这个函数是阻塞式的,会卡在while循环里等按键释放。如果主循环里还有其他任务要处理,建议用状态机或者定时器来做非阻塞消抖。我们后面章节会讲。

3.6 实战:按键控制LED

好了,理论讲完了,咱们来写个完整的例子。功能很简单:按键按下,LED状态翻转。

硬件连接:

  • PA0 接按键到GND(内部上拉)
  • PB0 接LED(串联220Ω电阻到VCC,低电平点亮)

完整代码:

#include "stm32f1xx_hal.h"

// 按键和LED的引脚定义
#define KEY_PIN     GPIO_PIN_0
#define KEY_PORT    GPIOA
#define LED_PIN     GPIO_PIN_0
#define LED_PORT    GPIOB

void GPIO_Init(void) {
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  
  // 配置按键引脚:输入、上拉
  GPIO_InitStruct.Pin = KEY_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(KEY_PORT, &GPIO_InitStruct);
  
  // 配置LED引脚:推挽输出
  GPIO_InitStruct.Pin = LED_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
  
  // 初始状态:LED熄灭
  HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);
}

int main(void) {
  HAL_Init();
  GPIO_Init();
  
  while(1) {
    if(Key_Scan(KEY_PORT, KEY_PIN)) {
      // 翻转LED状态
      HAL_GPIO_WritePin(LED_PORT, LED_PIN,
        (GPIO_PinState)!HAL_GPIO_ReadPin(LED_PORT, LED_PIN));
    }
  }
}

这段代码跑起来后,你按一下按键,LED就亮;再按一下,LED就灭。是不是很简单?

但我想提醒你一点:实际项目中,按键检测通常不会放在主循环里轮询,而是用中断或者定时器扫描。不过作为入门,轮询方式最容易理解。

本章小结:

  • HAL_GPIO_Init 的核心是配置好Mode、Pull、Speed三个参数
  • 输出用WritePin,输入用ReadPin,注意电平逻辑
  • 按键消抖不能省,10ms延时是经验值
  • 实战代码虽然简单,但包含了GPIO开发的基本套路

好了,今天就到这儿。GPIO这部分看似基础,但很多坑都是在基础里埋下的。你写代码的时候多想想:「这个引脚现在是什么状态?会不会受干扰?」——养成这个习惯,能省不少调试时间。


公众号:蓝海资料掘金营,微信deep3321