# XxxSwitchScan_Driver **Repository Path**: jimmy_king/xxx-switch-scan_-driver ## Basic Information - **Project Name**: XxxSwitchScan_Driver - **Description**: XxxSwitchScan_Driver可以简单的看作为一个C语言的按键驱动,使用简单、灵活且解耦,以面向对象思想结合状态机编写,同时适用于裸机与操作系统。最终实现响应事件有:短按/短按抬起/长按/持续长按/长按抬起/连击/单边沿触发。 一开始仅为了实现按键驱动。后面把按键结合如高低电平的传感器、开关量的限位等进一步抽象为开关量的输入设备。由此我常会把项目中的开关量的输入设备通过该驱动统一管理。 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 9 - **Created**: 2024-02-05 - **Last Updated**: 2024-02-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # XxxSwitchScan_Driver ## 介绍 ### 简介 > XxxSwitchScan_Driver可以简单的看作为一个C语言按键驱动,使用简单、灵活且解耦,以面向对象思想结合状态机编写,同时适用于裸机与操作系统。 > > 一开始仅为了实现按键驱动。后面把按键结合如高低电平的传感器、开关量的限位等进一步抽象为 ***开关量的输入设备*** 。最终实现响应事件有:**短按/短按抬起/长按/持续长按/长按抬起/连击/单边沿触发**。由此我常会把项目中的*开关量的输入设备*通过该驱动统一管理,使得项目架构合理简化,也让应用层逻辑清晰明了。 ### 交流 邮箱:2537274979@qq.com CSDN:https://blog.csdn.net/Lstatxxx ### 特性 > + 基于链表,可动态注册新增设备与删除设备(只要内存足够大,设备数量不限量); > + 完全隔离硬件,对输入条件进行抽象,独立/矩阵/组合按键等均可适用; > + 使用了回调机制,驱动逻辑与业务逻辑分离,调用者专心实现事件响应的业务逻辑即可; > + 隔离内存管理,内存分配与回收由调用者主导; > + 采取隐藏结构体方式,防止调用者直接访问对象的成员; > + tick由外部主导,适用于裸机与操作系统提供的任意tick环境(定时器中断/阻塞延时/时间片/操作系统下的任务或定时器任务等); > + 功能丰富,支持多种事件响应:短按/短按抬起/长按/持续长按/长按抬起/连击/单边沿触发; > + 支持每个设备的按下消抖与抬起消抖独立配置,数值配置为零即为**不消抖**(可应用于限位/传感器/硬件滤波良好的按键等); > + 支持每个设备的每个响应事件独立配置开启与关闭,最基础版本注册仅响应**短按**与**短按抬起**事件; > + 支持特殊应用场景(设备间的互斥):可利用`XxxSwitchDevice_Reset()`函数对设备进行复位,实现互斥或屏蔽后续事件; > + 支持特殊应用场景(设备间的联系):可利用`XxxSwitchDevice_IsTrigger()`函数读取设备是否触发; ### 改进点 > + 由于自由度高且功能支持多,代码量会比其他纯按键驱动大。后续会推出精简版; ### 本人实际应用场景 1. 多个独立按键,组合按键; 2. 矩阵键盘; 3. 高低电平信号传感器(电机限位(槽型光电开关等)/霍尔开关/液位传感器等); 4. 气压传感器(别疑惑,通过数值范围的比较条件判断即可变成true/false,一样视为开关量); 5. 异步/事件驱动,如自制队列、GUI按键事件、变量条件(协议通信方式)、IPC(非阻塞方式队列)等; 6. 以上多种任意场景的混合应用; ***期待别的小伙伴能应用到其他的场景中,最大化开发驱动潜力*** ### 格式 编码格式:UTF-8 注释格式:doxygen ----------------------------------------------------------------------------------------------------------------------------- ## 文件介绍 ### 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,由实际抖动程度决定; ----------------------------------------------------------------------------------------------------------------------------- ## 使用流程 ### 提醒 + 由于使用到"for(声明;表达式;表达式)"的特性,请选择C99或以上的标准; + 下面的模版内在"注册"的那部分里为了清晰注释,导致了一些语法错误。所以复制代码时发现提示错误请把右侧注释去除即可; ### 移植 把XxxSwitchScan_Driver.c与XxxSwitchScan_Driver.h两个文件复制粘贴到自己工程内即可。 ### 应用步骤 1. 相关硬件初始化由调用者自己完成 2. 定义一个设备对象的指针(由于采取了隐藏结构体的方式,这里定义**只能是指针!**) 3. 提供状态读取函数 4. 提供事件处理函数 5. 为对象申请内存 6. 注册 7. 调用扫描函数 #### 基于rtos的应用基础模版 ```c #include "XxxSwitchScan_Driver.h" /* 本驱动头文件 */ #include "stm32f10x.h" /* stm32f10x标准库 */ #include "cmsis_os.h" /* rtos标准接口(CMSIS-RTOS2) */ #include /* 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); } ``` #### 其他形式的应用参考 1. 相关硬件初始化由调用者自己完成 2. 定义一个设备对象的指针 ```c STR_XxxSwitchDevice* m_pKEY1; /**< 按键1对象指针 */ ``` 3. 提供状态读取函数 ```c /*读取按键1状态的函数(以stm32标准库为例)*/ #define ReadValid_KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4) /*读取设备1状态的函数*/ unsigned char ReadSwitchDevice1(void) { return ReadValid_KEY1(); } ``` 4. 提供事件处理函数 ```c /*设备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; } } ``` 5. 为对象申请内存 ```c //方法①静态分配大小 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 /* 头文件 */ m_pKEY1 = (STR_XxxSwitchDevice*)malloc(XxxSwitchDevice_GetSize()); /* 使用malloc分配堆空间 */ //方法③使用 自己 或 第三方如rtos 的内存管理函数 #include "Xxx_Malloc.h" /* 头文件 */ m_pKEY1 = (STR_XxxSwitchDevice*)Xxx_Malloc(XxxSwitchDevice_GetSize()); /* 使用自己的内存管理函数分配空间 */ ``` 6. 注册 ```c //方式①使用自定义注册函数,假设该按键开启长按,持续长按功能 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 */ ``` 7. 调用扫描函数 ```c //方式①裸机使用定时器中断提供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:裁剪; #### 响应事件 |枚举|说明| |:-:|:-:| |SwitchCheckState_Click|短按事件| |SwitchCheckState_Click2Up|短按抬起事件| |SwitchCheckState_Long|长按事件| |SwitchCheckState_Continue|持续长按事件| |SwitchCheckState_Long2Up|长按抬起事件| |SwitchCheckState_ComboHit|连击事件| |SwitchCheckState_EdgeTrigger|有效边沿触发事件| ### 源文件XxxSwitchScan_Driver.c #### 数据结构 STR_XxxSwitchDevice结构体 ```c 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||检测步骤,`XxxSwitchDevice_Check()`设备的检测状态机的当前状态| |trigger|✔xx|[0,1]|有效触发条件,设备触发时的条件状态| |type|✔xx|枚举`enum_XxxSwitchDeviceType`|设备类型,有`SwitchDeviceType_Common`普通 `SwitchDeviceType_ComboHit`附带连击 `SwitchDeviceType_EdgeTrigger`边沿触发| |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||连击次数,在触发`SwitchCheckState_ComboHit`连击事件时,调用`XxxSwitchDevice_ReadComboHitNum()`获取,连击行为一断就会自清零| |readInputStateFunc|✔xx||读取设备输入状态函数的函数指针,隔离硬件与抽象为开关量设备的关键| |handleFunc|✔xx||处理函数的函数指针,隔离应用层逻辑的关键| |pNext|xxx||指向下一个设备,用于链表操作| ----------------------------------------------------------------------------------------------------------------------------- ## 更新日志 ### 2024_01_19 驱动发布版本完成 ### 2024_01_22 README文档初版完成 ### 2024_01_23 `XxxSwitchDevice_Reset`复位函数添加对边沿触发类型的判断 ### 2024_01_25 新增"多独立按键示例",位于Example文件夹内; 完善XxxSwitchScan_Driver.c的代码注释; ### 2024_01_29 添加"组合按键示例",位于Example文件夹内; 修复bug:链表为空时,注销设备会段错误问题; ----------------------------------------------------------------------------------------------------------------------------- ## 更新计划 1. 上传"矩阵键盘"、"位操作"组件 2. 上传不同场景的例程