单片机通识之按键扫描框架(非死等消抖)

文章介绍了如何避免在STM32按键消抖中使用低效的死等方法,提出了一个基于数组和指针数组的通用框架,允许通过改变头文件来适应不同数量和引脚的按键。该框架利用遍历检测IO口,动态调用按键事件处理函数,提高了代码的复用性和灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

按键是用户向单片机输入信息的重要方式之一,最近遇到很多刚大学毕业的学生在写按键消抖时,都是在死等,这种方式并不实用,应该没有产品会采用这种低效的方式。

本文就该问题展开叙述,提供一个可行的思想。在示例代码中,主要实现了: 若按键数目或所用引脚发生变化,仅需改变头文件按键定义即可,代码部分无需改动。

开发环境

STM32F103C8T6: 价格低,用来学习非常合适。

核心思想

在商业用途的项目开发中,一个通用的框架是必须的,写一次框架,可应对多种场合、多次使用,这会很大的提升效率,所以在实现一个模块时,一定要考虑它的复用性。

在示例代码中,实现 仅需改动头文件即可 这一功能的思想就是利用数组、指针数组,遍历的方式去检测每一个数组中的IO口,并且判断按键是否按下,再利用指针数组去调用按键对应的函数。在示例代码中会详细讲解。

数组包括:

  1. 用到的GPIO,例如 GPIOA、GPIOB等
  2. 用到的引脚,例如 GPIOA的 第4个引脚、GPIOB的 第5个引脚等

指针数组:

  1. 把每一个按键按下、松开要调用的函数地址放入数组中,这样就可以灵活的根据索引去调用对应的函数

示例代码

头文件

#ifndef __INDEPENDENTKEYH
#define __INDEPENDENTKEYH
#include "stm32f10x.h"                  // Device header


// Debounce time (INDEPENDENT_KEY_DEBOUNCE_TIME) * 10us 
// 消抖时间为 320ms,利用 TIM2 计时,中断周期为 10us。
// 这里可以根据自己实际用到的定时器去 计时
#define INDEPENDENT_KEY_DEBOUNCE_TIME 320

// INDEPENDENT_KEY_GPIO_NUMBER 定义用到了几个GPIO
// 因为STM32对每一个外设都设计了一个时钟,所以在用到GPIO前,都需要预先开启对应的时钟
// 这里是采用遍历的方式,去开启用到的GPIO,所以会产生以下两个宏定义
#define INDEPENDENT_KEY_GPIO_NUMBER 2
#define INDEPENDENT_KEY_GPIO_ENABLE_TABLE \
						/* GPIOA */ RCC_APB2Periph_GPIOA, \
						/* GPIOB */ RCC_APB2Periph_GPIOB

// INDEPENDENT_KEY_NUMBER 一共用到了多少个 IO 口
// INDEPENDENT_KEY_PORT_TABLE 定义每一个按键对应的 IO 口
// INDEPENDENT_KEY_PIN_TABLE 定义每一个按键对用 IO口 的哪个引脚
// 这两个数组定义均要按照实际顺序,例如Key1 用到 GPIOA 的第4个引脚,KEY3 用到GPIOA的引脚的第6个引角
#define INDEPENDENT_KEY_NUMBER 5
#define INDEPENDENT_KEY_PORT_TABLE \
						/* Port of key1 */ GPIOA, \
						/* Port of key2 */ GPIOA, \
						/* Port of key3 */ GPIOA, \
						/* Port of key4 */ GPIOA, \
						/* Port of key5 */ GPIOB,
							  
#define INDEPENDENT_KEY_PIN_TABLE \
						/* Pin of key1 */ GPIO_Pin_4, \
						/* Pin of key2 */ GPIO_Pin_5, \
						/* Pin of key3 */ GPIO_Pin_6, \
						/* Pin of key4 */ GPIO_Pin_7, \
						/* Pin of key5 */ GPIO_Pin_11, 

// 这个数组定义了每个按键的触发电平,以适用不同电平的触发方式
// 这个触发电平要和 STM32 定义的BitAction一致,即 仅允许 低电平 Bit_RESET 和 高电平 Bit_SET
#define INDEPENDENT_KEY_TRIGGER_LEVEL_TABLE \
						/* Trigger level of key1 */ Bit_SET, \
						/* Trigger level of key2 */ Bit_SET, \
						/* Trigger level of key3 */ Bit_SET, \
						/* Trigger level of key4 */ Bit_SET, \
						/* Trigger level of key5 */ Bit_SET
/*
 * @brief: 对按键进行初始化,包括 GPIO 和 用到的变量
 * @param  void
 * @retval void
 */
void Independent_Key_Init(void);

/*
 * @brief: 放在主循环中检测按键状态
 * @param  void
 * @retval void
 */
void Independent_Key_Scan(void);


/*
 * @brief: 按键状态发生变化,消抖时间需要清零,重新开始计时
 * @param  void
 * @retval void
 */
void Key_Changed(uint16_t current_buffer);


/*
 * @brief: 按键状态未发生变化
 * @param  void
 * @retval void
 */
void Key_No_Changed(uint16_t current_buffer);


/*
 * @brief: Key_Press_1 之后的函数为按键按下松开对应的触发函数,根据需要去添加功能
 * @param  void
 * @retval void
 */
void Key_Press_1(void);

void Key_Press_2(void);

void Key_Press_3(void);

void Key_Press_4(void);

void Key_Press_5(void);

void Key_Release_1(void);

void Key_Release_2(void);

void Key_Release_3(void);

void Key_Release_4(void);

void Key_Release_5(void);

#endif 


源文件

#include "IndependentKey.h"
// OLED.h 是使用 OLED屏幕 模块,不影响本文的阅读
#include "OLED.h"

// key_flag 保存按键的状态
// 假若 key1 key3 在程序中已经确认被按下了,那么 key_flag 对应二进制 0000 0101
uint16_t key_flag;

// key_buffer 临时保存IO口的状态
// 假若key1 key3 被按下了,但是消抖时间还没到达,那么 key_buffer 对应二进制 0000 0101
// 注意,这时候 key_flag 的值 仍然是此刻之前的值
// 只有当消抖时间到达后,key_flag 的值才会更新为 key_buffer
uint16_t key_buffer;

// key_press 记录哪个按键被按下了
// 假若 key1 key3 被按下了,那么key_press 对应二进制 0000 0101
uint16_t key_press;
// 同 key_press 
uint16_t key_release;

// key_debounce 记录消抖时间
uint16_t key_debounce;

// 定义 GPIO 指针数组,STM32 中的 GPIOA、GPIOB 均为一个指针,所以这个数组保存的全部是指针
// 同时,也是通过遍历这个指针数组,去开启用到的GPIO的时钟
GPIO_TypeDef* independent_key_port_table[] = { INDEPENDENT_KEY_PORT_TABLE };

// 下方三个数组,定义了按键对应的 GPIO 和 触发电平,在这里不容易讲解清楚,在下方代码处会讲解
uint32_t independent_key_gpio[] = { INDEPENDENT_KEY_GPIO_ENABLE_TABLE };
const uint16_t independent_key_pin_table[] = { INDEPENDENT_KEY_PIN_TABLE };
const uint8_t independent_key_trigger_level_table[] = { INDEPENDENT_KEY_TRIGGER_LEVEL_TABLE };

// 下方两个指针数组,定义了按键松开对应的函数
void (*independent_key_press_handler[INDEPENDENT_KEY_NUMBER + 1])() = { Key_Press_1, Key_Press_2, Key_Press_3, Key_Press_4, Key_Press_5};
void (*independent_key_release_handler[INDEPENDENT_KEY_NUMBER + 1])() = { Key_Release_1, Key_Release_2, Key_Release_3, Key_Release_4, Key_Release_5};


void Independent_Key_Init(void)
{
	// 遍历指针数组,开启用到的GPIO时钟
	for (uint16_t i = 0; i < INDEPENDENT_KEY_GPIO_NUMBER; i++)
	{
		RCC_APB2PeriphClockCmd(independent_key_gpio[i], ENABLE);
	}
	
	// 遍历数组,对用到的GPIO进行配置,
	for (uint16_t i = 0; i < INDEPENDENT_KEY_NUMBER; i++)
	{
		// 这里就是用到定义的 independent_key_pin_table independent_key_port_table
		// 配置 GPIO的某个引脚 
		GPIO_InitTypeDef gpio_init_structure;
		gpio_init_structure.GPIO_Mode = GPIO_Mode_IPD;
		gpio_init_structure.GPIO_Pin = independent_key_pin_table[i];
		gpio_init_structure.GPIO_Speed = GPIO_Speed_50MHz;
		GPIO_Init(independent_key_port_table[i], &gpio_init_structure);
	}
	// 初始化变量
	key_flag = 0;
	key_buffer = 0;
	key_debounce = 0;
	key_press = 0;
	key_release = 0;
}

/**
  * @brief  
  * @param  
  * @retval  
  */
void Independent_Key_Scan(void)
{
	uint16_t temp_buffer = 0x0000;
	
	// 通过遍历的方式去检测每一个IO口电平是否等于定义的触发电平
	for (uint16_t i = 0; i < INDEPENDENT_KEY_NUMBER; i++)
	{
		uint8_t gpio_read_res = GPIO_ReadInputDataBit(independent_key_port_table[i], independent_key_pin_table[i]);
		
		// Check if current level == trigger level
		if (gpio_read_res == independent_key_trigger_level_table[i])
		{
			uint16_t temp_level = 0x0001;
			temp_level = temp_level << i;
			temp_buffer |= temp_level;
		}
	}
	
	// 如果 检测到的 temp_buffer 等于 key_flag,说明IO口状态没有发生变化
	// 将 IO 口状态保存至 key_buffer, 退出即可
	if (temp_buffer == key_flag)
	{
		Key_No_Changed(temp_buffer);
		return;
	}
	
	// 程序到这里,说明 IO 状态发生了变化,并判断此刻状态和上一次扫描后的状态是否一致
	// 可以理解为,按下了key1,这是IO状态是 0000 0001,假如 key2 按下了,IO状态就变为 0000 0011
	// 那么这时候就需要重新消抖
	if (temp_buffer != key_buffer)
	{
		Key_Changed(temp_buffer);
		return;
	}
	
	// 程序到这里,说明 IO 稳定在了一个状态,并判断消抖时间是否到即可
	if (key_debounce != 0)
	{
		return;
	}
	
	// 这里异或可以理解为把 变化的 IO 口挑选出来
	// 加入此次按下是初始化过后的第一次按下,按下的按键时key1
	// 那么 key_flag 为 0000 0000, key_buffer 为 0000 0001 
	// 异或后的结果就是 0000 0001,即把状态变化的IO口给挑了出来 
	uint16_t changed_key = key_buffer ^ key_flag;
	// key_buffer 代表着此刻 IO口 状态,和 状态变化的 IO 口 与运算,就表示按键按下了
	key_press = changed_key & key_buffer;
	// key_flag 代表着上一刻 IO口 状态,和 状态变化 IO口 与运算,就表示按键松开了
	key_release = changed_key & key_flag;
	
	// 这个函数就是通过索引去调用对应的按下函数
	for (uint16_t i = 0; i < INDEPENDENT_KEY_NUMBER; i++)
	{
		uint16_t temp_level = 0x0001 << i;
		temp_level &= key_press;
		if (temp_level)
		{
			// 在这里,假若 key1 和 key2 都按下了,那么就仅执行 key1 的功能
			// 可以根据自己的需要去修改
			(*independent_key_press_handler[i])();
			break;
		}
	}

	// 同按键按下
	for (uint16_t i = 0; i < INDEPENDENT_KEY_NUMBER; i++)
	{
		uint16_t temp_level = 0x0001 << i;
		temp_level &= key_release;
		if (temp_level)
		{
			(*independent_key_release_handler[i])();
			break;
		}
	}
	
	// 更新按键状态
	key_flag = key_buffer;
	
	return;
}	

void Key_Changed(uint16_t current_buffer)
{
	key_debounce = INDEPENDENT_KEY_DEBOUNCE_TIME;
	key_buffer = current_buffer;
}

void Key_No_Changed(uint16_t current_buffer)
{
	key_buffer = current_buffer;
}

void Key_Press_1(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 1 pressed");
	return;
}

void Key_Press_2(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 2 pressed");
	return;
}

void Key_Press_3(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 3 pressed");
	return;
}

void Key_Press_4(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 4 pressed");
	return;
}

void Key_Press_5(void)
{
	OLED_Clear();
	return;
}

void Key_Release_1(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 1 released");
	return;
}

void Key_Release_2(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 2 released");
	return;
}

void Key_Release_3(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 3 released");
	return;
}

void Key_Release_4(void)
{
	OLED_Clear();
	OLED_Show_String(1, 1, "Key 4 released");
	return;
}

void Key_Release_5(void)
{
	return;
}

结语

本文仅讲述了一个思路,供一些基础不太好的朋友学习,仅是一个思路,有没有讲解清楚的地方,可以邮箱联系,一同探讨. CubicJar@163.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值