[应用相关] STM32 按键输入检测 轮询和中断

[复制链接]
114|0
wowu 发表于 2025-8-13 17:30 | 显示全部楼层 |阅读模式
Overview
原理
用的平台是STM32F103,有三个按键,原理图分别如下




WK_UP 连接到PA0
KEY1连接到PE3
KEY0连接到PE4
当按键都按下时,WK_UP(PA0) 会输入高电平,KEY0(PE4) 和 KEY1(PE3) 会输入低电平

所以我们可以对GPIO口的输入进行检测,来判断按键是否被按下。

按键消抖
按键消抖,是为解决机械按键按下 / 松开时,内部金属弹片因弹性会短暂抖动、使电平快速跳变的问题。若不处理,MCU 会误判为多次按键。




为啥要消抖?
机械按键按下 / 松开时,内部弹片会短暂 “抖动”(5 - 20ms),导致电平疯狂跳变。若直接读取,MCU 会把一次按键误判成多次,程序可能会变得混乱。

如何消抖?
硬件消抖:在按键回路加小电容(0.1μF - 1μF)“稳住” 电平,或用 RS 触发器电路过滤抖动。
软件消抖:检测到按键变化后,延时 10 - 20ms 再读状态,若状态不变,才认定为有效按键(最常用,简单省成本)。
简单说,消抖就是让 MCU “等一等”,跳过按键抖动的不稳定阶段,只认真正的按下 / 松开动作~

例子:
当PA0连接的按键按下的时候,PA0是高电平

先检查PA0是否高电平
如果第一次是高电平,延时10ms,再度检测是否还是高电平
如果10ms后还是高电平,那么可以确认按键被按下
// 检测按键
uint8_t CheckKey()
{
        uint8_t now = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
        if (now == 0)
        {
                // 检测到可能按下
                delay_ms(10);  // 等10ms
                if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
                {
                        return 1;  // 确认按下
                }
        }
        return 0;
}



检测方式
有两种方式可以检测按键输入,分别是轮询和中断

1. 轮询方式
原理:CPU 不断循环检查按键状态(就像人反复看按钮是否被按)
特点:简单易实现,但一直占用 CPU 资源
2. 中断方式
原理:按键按下时主动 “打断” CPU,CPU 暂停当前工作先处理按键(类似手机来电打断当前操作)
特点:响应快,不占用额外资源,但配置稍复杂
代码:
GPIO配置
无论是轮询还是中断,对于GPIO的配置其实是一样的

因为当按键都按下时:

WK_UP(PA0) 会输入高电平
KEY0(PE4) 和 KEY1(PE3) 会输入低电平
所以未按下的时候,我们要有相反的电平:

WK_UP(PA0)配置为下拉(输出低电平)
KEY0(PE4) 和 KEY1(PE3) 配置为上拉(输出高电平)
void KEY_Init(void) //IO初始化
{
        GPIO_InitTypeDef GPIO_InitStructure;

        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟

        // 初始化 WK_UP-->GPIOA.0          下拉输入
        GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉          
        GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0

    // KEY0(PE4) 和 KEY1(PE3) 配置为上拉
        GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4|GPIO_Pin_3;//KEY0-KEY1
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
        GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE4,3

}



轮询检测:
扫描按键函数
#define KEY0_PRES         1        //KEY0按下
#define KEY1_PRES        2        //KEY1按下
#define WKUP_PRES   3        //KEY_UP按下(即WK_UP/KEY_UP)


//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下
//1,KEY0按下
//2,KEY1按下
//3,KEY3按下 WK_UP
u8 KEY_Scan(u8 mode)
{
    static u8 key_up = 1; // 按键按松开标志
    if (mode)
    {
        key_up = 1; // 支持连按
    }
    if (key_up && (KEY0 == 0 || KEY1 == 0 || WK_UP == 1))
    {
        delay_ms(10); // 去抖动
        key_up = 0;
        if (KEY0 == 0)
        {
            return KEY0_PRES;
        }
        else if (KEY1 == 0)
        {
            return KEY1_PRES;
        }
        else if (WK_UP == 1)
        {
            return WKUP_PRES;
        }
    }
    else if (KEY0 == 1 && KEY1 == 1 && WK_UP == 0)
    {
        key_up = 1;
    }
    return 0; // 无按键按下
}



调用扫描函数
while(1) 一直调用 KEY_Scan函数

void Key_task()
{
        vu8 key=0;       

    printf("Key_task start\r\n");
        while(1)
        {
                key=KEY_Scan(0);        //得到键值
            if(key)
                {                                                  
                        switch(key)
                        {                         
                                case WKUP_PRES:        //控制蜂鸣器
                                        printf("Task 2 WKUP_PRES\r\n");
                                        break;
                                case KEY1_PRES:
                                        printf("Task 2 KEY1_PRES\r\n");
                                        break;
                                case KEY0_PRES:
                                        printf("Task 2 KEY0_PRES\r\n");
                                        break;
                        }
                }
                else
                {
                        delay_ms(10);
                }

        }
}


中断方式检测
我们不应该在中断中使用延时来做消抖(中断处理函数应该快进快出,设立标志就退出)。那么我们可以使用定时器来做这个延时判断。

外部中断配置
我把对GPIO的配置也放在这个函数中,这部分代码其实和上面GPIO配置是一样的。
这里最主要是配置引脚的外部中断,并且注意触发方式,需要根据原理图来对应设置。
如果按键是按下为低电平,未按下是高电平时,此时触发方式应该是下降沿(高电平->低电平)

// 按键初始化函数,使用外部中断
void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 使能 GPIO 和 AFIO 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE);

    // 初始化 WK_UP --> GPIOA.0  下拉输入
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // PA0 设置成输入,默认下拉          
    GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化 GPIOA.0

    // 初始化 KEY0-KEY1 --> GPIOE.4, GPIOE.3 上拉输入
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4 | GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置成上拉输入
    GPIO_Init(GPIOE, &GPIO_InitStructure); // 初始化 GPIOE4,3

    // 配置 GPIO 引脚作为外部中断线
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource4);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource3);

    // 配置 EXTI 线路
    // 配置 WK_UP 外部中断
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    // 配置 KEY0 外部中断
    EXTI_InitStructure.EXTI_Line = EXTI_Line4;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
    EXTI_Init(&EXTI_InitStructure);

    // 配置 KEY1 外部中断
    EXTI_InitStructure.EXTI_Line = EXTI_Line3;
    EXTI_Init(&EXTI_InitStructure);

    // 配置 NVIC 中断优先级
    // 配置 WK_UP 中断
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // 配置 KEY0 中断
    NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
    NVIC_Init(&NVIC_InitStructure);

    // 配置 KEY1 中断
    NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
    NVIC_Init(&NVIC_InitStructure);

    // 初始化定时器
    TIM_Configuration();
}


定时器配置
// 定时器初始化函数
void TIM_Configuration(void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 使能定时器时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 定时器基本配置
    TIM_TimeBaseStructure.TIM_Period = 10 - 1; // 10ms 定时
    TIM_TimeBaseStructure.TIM_Prescaler = SystemCoreClock / 1000000 - 1; // 1MHz 计数频率
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 使能定时器中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    // 配置 NVIC
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // 使能定时器
    TIM_Cmd(TIM2, ENABLE);
}



外部中断处理函数:
对应每个按键的引脚

这里其实优化的点还有很多,应该是中断服务函数的时候才去开启定时器会更合理


// KEY0 中断服务函数 (PE4 -> EXTI4)
void EXTI4_IRQHandler(void)
{
    if(EXTI_GetITStatus(EXTI_Line4) != RESET)
    {
        key0_debounce_flag = 1; // 置位消抖标志
        EXTI_ClearITPendingBit(EXTI_Line4);  // 清除中断标志
    }
}

// KEY1 中断服务函数 (PE3 -> EXTI3)
void EXTI3_IRQHandler(void)
{
    if(EXTI_GetITStatus(EXTI_Line3) != RESET)
    {
        key1_debounce_flag = 1; // 置位消抖标志
        EXTI_ClearITPendingBit(EXTI_Line3);  // 清除中断标志
    }
}

// WK_UP 中断服务函数 (PA0 -> EXTI0)
void EXTI0_IRQHandler(void)
{
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        wkup_debounce_flag = 1; // 置位消抖标志
        EXTI_ClearITPendingBit(EXTI_Line0);  // 清除中断标志
    }
}



定时器中断处理函数
// 定时器 2 中断服务函数
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        // 处理 KEY0 消抖
        if (key0_debounce_flag)
        {
            if (KEY0 == 0 && !key0_pressed)
            {
                // 确认按键按下
                printf("KEY0 pressed!\r\n");
                key0_pressed = 1; // 标记按键已按下
            }
            else if (KEY0 == 1 && key0_pressed)
            {
                key0_pressed = 0; // 标记按键已释放
            }
            // 清除消抖标志
            key0_debounce_flag = 0;
        }

        // 处理 KEY1 消抖
        if (key1_debounce_flag)
        {
            if (KEY1 == 0 && !key1_pressed)
            {
                // 确认按键按下
                printf("KEY1 pressed!\r\n");
                key1_pressed = 1; // 标记按键已按下
            }
            else if (KEY1 == 1 && key1_pressed)
            {
                key1_pressed = 0; // 标记按键已释放
            }
            // 清除消抖标志
            key1_debounce_flag = 0;
        }

        // 处理 WK_UP 消抖
        if (wkup_debounce_flag)
        {
            if (WK_UP == 1 && !wkup_pressed)
            {
                // 确认按键按下
                printf("WK_UP pressed!\r\n");
                wkup_pressed = 1; // 标记按键已按下
            }
            else if (WK_UP == 0 && wkup_pressed)
            {
                wkup_pressed = 0; // 标记按键已释放
            }
            // 清除消抖标志
            wkup_debounce_flag = 0;
        }

        // 清除定时器中断标志
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}



相关的变量标志
// 定义按键状态标志
volatile uint8_t key0_debounce_flag = 0;
volatile uint8_t key1_debounce_flag = 0;
volatile uint8_t wkup_debounce_flag = 0;
// 新增按键按下标志
volatile uint8_t key0_pressed = 0;
volatile uint8_t key1_pressed = 0;
volatile uint8_t wkup_pressed = 0;
————————————————
版权声明:本文为CSDN博主「扣篮发型不乱」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/KawhiLeo/article/details/149690552

您需要登录后才可以回帖 登录 | 注册

本版积分规则

136

主题

4344

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部