物理按键,在很多嵌入式产品里面应用得非常广泛,很多嵌入式软件工程师在刚刚开始入门的时候,点完灯之后就开始学习按键输入检测。按键输入可以说是继点灯之后,又一经典的嵌入式入门必学内容之一。
在很多嵌入式入门学习的教程里面,按键原理普遍被认为是“很简单”的知识点之一,按键输入检测的原理,无非就是通过CPU不断扫描按键引脚的电平状态,或者采用单片机引脚外部中断方式,然后在死循环或者中断服务程序里面处理按键被按下后的逻辑。
然而,在这个“很简单的高低电平检测”的原理背后,通过产品经理给物理按键各个动作赋予的(难以理解的)意义,一个小小的物理按键开始变得复杂起来,这些动作包括:按下、抬起、单击、双击、点动、长按、组合按键。。。等等。
以上这些复杂的按键动作,已经不是一个“简单的高低电平检测”所能描述清楚的了,成熟的单片机按键检测模块,必须能很好地处理以上按键动作,并且具有很高的内聚度,与单片机的底层引脚尽量低耦合,且能提供灵活的应用层调用接口。
采用嵌入式 C 语言面向对象的思想,通过状态机和回调函数的方式,我们来编写一个通用的按键检测模块,以更好地覆盖单片机的物理按键应用场合。
以下是物理按键模块的设计过程。
1、这个通用的物理按键模块,主要是由4个源代码文件组成,key_driver.c和key_driver.h主要是驱动层接口,主要面向不同的单片机引脚适配。key_module.c和key_module.h主要是面向应用层接口,与芯片硬件引脚无关。
2、key_driver.c 和 key_driver.h主要是用来适配不同的单片机GPIO外设的,在key_driver.h里面,声明了一个key_driver_t类型的结构体,主要提供GPIO引脚初始化接口以及引脚电平读取接口,如下图所示。
3、在key_driver.c里面,主要是对初始化接口和引脚电平读取接口的具体实现,比如,引脚初始化接口_init()函数和电平读取接口_read_pin_state(),其具体实现如下图所示。
4、在key_driver.c里面,定义了一个key_driver结构体变量,记住这个变量,很重要,后面会被key_module进行调用,key_driver的具体内容如下图所示。
5、在key_module.h里面,主要是声明了两个重要的结构体,key_t结构体是面向单个按键对象的,主要是包括按键ID以及按键状态枚举,还有一些变量是用来进行按键检测过程的,key_manager_t结构体主要是用来管理多个按键对象的,包括各个按键动作的函数接口,还有按键引脚的驱动程序,如下图所示。
6、按键模块还对外提供了多个外部调用接口,包括模块初始化,按键模块时间更新,按键模块的时基更新,按键模块的按键动作回调函数处理,如下图所示。
7、在key_module.c里面,主要是对以上外部接口的具体实现,比如,key_module_init()主要是对按键模块的各个参数初始化,以及注册按键模块的引脚驱动程序,代码如下图所示。
8、在key_module_update()函数里面,主要是以状态机和回调函数的方式,处理各个按键状态和动作,按键状态有KEY_IDLE、KEY_PRESSED、KEY_RELEASED、KEY_SINGLE_CLICK、KEY_DOUBLE_CLICK、KEY_LONG_PRESS。代码如下图所示。
9、在各个不同的状态里面,通过回调函数的方式,分别对按下、抬起、单击、双击、长按、等按键动作进行处理,限于篇幅,这里只列出部分代码,具体实现请参考具体源码和注释。
10、按键模块需要对其提供系统时基,通常以1毫秒或者10毫秒作为时间基准,key_module_ticks_update()主要是在外部定时器或者外部1毫秒线程中被调用,key_module_set_event_handler()主要是用来设置各个按键状态的回调函数,如下图所示。
11、如何使用key_module?假如项目采用RT-Thread进行调度,在main()函数里面,先创建一个key_module_thread()线程,然后在该线程里面先对按键管理器进行初始化,然后注册各种按键状态的回调函数,最后在while循环里面,更新按键管理器的时基以及状态更新函数,线程主体以1毫秒的间隔进行调度,如下图所示。
12、以上,就是一个通用的单片机按键模块具体设计,通过这个按键检测模块,可以很好地处理各种按键状态事件,并且该按键模块在设计上遵循设备与驱动分离的原则,尽量做到了高内聚低耦合,具体很好的移植性和单片机平台适配性。
13、美中不足的是,这个模块还没有加入组合按键处理,感兴趣的读者,可以下载该模块的源码,对其进行修改和扩展。