2025年信息科学与工程学院科协单片机编程介绍——按键拓展编程
按键编程拓展与状态机编程
起源
在学习按键的过程中,随着学习的不断深入,我们常常会遇到双击,长按,多次单机的判断,这样就需要更完善的按键代码逻辑,以下介绍两种代码编程逻辑,供大家参考。PS:此处代码逻辑以STM32为基础,并且默认大家已经学会最为基本的按键编写与消抖逻辑。
四行代码与编程判断
uint8_t key_val;
uint8_t key_old;
uint8_t key_up;
uint8_t key_down;
uint8_t key_read(void){
uint8_t temp=0;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==GPIO_PIN_RESET)
temp=1;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)==GPIO_PIN_RESET)
temp=2;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)==GPIO_PIN_RESET)
temp=3;
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_RESET)
temp=4;
return temp;
}
void key_proc(){
key_val=key_read();
key_down=key_val&(key_old^key_val);
key_up=~key_val&(key_old^key_val);
key_old=key_val;
此处判断的逻辑较为简单基本,通过key_read函数读取并返回一个temp值,在key_proc函数中通过定义四个变量进行判断,其中key_val用来储存返回的temp数值,key_down=key_val&(key_val^key_old)可以通过与上次读取的数值进行异或运算,并通过与操作得到下降沿数值,同理key_up即为上升沿数值。
那么,如何才能判断长按与双击呢?
if(key_down==4){
if(lcd_disp_mode==1){
switch(data_choose_mode){
case 0:
if(R_value--==1){
R_value=10;
}
break;
case 1:
if(K_value--==1){
K_value=10;
}
break;
}
}
if(lcd_disp_mode==2){
if(unlock_flag==1) {unlock_flag=0;key_tick_2=HAL_GetTick();ucLed[2]=0;}
else key_tick_2=HAL_GetTick();
}
}
if(key_up==4&&lcd_disp_mode==2){
uint32_t key_now_time_1;
key_now_time_1=HAL_GetTick();
if(key_now_time_1-key_tick_2>=2000){
ucLed[2]=1;
unlock_flag=1;
}
}
以上是一段示例代码,可以发现在key_down为4时,我们定义了key_tick_2来储存按下时的时间,当检测到上升沿key_up=同样为4时,即上升沿为4,按键4被松开时,判断此时的时间和上次的时间差值是否超过一定值,如果超过了,则满足长按的条件,而双击则是相似的代码逻辑。
状态机判断
typedef struct{
GPIO_TypeDef *gpiox;
uint16_t pin;
uint16_t ticks;
uint8_t level;
uint8_t id;
uint8_t state;
uint8_t debouce_cnt;
uint8_t repeat;
} button;
button btns[4];
void key_init(void)
{
btns[0].gpiox = GPIOB;
btns[0].pin = GPIO_PIN_0;
btns[0].level = 1;
btns[0].id = 0;
btns[1].gpiox = GPIOB;
btns[1].pin = GPIO_PIN_1;
btns[1].level = 1;
btns[1].id = 1;
btns[2].gpiox = GPIOB;
btns[2].pin = GPIO_PIN_2;
btns[2].level = 1;
btns[2].id = 2;
btns[3].gpiox = GPIOA;
btns[3].pin = GPIO_PIN_0;
btns[3].level = 1;
btns[3].id = 3;
}
void key_task(button *btn)
{
uint8_t gpio_level = HAL_GPIO_ReadPin(btn->gpiox, btn->pin);
if (btn->state > 0) {
btn->ticks++;
}
if (btn->level != gpio_level) {
if (++(btn->debouce_cnt) > 3) {
btn->level = gpio_level;
btn->debouce_cnt = 0;
}
} else {
btn->debouce_cnt = 0;
}
switch (btn->state) {
case 0:
if (btn->level == 0) {
btn->ticks = 0;
btn->repeat = 1;
btn->state = 1;
}
break;
case 1:
if (btn->level != 0) {
if (btn->ticks >= 30) {
btn->repeat = 0;
ucLED[0] ^= 1;
btn->state = 0;
} else {
btn->state = 2;
}
btn->ticks = 0;
}
break;
case 2:
if (btn->ticks >= 10&&btn->repeat==2) {
btn->state = 0;
ucLED[2] ^= 1;
}
if (btn->ticks >= 10&&btn->repeat==1) {
btn->state = 0;
ucLED[1] ^= 1;
} else {
if (btn->level == 0) {
btn->repeat++;
btn->state = 1;
btn->ticks = 0;
}
}
break;
}
}
void key_state(void){
for(uint8_t i=0;i<4;i++){
key_task(&btns[i]);
}
}
状态机的基本逻辑:
1.通过state判断按键初始状态
当state=0时表示按键处于初始状态,当state=1时表示按键处于被按下状态,当state=2时表示按键处于被按下后的释放状态
2.当state=1或者state=2时
btn->ticks不断计时,计算处于每个状态的时间,并通过循环检查按键对应所产生的电平,
3.当state=0时
如果检查到btn->level==0,说明按键第一次被按下,btn->repeat=1,btn->ticks=0开始计时,btn->state=1进入按键按下判断状态。
4.当state=1时
不断计时,计时到检测到按键被松开时如果btn->ticks时间过长,则表示为长按,btn->repeat重置为0,btn->state重置为0,如果检测到一次短按,btn->state=2。两种情况btn->ticks都需要重新置0。
5.当state=2时
如果btn->ticks时间过长,则表示为一次短按,在释放状态下btn->ticks时间较短时且检测到按键被按下时,btn->repeat加一,btn->state重新变为1,btn->ticks重新计时,此时表示一次双击。
6.逻辑注意点:
通过上述逻辑的描述,较为难为理解的两个注意点是:
当state=1时,因为state表示按键被按下的状态,因此判断条件应该为btn->level!=0按键被释放的状态
同理,当state=2时,因为state表示按键被释放的状态,因此判断条件应为btn->level=0按键被按下的状态
总结
总的来说,按键的复杂逻辑判断都是通过按键引脚的时序逻辑编写对应的函数进行相应的判断,第一种方式较为简单,便于端平快的项目,第二种项目较为复杂,但是有着更好的实用性与移植性。你,学会了吗?
------------------------------------------------------------- L.M.
-----------------------------------------------------2025-2-20