按键收集单击,双击和长按
引言
在我们生活中, 按键是必不可少的, 不同的电器, 有不同的按键, 但是按键总有不够用的时候, 那么给与一个按键赋予不同的功能,就必不可少了. 一个按键可以通过按下的时间长短和频次, 来定义其类型。
一次按键收集, 都是在一个按键收集周期的, 比如500毫秒内, 我要收集一个按键类型, 在500毫秒内,按键按下一次按键, 那么到达500毫秒后, 我们就宣布答案, 此次按键是单击。
同理, 在500毫秒内, 如果我们按下了, 两次按键,那么就是双击, 以此类推。
那长按呢? 长按就是, 在500毫秒内, 我们按下了按键,并且500ms时间到时, 按键还未松开, 就代表着长按。
本博客最终修改项目链接下载:
https://wwyz.lanzoul.com/itXC327v9dra
按键收集模型
![image-20240818183801773](https://img-blog.csdnimg.cn/img_convert/aed99ba42e66379740328f0e211f1637.png)
单片机实现方法
因为按键, 需要每时每刻的去检测, 是否按下, 如果使用while阻塞循环, 则会大大影响程序效率, 所以我们使用外部中断去检测中断。
按键事件触发检测
按键按下的瞬间, 单片机的io口, 就会被从高电平拉到低电平, 然后单片机io口就会检测到, 从而触发下降沿中断。
此时,就代表着按键事件已经触发了。
按键类型判断
按键事件触发,仅仅代表着我们按下了按键, 但是我们想区分单击,双击和长按, 那么我们就得从按键按下开始计算,直到按键计算周期结束,这一段时间我们定义成 500毫秒。
所以, 我们在按键事件触发后,我们需要启动定时器,计时500ms, 然后500ms时间到后, 判断按键按下的次数, 还有按键此时是否仍然被按下.
在这 500毫秒内, 我们重复按下按键, 就需要重复的检测按键按下的事件, 然后计入按键次数内.
所以我们的处理模型是
代码实现
按键中断
按键中断初始化
我们初始化io端口PB1,配置下降沿中断
void Key_Init(void)
{
GPIO_InitTypeDef gpio_initstruct;
EXTI_InitTypeDef exti_initstruct;
NVIC_InitTypeDef nvic_initstruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //打开GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //打开复用时钟
gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU; //设置为输出
gpio_initstruct.GPIO_Pin = GPIO_Pin_1; //将初始化的Pin脚
gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz; //可承载的最大频率
GPIO_Init(GPIOB, &gpio_initstruct); //初始化GPIO
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
exti_initstruct.EXTI_Line = EXTI_Line1;
exti_initstruct.EXTI_Mode = EXTI_Mode_Interrupt;
exti_initstruct.EXTI_Trigger = EXTI_Trigger_Falling;
exti_initstruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&exti_initstruct);
nvic_initstruct.NVIC_IRQChannel = EXTI1_IRQn;
nvic_initstruct.NVIC_IRQChannelPreemptionPriority = 2;
nvic_initstruct.NVIC_IRQChannelSubPriority = 2;
nvic_initstruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_initstruct);
}
触发中断,按键标志位赋1
extern _Bool key_down;
void EXTI1_IRQHandler(void)
{
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
//代表按键按下(按键标志清除--一次时间处理完成)
key_down = 1;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
定时器中断扫描判断按键是否按下
当触发中断后,我们把key_down置成1后, 就会被定时器扫描,检测到, 我们就 把按键次数加1, 并且启动 500ms的计时, count = 1, 然后if(count >0), 就会启动 count++计时, 因为我们定时器是1ms运行一次,所以当count == 500的时候,进行按键类型检测
按键类型判断
当到达500ms后, 我们就可以通过判断按键次数, 区分单双击, 单击的情况下,通过判断500ms时,io口的状态,从而判断单击还是长按。
单击和长按区分
单击和长按, 按键次数 button_count 都是1, 不同的是,500毫秒之后,单击按键类型, 已经松开, 长按,则是按下状态, 所以我们通过读取检测500ms时间到时, io的状态, 就可以区分长按和单击了。
长按, io口是低电平, 单击,io是高电平, (我们是下降沿触发, 按键一端连io口,一端连 地, io口输出状态是, 推挽输出)
button_mode = 1代表 单击 ,
button_mode = 3代表 长按
![image-20240819130853341](https://img-blog.csdnimg.cn/img_convert/248424ceae20ffa7e6f8a06dbfe7a265.png)
uint8_t key_scan(void)
{
uint8_t value;
value = 0; //默认是不是一直低电平
//检测是否长按
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)// 判断按键是否仍然在按下
{
//这时候还是按下状态, 不一定是真正的按键按下, 有可能是抖动造成的,接下来延时消抖
//再次判断按键是否按下, 如果按下, 代表真正的按键仍然在按下
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
//那就等抬起了再做事
value = 1;//这时候才可以判断是 长按下
}
}
return value;
}
双击区分
通过判断, 按键的次数, 就可以得知双击, 记得按键类型判断完毕后, 要把按键次数置0, 为下次判断做准备。 button_mode = 2, 代表按键双击
定时器扫描函数代码
void TIM4_IRQHandler(void)
{
if(TIM_GetFlagStatus(TIM4,TIM_FLAG_Update))//一毫秒运行一次
{
//当 button_atom的时候,才能进行读取按键信息
if(key_down == 1)
{
key_down = 0;
button_count++;
count = 1; //开始计时
}
if(count > 0)
{
count++;
if(count > 500)
{
count = 0;
if(button_count == 1)
{
button_count = 0;
if(key_scan() == 0) //短按
button_mode = 1;
else if(key_scan() == 1) //长按
button_mode = 3;
}
else
if(button_count == 2)
{
button_count = 0;
button_mode = 2;
}
else
{
button_count = 0;//判断完,到时间照样得button_count 清零,为后面做准备
}
}
}
TIM_ClearFlag(TIM4,TIM_FLAG_Update);
}
}
现象调试
判断按键类型方式
我们通过代码实操 ,已经可以得知我们按下的按键是单击,双击还是长按了,通过判断 button_mode 的数值。
![image-20240819143221376](https://img-blog.csdnimg.cn/img_convert/34be57e0536774a9798e2e6a6061fa0a.png)
调用相关现象
我们选用PA0, PA1, PA2, 来分别代表 单击,双击和长按的小灯。
我们在检测到 button_mode 的数值改变的时候, 就赋值对应的小灯亮灭,这样我们就可以看到现象了
![image-20240819144039916](https://img-blog.csdnimg.cn/img_convert/e3cefdcc5738553791d02bf0b3044951.png)
初始化PA0,PA1,PA2
设置这三个io口分别为推挽输出模式
void Led_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
//使用各个外设前必须开启时钟,否则对外设的操作无效
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO模式,赋值为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIO引脚,赋值为第0号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速度,赋值为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //将赋值后的构体变量传递给GPIO_Init函数
//函数内部会自动根据结构体的参数配置相应寄存器
//实现GPIOA的初始化
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO模式,赋值为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //GPIO引脚,赋值为第0号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速度,赋值为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //将赋值后的构体变量传递给GPIO_Init函数
//函数内部会自动根据结构体的参数配置相应寄存器
//实现GPIOA的初始化
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //GPIO模式,赋值为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //GPIO引脚,赋值为第0号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速度,赋值为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //将赋值后的构体变量传递给GPIO_Init函数
//函数内部会自动根据结构体的参数配置相应寄存器
//引脚初始化
led_go(0,0);
led_go(1,0);
led_go(2,0);
}
单独设置对应的小灯亮灭
void led_go(uint8_t led,_Bool go)
比如我们控制 PA0 io口, 我们传入 0, 代表我们要控制位序为0的小灯, 第二个参数 go = 1, 代表着让灯两, go = 0, 代表着让灯灭。这样我们就可以灵活的控制小灯亮灭了。
![image-20240819182225221](https://img-blog.csdnimg.cn/img_convert/eae468143a12065cbc4f538e6142a09d.png)
void led_go(uint8_t led,_Bool go)
{
if(led == 0)
{
if(go == 1)
{
GPIO_SetBits(GPIOA, GPIO_Pin_0); //亮
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); //灭
}
}
else
if(led == 1)
{
if(go == 1)
{
GPIO_SetBits(GPIOA, GPIO_Pin_1); //亮
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_1); //灭
}
}
else
if(led == 2)
{
if(go == 1)
{
GPIO_SetBits(GPIOA, GPIO_Pin_2); //亮
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_2); //灭
}
}
}
根据 button_mode亮灭小灯
还是那个图, 我们有了底层构建, 我们根据单双击,然后会有不同的butoon_mode 按键模式, 所以我们根据这个数值, 赋值对应的小灯亮灭, 这样我们就可以看到现象了.
led控制图(ctrl 加鼠标左键,快速跳转)
![image-20240819182628204](https://img-blog.csdnimg.cn/img_convert/ba995ae9013302dc4c5109cad476f463.png)
void chose_led(void)
{
if(button_mode == 0)
{
led_go(0,0);
led_go(1,0);
led_go(2,0);
}
else
if(button_mode == 1)
{
led_go(0,1);
led_go(1,0);
led_go(2,0);
}
else
if(button_mode == 2)
{
led_go(0,0);
led_go(1,1);
led_go(2,0);
}
else
if(button_mode == 3)
{
led_go(0,0);
led_go(1,0);
led_go(2,1);
}
}
main.c主函数调用
初始化,按键和定时器, 还有调试的小灯, 我们根据按键模式, 控制对应的小灯亮灭, 观察现象
![]()
_Bool key_down = 0; //按键是否按下
uint16_t count; //定时器计数毫秒数(按键按下时长)
uint8_t button_count = 0;
uint8_t button_mode = 0; //按键模式:无-0 , 单击- 1, 双击 - 2, 长按 - 3
int main(void)
{
Key_Init();
TIM4_Init();
Led_Init();
while(1)
{
chose_led();
}
}
工程代码构建
我们上面, 代码的逻辑思路, 已经构建完毕, 同时我们也提供构建完成的工程, 我们这里带领大家, 复制黏贴
1.基本工程构建方法
创建stm32f103c8t6基本工程_stm32f103c8启动文件配置-CSDN博客
2.我们工程名字叫做
Key_detection_model
点击品字,然后修改工程名字
3.我们用到main函数, 所以有User用户文件夹
我们用到按键检测, 所以用到 Key_model按键模型
我们用到系统的延时函数, 所以用到System系统文件夹
我们用到定时器,所以用到Time定时器文件夹
User
Key_model
System
Time
4.具体文件添加方式, 看最小例程构建,从第九步开始
我们User文件夹下, 添加
main.c
led.c
led.h
Key_model文件夹下添加
key.c
key.h
![image-20240819183757371](https://img-blog.csdnimg.cn/img_convert/fd5d0297b53eac80ce9b73a987e06aa2.png)
System文件夹下添加
delay.c
delay.h
sys.h
![image-20240819200115950](https://img-blog.csdnimg.cn/img_convert/95155030ebddf69d2f86e27a05e2ff62.png)
Time文件夹下添加
TIM4.c
TIM4.h
![image-20240819200130023](https://img-blog.csdnimg.cn/img_convert/81d369f3e5c9a6bc514517f73ae745e8.png)
5.记得包含对应的文件夹路径
6.按键收集模型代码快速复制
各文件对应的代码
- 构建完成如图
8.编译运行,烧录
9.小灯辨别正负极方法
灯芯放在光下看, 大头是负极,小头是正极, 我们正极插在io口, 负极插在地, io口分别是 PA0, PA1, PA2
10.按键演示视频