XxxSwitchScan_Driver
介绍
简介
XxxSwitchScan_Driver可以简单的看作为一个C语言按键驱动,使用简单、灵活且解耦,基于时间片轮询架构编写,同时适用于裸机与操作系统。
一开始仅为了实现按键驱动。后面把按键结合如高低电平的传感器、开关量的限位等进一步抽象为 开关量的输入设备 。最终实现响应事件有:短按/短按抬起/长按/持续长按/长按抬起/连击/单边沿触发。由此我常会把项目中的开关量的输入设备通过该驱动统一管理,使得项目架构合理简化,也让应用层逻辑清晰明了。
获取方式
我的CSDN资源
源码仓库
特性
- 基于链表,可动态注册新增设备与删除设备(只要内存足够大,设备数量不限量);
- 完全隔离硬件,对输入条件进行抽象,独立/矩阵/组合按键等均可适用;
- 使用了回调机制,驱动逻辑与业务逻辑分离,调用者专心实现事件响应的业务逻辑即可;
- 隔离内存管理,内存分配与回收由调用者主导;
- tick由外部主导,适用于裸机与操作系统提供的任意tick环境(定时器中断/阻塞延时/时间片/操作系统下的任务或定时器任务等);
- 每个设备的按下消抖与抬起消抖均可独立配置,数值配置为零即为不消抖(可应用于限位/传感器/硬件滤波良好的按键等);
- 每个设备的每个响应事件均可独立配置开启与关闭,最基础版本仅响应短按与短按抬起事件;
- 可利用
XxxSwitchDevice_Reset() 复位函数对设备进行互斥或屏蔽后续事件;
改进点
- 由于自由度高且功能支持多,代码量会比其他纯按键驱动大。后续会推出精简版;
本人实际应用场景
1. 多个独立按键,组合按键; 2. 矩阵键盘; 3. 高低电平信号传感器(电机限位(槽型光电开关等)/霍尔开关/液位传感器等); 4. 气压传感器(别疑惑,通过数值范围的比较条件判断即可变成true/false,一样视为开关量); 5. 异步/事件驱动,如自制队列、GUI按键事件、变量条件(协议通信方式)、IPC(非阻塞方式队列)等; 6. 以上多种任意场景的混合应用;
期待别的小伙伴能应用到其他的场景中,最大化开发驱动潜力
格式
编码格式:UTF-8 注释格式:doxyfile
文件介绍
Components
提供部件有:“矩阵键盘扫描驱动”、“位操作”;
Example
提供"多个独立按键"、“组合按键”、“矩阵键盘”、"高低电平信号传感器"应用场景例程;
README_Picture
README文档内用到的图片
LICENSE
许可证,遵循MIT协议
README.md
说明文档
XxxSwitchScan_Driver.c 和 XxxSwitchScan_Driver.h
核心驱动代码
注意事项
- 首次触发的连击事件即为双击,此时
XxxSwitchDevice_ReadComboHitNum() 函数读取到的连击次数返回即为2; - 连击一旦没续上则连击次数会被清零,所以在触发连击事件时按需求是否利用或保存连击次数;
- 边沿模式仅能单边沿触发;
- 在操作系统中使用时,如果其中一个设备使用到的IPC是阻塞方式,会导致其他设备的扫描被阻塞;
- tick是由调用者外部决定的,以1~20ms为一次tick均可。tick越小,功耗与cpu占用越高;
- 消抖时长(计数数值*tick),一般为10~50ms,由实际抖动程度决定;
使用流程
移植
把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短按 "); break; case SwitchCheckState_Click2Up: printf("设备1短按抬起 "); 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 "); 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短按 "); break; case SwitchCheckState_Click2Up: printf("设备1短按抬起 "); break; case SwitchCheckState_Long: printf("设备1长按 "); break; case SwitchCheckState_Continue: printf("设备1持续长按 "); break; case SwitchCheckState_Long2Up: printf("设备1长按抬起 "); 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
裁剪配置项
宏
宏
响应事件
枚举 | 说明 |
---|---|
SwitchCheckState_Click | 短按事件 |
SwitchCheckState_Click2Up | 短按抬起事件 |
SwitchCheckState_Long | 长按事件 |
SwitchCheckState_Continue | 持续长按事件 |
SwitchCheckState_Long2Up | 长按抬起事件 |
SwitchCheckState_ComboHit | 连击事件 |
SwitchCheckState_EdgeTrigger | 有效边沿触发事件 |
源文件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_COMBOHIT unsigned short comboHitInterval; /**< 连击间最大间隔 */ unsigned short comboHitNum; /**< 连击次数 */ #endif unsigned char (*readInputStateFunc)(void); /**< 读取输入状态函数函数指针 */ void (*handleFunc)(enum_XxxSwitchCheckState state); /**< 处理函数函数指针 */ STR_XxxSwitchDevice* pNext; /**< 指向下一个设备 */ };
成员 |
调用者 |
赋值范围限制 | 作用 |
---|---|---|---|
step | xxx | 检测步骤, |
|
trigger | ?xx | [0,1] | 有效触发条件,设备触发时的条件状态 |
type | ?xx | 枚举 |
设备类型,有 |
triggerCount | xxx | 主要用于触发时的消抖计数器,但也会用在其他判断情况 | |
upCount | xxx | 主要用于抬起时的消抖计数器,也会用在连击最大间隔的计数 | |
triggerWaitVal | ?xx | [0,65535] | 触发时的消抖tick次数,0即触发时不消抖 |
upWaitVal | ?xx | [0,65535] | 抬起时的消抖tick次数,0即抬起时不消抖 |
longVal | ?x? | [0,65535] | 从触发短按事件后到长按事件触发间的tick次数 |
continueVal | ?x? | [0,65535] | 从触发长按事件后到持续长按事件触发以及后续的每一次持续长按事件间的tick次数 |
comboHitInterval | ?x? | [0,65535] | 从触发短按抬起事件后到下次按下触发连击事件以及后续每一次连击事件间的tick次数 |
comboHitNum | x?x | 连击次数,在触发 |
|
readInputStateFunc | ?xx | 读取设备输入状态函数的函数指针,隔离硬件与抽象为开关量设备的关键 | |
handleFunc | ?xx | 处理函数的函数指针,隔离应用层逻辑的关键 | |
pNext | xxx | 指向下一个设备,用于链表操作 |