3. GPIO驱动开发:从初始化到中断实战

GPIO,说白了就是芯片的「手脚」。你要让LED亮,得靠它;你要读按键,也得靠它。我刚开始用HAL库时,总觉得GPIO太简单,结果在中断优先级上栽过跟头。今天咱们就把GPIO的初始化、读写操作、轮询和中断,一个一个捋清楚。

本章核心脉络:初始化结构体配置 → HAL_GPIO_WritePin/ReadPin → 按键轮询 → 外部中断。一条线走到底。

GPIO驱动开发知识体系 GPIO初始化结构体 HAL_GPIO_WritePin HAL_GPIO_ReadPin 外部中断 EXTI 按键轮询 / 按键中断 → 控制LED 核心:初始化决定模式 → 读写操作实现控制 → 中断实现异步响应

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才响应。这才是嵌入式该有的样子。

配置外部中断分三步:

  1. GPIO配置:模式设为 GPIO_MODE_IT_FALLING(下降沿触发)或 GPIO_MODE_IT_RISING
  2. NVIC配置:使能中断通道,设置优先级
  3. 编写回调函数:在 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%的硬件问题都出在这里。你把这些吃透了,后面的外设驱动就是水到渠成的事。