3. 按键输入与中断:GPIO输入模式、按键消抖与EXTI配置

按键,是嵌入式系统里最基础、也最容易出幺蛾子的外设。你想想看,一个简单的开关动作,到了单片机眼里却可能变成一连串的“抖动”信号。这一章,我们就来彻底搞定按键输入——从GPIO怎么配,到消抖怎么处理,再到中断怎么用。

本章核心脉络:GPIO输入模式配置 → 硬件/软件消抖 → EXTI外部中断 → 中断优先级管理。说白了,就是让单片机“看懂”按键按下这个动作,并且能及时响应。

3.1 GPIO输入模式:不只是“读引脚”那么简单

很多人觉得GPIO输入就是配个模式、读个电平。其实没那么简单。我刚开始做项目时,就因为在输入模式上偷懒,吃了不少苦头。

STM32的GPIO输入模式,主要有这么几种配置:

配置项 选项 说明
GPIO_Mode GPIO_Mode_IN_FLOATING 浮空输入,电平由外部决定,不稳定
GPIO_Mode GPIO_Mode_IPU 上拉输入,默认高电平,按键按下拉低
GPIO_Mode GPIO_Mode_IPD 下拉输入,默认低电平,按键按下拉高
GPIO_Speed 10MHz / 50MHz 输入模式下速度影响不大,但建议统一配置

我个人习惯:按键电路如果外部已经接了上拉电阻,就用浮空输入;如果外部没接,就用内部上拉。千万别用下拉——你想想看,按键按下拉高,松开后引脚悬空,电平飘忽不定,那画面太美我不敢看。

小技巧:配置GPIO输入时,记得开启对应的AFIO时钟(如果需要复用功能),但普通输入不需要。我见过有人把输入口配成推挽输出,结果按键按下直接短路——嗯,芯片没烧算运气好。

3.2 按键消抖:硬件与软件两手抓

按键按下和松开的瞬间,机械触点会弹跳,产生一连串的脉冲。这个时间大概5~20ms。如果不处理,单片机可能认为你按了十几次。

消抖有两种方式:硬件消抖和软件消抖。

3.2.1 硬件消抖

硬件消抖,说白了就是用电容把毛刺滤掉。典型电路如下:

VCC → 10kΩ上拉电阻 → 按键 → GND
                         |
                       100nF电容 → GND

电容的作用是吸收弹跳产生的高频分量。RC时间常数τ = R × C,一般取1~10ms。我建议用10kΩ + 100nF,τ ≈ 1ms,效果不错。

注意:硬件消抖虽然省事,但会增加BOM成本和PCB面积。量产项目里,如果按键数量多,成本压力就上来了。我曾经在一个项目里用了8个按键,每个都加电容,结果被硬件经理骂了一顿——后来全改成软件消抖了。

3.2.2 软件消抖

软件消抖更灵活,也省钱。核心思路就是:检测到电平变化后,延时一段时间再读一次,确认状态稳定。

我常用的软件消抖流程:

  1. 检测到按键按下(电平变化)
  2. 延时10~20ms(用定时器或简单循环)
  3. 再次读取按键电平
  4. 如果状态一致,确认按键动作
  5. 等待按键释放(可选,防止重复触发)

代码示例:

uint8_t Key_Scan(void)
{
    static uint8_t last_state = 1;  // 假设上拉,默认高电平
    uint8_t current_state;
    uint8_t key_press = 0;

    current_state = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);

    if (current_state == 0 && last_state == 1)  // 检测到下降沿
    {
        Delay_ms(20);  // 消抖延时
        current_state = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
        if (current_state == 0)  // 确认按下
        {
            key_press = 1;
        }
    }

    last_state = current_state;
    return key_press;
}

避坑指南:Delay_ms(20) 如果用的是SysTick延时,注意不要在中断里调用。我曾经在中断服务函数里直接调延时,结果系统卡死——因为SysTick中断优先级比当前中断低,永远等不到延时结束。嗯,这个坑我踩过,你们别踩。

3.3 外部中断(EXTI):让CPU不再“傻等”

轮询按键有个问题:CPU得一直跑循环去读引脚,干不了别的。用中断就不一样了——按键按下时,CPU自动跳去处理,处理完回来继续干正事。

STM32的EXTI(External Interrupt)支持最多16个外部中断线(EXTI0~EXTI15),每个中断线可以映射到多个GPIO引脚,但同一时间只能选一个。

3.3.1 EXTI配置步骤

  1. 使能GPIO时钟和AFIO时钟
  2. 配置GPIO为输入模式(上拉或浮空)
  3. 配置GPIO到EXTI的映射(GPIO_EXTILineConfig)
  4. 配置EXTI中断线(触发方式、使能)
  5. 配置NVIC中断优先级
  6. 编写中断服务函数

代码示例(基于标准库):

void EXTI_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

    // 2. 配置GPIO
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  // 上拉输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. 映射到EXTI0
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    // 4. 配置EXTI
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  // 下降沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    // 5. 配置NVIC
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

// 6. 中断服务函数
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 延时消抖(注意:这里不能用阻塞延时!)
        // 建议用定时器或状态机处理
        // 处理按键逻辑

        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

重要提醒:中断服务函数里不要做复杂操作,更不要用阻塞延时。我见过有人在中断里调printf,结果串口还没发完,下一个中断又来了——直接死机。中断里只做标志位设置,具体逻辑放主循环处理。

3.4 中断优先级管理:谁先谁后,得有个规矩

STM32的NVIC支持最多16级抢占优先级和16级子优先级(具体取决于芯片型号)。优先级管理说白了就是:当多个中断同时发生时,谁先执行。

NVIC优先级分组:

分组 抢占优先级位数 子优先级位数 说明
NVIC_PriorityGroup_0 0 4 只有子优先级,无抢占
NVIC_PriorityGroup_1 1 3 2级抢占,8级子优先级
NVIC_PriorityGroup_2 2 2 4级抢占,4级子优先级
NVIC_PriorityGroup_3 3 1 8级抢占,2级子优先级
NVIC_PriorityGroup_4 4 0 16级抢占,无子优先级

我个人习惯:用NVIC_PriorityGroup_2,4级抢占够用,4级子优先级也灵活。按键中断一般给个中等优先级(比如抢占2,子优先级0),别太高也别太低。

经验之谈:中断优先级分配有个原则——实时性要求高的给高优先级,比如定时器、通信中断;按键这种可以稍微低一点。我曾经在一个项目里把按键中断优先级设得比系统滴答定时器还高,结果按键按下去,系统时钟乱了,整个调度都崩了。嗯,从那以后我再也不敢乱设优先级了。

3.5 本章知识体系

下面这张图,把按键输入与中断的整个知识结构串起来了。你可以把它当作一个快速索引,哪里不会看哪里。

按键输入与中断知识体系 按键输入与中断知识体系 按键输入与中断 GPIO输入模式 按键消抖 EXTI外部中断 浮空/上拉/下拉 速度配置 时钟使能 硬件消抖 软件消抖 GPIO映射到EXTI线 触发方式配置 中断服务函数 中断优先级管理(NVIC) 抢占优先级 子优先级 优先级分组 中断里别做复杂操作

这张图把本章的核心知识点都串起来了。从GPIO输入模式开始,到消抖处理,再到EXTI配置和优先级管理,每一步都有对应的子项。你可以把它当作一个快速索引,哪里不会看哪里。

本章小结:按键输入看似简单,但涉及的知识点不少。GPIO输入模式要选对,消抖要处理好,中断配置要规范,优先级要合理。把这些都搞定了,按键这块就算真正掌握了。