C语言实现事件驱动型按键驱动模块MultiButton

C语言实现事件驱动型按键驱动模块MultiButton

简介

在嵌入式系统尤其是单片机系统中经常用到按键检测和处理,这里提供一个标准的驱动函数模块MultiButton,能够提供按下、弹起、单击、双击、连击、长按等按键事件。

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

驱动模块代码的原作者是Zibin Zheng,发布于github上开源,代码写的非常精彩,可以拿来学习。

这里介绍其使用方法,并分析代码实现的原理,重要代码本文作者均额外加了中文注释,方便理解;

github上原作者代码有bug,本文已经给予修订。无bug版的完整源代码和使用demo请在这里下载,可以直接在项目中使用。

使用方法

1.先申请一个按键结构

struct Button button1;

2.初始化按键对象,绑定按键的GPIO电平读取接口read_button_pin() ,后一个参数设置有效触发电平

button_init(&button1, read_button_pin, 0);

3.注册按键事件

button_attach(&button1, SINGLE_CLICK, Callback_SINGLE_CLICK_Handler);
button_attach(&button1, DOUBLE_CLICK, Callback_DOUBLE_Click_Handler);
...

4.启动按键

button_start(&button1);

5.设置一个5ms间隔的定时器循环调用后台处理函数button_ticks(),

如果是在嵌入式操作系统使用,则可以搞一个每5ms调用一次的按键任务去执行button_ticks()即可

while(1) {
    ...
    if(timer_ticks == 5) {
        timer_ticks = 0;

        button_ticks();
    }
}

特性

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

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_handler(struct Button handle)* 状态机处理,所以每个按键的状态彼此独立。

按键事件

事件说明
PRESS_DOWN按键按下,每次按下都触发
PRESS_UP按键弹起,每次松开都触发
PRESS_REPEAT重复按下触发,变量repeat计数连击次数
SINGLE_CLICK单击按键事件
DOUBLE_CLICK双击按键事件
LONG_PRESS_START达到长按时间阈值时触发一次
LONG_PRESS_HOLD长按期间一直触发

使用方法举例

#include "multi_button.h"

struct Button btn1;

uint8_t read_button1_GPIO()
{
	return HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);
}
void BTN1_PRESS_DOWN_Handler(void* btn)
{
	//do something...
}

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

...
int main()
{
	button_init(&btn1, read_button1_GPIO, 0);   
	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(&btn2, LONG_PRESS_HOLD,  BTN1_LONG_PRESS_HOLD_Handler);
	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)
	{}
}

核心代码分析

头文件声明

#include "stdint.h"
#include "string.h"

//According to your need to modify the constants.
#define TICKS_INTERVAL 5 //ms,系统节拍5ms
#define DEBOUNCE_TICKS 3 //MAX 8,去抖的次数
#define SHORT_TICKS (300 / TICKS_INTERVAL)	//单击短按键的时间是300ms以上
#define LONG_TICKS (1000 / TICKS_INTERVAL)	//长按键的时间是1000ms以上
#define LONG_HOLD_CYC (500 / TICKS_INTERVAL)	//处于长按键保持触发状态时,每500ms调用一次回调函数

typedef void (*BtnCallback)(void *);

//声明一个按键事件的枚举类型,包括了所有的按键操作类型
typedef enum
{
	PRESS_DOWN = 0,	//按键按下,每次按下都触发 
	PRESS_UP,		//按键弹起,每次松开都触发
	PRESS_REPEAT,	//重复按下触发,变量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;

#ifdef __cplusplus
extern "C"
{
#endif
	//按键结构体初始化,赋予按键获取电平的函数,并读出当前电平
	void button_init(struct Button *handle, uint8_t (*pin_level)(), uint8_t active_level);
	//按键注册(赋予特定的按键事件以回调函数)
	void button_attach(struct Button *handle, PressEvent event, BtnCallback cb);
	//获取按键事件返回到按键结构体
	PressEvent get_button_event(struct Button *handle);
	//启动一个按键,即新增一个按键结构体链表节点
	int button_start(struct Button *handle);
	//停止一个按键,从链表中删除节点
	void button_stop(struct Button *handle);
	//按键节拍,每一次节拍事件遍历按键列表中所有按键,调用驱动函数
	void button_ticks(void);

#ifdef __cplusplus
}
#endif

函数定义源代码

模块的核心函数是void button_handler(struct Button *handle),这是一个状态机程序,该函数流程如下图所示:
在这里插入图片描述

代码实现如下:

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

//button handle list head.
//按键结构体链表,初始化指针为NULL,空链表
static struct Button *head_handle = NULL;

/**
  * @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.
  * @retval None
  */
 //按键结构体初始化,赋予按键获取电平的函数指针,并读出当前电平
void button_init(struct Button *handle, uint8_t (*pin_level)(), uint8_t active_level)
{
	memset(handle, 0, sizeof(struct Button));		   //结构体初始化为全0
	handle->event = (uint8_t)NONE_PRESS;			   //初始化为没有按键事件
	handle->hal_button_Level = pin_level;			   //读取按键电平状态函数指针
	handle->button_level = handle->hal_button_Level(); //读取一次当前按键电平
	handle->active_level = active_level;			   //按下的有效电平是1还是0
}

/**
  * @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; //赋予某特定按键事件的回调函数指针
}

/**
  * @brief  Inquire the button event happen.
  * @param  handle: the button handle strcut.
  * @retval button event.
  */
 //获取按键事件返回到按键结构体
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..
	//只要按键状态不是0,就递增记录系统节拍次数
	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: //如果前次按键状态是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;					 //事件重复次数为1
			handle->state = 1;					 //按键状态更新为1(按下)
		}
		else //如果不是有效电平,则无按键事件
		{
			handle->event = (uint8_t)NONE_PRESS;
		}
		break;

	case 1: //如果前次按键状态是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;				   //按键状态更新为2(松开)
		}
		//如果节拍计数超过了长按键阀值,则认为是一次长按键事件
		else if (handle->ticks > LONG_TICKS)
		{
			handle->event = (uint8_t)LONG_PRESS_START; //有长按键开始事件发生
			EVENT_CB(LONG_PRESS_START);				   //如果存在回调函数则执行
			handle->state = 5;						   //按键状态更新为5(长按开始)
		}
		break;

	case 2: //如果前次按键状态是2(松开了),且当前电平是有效电平,则认为按键又按下了
		if (handle->button_level == handle->active_level)
		{										 //press down again
			handle->event = (uint8_t)PRESS_DOWN; //有按键按下事件发生
			EVENT_CB(PRESS_DOWN);				 //如果存在回调函数则执行
			handle->repeat++;					 //又一次按下,重复次数加1
			EVENT_CB(PRESS_REPEAT);				 // repeat hit,如果存在回调函数则执行
			handle->ticks = 0;					 //节拍计数清零
			handle->state = 3;					 //按键状态更新为3(重复按键)
		}
		//如果不是有效电平,说明松开了,那么检查摁下的时间是否超过了短按键的节拍计数阀值
		else if (handle->ticks > SHORT_TICKS)
		{ //released timeout
			//超过阀值,且只按下了1次,则认为是一次单击事件
			if (handle->repeat == 1)
			{
				handle->event = (uint8_t)SINGLE_CLICK; //有单击事件发生
				EVENT_CB(SINGLE_CLICK);				   //如果存在回调函数则执行
			}
			//如果按下了2次,则认为是一次双击事件
			else if (handle->repeat == 2)
			{
				handle->event = (uint8_t)DOUBLE_CLICK; //有双击事件发生
				EVENT_CB(DOUBLE_CLICK);				   // repeat hit 如果存在回调函数则执行
			}
			handle->state = 0; //按键状态更新为0(未按下)
		}
		break;

	case 3: //如果前次按键状态是3(重复按下),且当前电平不是有效电平,则认为是按键弹起或短按键事件
		if (handle->button_level != handle->active_level)
		{									   //released press up
			handle->event = (uint8_t)PRESS_UP; //有按键弹起事件发生
			EVENT_CB(PRESS_UP);				   //如果存在回调函数则执行
			//如果记录的节拍时间小于300ms,
			if (handle->ticks < SHORT_TICKS)
			{
				handle->ticks = 0; //节拍计数清零
				handle->state = 2; //repeat press按键状态更新为2(松开)
			}
			else
			{
				handle->state = 0; //按键状态更新为0(未按下)
			}
		}
		break;

	case 5: //如果前次按键状态是5(长按键开始),且当前电平是有效电平,则认为处于长按键保持出发状态
		if (handle->button_level == handle->active_level)
		{
			//continue hold trigger
			handle->event = (uint8_t)LONG_PRESS_HOLD; //有按键弹起事件发生
			if (handle->ticks % LONG_HOLD_CYC == 0)	//每500ms调用一次回调函数
			{
				EVENT_CB(LONG_PRESS_HOLD); //如果存在回调函数则执行
			}
		}
		else								   //如果不是有效电平,则认为是按键弹起事件
		{									   //releasd
			handle->event = (uint8_t)PRESS_UP; //有按键弹起事件发生
			EVENT_CB(PRESS_UP);				   //如果存在回调函数则执行
			handle->state = 0;				   //reset按键状态复位
		}
		break;
	}
}

/**
  * @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;
}

/**
  * @brief  Stop the button work, remove the handle off work list.
  * @param  handle: target handle strcut.
  * @retval None
  */
 //停止一个按键,即从链表中删除该按键节点
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);
		}
		else
			curr = &entry->next;
	}
}

/**
  * @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);
	}
}

无bug版的完整源代码和使用demo请在这里下载

  • 10
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
### 回答1: 好的,我可以回答这个问题。您可以使用C语言编写按键事件驱动的程序,其中可以使用操作系统提供的API函数来实现按键事件的响应。大多数操作系统都提供了类似的API函数来帮助您编写这样的程序。您可以查阅操作系统的文档以获取更多信息。 ### 回答2: 按键事件驱动的程序可以用C语言编写。首先,我们需要引入相关的头文件,如stdio.h和windows.h。接下来,我们定义一个无限循环,用来监听按键事件。 在循环中,我们使用GetAsyncKeyState函数来检测按键是否被按下。该函数的返回值表示按键的状态,如果返回值的最高位是1,则表示按键当前被按下。我们可以使用该函数来检测特定的按键,例如GetAsyncKeyState(VK_SPACE)来检测空格键的状态。 一旦检测到按键被按下,我们可以执行相应的操作。例如,可以输出一条消息,或者调用其他的函数。然后,我们可以使用Sleep函数来延迟一段时间,以避免程序过于频繁地检测按键状态。 整个程序的框架如下: #include <stdio.h> #include <windows.h> int main() { while (1) { if (GetAsyncKeyState(VK_SPACE)) { //检测空格键 printf("空格键被按下\n"); Sleep(200); //延迟200毫秒 } if (GetAsyncKeyState(VK_RETURN)) { //检测回车键 printf("回车键被按下\n"); Sleep(200); //延迟200毫秒 } //其他按键的检测和操作可以在这里添加 } return 0; } 以上是一个简单的按键事件驱动的程序,可以根据需要来添加其他按键的检测和操作。通过这种方式,我们可以实现各种交互式的应用程序,例如游戏、用户界面等。 ### 回答3: 编写一个按键事件驱动的程序可以实现对键盘按键的检测和响应。以下是一个使用C语言编写的简单示例: ```c #include <stdio.h> #include <conio.h> int main() { char ch; while (1) { if (kbhit()) { // 判断键盘是否有按键输入 ch = getch(); // 获取键盘按键 switch (ch) { case 'q': case 'Q': printf("程序退出\n"); return 0; case 'a': case 'A': printf("按下了 A 键\n"); break; case 'b': case 'B': printf("按下了 B 键\n"); break; default: printf("按下了其他键\n"); break; } } } return 0; } ``` 上述程序的运行流程如下: 1. 在一个无限循环中,通过`kbhit()`函数判断键盘是否有按键输入。 2. 如果有按键输入,则使用`getch()`函数获取键盘按键的字符。 3. 利用`switch`语句判断获取的字符是哪个键,并执行相应的操作。 4. 如果按下的是 `q` 或 `Q` 键,则程序退出。 5. 如果按下的是 `a` 或 `A` 键,则输出按下了 A 键的提示。 6. 如果按下的是 `b` 或 `B` 键,则输出按下了 B 键的提示。 7. 如果输入其他键,则输出按下了其他键的提示。 8. 循环回到第一步。 注意,以上示例程序在Windows环境下使用了`conio.h`头文件中的`kbhit()`和`getch()`函数来实现按键检测和获取按键字符的功能。在其他操作系统或开发环境中可能需要使用不同的函数或库来实现相同的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值