3. GPIO驱动开发:从初始化到中断实战
GPIO,说白了就是芯片的「手脚」。你要让LED亮,得靠它;你要读按键,也得靠它。我刚开始用HAL库时,总觉得GPIO太简单,结果在中断优先级上栽过跟头。今天咱们就把GPIO的初始化、读写操作、轮询和中断,一个一个捋清楚。
本章核心脉络:初始化结构体配置 → HAL_GPIO_WritePin/ReadPin → 按键轮询 → 外部中断。一条线走到底。
3.1 GPIO初始化结构体——别小看这几行配置
每个GPIO在使用前,都得先「打招呼」。HAL库用GPIO_InitTypeDef这个结构体来完成配置。我见过不少新手,直接复制粘贴,结果Pin脚不对、模式搞错,查半天。
结构体长这样:
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0; // 选哪个引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不上拉不下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
这里有几个关键点,我挨个说:
| 成员 | 常用值 | 我的经验 |
|---|---|---|
| Pin | GPIO_PIN_0 ~ GPIO_PIN_15 或 GPIO_PIN_All |
可以用 | 组合多个引脚,比如 GPIO_PIN_0 | GPIO_PIN_1 |
| Mode | OUTPUT_PP / OUTPUT_OD INPUT / AF_PP / AF_OD |
控制LED用推挽输出;读按键用输入;I2C用开漏输出 |
| Pull | NOPULL / PULLUP / PULLDOWN | 按键常用上拉,省外部电阻;浮空输入容易误触发 |
| Speed | LOW / MEDIUM / HIGH / VERY_HIGH | 普通IO用LOW就够,高速通信才需要HIGH |
我曾经踩过的坑:初始化结构体一定要先清零!用 GPIO_InitStruct = {0} 或 memset。否则结构体里的随机值会导致配置异常,芯片直接跑飞。别问我怎么知道的……
3.2 HAL_GPIO_WritePin 与 HAL_GPIO_ReadPin——最常用的两个函数
这两个函数,你写100个工程,99个都会用到。用法极其简单:
// 写引脚:控制LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 高电平,LED亮
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 低电平,LED灭
// 读引脚:读取按键
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
if (state == GPIO_PIN_RESET) {
// 按键按下(假设低电平有效)
}
有个小技巧:想翻转引脚,不用先读再写。直接用 HAL_GPIO_TogglePin:
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); // 一键翻转
我在项目中经常用这个函数做调试——让LED闪烁,看一眼就知道程序有没有跑到这里。
3.3 按键轮询——简单但别忘消抖
轮询就是主循环里不断读引脚。代码写起来很直接:
while (1) {
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {
HAL_Delay(20); // 消抖
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {
// 确认按键按下
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET); // 等待松手
}
}
}
这里有个问题:while等待松手会阻塞整个程序。我建议加个松手标志,或者用状态机。不过初学者先跑通这个,再优化。
我的习惯:消抖延时20ms就够了。太短消不掉抖动,太长手感变差。如果你用RTOS,可以用信号量替代延时阻塞。
3.4 按键中断——别再傻等了
轮询的缺点很明显:CPU一直在查,浪费。中断就不一样——按键按下,CPU才响应。这才是嵌入式该有的样子。
配置外部中断分三步:
- GPIO配置:模式设为
GPIO_MODE_IT_FALLING(下降沿触发)或GPIO_MODE_IT_RISING - NVIC配置:使能中断通道,设置优先级
- 编写回调函数:在
HAL_GPIO_EXTI_Callback里写处理逻辑
代码示例:
// 1. 初始化(在MX_GPIO_Init中)
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 2. NVIC配置(HAL自动生成,也可手动)
HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
// 3. 回调函数(写在用户代码区)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_1) {
// 注意:中断里不要用HAL_Delay!
// 设置一个标志,主循环处理
key_pressed = 1;
}
}
重要提醒:中断服务函数里别做耗时操作!我见过有人在中断里加 HAL_Delay(1000),结果系统直接卡死。中断里只设标志,具体处理放主循环。
3.5 轮询 vs 中断——怎么选?
我直接给结论:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 按键少(1-2个),系统简单 | 轮询 | 代码简单,调试方便 |
| 按键多,或需要快速响应 | 中断 | 不阻塞主循环,响应及时 |
| 低功耗场景 | 中断 | CPU可以休眠,按键唤醒 |
| 调试阶段 | 轮询 | 容易加打印,定位问题快 |
我个人习惯:原型阶段用轮询,快速验证;正式产品用中断,省电又高效。
3.6 实战:按键控制LED——两种实现
咱们做个完整例子:按键PB1按下,LED PA0翻转。
方案一:轮询实现
int main(void) {
HAL_Init();
MX_GPIO_Init(); // 包含LED和按键的初始化
while (1) {
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {
HAL_Delay(20);
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
}
}
}
}
方案二:中断实现
volatile uint8_t key_flag = 0;
int main(void) {
HAL_Init();
MX_GPIO_Init();
HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
while (1) {
if (key_flag) {
key_flag = 0;
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
}
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_1) {
// 中断里只设标志,不处理具体逻辑
key_flag = 1;
}
}
调试小技巧:如果中断不触发,先查NVIC有没有使能。我习惯在回调函数第一行加个GPIO翻转——用示波器看波形,比看代码快多了。
3.7 常见故障与定位
最后,我把这些年遇到的GPIO问题列个清单:
- LED不亮:检查GPIO模式是不是OUTPUT_PP,检查引脚有没有被其他外设复用
- 按键没反应:先量电压,看看按下时电平有没有变化。没有变化?检查上拉电阻
- 中断不触发:查NVIC使能、查中断线配置、查回调函数名有没有写错
- 误触发:加硬件滤波(RC电路)或软件消抖
- 引脚电平不对:检查初始化顺序——先初始化时钟,再初始化GPIO
嗯,GPIO这部分就这些。看似基础,但80%的硬件问题都出在这里。你把这些吃透了,后面的外设驱动就是水到渠成的事。