面向对象的单片机按键驱动程序,支持gpio和adc按键,具有多种事件处理方式

本文介绍了一个名为CherryKey的按键驱动,它具有面向对象、静态链表管理的特点,支持GPIO和ADC按键,提供按下、弹起、单击、双击、长按等基础事件。每个事件可以有多个回调函数,同步/异步处理,且支持组合按键。文章详细讲解了驱动设计思路和使用方法,包括如何注册回调函数、处理异步回调以及兼容不同类型的按键。
摘要由CSDN通过智能技术生成

一、前言

1.1 文章的背景和目的

  相信玩嵌入式的朋友大家有写过不少按键驱动,可能很多时候都是驱动层跟应用层耦合严重,内容一多起来逻辑就变得非常复杂,应用层功能稍有改变或按键的增减、类型发送变化,程序都得大改。为了以后在新板子上使用按键不需要过多考虑驱动层,我特意写了个按键驱动。

1.2 下载地址

驱动下载地址:CherryKey
视频讲解:https://www.bilibili.com/video/BV1is4y1n7rR/?share_source=copy_web&vd_source=50bde786a88edb8a73bf68836c62e1a7

1.3 文章的主要内容和结构

  该帖子主要介绍了CherryKey的特点、设计思路、使用方法。

二、驱动介绍

2.1驱动特点

  1. 面向对象
  2. 静态链表管理按键
  3. 参考了键盘、鼠标的驱动,给出了适合单片机的基础事件:按下、弹起、单击、双击、长按
  4. 每个按键的每个事件有独立的多个回调函数、已注册的回调函数使用列表存储。回调函数分同步/异步处理,异步处理的回调函数使用队列管理,让你的程序更有主次
  5. 驱动兼容GPIO按键、ADC按键,支持连按,组合按键。

2.2驱动讲解

1.为什么使用链表。因为使用链表我们就可以像下面的程序一样,一份代码适用多个对象。

void key_detect_scan() {
	uint16_t i, length;

	length = slink_length(&key_link);
	
	for (i=0; i<length; i++) {	
		key_detect((key_t*)slink_getData(&key_link, i+1));
	}	
}

2.为什么给出五个基础事件:按下、弹起、单击、双击、长按。在平时使用的单片机按键驱动中,常常只有按下、双击、长按,而单击(click)跟按下(press)是不一样的,有些时候我们需求用户有一个按下弹起才触发的情况,这就是这份代码的考虑。在给出的五个基础事件中,用户可以根据需求在回调函数设计三击、四击等动作。
另外也支持组合按键例如下面是ADC按键跟GPIO按键的组合:

	if ((key_get_event(&key2) == LONG_PRESS) && (key_get_event(&key3) == LONG_PRESS)) {
		HAL_GPIO_WritePin(rgb_g_GPIO_Port, rgb_g_Pin, GPIO_PIN_RESET);  // rgb灯绿色
		HAL_GPIO_WritePin(rgb_g_GPIO_Port, rgb_r_Pin, GPIO_PIN_RESET);  // rgb灯红色
	}	

使用key_get_event可以获取按键当前的事件

3.每个按键的每个事件有自己独立的多个回调函数,回调函数使用虚函数表存储

	typedef void (*key_cb)(void);
	
	key_cb_t     press_cb[KEY_CB_NUM];		// 按下回调	
	key_cb_t     bounce_cb[KEY_CB_NUM];		// 弹起
	key_cb_t     click_cb[KEY_CB_NUM];    // 按下弹起(单击)
	key_cb_t     dbclick_cb[KEY_CB_NUM];	// 双击
	key_cb_t     lp_cb[KEY_CB_NUM];		    // 长按

在回调函数注册时可根据需求设置同步/异步处理

void key_onpress_register(key_t *key_num, key_cb callback, uint8_t index, uint8_t async) {
	
	if((index > KEY_CB_NUM-1) || (index < 0) || (callback == NULL))
		return;
	
	key_num->press_cb[index].callback = callback;
	key_num->press_cb[index].async = async;
}

比如对于hello world这种重要的事情(手动狗头)我们采用同步处理,而点亮LED这种可以选择异步处理。  

4.异步处理的函数使用队列管理

static void key_press_callback(key_t *key_num) {
	uint8_t i;
	
	for (i=0; i<KEY_CB_NUM; i++) {
		if (key_num->press_cb[i].async == SYNC) {     // 同步,立即执行
			key_num->press_cb[i].callback();  							
		}     
		else if(key_num->press_cb[i].async == ASYNC) {				// 异步,加入队列
			queue_insert(&key_cb_queue, key_num->press_cb[i].callback);
		}																 						
	}	
}
void key_callback_handle(void) {
	uint8_t     i, len;
	key_cb      callback = NULL;
	
	len = queue_length(key_cb_queue);
	for (i=0; i<len; i++) {
		queue_extract(&key_cb_queue, (QElemType*)&callback);
		callback();
	}
}

这样我们就只需要让key_callback_handle定期执行就可以了,如10ms。
5.兼容ADC按键跟GPIO按键

#define key_port    					void                               //第几组引脚
#define key_pin 						uint16_t						   //第几个引脚
// 对应GPIO_TypeDef
typedef struct {
	key_pin 	pin[KEY_ADC_NUM];
} key_adc_port_t;

把一个ADC引脚抽象为一个key_adc_port_t,ADC引脚控制的每个按键抽象为pin[i],这样就可以类似GPIO一样使用ADC引脚了。ADC按键读键值函数如下:

static key_level_e key_adc_read(key_port *port, key_pin pin) {
	key_adc_port_t *adc_port = (key_adc_port_t *)port;	
	return adc_port->pin[pin] ? KEY_HIGH : KEY_LOW;
}

而pin[i]的值由以下函数计算:

void key_adc_update(key_adc_port_t *KeyADCPort, uint32_t adc_value) {
	uint8_t i; 
  uint32_t voltage = adc_value;
	//单个按键按下
	for (i = 0; i < KEY_ADC_NUM; i++) {
			KeyADCPort->pin[i] = (voltage < key_adcRange[i][0] && voltage > key_adcRange[i][1]) ? KEY_HIGH : KEY_LOW;
	}	

	//2个按键按下
	if (voltage < key_adcRange[4][0] && voltage > key_adcRange[4][1]) {
		KeyADCPort->pin[0] = KEY_HIGH;
		KeyADCPort->pin[1] = KEY_HIGH;
	}		
	......	

在我的板子里,ADC按键是支持多个按键同时按下的,所以四个按键有15种情况,后面就是用 if 把各种情况列出来。如果只支持单个按键按下,那么只要开始的for循环就行。

三、程序的使用

1.首先在key_config.h文件配置我们的参数,例如:

#define key_port    				void                               //第几组引脚
#define key_pin 					uint16_t						   //第几个引脚

/* 定期执行时间 */
#define KEY_TIME_BASE     1   //ms  

#define 	KEY_NUMBER    	  6    // 按键数量
#define   KEY_ADC_NUM				4		 // ADC按键数量
#define   KEY_CB_NUM			  6    // 每个按键可注册的回调函数数量
#define   KEY_QUEUE_LEN     50	 // 队列长度

2.然后是初始化,每个按键就是一个对象,初始化时需要传入对象、读键值函数、引脚端口、ID,同时根据实际情况使用key_set_level设置按键的有效电平。

根据需要创建按键

extern key_t   key1;
extern key_t   key2;
extern key_t   key3; 
extern key_t   key4;
extern key_t   key5; 
extern key_t   key6;
void key_init(void) {
	
	/* 链表、队列初始化 */
	slink_init(&key_link, key_sll, KEY_NUMBER+2);
	queue_init(&key_cb_queue, key_cb_array, KEY_QUEUE_LEN);
	
	/* 创建按键 */
	/* gpio按键 */
	key_create(&key1, &key_level_read, (key_port*)keya_GPIO_Port, keya_Pin, 1);
	key_create(&key2, &key_level_read, (key_port*)keyb_GPIO_Port, keyb_Pin, 2);	
	
	/* adc按键 */
	key_create(&key3, &key_adc_read, (key_port*)&KeyADCPort, 0, 3);	
	key_create(&key4, &key_adc_read, (key_port*)&KeyADCPort, 1, 4);	
	key_create(&key5, &key_adc_read, (key_port*)&KeyADCPort, 2, 5);	
	key_create(&key6, &key_adc_read, (key_port*)&KeyADCPort, 3, 6);	
	
	/* 设置adc按键高有效 */
	key_set_level(&key3, KEY_HIGH);
	key_set_level(&key4, KEY_HIGH);
	key_set_level(&key5, KEY_HIGH);
	key_set_level(&key6, KEY_HIGH);
}

3.注册回调函数

void key_demo_init(void) {
	/* 注册回调函数 */
	key_onpress_register(&key1, key1_press, 0, SYNC);
	key_onbounce_register(&key1, key1_bounce, 0, SYNC);
	key_onclick_register(&key1, key1_click, 0, SYNC);
	key_ondbclick_register(&key1, key1_dbclick, 0, SYNC);
	key_onlp_register(&key1, key1_lp, 0, SYNC);
		
	key_onclick_register(&key3, key3_click, 0, ASYNC);
	key_onclick_register(&key4, key4_click, 0, ASYNC);
	key_onclick_register(&key5, key5_click, 0, ASYNC);
	key_onclick_register(&key6, key6_click, 0, ASYNC);		
}

当然,回调函数可以在任何时候注册/注销,比如

void key1_click(void)
{
	key_onclick_unregister(&key3, 0);
	printf("按键1单击\n");
}

void key1_dbclick(void)
{
	key_onclick_register(&key3, key3_click, 0, SYNC);
	printf("按键1双击\n");
}

这在我们写屏幕菜单的时候很有用,比如在第一页的按键是功能1,进入第二页则是功能2,这时我们可以把功能1回调函数注销后注册功能2。

4.主要的代码在key.c、key.h、key_config.h其余依赖文件有:
public.c/public.h
staticLinkList.c/staticLinkList.h
my_queue.c/my_queue.h

四、按键驱动的测试结果和效果展示

在这里插入图片描述
图里的效果是根据demo里的回调函数产生的,按顺序分别是单击、双击、连按、长按、连按、单击

五、总结与展望

  这个按键驱动应该能满足大部分单片机的应用场景,而且使用简单,里面还有多种数据结构,方便入门学习。目前使用笔记多的是GPIO按键跟ADC按键,以后可能会根据需求添加别的按键类型。
  还有个题外话,就是在触发双击事件的同时会触发两次单击,可能有的同学会问为什么不在驱动层解决这个问题。其实一般的解决办法是检测到单击后先不触发事件,等过了双击时间还没双击事件再触发单击事件,这样一来就使单击的灵敏度变低,就不适合一些实时应用场景了。所以驱动层只给出基础的功能比较好,让用户根据自己需要进行设计。
  这里我抛砖引玉给出一个办法,就是使用软件定时器。在不想被双击触发的单击回调函数里开启一个定时器x(如500ms),在这段时间里如果没有双击触发,则会触发定时器回调,在定时器回调函数执行单击事件。如果有双击事件触发(双击回调带有停止定时器x语句),则不会进入定时器回调,这样就解决了上面的问题。
  因个人能力有限,如果有写得不对的地方欢迎大家提出/补充。

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值