本文主要是介绍STM32按键设计三之两个按键操控整个系统十几乃至几十种功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本篇任务
前言:
按键,基本上所有设备的必备,可以毫不夸张的说:目之所及,皆有按键。
几个按键合适?根据系统控制需要,量才而用。按键是大才,能轻易的控制整个系统进行不同状态或者行为的切换,同时也是耗材,浪费IO口,占据大量体积。所以,需要量才而用,买足系统需求的前提下,越精简越好。
本篇将在上一篇按键中断的基础上,实现两个按键控制系统十几乃至几十中状态,节省器件,节约空间,节约IO口,同时又能实现复杂功能,目标就两个字”节约精简“。
按键实现
需要了解本篇,需要对上篇有个大致了解。只需要知道上篇按键中断里面预留的接口函数,本篇将构建与上篇的接口之上。上一篇传送门:STM32按键设计二之按键中断。
- 按照质量守恒定律,如果按键即是质量中的量,软件为质量中的质。在减少了按键的数量的同时需要想要实现复杂按键的功能,需要提高软件的质。计算机中的代码优化和很多算法优化大多也是如此,都是在空间复杂度和时间复杂度中做权衡。
- 多功能按键需要在软件中将底层的按键设备抽离到软件层面,封装为一个大的结构体类型,整体上进行思考。
- 需要抽离出一个软件定时器类型,用于调度整个过程,甚至控制整个系统。
- 编写软件的过程实际是在反复验证逻辑的缜密性,很多时候需要大脑对整个过程进行模拟。
多功能按键实现基本逻辑图
- 以上是按键实现的基本逻辑图,基本有两点:
- 需要在按键按下到松开的时候,进行按键按下时间长度的采集。用作分别设置短按,长按和复位按多种形式。
- 在本次按键和下次按键按下之间进行时间判定,如果超时,本次连续按键事件结束,系统控制程序运行。需要设置连续按键间隔时间,可以根据各自的系统按键情况进行设置。一般连按时间多很小。
按键软件实现
实现按键前,需要明白两个接口函数KEY0_Down_callback和KEY0_Up_callback,这是在硬件按键层设置的按键按下和松开接口处理函数,如此实现抽象出来了接口,为了更加容易维护和复用。实现方式和上篇中的传送门:STM32按键设计二之按键中断一模一样,没有做任何更改,可以直接使用。(整个代码篇幅太长,很多不是重点的内容或者重复内容不展示了)
软件定时器实现全局调度功能
- 通过以上逻辑图,可以看出整个过程需要对时间进行计时,本篇采用软件定时器实现。也可以通过基本硬件定时器实现,控制方式都是一样的,但实现原理上软件定时器稍微复制一些,但是可以提供给整个系统使用,方便万能。直接贴代码:
- 软件定时器结构体定义
/* 软件定时器结构定义 ---------------- */
STRUCT(Timer_t)
{Timer_Sta_t state; /* 是否在使用 */int32_t val; /* 下次触发值 */int32_t arr_val; /* 重装载值 */u32 cycles; /* 循环次数: 0xFF: 永远循环,1 ~ 0xFE: 循环次数 */TimerCallback_t callback;Callback_Block_t is_block;
};
- 首先,定义一个软件定时器列表,用于
Timer_t Timers[TIMERS_SIZE];
- 软件定时器设置函数,提供给系统任务,每个任务都可以为自己或者其他任务设置软件定时器
/* 函数定义 ------------------------------------------------------- */
/*** @name: Timer_SetAlarm* @description: 添加定时器* @param {u32} cycles* @param {u32} arr* @param {TimerCallback_t} callback* @return {*}*/
int8_t Timer_SetAlarm(u32 cycles, u32 arr, TimerCallback_t callback, Callback_Block_t is_block)
{Timer_t *timer;int8_t timer_index;int8_t ret = TIMER_NONE;for(timer_index = 0, timer = Timers; timer_index <= timers_ptr + 1 && timer_index < (sizeof(Timers) / sizeof(Timers[0])); timer_index++, timer++){if((timer->state == TIMER_FREE) && callback){uint16_t next_time;/* 设置软件定时器重装载值 */timer->arr_val = arr;/* Timers指针增加条件 */if(timer_index == timers_ptr + 1) timers_ptr++;next_time = getNextTimerInterrupt();if(arr < next_time){total_sleep_time += next_wakeup - next_time;next_wakeup = arr + next_wakeup - next_time;setTimer(arr);}/* 设置定时时间和状态 */timer->state = TIMER_WORKING;timer->cycles = cycles;timer->val = arr + next_wakeup - next_time;timer->callback = callback;timer->is_block = is_block;ret = timer_index;break;}} return ret;
}
- 软件定时器实现基本调度,在硬件定时器中断中进行调用,本篇硬件定时器配置的时钟分频为10us。
/*** @name: Timer_Dispatch* @description: 时间调度,可以在中断中进行* @param {*}* @return {*}*/
void Timer_Dispatch(void)
{Timer_t *timer;int8_t timer_index;u32 temp = 0xffffffff;for(timer_index = 0, timer = Timers; timer_index <= timers_ptr && timer_index < sizeof(Timers) / sizeof(Timers[0]); timer_index++, timer++){if(timer->state == TIMER_WORKING){timer->val -= next_wakeup;if(timer->val <= 0){ if(timer->cycles == 0xFF){timer->val = timer->arr_val;}else if(timer->cycles > 1){timer->val = timer->arr_val;timer->cycles--;}else if(timer->cycles == 1){/* 当软件定时器执行一次的时候,执行完成 */timer->state = TIMER_FREE;}/* 将定时器时间到达,需要执行的函数加入回调函数列表 */if(timer->is_block == CALLBACK_NOBLOCK){Timers_Callback_noblock[++callback_noblock_ptr] = timer->callback;}else{Timers_Callback_block[++callback_block_ptr] = timer->callback;}}else{if((u32)timer->val < temp)temp = timer->val;}}}/* 设置下次定时器时间 */next_wakeup = temp > TIMn_ARR ? TIMn_ARR : ((uint16_t)temp);setTimer(next_wakeup);}
由于软件定时器不是本篇重点,所以很多内容从简(主要由于写完篇幅过长)。
按键实现
- 在本篇中依然调用了这两个接口函数进行。
/* 任务回调函数,按键key.c中的接口回调 */
void KEY0_Down_callback(void)
{KEY_Down(KEY0_DEV);
}/* 任务回调函数,按键key.c中的接口回调 */
void KEY0_Up_callback(void)
{KEY_Up(KEY0_DEV);
}
KEY_Down和KEY_Up是具体的处理函数,后续有源码。
- 使用一个位段,对按键结构进行状态设置,根据状态进行相应的操作
typedef struct _tag_KEY_STA_BIT_t KEY_STA_BIT_t;
struct _tag_KEY_STA_BIT_t
{uint8_t using: 1; /* 正在采集标志位 */uint8_t processing: 1; /* 采集完成需要处理标志位 */ uint8_t press_long: 1; /* 长按标志位 */uint8_t press_3s: 1; /* 长按标志位 */uint8_t times :4; /* 按键按下次数 */
};
- 同时定义一个共用体,方便对状态位进行一次性操作
typedef union _tag_KEY_STA_t KEY_STA_t;
union _tag_KEY_STA_t
{uint8_t flag; /* 通过flag操作state,方便清0 */KEY_STA_BIT_t state;
};
- 定义按键结构体,进行按键的操作,此后基本的按键都基于此
/* 实际按键设备编号 */
typedef enum _tag_KEY_Dev_t KEY_Dev_t;
enum _tag_KEY_Dev_t
{KEY0_DEV = 1,KEY1_DEV,
};/* 软件key结构体 */
STRUCT(KEY_t)
{KEY_STA_t key_state; /* KEY状态值 */KEY_Dev_t keys_table[KEY_TABLE_SIZE]; /* 按键列表 */u32 time_val; /* 按下时的系统时间 */
};
- 下面就是对开头的函数接口中函数的实现了,按键松开处理函数
/*** @name: KEY_Up* @description: 按键松开处理函数* @param {KEY_Dev_t} key_dev 按键设备号* @return {*}*/
static void KEY_Up(KEY_Dev_t key_dev)
{u32 temp;if(!Key.key_state.state.processing) /* 判断没有按键采集完成,未处理 */{temp = get_SystemTime();if(!Key.key_state.state.using) /* 没开始采集 */{/* 没采集的操作 */}else{temp = temp > Key.time_val ? temp : (temp + TOTAL_SLEEP_TIME_MAX);if(temp - Key.time_val > KEY_PRESS_3sLONG) /* 长按键3s */{/* 长按键只能单独使用 */Key.key_state.state.processing = 1; /* 结束按键采集 */Key.key_state.state.press_3s = 1;Key.key_state.state.times = 1;}else if(temp - Key.time_val > KEY_PRESS_LONG) /* 长按键 */{/* 长按键只能单独使用 */Key.key_state.state.processing = 1; /* 结束按键采集 */Key.key_state.state.press_long = 1;Key.key_state.state.times = 1;}else /* 短按键 */{/* 短按键可以叠加 */if(Key.key_state.state.times > KEY_COLLECTION_TIMES){Key.key_state.state.processing = 1; /* 结束按键采集 */}else{Key.key_state.state.times++; /* 按键次数记录 */callback_num = Timer_SetAlarm(1, NEXT_KEY_SLICE, KEY_Timer_Callback, CALLBACK_NOBLOCK);}}Key.keys_table[Key.key_state.state.times - 1] = key_dev; /* 按键设备号记录 */}}else{/* 按键未处理,新按键按下,处理程序 */}
}
- 首先说明:本系统中长按和复位按(3s按)不会进行连按设置,也就是这两种不支持连按。只有短按(接触按)可以使用连按,主要是考虑到实际当中长按用户的一般使用和体验,如果需要也可能实现其逻辑。
- KEY_Up进行了按键事件的采集,并且和按下时间进行计算,分析长按和短按模式。长按和复位按不支持连按会设置按键采集完成,需要处理标志位;短按则会判断按键次数和设置一个定时器时间,用于判断是否一次有效的连续采集。
- 最后记录了按键的物理设备按键码。
- 注意:在写代码的过程中,难免手上动作和思维不一样,想清楚而没写对型。callback_num = Timer_SetAlarm(1, NEXT_KEY_SLICE, KEY_Timer_Callback, CALLBACK_NOBLOCK);只写了Timer_SetAlarm(1, NEXT_KEY_SLICE, KEY_Timer_Callback, CALLBACK_NOBLOCK);。编译,运行都可以,出现了逻辑错误,差点没要老命啊!逻辑错误最难找,后面有图示。
- 定时器调用的函数,简短
/*** @name: KEY_Timer_Callback* @description: 如果在下次之间没有按键,进行按键收尾设置* @param {*}* @return {*}*/
static void KEY_Timer_Callback(void)
{Key.key_state.state.processing = 1;
}
- 按键按下处理函数
/*** @name: KEY_Down* @description: 按键按下处理函数* @param {KEY_Dev_t} key_dev 按键设备号* @return {*}*/
static void KEY_Down(KEY_Dev_t key_dev)
{if(!Key.key_state.state.processing) /* 判断没有按键采集完成,未处理 */{printf("key %d down ! \n", key_dev);Key.time_val = get_SystemTime();if(!Key.key_state.state.using) /* 没开始采集 */{Key.key_state.state.using = 1; /* 第一次采集 */}else{Timer_DelAlarm(callback_num);}}else{/* 按键未处理,新按键按下,处理程序 */}
}
- 首先判断是否上次的按键事件没有正在处理的。
- 记录按键按下时间。
- 如果是第一次按键按下,将按键使用中标志位置1;如果是连续按键,则需要将软件定时器清除。
- 主要部分完成。
按键事件状态机实现
多功能按键事件相对简单,可以使用简单的if/else或者switch/case进行实现。对各个状态进行不同的操作可以提供一个函数接口,根据需要对其接口进行实现就可以实现具体的功能。
- 按键状态机实现代码
/*** @name: KEY_Event_Handler* @description: 按键事件处理函数* @param {*}* @return {*}*/
void KEY_Event_Handler(void)
{u32 short_press_val = 0;if(1 == Key.key_state.state.processing) /* 按键采集完成,需要处理 */{// printf("processing: %d \n", Key.key_state.state.processing);if(1 == Key.key_state.state.press_3s) /* 按键3s */{KEY_Press_3sLong();} else if(1 == Key.key_state.state.press_long) /* 长按键处理 */{KEY_Press_Long();}else{ /* 短按键处理 */for(int8_t i = 0; i < Key.key_state.state.times; i++){/* 短按键计算按键结果 */short_press_val += (u32)pow(10, Key.key_state.state.times-1-i) * Key.keys_table[i];}// printf("press short val: %d \n", short_press_val);Short_Press_Handler(short_press_val);}KEY_State_Clear();}
}/*** @name: Short_Press_Handler* @description: 短按键状态机,连按处理方式* @param {u32} val* @return {*}*/
static void Short_Press_Handler(u32 val)
{switch(val){case PRESS_1:KEY_Press_1();break;case PRESS_2:KEY_Press_2();break;case PRESS_11:KEY_Press_11();break;case PRESS_12:KEY_Press_12();break;case PRESS_21:KEY_Press_21();break;case PRESS_22:KEY_Press_22();break;case PRESS_111:KEY_Press_111();break;default:Press_Err_Handler();break;}
}
- KEY_Event_Handler 和 Short_Press_Handler一起实现了按键事件的整个状态机,Short_Press_Handler为一个内部函数,只在KEY_Event_Handler 中进行调用,同时又为段案件时间提供了可以编程的接口函数,用于外部实现不同的功能。
- 短按键状态判断,根据其按键顺序进行,比如先按1,再按2,则其值为12,调用KEY_Press_12接口进行处理;在Press_Err_Handler未做任何处理。
- 所有的函数已经内部实现,但是不影响外部使用,因为内部使用了weak连接进行编译链接,如下放上全部,利于查看调试结果。
/* 按键事件函数接口,对外提供调用接口 ----------------------------------- */
__attribute__((weak)) void Press_Err_Handler(void)
{printf("weak: key press error \n");
}__attribute__((weak)) void KEY_Press_3sLong(void)
{printf("weak: press 3s handle func, system reset! \n");
}__attribute__((weak)) void KEY_Press_Long(void)
{printf("weak: press Long handle func, ! \n");
}__attribute__((weak)) void KEY_Press_1(void)
{printf("weak: KEY_Press_1 \n");
}__attribute__((weak)) void KEY_Press_2(void)
{printf("weak: KEY_Press_2 \n");
}__attribute__((weak)) void KEY_Press_11(void)
{printf("weak: KEY_Press_11 \n");
}__attribute__((weak)) void KEY_Press_12(void)
{printf("weak: KEY_Press_12 \n");
}__attribute__((weak)) void KEY_Press_21(void)
{printf("weak: KEY_Press_21 \n");
}__attribute__((weak)) void KEY_Press_22(void)
{printf("weak: KEY_Press_22 \n");
}__attribute__((weak)) void KEY_Press_111(void)
{printf("weak: KEY_Press_111 \n");
}
- 同时在APP中对,需要调用接口函数进行了再次实现
void KEY_Press_3sLong(void)
{printf("In APP, 3s press! reset now ... \n");
}void KEY_Press_11(void)
{printf("In APP, press_11 ... \n");
}
结果分析
- 终于对了
以上进行了简单测试
- 1为只按了一次按键1,调用了默认的接口函数KEY_Press_1 ;
- 3为连续短按了按键1和按键2,调用了默认的接口函数KEY_Press_12;
- 4为按下两次按键1,调用了外部实现的任务函数接口KEY_Press_11,打印结果为In APP,说明不是默认函数实现;
- 5按了3s的按键1,调用系统复位;
- 6为按下三次按键2,由于此按键事件没有定义和实现,所以调用了默认Press_Err_Handler错误处理函数。
- 在终于对了之前,出现的问题:
多出三只鬼,消灭三只 …
尾声
- 至此,多功能按键基本完了。
- 2个按键的多功能实现,按键最多按一次,可以有6种状态,分别为:4+2 = 6
按键1,按键2,按键1长按,按键2长按,按键1复位按,按键2复位按- 2个按键,按键次数设置位2,可以实现10种状态;按键次数设置位3,可以实现18种状态;
- 总公式为 : 4 + 21 + 22+ … + 2n
- 推荐使用按键次数设置为2或者3,同时保留长按或者复位按中的一个,这样可以使用的状态有8~~16种之多,如果不够用可以使用状态机,设置不同系统状态下,按键状态对应的功能不一样,有限无穷,周而复始。
- 按键此时判断在上面KEY_Up函数中,KEY_COLLECTION_TIMES宏定义了按键次数,可以方便更改。
码字不易,请点个赞哦,谢谢!
这篇关于STM32按键设计三之两个按键操控整个系统十几乃至几十种功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!