3. 按键输入与中断:从抖动到精准控制

按键,看起来最简单的外设,其实坑不少。我刚开始做项目时,总觉得按键不就是个开关嘛,结果被“抖动”折磨得够呛。这一章,咱们就把按键消抖、外部中断、NVIC优先级这些硬骨头啃下来,最后用按键控制LED做个实战。

3.1 按键消抖原理

机械按键按下和松开时,触点不会瞬间稳定。你想想看,金属片碰撞会产生一连串的“弹跳”,持续时间大概5~20ms。如果不处理,一次按下可能被识别成好几次。

消抖的本质:等待信号稳定后再读取状态。

消抖方式主要有两种:

方式 原理 适用场景
硬件消抖 RC低通滤波 + 施密特触发器 对成本不敏感、要求高可靠
软件消抖 延时20ms后再次读取 绝大多数嵌入式项目

我个人习惯用软件消抖,成本低、灵活。核心思路就一句话:检测到电平变化后,等20ms再读一次,确认还是那个电平,才算有效。

避坑指南:我曾经在一个项目中,按键接的是长线,干扰很大。20ms延时不够,最后加到了50ms才稳定。具体延时值,建议用示波器实测你的按键波形。

3.2 外部中断配置

轮询按键太浪费CPU了。你想,主循环一直在查按键状态,啥正事都干不了。外部中断就是解决方案——按键按下时,CPU才响应。

STM32的外部中断,说白了就是GPIO引脚检测到电平变化,触发一个中断请求。配置步骤我总结为四步:

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置GPIO为输入模式(上拉或下拉看电路)
  3. 将GPIO连接到EXTI线(通过SYSCFG)
  4. 配置EXTI触发方式(上升沿、下降沿或双边沿)

这里有个关键点:EXTI线0~15对应GPIO的Pin0~Pin15。比如PA0和PB0不能同时用EXTI0,因为它们是共享一条中断线的。

注意:同一个EXTI线只能映射一个GPIO引脚。如果你需要多个按键,必须用不同的Pin,比如PA0用EXTI0,PA1用EXTI1。

代码示例(以PA0为例,下降沿触发):

// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// 2. 配置PA0为输入,上拉
GPIOA->MODER &= ~(3 << 0);  // 输入模式
GPIOA->PUPDR |= (1 << 0);   // 上拉

// 3. 连接EXTI0到PA0
SYSCFG->EXTICR[0] &= ~(0xF << 0);
SYSCFG->EXTICR[0] |= (0 << 0);  // PA口

// 4. 配置EXTI
EXTI->IMR |= (1 << 0);    // 不屏蔽
EXTI->FTSR |= (1 << 0);   // 下降沿触发

3.3 中断优先级与NVIC设置

中断优先级,说白了就是“谁更紧急”。STM32的NVIC支持最多16级抢占优先级和16级子优先级(具体看芯片型号)。

我见过不少新手,所有中断都设成一样的优先级,结果高频率中断把低优先级任务饿死了。嗯,这里要讲清楚两个概念:

  • 抢占优先级:数值越小,优先级越高。高抢占优先级可以打断低抢占优先级的中断。
  • 子优先级:当抢占优先级相同时,子优先级决定谁先执行,但不能互相打断。

配置NVIC的步骤:

  1. 调用NVIC_PriorityGroupConfig()设置优先级分组
  2. 设置每个中断的抢占优先级和子优先级
  3. 使能中断

举个例子,按键中断优先级设得比定时器高,这样按键响应更及时:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  // 2位抢占,2位子优先级

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  // 最高抢占
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

我的经验:中断服务函数里尽量少做事。我习惯只设一个标志位,主循环里再处理。这样中断占用时间短,系统响应更快。

3.4 按键控制LED实战

理论讲完了,咱们动手。目标:按下按键,LED状态翻转。

整体流程我用一张图来说明:

按键控制LED中断流程图 按键按下 硬件抖动 触发EXTI中断 中断服务函数 软件消抖20ms 设置按键标志位 主循环检测标志位 → 翻转LED 中断里只设标志 主循环里做动作

完整代码实现:

volatile uint8_t key_flag = 0;  // 按键标志位

void EXTI0_IRQHandler(void)
{
    if(EXTI->PR & (1 << 0))
    {
        // 软件消抖:延时20ms
        delay_ms(20);
        
        // 再次确认按键是否真的按下
        if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
        {
            key_flag = 1;  // 设置标志位
        }
        
        EXTI->PR = (1 << 0);  // 清除中断标志
    }
}

int main(void)
{
    // 初始化...
    LED_Init();
    KEY_Init();
    NVIC_Configuration();
    
    while(1)
    {
        if(key_flag)
        {
            key_flag = 0;  // 清除标志
            LED_Toggle();  // 翻转LED
        }
        // 其他任务...
    }
}

重要提醒:中断服务函数里用delay_ms(20)会阻塞CPU。如果系统里还有其他中断,这个延时会影响响应。更好的做法是用定时器做延时,或者用状态机。不过对于入门学习,这样写最直观。

实战中还有几个细节要注意:

  • 按键电路:外部加上拉电阻(10kΩ),防止浮空电平误触发
  • 中断标志:一定要在中断退出前清除,否则会一直触发
  • 优先级:按键中断优先级别设太高,否则可能影响系统关键中断

好了,这一章的内容就这些。按键消抖、外部中断、NVIC优先级,这三个知识点串起来,你就能做出一个响应灵敏、稳定可靠的按键控制系统。下次遇到按键问题,记得先查抖动,再查中断配置。