3. STM32与HAL库基础

按键,可以说是嵌入式开发里最基础、也最容易出问题的外设了。很多初学者觉得按键不就是读个IO电平吗?但真正做产品时你会发现,按键消抖、中断响应、优先级配置,每一个环节都可能让你加班到深夜。今天我就把这块内容掰开了讲,结合我这些年踩过的坑,帮你把按键驱动彻底搞明白。

3.1 按键消抖原理:为什么不能直接读?

先问个问题:你用手按一下按键,从按下到稳定,中间发生了什么?

实际上,机械按键在按下和释放的瞬间,触点会弹跳几次,产生一连串的脉冲信号。这个过程通常持续5-20ms。如果你直接读IO,可能会读到多次电平变化——按一次,程序却认为按了十次。

消抖的本质:跳过弹跳期,只认稳定状态。

消抖有两种主流方式:

方式 原理 适用场景 缺点
硬件消抖 RC低通滤波 + 施密特触发器 对实时性要求高的产品 增加BOM成本
软件消抖 延时采样或定时器去抖 大多数嵌入式项目 占用CPU时间

我个人习惯用软件消抖,毕竟省成本。但要注意,延时消抖不能用阻塞式delay,否则整个系统都被你卡死了。后面我会讲怎么用定时器或状态机来做。

3.2 轮询模式:最简单也最危险

轮询模式,就是主循环里不断读GPIO电平。代码写起来确实简单:

// 轮询模式按键检测(不推荐用于产品)
while (1) {
    if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
        HAL_Delay(20);  // 消抖延时
        if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
            // 按键有效,执行动作
        }
    }
}

这段代码有什么问题?我告诉你,问题大了去了。

  • 阻塞延时:HAL_Delay(20) 让CPU空转20ms,系统响应能力直接下降
  • 无法处理长按:你只能检测到按下,区分不了短按、长按、双击
  • 功耗高:CPU一直在跑,没法进休眠模式

我曾经在一个智能家居项目里用了纯轮询按键,结果用户反馈“按一下灯闪两下”。查了一整天,发现是轮询周期和按键弹跳周期重叠了。从那以后,我对轮询模式就格外谨慎。

轮询模式不是不能用,但只适合非实时、单任务、对功耗不敏感的场景。但凡你的系统里还有别的任务在跑,我建议你直接上中断。

3.3 外部中断EXTI配置:一步步来

EXTI(External Interrupt)是STM32处理外部事件的标准方式。配置起来其实不复杂,但有几个关键点容易忽略。

3.3.1 配置步骤

  1. 使能GPIO时钟:没有时钟,IO口就是死的
  2. 配置GPIO为输入模式:上拉或下拉根据电路决定
  3. 连接EXTI线:GPIO和EXTI线之间的映射,不同引脚对应不同线
  4. 配置中断触发方式:上升沿、下降沿、双边沿
  5. 配置NVIC优先级:中断嵌套全靠它
  6. 编写中断服务函数:ISR里要快进快出

用HAL库的话,大部分工作可以在CubeMX里图形化完成。但你要理解背后发生了什么,不然出了问题都不知道怎么查。

3.3.2 关键参数表

参数 选项 说明
GPIO模式 GPIO_MODE_IT_FALLING 下降沿触发(按键按下)
GPIO模式 GPIO_MODE_IT_RISING 上升沿触发(按键释放)
GPIO模式 GPIO_MODE_IT_RISING_FALLING 双边沿触发
上拉/下拉 GPIO_PULLUP 外部按键接地时使用
优先级分组 NVIC_PRIORITYGROUP_4 4位抢占优先级,0位子优先级

我的习惯:按键按下用下降沿触发,释放用上升沿。如果只关心按下动作,用下降沿就够了,省得ISR被频繁调用。

3.4 中断服务函数编写:快进快出是铁律

中断服务函数(ISR)里最忌讳什么?做复杂的事情。你在ISR里跑个延时、做个浮点运算、调个printf,系统不崩才怪。

正确的做法是:ISR里只做标记,具体处理交给主循环或任务。

// 正确的ISR写法
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == KEY_Pin) {
        // 只设置标志位,不做具体处理
        key_pressed_flag = 1;
        // 记录按下时刻,用于长按检测
        key_press_tick = HAL_GetTick();
    }
}

// 主循环中处理
void Key_Process(void) {
    if (key_pressed_flag) {
        key_pressed_flag = 0;
        // 这里做消抖和按键识别
        uint32_t duration = HAL_GetTick() - key_press_tick;
        if (duration > 50) {  // 消抖阈值
            if (duration < 1000) {
                // 短按
            } else {
                // 长按
            }
        }
    }
}

你看,ISR里只做了两件事:置标志位、记录时间戳。真正的消抖和按键识别放到了主循环里。这样既保证了中断的实时性,又不会阻塞系统。

我曾经在一个项目里把按键处理全塞进了ISR,结果中断嵌套一多,系统直接死机。后来花了三天才定位到问题——ISR里调用了HAL_Delay。记住:中断里绝对不要用延时函数。

3.5 知识体系总览

下面这张图把按键输入与中断的核心流程串起来了,你可以对照着理解:

按键输入与中断知识体系 按键输入与中断知识体系 物理按键 按键消抖 硬件消抖(RC) 软件消抖 读取方式 轮询模式 EXTI中断 中断服务函数 核心原则:ISR快进快出 → 主循环处理 → 状态机管理按键状态

3.6 实战中的避坑指南

最后,我把自己这些年踩过的坑整理成清单,你写代码时对着检查一遍:

  • 中断优先级别乱设:按键中断优先级不要设得太高,否则会抢占系统心跳。我一般设到2或3(数值越小优先级越高)。
  • 别忘了清除中断标志位:HAL库的EXTI回调函数会自动清除,但如果你直接操作寄存器,一定要手动清。
  • 上拉电阻要选对:外部按键接地时用内部上拉,按键接VCC时用内部下拉。搞反了IO电平会一直跳。
  • 消抖时间不是越长越好:20ms是经验值,但不同按键的弹跳特性不一样。我习惯用示波器抓一下实际波形,再定消抖时间。
  • 长按和短按要分开处理:用定时器记录按下时长,而不是在ISR里做延时判断。

嗯,这里再补充一句:如果你要在FreeRTOS里用按键中断,记得在ISR里用xSemaphoreGiveFromISR()或xQueueSendFromISR()来通知任务,千万别在ISR里直接调用任务处理函数。这部分内容后面讲FreeRTOS移植时会详细展开。

按键驱动看似简单,但做好了能体现一个嵌入式工程师的基本功。从轮询到中断,从消抖到状态机,每一步都有讲究。希望今天的内容能帮你少走弯路。