ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/8/4 4:08:13 拓冰建站 浏览量
嵌入式中面向对象的按键驱动模块实现

大家好,今天分享一款开源的按键驱动项目 -- 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标准库为例)*/
    #define ReadValid_KEY1   GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4)/*读取设备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;case SwitchCheckState_Long:printf("设备1长按\n");break;case SwitchCheckState_Continue:printf("设备1持续长按\n");break;case SwitchCheckState_Long2Up:printf("设备1长按抬起\n");break;}
    }
  5. 为对象申请内存

    //方法①静态分配大小
    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());  /* 使用自己的内存管理函数分配空间 */
  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_COMBOHITunsigned short comboHitInterval;                    /**< 连击间最大间隔 */unsigned short comboHitNum;                         /**< 连击次数 */#endifunsigned char (*readInputStateFunc)(void);          /**< 读取输入状态函数函数指针 */void (*handleFunc)(enum_XxxSwitchCheckState state); /**< 处理函数函数指针 */STR_XxxSwitchDevice* pNext;                         /**< 指向下一个设备 */
};

图片

更新计划

  • 上传"矩阵键盘"、"位操作"组件

  • 上传不同场景的例程