STM32 按键输入检测 轮询和中断

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是高电平

  1. 先检查PA0是否高电平
  2. 如果第一次是高电平,延时10ms,再度检测是否还是高电平
  3. 如果10ms后还是高电平,那么可以确认按键被按下
cpp 复制代码
// 检测按键
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) 配置为上拉(输出高电平)
cpp 复制代码
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

}

轮询检测:

扫描按键函数

cpp 复制代码
#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函数

cpp 复制代码
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配置是一样的。
  • 这里最主要是配置引脚的外部中断,并且注意触发方式,需要根据原理图来对应设置。

如果按键是按下为低电平,未按下是高电平时,此时触发方式应该是下降沿(高电平->低电平)

cpp 复制代码
// 按键初始化函数,使用外部中断
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();
}

定时器配置

cpp 复制代码
// 定时器初始化函数
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);
}

外部中断处理函数:

对应每个按键的引脚

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

cpp 复制代码
// 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);  // 清除中断标志
    }
}

定时器中断处理函数

cpp 复制代码
// 定时器 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); 
    }
}

相关的变量标志

cpp 复制代码
// 定义按键状态标志
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;
相关推荐
安庆平.Я28 分钟前
STM32——HAL 库MDK工程创建
stm32·单片机·嵌入式硬件
Yuroo zhou4 小时前
无人机在复杂气流中,IMU 如何精准捕捉姿态变化以维持稳定?
单片机·嵌入式硬件·算法·机器人·无人机
李永奉12 小时前
STM32-USART串口实现接收数据三种方法(1.根据\r\n标志符、2.空闲帧中断、3.根据定时器辅助接收)
stm32·单片机·嵌入式硬件
嵌入式×边缘AI:打怪升级日志12 小时前
【7】串口编程三种模式(查询/中断/DMA)韦东山老师学习笔记(课程听不懂的话试着来看看我的学习笔记吧)
单片机·嵌入式硬件
jghhh0112 小时前
stm32的PID控制算法
stm32·单片机·嵌入式硬件
你好!蒋韦杰-(烟雨平生)14 小时前
扫雷游戏C++
c++·单片机·游戏
来点光吧17 小时前
STM32F1 Flash的操作
stm32·单片机·嵌入式硬件
四谎真好看20 小时前
第六章第一节 TIM 定时中断
stm32·单片机·嵌入式硬件·tim·江科大
扣篮发型不乱20 小时前
STM32 外部中断 和 定时器中断
stm32·单片机·嵌入式硬件