MultiButton开源项目实现单击双击长按

本文详细介绍了MultiButton,一个轻量级的按键驱动模块,它通过面向对象设计和事件回调简化编程。文章涵盖了按键特性、状态机实现、源码剖析以及使用示例,适合学习嵌入式编程和事件处理的开发者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、MultiButton简介

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

源码地址:GitHub - 0x1abin/MultiButton: Button driver for embedded system

二、MultiButton源码分析

1.按键特性

MultiButton 使用C语言实现,基于面向对象方式设计思路,每个按键对象单独用一份数据结构管理:

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  button_id                                 //按键id
	uint8_t  (*hal_button_Level)(uint8_t  button_id);  //返回按键电平函数
	BtnCallback  cb[number_of_event];                  //回调函数
	struct Button* next;                               //链表指针
}Button;

其中使用位域充分利用内存,这样每个按键使用单向链表相连,依次进入 button_handler(struct Button* handle) 状态机处理,所以每个按键的状态彼此独立。

2.按键事件

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;

结合button_handler(struct Button* handle) 状态机,触发不同时间表现不同特性,以及触发不同事件的回调函数。

3.链表设计思想

按键特性的Button结构体就是一个链表节点类型,比如我下面声明两个链表节点

static struct Button bt_key1; 
static struct Button bt_key2;  

button_init()接口给每个节点写入属于自己的数据

/**
  * @brief  Initializes the button struct handle.
  * @param  handle: the button handle strcut.
  * @param  pin_level: read the HAL GPIO of the connet button level.
  * @param  active_level: pressed GPIO level.
  * @param  button_id: the button id.
  * @retval None
  */
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;
}

初始化一个链表头指针,将链表节点依次连接起来

static struct Button* head_handle = NULL;
/**
  * @brief  Start the button work, add the handle into work list.
  * @param  handle: target handle strcut.
  * @retval 0: succeed. -1: already exist.
  */
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;
}

每次连接新节点都会遍历当前链表是否存在该节点,所以button1进来target为null会跳过while循环,head_handle指向最新节点。

 4.回调函数

给BtnCallback命名为函数指针类型

typedef void (*BtnCallback)(void*);

前面在Button结构体里面使用BtnCallback定义了一个回调函数指针数组cb[number_of_event],相当于声明了number_of_event个回调函数

BtnCallback  cb[number_of_event];

注册回调函数,给Button出发事件绑定对应的回调函数

/**
  * @brief  Attach the button event callback function.
  * @param  handle: the button handle strcut.
  * @param  event: trigger event type.
  * @param  cb: callback function.
  * @retval None
  */
void button_attach(struct Button* handle, PressEvent event, BtnCallback cb)
{
	handle->cb[event] = cb;
}

执行回调函数接口,出入触发事件即调用上面对应Button绑定的回调函数

#define EVENT_CB(ev)   if(handle->cb[ev])handle->cb[ev]((Button*)handle)

5.核心代码-状态机实现

//According to your need to modify the constants.
#define TICKS_INTERVAL    5	                   //时间间隔
#define DEBOUNCE_TICKS    3	                   //去抖动时间
#define SHORT_TICKS       (300 /TICKS_INTERVAL)//单击触发时间
#define LONG_TICKS        (1000 /TICKS_INTERVAL)//长按触发时间

使用状态机根据按键输入电平判断对应事件的触发,然后跳转不同的状态,触发对应事件时会调用相对于的事件回调函数。进入状态机之前会对按键电平进行去抖动操作。

/**
  * @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(handle->button_id);

	//ticks counter working..
	if((handle->state) > 0) handle->ticks++;

	/*------------button debounce handle---------------*/
	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;
    default:
        handle->state = 0; //reset
        break;
	}
}

 遍历链表依次进入button_handler()接口

​
/**
  * @brief  background ticks, timer repeat invoking interval 5ms.
  * @param  None.
  * @retval None
  */
void button_ticks()
{
	struct Button* target;
	for(target=head_handle; target; target=target->next) {
		button_handler(target);
	}
}

​

 遍历链表依次进入button_handler()接口

​
/**
  * @brief  background ticks, timer repeat invoking interval 5ms.
  * @param  None.
  * @retval None
  */
void button_ticks()
{
	struct Button* target;
	for(target=head_handle; target; target=target->next) {
		button_handler(target);
	}
}

​

三、使用例程

 1.事件获取处理方式

#include "multi_button.h"

unit8_t btn1_id = 0;
struct Button btn1;

uint8_t read_button_GPIO(uint8_t button_id)
{
	// you can share the GPIO read function with multiple Buttons
	switch(button_id)
	{
		case btn1_id:
			return HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);
		default:
			return 0;
	}
}


int main()
{
	static uint8_t btn1_event_val;

	button_init(&btn1, read_button_GPIO, 0, btn1_id);
	button_start(&btn1);

	//make the timer invoking the button_ticks() interval 5ms.
	//This function is implemented by yourself.
	__timer_start(button_ticks, 0, 5);

	while(1)
	{
		if(btn1_event_val != get_button_event(&btn1)) {
			btn1_event_val = get_button_event(&btn1);

			if(btn1_event_val == PRESS_DOWN) {
				//do something
			} else if(btn1_event_val == PRESS_UP) {
				//do something
			} else if(btn1_event_val == LONG_PRESS_HOLD) {
				//do something
			}
		}
	}
}

2.回调函数处理方式

#include "multi_button.h"

enum Button_IDs {
	btn1_id,
	btn2_id,
};

struct Button btn1;
struct Button btn2;

uint8_t read_button_GPIO(uint8_t button_id)
{
	// you can share the GPIO read function with multiple Buttons
	switch(button_id)
	{
		case btn1_id:
			return HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);
		case btn2_id:
			return HAL_GPIO_ReadPin(B2_GPIO_Port, B2_Pin);
		default:
			return 0;
	}
}

int main()
{
	button_init(&btn1, read_button_GPIO, 0, btn1_id);
	button_init(&btn2, read_button_GPIO, 0, btn2_id);

	button_attach(&btn1, PRESS_DOWN,       BTN1_PRESS_DOWN_Handler);
	button_attach(&btn1, PRESS_UP,         BTN1_PRESS_UP_Handler);
	button_attach(&btn1, PRESS_REPEAT,     BTN1_PRESS_REPEAT_Handler);
	button_attach(&btn1, SINGLE_CLICK,     BTN1_SINGLE_Click_Handler);
	button_attach(&btn1, DOUBLE_CLICK,     BTN1_DOUBLE_Click_Handler);
	button_attach(&btn1, LONG_PRESS_START, BTN1_LONG_PRESS_START_Handler);
	button_attach(&btn1, LONG_PRESS_HOLD,  BTN1_LONG_PRESS_HOLD_Handler);

	button_attach(&btn2, PRESS_DOWN,       BTN2_PRESS_DOWN_Handler);
	button_attach(&btn2, PRESS_UP,         BTN2_PRESS_UP_Handler);
	button_attach(&btn2, PRESS_REPEAT,     BTN2_PRESS_REPEAT_Handler);
	button_attach(&btn2, SINGLE_CLICK,     BTN2_SINGLE_Click_Handler);
	button_attach(&btn2, DOUBLE_CLICK,     BTN2_DOUBLE_Click_Handler);
	button_attach(&btn2, LONG_PRESS_START, BTN2_LONG_PRESS_START_Handler);
	button_attach(&btn2, LONG_PRESS_HOLD,  BTN2_LONG_PRESS_HOLD_Handler);

	button_start(&btn1);
	button_start(&btn2);

	//make the timer invoking the button_ticks() interval 5ms.
	//This function is implemented by yourself.
	__timer_start(button_ticks, 0, 5);

	while(1)
	{}
}

void BTN1_PRESS_DOWN_Handler(void* btn)
{
	//do something...
}

void BTN1_PRESS_UP_Handler(void* btn)
{
	//do something...
}

四、总结

最近几天反复研究这个源码,写的实在太好了,对整个框架进行学习,收获颇深,特别是对状态机、链表、回调函数这些知识有了新的认识。

文章参考:MultiButton | 一个小巧简单易用的事件驱动型按键驱动模块_Mculover666的博客-优快云博客_multibutton

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值