3. HAL库GPIO驱动开发:从入门到实战
各位同学好,我是老周。今天咱们聊聊GPIO——这玩意儿可以说是STM32最基础、最常用的外设了。你想想看,不管做什么项目,总得跟外部打交道吧?要么控制个LED,要么读个按键状态,这些都离不开GPIO。
我个人习惯把GPIO比作芯片的「手脚」——它负责输出信号去控制外部设备,也负责读取外部信号进来。HAL库把底层的寄存器操作都封装好了,我们直接用API就行。但说实话,光会用API还不够,你得知道背后发生了什么,才能写出靠谱的代码。
本章核心内容:
- HAL_GPIO_Init 配置详解——每个参数到底干啥用
- HAL_GPIO_WritePin 与 ReadPin——输出和输入的正确姿势
- 按键输入检测与消抖——别让抖动坑了你
- 实战:按键控制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