嵌入式中面向对象的按键驱动模块实现
大家好,今天分享一款开源的按键驱动项目 -- XxxSwitchScan_Driver。
gitee仓库地址为(复制到浏览器打开):
https://gitee.com/wx_372d4eb42f/xxx-switch-scan_-driver
简介
XxxSwitchScan_Driver 可以简单的看作为一个C语言按键驱动,使用简单、灵活且解耦,以面向对象思想结合状态机编写,同时适用于裸机与操作系统。
一开始仅为了实现按键驱动。后面把按键结合如高低电平的传感器、开关量的限位等进一步抽象为 开关量的输入设备。
最终实现响应事件有:短按/短按抬起/长按/持续长按/长按抬起/连击/单边沿触发。
由此我常会把项目中的开关量的输入设备通过该驱动统一管理,使得项目架构合理简化,也让应用层逻辑清晰明了。
特性
-
基于链表,可动态注册新增设备与删除设备(只要内存足够大,设备数量不限量);
-
完全隔离硬件,对输入条件进行抽象,独立/矩阵/组合按键等均可适用;
-
使用了回调机制,驱动逻辑与业务逻辑分离,调用者专心实现事件响应的业务逻辑即可;
-
隔离内存管理,内存分配与回收由调用者主导;
-
tick由外部主导,适用于裸机与操作系统提供的任意tick环境(定时器中断/阻塞延时/时间片/操作系统下的任务或定时器任务等);
-
功能丰富,支持多种事件响应:短按/短按抬起/长按/持续长按/长按抬起/连击/单边沿触发;
-
支持每个设备的按下消抖与抬起消抖独立配置,数值配置为零即为不消抖(可应用于限位/传感器/硬件滤波良好的按键等);
-
支持每个设备的每个响应事件独立配置开启与关闭,最基础版本注册仅响应短按与短按抬起事件;
-
支持特殊应用场景(设备间的互斥):可利用
XxxSwitchDevice_Reset()函数对设备进行互斥或屏蔽后续事件; -
支持特殊应用场景(设备间的联系):可利用
XxxSwitchDevice_IsTrigger()函数读取设备是否触发;
改进点
-
由于自由度高且功能支持多,代码量会比其他纯按键驱动大。后续会推出精简版;
已用的实际场景
-
多个独立按键,组合按键;
-
矩阵键盘;
-
高低电平信号传感器(电机限位(槽型光电开关等)/霍尔开关/液位传感器等);
-
气压传感器(别疑惑,通过数值范围的比较条件判断即可变成true/false,一样视为开关量);
-
异步/事件驱动,如自制队列、GUI按键事件、变量条件(协议通信方式)、IPC(非阻塞方式队列)等;
-
以上多种任意场景的混合应用;
期待别的小伙伴能应用到其他的场景中,最大化开发驱动潜力
格式
编码格式:UTF-8 注释格式:doxygen
文件介绍
Example
提供"多个独立按键"、"组合按键"、"矩阵键盘"、"高低电平信号传感器"应用场景例程;
LICENSE
许可证,遵循MIT协议
README.md
说明文档
XxxSwitchScan_Driver.c 和 XxxSwitchScan_Driver.h
核心驱动代码
注意事项
-
首次触发的连击事件即为双击,此时
XxxSwitchDevice_ReadComboHitNum()函数读取到的连击次数返回即为2; -
连击一旦没续上则连击次数会被清零,所以在触发连击事件时按需求是否利用或保存连击次数;
-
边沿模式仅能单边沿触发;
-
在操作系统中使用时,如果其中一个设备使用到的IPC是阻塞方式,会导致其他设备的扫描被阻塞;
-
tick是由调用者外部决定的,以1~20ms为一次tick均可。tick越小,功耗与cpu占用越高;
-
消抖时长(计数数值*tick),一般为10~50ms,由实际抖动程度决定;
使用流程
提醒
-
由于使用到"for(声明;表达式;表达式)"的特性,请选择C99或以上的标准;
-
下面的模版内在"注册"的那部分里为了清晰注释,导致了一些语法错误。所以复制代码时发现提示错误请把右侧注释去除即可;
移植
把XxxSwitchScan_Driver.c与XxxSwitchScan_Driver.h两个文件复制粘贴到自己工程内即可。
应用步骤
-
相关硬件初始化由调用者自己完成
-
定义一个设备对象的指针
-
提供状态读取函数
-
提供事件处理函数
-
为对象申请内存
-
注册
-
调用扫描函数
基于rtos的应用基础模版
#include "XxxSwitchScan_Driver.h" /* 本驱动头文件 */
#include "stm32f10x.h" /* stm32f10x标准库 */
#include "cmsis_os.h" /* rtos标准接口(CMSIS-RTOS2) */
#include <stdlib.h> /* malloc函数的头文件 */#define ReadValid_KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4) /**< 按键1状态读取函数 *//*2.定义按键对象指针*/
STR_XxxSwitchDevice* m_pKEY1;
/*3.提供读取设备1状态的函数*/
unsigned char ReadSwitchDevice1(void)
{return ReadValid_KEY1();
}
/*4.提供设备1的事件处理函数(假设是基础按键仅开启短按与短按抬起事件)*/
void SwitchDevice1_Handle(enum_XxxSwitchCheckState state)
{switch(state){case SwitchCheckState_Click:printf("设备1短按\n");break;case SwitchCheckState_Click2Up:printf("设备1短按抬起\n");break;}
}osThreadId_t m_task1Info; /**< 任务句柄 */
const osThreadAttr_t m_task1Config = {.name = "Task_1",.stack_size = 128,.priority = 10,
};
/*任务1*/
void Task_1(void* p)
{/*5.申请空间*/m_pKEY1 = (STR_XxxSwitchDevice*)malloc(XxxSwitchDevice_GetSize());if(NULL == m_pKEY1){printf("allocation error\n");return;}/*6.调用基础注册,假设是基础按键仅开启短按与短按抬起事件*/XxxSwitchDevice_BaseRegister(m_pKEY1, SwitchDeviceType_Common, 1, \ /* m_pKEY1对象,SwitchDeviceType_Common为普通类型,有效电平为1 */
2, 2, \ /* 按下消抖为2*tick,抬起消抖为2*tick */
ReadSwitchDevice1, \ /* 第3步的读取设备1状态的函数 */
SwitchDevice1_Handle); /* 第4步的设备1的事件处理函数 */while(1){/*7.调用开关设备扫描函数*/XxxSwitchDevice_Scan();osDelay(10); /* 10ms为一次tick */}
}int main(void)
{.../*1.相关硬件初始化配置*/...osKernelInitialize(); /* 内核初始化 */osThreadNew(Task_1, NULL, &m_task1Config); /* 任务创建 */osKernelStart(); /* 内核启动 */while(1);
}
其他形式的应用参考
-
相关硬件初始化由调用者自己完成
-
定义一个设备对象的指针
STR_XxxSwitchDevice* m_pKEY1; /**< 按键1对象指针 */
-
提供状态读取函数
/*读取按键1状态的函数(以stm32标准库为例)*/ #define ReadValid_KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4)/*读取设备1状态的函数*/ unsigned char ReadSwitchDevice1(void) {return ReadValid_KEY1(); } -
提供事件处理函数
/*设备1的事件处理函数(假设该按键开启了长按与持续长按功能)*/ void SwitchDevice1_Handle(enum_XxxSwitchCheckState state) {switch(state){case SwitchCheckState_Click:printf("设备1短按\n");break;case SwitchCheckState_Click2Up:printf("设备1短按抬起\n");break;case SwitchCheckState_Long:printf("设备1长按\n");break;case SwitchCheckState_Continue:printf("设备1持续长按\n");break;case SwitchCheckState_Long2Up:printf("设备1长按抬起\n");break;} } -
为对象申请内存
//方法①静态分配大小 static unsigned char s_buf[32]; /**< 定义一个数组,32大小即为STR_XxxSwitchDevice对象的大小(全功能开启CFG_XXXSWITCH_COMBOHIT与CFG_XXXSWITCH_EDGETRIGGER为1),可以通过XxxSwitchDevice_GetSize()函数获取当前配置所得的准确大小 */ m_pKEY1 = s_buf; /* 把第2步定义的按键1对象指针指向该数组,即分配内存完毕 *///方法②malloc动态内存分配 #include <stdlib.h> /* 头文件 */m_pKEY1 = (STR_XxxSwitchDevice*)malloc(XxxSwitchDevice_GetSize()); /* 使用malloc分配堆空间 *///方法③使用 自己 或 第三方如rtos 的内存管理函数 #include "Xxx_Malloc.h" /* 头文件 */m_pKEY1 = (STR_XxxSwitchDevice*)Xxx_Malloc(XxxSwitchDevice_GetSize()); /* 使用自己的内存管理函数分配空间 */
-
注册
//方式①使用自定义注册函数,假设该按键开启长按,持续长按功能 XxxSwitchDevice_Register(m_pKEY1, SwitchDeviceType_Common, 1, \ /* m_pKEY1对象,SwitchDeviceType_Common为普通类型,有效电平为1 */ 2, 2,\ /* 按下消抖为2*tick,抬起消抖为2*tick */ 50, 25, \ /* 长按判定为50*tick,每次持续长按间隔为25*tick */ 0, \ /* 设置为0即关闭连击事件响应 */ ReadSwitchDevice1, \ /* 第3步的读取设备1状态的函数 */ SwitchDevice1_Handle); /* 第4步的设备1的事件处理函数 *///方式②使用基础注册函数,假设该按键开启长按,持续长按功能 XxxSwitchDevice_BaseRegister(m_pKEY1, SwitchDeviceType_Common, 1, \ /* m_pKEY1对象,SwitchDeviceType_Common为普通类型,有效电平为1 */ 2, 2, \ /* 按下消抖为2*tick,抬起消抖为2*tick */ ReadSwitchDevice1, \ /* 第3步的读取设备1状态的函数 */ SwitchDevice1_Handle); /* 第4步的设备1的事件处理函数 */ XxxSwitchDevice_CfgEvent_Long(m_pKEY1, 50); /* 开启长按事件响应,长按判定为50*tick */ XxxSwitchDevice_CfgEvent_Continue(m_pKEY1, 25); /* 开启持续长按事件响应,每次持续长按间隔为25*tick */
-
调用扫描函数
//方式①裸机使用定时器中断提供tick,配置为每10ms触发一次中断,即10ms为一次tick void TIM4_IRQHandler(void) {if(TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET){TIM_ClearITPendingBit(TIM4, TIM_IT_Update);XxxSwitchDevice_Scan(); /* 调用开关设备扫描函数 */} }//方式②使用操作系统,以任务osDelay提供tick。如果操作系统有定时器任务,使用定时器任务也行(下面采用的是CMSIS-RTOS2接口标准) osThreadId_t m_task1Info; const osThreadAttr_t m_task1Config = {.name = "Task_1",.stack_size = 256,.priority = 10, };void Task_1(void* p) {while(1){XxxSwitchDevice_Scan(); /* 调用开关设备扫描函数 */osDelay(10); /* 10ms tick */} }int main(void) {...osThreadNew(Task_1, NULL, &m_task1Config); /* 创建任务 */while(1); }//方式③基于时间片轮询架构,这里不展开时间片轮询架构,后续有时间了也会开源,我这里用到的是变种,我称之为时间片错位轮询 int main(void) {...while(1){if(CHECK_BIT(TIME_10MS, 0)) /* 10ms的tick */{RESET_BIT(TIME_10MS, 0);XxxSwitchDevice_Scan(); /* 调用开关设备扫描函数 */}} }//方式④裸机下的阻塞延时delay(不推荐) int main(void) {...while(1){XxxSwitchDevice_Scan(); /* 调用开关设备扫描函数 */delayMs(10); /* 阻塞延时(不推荐),10ms的tick */} }
详细可查阅"Example"文件夹,内部有实际的应用场景例程
代码说明
头文件XxxSwitchScan_Driver.h
裁剪配置项
宏CFG_XXXSWITCH_COMBOHIT用于裁剪"连击功能",1:开启/0:裁剪;
宏CFG_XXXSWITCH_EDGETRIGGER用于裁剪"边沿触发功能",1:开启/0:裁剪;
响应事件

源文件XxxSwitchScan_Driver.c
数据结构
STR_XxxSwitchDevice结构体
struct _STR_XxxSwitchDevice{enum_XxxSwitchCheckStep step; /**< 检测步骤 */unsigned char trigger; /**< 有效触发条件 */enum_XxxSwitchDeviceType type; /**< 类型 */unsigned short triggerCount; /**< 触发计数器 */unsigned short upCount; /**< 抬起计数器 */unsigned short triggerWaitVal; /**< 触发消抖计数值 */unsigned short upWaitVal; /**< 抬起消抖计数值 */unsigned short longVal; /**< 长按计数值 */unsigned short continueVal; /**< 持续长按间隔计数值 */#if CFG_XXXSWITCH_COMBOHITunsigned short comboHitInterval; /**< 连击间最大间隔 */unsigned short comboHitNum; /**< 连击次数 */#endifunsigned char (*readInputStateFunc)(void); /**< 读取输入状态函数函数指针 */void (*handleFunc)(enum_XxxSwitchCheckState state); /**< 处理函数函数指针 */STR_XxxSwitchDevice* pNext; /**< 指向下一个设备 */
};

更新计划
-
上传"矩阵键盘"、"位操作"组件
-
上传不同场景的例程