1、项目描述

一个小巧简单易用的事件驱动型按键驱动模块,可无限量扩展按键,按键事件的回调异步处理方式可以简化程序结构,去除冗余的按键处理硬编码,让按键业务逻辑更清晰。
GitHub源码地址如下:
Github源码地址

2、代码移植

本文使用的开发板是正点原子探索者F407,首先使用STM32CubMx初始化外设信息,要求:1、初始化按键输入引脚PE2、PE3、PE4任意一个,2、串口打印功能。
不熟悉的可以查阅之前的blog;
按键
串口
初始化配置界面如下所示:
在这里插入图片描述在这里插入图片描述

移植代码工程,添加MultiButton代码到新建工程文件目录下:
如图所示:

在这里插入图片描述
按键回调函数编写:

uint8_t read_button1_GPIO()
{
	return HAL_GPIO_ReadPin(K1_GPIO_Port,K1_Pin);
}

void btn1_press_down_Handler(void *btn)
{
	printf("K1 press down...\r\n");
	HAL_GPIO_TogglePin(L1_GPIO_Port,L1_Pin);
}

void btn1_press_up_Handler(void *btn)
{
	printf("K1 press up...\r\n");
	HAL_GPIO_TogglePin(L0_GPIO_Port,L0_Pin);
}

while(1)之前添加按键初始化以及启动按键功能:

	printf("MultiButton Test...\r\n");
	
	button_init(&button1,read_button1_GPIO,0);
	
	button_attach(&button1,PRESS_DOWN,btn1_press_down_Handler);
	
	button_attach(&button1,PRESS_UP,btn1_press_up_Handler);
	
	button_start(&button1);
	
	printf("MultiButton Test...\r\n");

在while(1)添加如下代码

while (1)
{
	button_ticks();
	HAL_Delay(5);
}

实验现象:
按下按键K1,LED0翻转,抬起K1LED1翻转。
在这里插入图片描述

MultiButton源码分析

首先,定义了按键的时间类型,包括按键按下、抬起、单击、重复按键按下、双击、长按单次触发、长按一直触发等事件。

typedef enum {
	PRESS_DOWN = 0,//按键按下
	PRESS_UP,//按键抬起
	PRESS_REPEAT,//按下计数
	SINGLE_CLICK,//单次按下
	DOUBLE_CLICK,//双击
	LONG_PRESS_START,//长按
	LONG_PRESS_HOLD,//长按触发
	number_of_event,
	NONE_PRESS
}PressEvent;

定义按键链表结构体,这里使用到了位域操作,解决字节的存储空间。

typedef struct Button {
	uint16_t ticks;
	uint8_t  repeat : 4;//重复计数
	uint8_t  event : 4;//按键事件
	uint8_t  state : 3;//状态机状态位
	uint8_t  debounce_cnt : 3;//双击计数
	uint8_t  active_level : 1;//实际电平
	uint8_t  button_level : 1;//按键电平
	uint8_t  (*hal_button_Level)(void);
	BtnCallback  cb[number_of_event];
	struct Button* next;
}Button;

按键对象结构体的初始化,初始化成员包括按键句柄,绑定GPIO电平读取函数,设置有效触发电平

void button_init(struct Button* handle, uint8_t(*pin_level)(), uint8_t active_level)
{
	memset(handle, 0, sizeof(struct Button));
	handle->event = (uint8_t)NONE_PRESS;
	handle->hal_button_Level = pin_level;
	handle->button_level = handle->hal_button_Level();
	handle->active_level = active_level;
}

初始化按键完成之后,进行按键绑定操作,将绑定按键结构体成员,按键触发事件,按键回调函数

void button_attach(struct Button* handle, PressEvent event, BtnCallback cb)
{
	handle->cb[event] = cb;
}

按键启动:也就是将按键加入链表当中,启动按键。这里选择的插入方式是头部插入法,在链表的头部插入按键节点。效率高,时间复杂度为O(1)。

int button_start(struct Button* handle)
{
	struct Button* target = head_handle;
	while(target) {
		if(target == handle) return -1;	//already exist.
		target = target->next;
	}
	handle->next = head_handle;
	head_handle = handle;
	return 0;
}

按键删除,将按键从当前链表中删除。使用到了二级指针删除一个按键元素。与之前的多定时器删除方法相同。

void button_stop(struct Button* handle)
{
	struct Button** curr;
	for(curr = &head_handle; *curr; ) {
		struct Button* entry = *curr;
		if (entry == handle) {
			*curr = entry->next;
//			free(entry);
			return;//glacier add 2021-8-18
		} else
			curr = &entry->next;
	}
}

按键滴答函数,每间隔5ms触发一次按键事件,驱动状态机运行。

void button_ticks()
{
	struct Button* target;
	//按键中断依次循环读取链表中所有的按键元素信息,并且驱动状态机工作
	for(target=head_handle; target; target=target->next) {
		button_handler(target);
	}
}

多按键设计灵魂:

状态机处理思想:读取当前引脚的状态,获取按键当前属于哪种状态。

PressEvent get_button_event(struct Button* handle)
{
	return (PressEvent)(handle->event);
}

状态机,按键处理核心函数,驱动状态机。

/**
  * @brief  Button driver core function, driver state machine.
  * @param  handle: the button handle strcut.
  * @retval None
  */
  
void button_handler(struct Button* handle)
{
	/*读取当前按键引脚电平*/
	uint8_t read_gpio_level = handle->hal_button_Level();

	//ticks counter working..如果有状态机在运行,那么滴答定时器继续工作
	if((handle->state) > 0) handle->ticks++;

	/*------------button debounce handle---------------*/
	/*读取句柄引脚电平,连续读取3次引脚电平实现按键消抖功能*/
	if(read_gpio_level != handle->button_level) { //not equal to prev one
		//continue read 3 times same new level change
		if(++(handle->debounce_cnt) >= DEBOUNCE_TICKS) {
			handle->button_level = read_gpio_level;
			handle->debounce_cnt = 0;
		}
	} else { //leved not change ,counter reset.
	/*假如引脚状态与先前的不同,那么会改变按键对象的引脚状态*/
		handle->debounce_cnt = 0;
	}

	/*-----------------State machine-------------------*/
	switch (handle->state) {
	case 0:
		if(handle->button_level == handle->active_level) {	//start press down
			handle->event = (uint8_t)PRESS_DOWN;
			EVENT_CB(PRESS_DOWN);
			handle->ticks = 0;
			handle->repeat = 1;
			handle->state = 1;
		} else {
			handle->event = (uint8_t)NONE_PRESS;
		}
		break;

	case 1:
		if(handle->button_level != handle->active_level) { //released press up
			handle->event = (uint8_t)PRESS_UP;
			EVENT_CB(PRESS_UP);
			handle->ticks = 0;
			handle->state = 2;

		} else if(handle->ticks > LONG_TICKS) {
			handle->event = (uint8_t)LONG_PRESS_START;
			EVENT_CB(LONG_PRESS_START);
			handle->state = 5;
		}
		break;

	case 2:
		if(handle->button_level == handle->active_level) { //press down again
			handle->event = (uint8_t)PRESS_DOWN;
			EVENT_CB(PRESS_DOWN);
			handle->repeat++;
			EVENT_CB(PRESS_REPEAT); // repeat hit
			handle->ticks = 0;
			handle->state = 3;
		} else if(handle->ticks > SHORT_TICKS) { //released timeout
			if(handle->repeat == 1) {
				handle->event = (uint8_t)SINGLE_CLICK;
				EVENT_CB(SINGLE_CLICK);
			} else if(handle->repeat == 2) {
				handle->event = (uint8_t)DOUBLE_CLICK;
				EVENT_CB(DOUBLE_CLICK); // repeat hit
			}
			handle->state = 0;
		}
		break;

	case 3:
		if(handle->button_level != handle->active_level) { //released press up
			handle->event = (uint8_t)PRESS_UP;
			EVENT_CB(PRESS_UP);
			if(handle->ticks < SHORT_TICKS) {
				handle->ticks = 0;
				handle->state = 2; //repeat press
			} else {
				handle->state = 0;
			}
		}else if(handle->ticks > SHORT_TICKS){ // long press up
			handle->state = 0;
		}
		break;

	case 5:
		if(handle->button_level == handle->active_level) {
			//continue hold trigger
			handle->event = (uint8_t)LONG_PRESS_HOLD;
			EVENT_CB(LONG_PRESS_HOLD);

		} else { //releasd
			handle->event = (uint8_t)PRESS_UP;
			EVENT_CB(PRESS_UP);
			handle->state = 0; //reset
		}
		break;
	}
}

状态机流程图
在这里插入图片描述

Logo

秉承“创新、开放、协作、共享”的开源价值观,致力于为大规模开源开放协同创新助力赋能,打造创新成果孵化和新时代开发者培养的开源创新生态!支持公有云使用、私有化部署以及软硬一体化私有部署。

更多推荐