按键是用户向单片机输入信息的重要方式之一,最近遇到很多刚大学毕业的学生在写按键消抖时,都是在死等,这种方式并不实用,应该没有产品会采用这种低效的方式。
本文就该问题展开叙述,提供一个可行的思想。在示例代码中,主要实现了: 若按键数目或所用引脚发生变化,仅需改变头文件按键定义即可,代码部分无需改动。
开发环境
STM32F103C8T6: 价格低,用来学习非常合适。
核心思想
在商业用途的项目开发中,一个通用的框架是必须的,写一次框架,可应对多种场合、多次使用,这会很大的提升效率,所以在实现一个模块时,一定要考虑它的复用性。
在示例代码中,实现 仅需改动头文件即可 这一功能的思想就是利用数组、指针数组,遍历的方式去检测每一个数组中的IO口,并且判断按键是否按下,再利用指针数组去调用按键对应的函数。在示例代码中会详细讲解。
数组包括:
- 用到的GPIO,例如 GPIOA、GPIOB等
- 用到的引脚,例如 GPIOA的 第4个引脚、GPIOB的 第5个引脚等
指针数组:
- 把每一个按键按下、松开要调用的函数地址放入数组中,这样就可以灵活的根据索引去调用对应的函数
示例代码
头文件
#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