0
点赞
收藏
分享

微信扫一扫

嵌入式中面向对象的按键驱动模块实现


大家好,今天分享一款开源的按键驱动项目 -- 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);
}


其他形式的应用参考
  1. 相关硬件初始化由调用者自己完成
  2. 定义一个设备对象的指针 STR_XxxSwitchDevice* m_pKEY1;   /**< 按键1对象指针 */
  3. 提供状态读取函数 /*读取按键1状态的函数(以stm32标准库为例)*/

1. #define ReadValid_KEY1   GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4)

/*读取设备1状态的函数*/
unsigned char ReadSwitchDevice1(void)
{
   return ReadValid_KEY1();
}

  1. 提供事件处理函数 /*设备1的事件处理函数(假设该按键开启了长按与持续长按功能)*/

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;
  }
}

  1. 为对象申请内存 //方法①静态分配大小

1. 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());  /* 使用自己的内存管理函数分配空间 */

  1. 注册 //方式①使用自定义注册函数,假设该按键开启长按,持续长按功能

6. 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. 调用扫描函数 //方式①裸机使用定时器中断提供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_COMBOHIT
   unsigned short comboHitInterval;                    /**< 连击间最大间隔 */
   unsigned short comboHitNum;                         /**< 连击次数 */
   #endif

   unsigned char (*readInputStateFunc)(void);          /**< 读取输入状态函数函数指针 */
   void (*handleFunc)(enum_XxxSwitchCheckState state); /**< 处理函数函数指针 */
   STR_XxxSwitchDevice* pNext;                         /**< 指向下一个设备 */
};


嵌入式中面向对象的按键驱动模块实现_Common_02

更新计划

  • 上传"矩阵键盘"、"位操作"组件
  • 上传不同场景的例程
举报

相关推荐

0 条评论