目录

1.为什么引入MultiButton

1)普通按键扫描实现

2.MultiButton源码分析

1)源码一览

2)源码分析

3.MultiButton移植

1)移植需要准备一份带有按键驱动的代码

2)将MultiButton源码直接拷贝到工程目录下

3)将源码添加到工程中

4)修改struct Button中的函数指针hal_button_Level

 5)修改button_init函数

6)注册按键

7)系统滴答产生5ms中断,在中断服务函数中处理按键时基

8)主函数中遍历按键事件

9)测试结果如下


1.为什么引入MultiButton

笔者为大家带来一个开源的按键识别程序,可能有读者会有疑问,一个按键扫描不是很简单吗?那么如果在简单的按键识别的基础上,需要识别按键的单击、双击、三击、长按等操作呢,那么按照裸机的思想是利用定时器中断去轮询,然后利用一直切换状态标志,最后得出按键状态信息。

MultiButton 是一个小巧简单易用的事件驱动型按键驱动模块,可无限量扩展按键,按键事件的回调异步处理方式可以简化你的程序结构,去除冗余的按键处理硬编码,让你的按键业务逻辑更清晰。

那么笔者接下来就协同大家一起来解析大佬的源码,并协同大家一起移植使用。

在使用这个框架之前,我们先来看看一般的按键扫描程序如何书写:如下笔者给大家列举了一个按键的单击防长按的代码,其实实现原理非常简单,就是结合利用了一个静态标志位,做按键松手检测:

1)普通按键扫描实现

#define KEY !!(GPIOC->IDR & (0x1 << 4))
#define KEY_UP         0
#define KEY_DOWN     1
#define KEY_RELEASE 0
#define KEY_PRESS   1

u8 Key_Scan(void)
{
    static u8 keyFlag = KEY_RELEASE;
    u8 keyCode = KEY_UP;

    if(KEY & keyFlag)
    {
        delay_ms(10);
        if(KEY)
        {
            KeyCode = KEY_DOWN;
            KeyFlag = KEY_PRESS;
        }
    }else if(!KEY)
    {
        KeyFlag = KEY_RELEASE;
    }

    return keyCode;
}

 

上述代码实现,简单的按键识别没有任何问题,但是消抖的10ms会在一定程度上影响程序实时性,一般情况会通过轮询n次来规避延时消抖问题,笔者这里就过多剖析该源码了。

2.MultiButton源码分析

MultiButton源码地址:https://github.com/0x1abin/MultiButton

1)源码一览

2)源码分析

MultiButton总共通过7个API函数实现按键识别的管理,下面我们每个API都来分析一下:

  • 一些默认设置项 
#define TICKS_INTERVAL    5    //按键时基-- ms 
#define DEBOUNCE_TICKS    3    //去抖动次数 - 最大 8
#define SHORT_TICKS       (300 /TICKS_INTERVAL)//设置短按时长
#define LONG_TICKS        (1000 /TICKS_INTERVAL)//设置长按时长
  • 申请一个全局的结构体指针-用来指向按键链表用

typedef struct Button {
    uint16_t ticks;
    uint8_t  repeat : 4;
    uint8_t  event : 4;
    uint8_t  state : 3;
    uint8_t  debounce_cnt : 3;
    uint8_t  active_level : 1;
    uint8_t  button_level : 1;
    uint8_t  button_id;
    uint8_t  (*hal_button_Level)(uint8_t button_id_);
    BtnCallback  cb[number_of_event];
    struct Button* next;
}Button;//相当于抽象出来的一个按键对象

//定义一个结构体指针,指向表头
static struct Button* head_handle = NULL;
  • 初始化一个按键

/**
  * @brief  初始化按键对象
  * @param  handle:      指向一个初始的按键对象结构体
  * @param  pin_level:   函数指针,一般传递管脚识别函数
  * @param  active_level:按键的按下单片机识别到的电平值
  * @param  button_id:   为注册的按键注册一个ID
  * @retval None
  */
void button_init(struct Button* handle, uint8_t(*pin_level)(), uint8_t active_level, uint8_t button_id)
{
    memset(handle, 0, sizeof(struct Button));
    handle->event = (uint8_t)NONE_PRESS;//按键状态
    handle->hal_button_Level = pin_level;//按键管脚检测函数
    handle->button_level = handle->hal_button_Level(button_id);//指定一次按键边沿
    handle->active_level = active_level;//按键按下电平状态
    handle->button_id = button_id;//按键id
}

  • 增加按键附加功能

/**
  * @brief  为按键事件追加附加事件,通过回调函数.
  * @param  handle: 按键对象结构体.
  * @param  event: 触发附加功能回调的按键事件类型.
  * @param  cb: 触发时执行的回调函数.
  * @retval None
  */
void button_attach(struct Button* handle, PressEvent event, BtnCallback cb)
{
    handle->cb[event] = cb;
}

  • 查询按键事件

typedef enum {
    PRESS_DOWN = 0,//按下
    PRESS_UP,//松开
    PRESS_REPEAT,//重复
    SINGLE_CLICK,//单击
    DOUBLE_CLICK,//双击
    LONG_PRESS_START,//长按开始
    LONG_PRESS_HOLD,//长按保持
    number_of_event,//事件数量 - 通过枚举的附加特性(后面的值为前对象+1 = 前面所有的枚举对象)
    NONE_PRESS//无操作
}PressEvent;
/**
  * @brief  查询按键事件.
  * @param  handle: 按键对象结构体.
  * @retval 按键事件枚举.
  */
PressEvent get_button_event(struct Button* handle)
{
    return (PressEvent)(handle->event);
}

  • 按键事件处理

/**
  * @brief  按键驱动核心函数, 状态机方式.
  * @param  handle: 按键对象结构体.
  * @retval None
  */
void button_handler(struct Button* handle)
{
    uint8_t read_gpio_level = handle->hal_button_Level(handle->button_id);//获取按键电平状态

    //如果有按键事件,按键时基ticks自增
    if((handle->state) > 0) handlrue->ticks++;

    /*------------按键消抖---------------*/
    if(read_gpio_level != handle->button_level) { //not equal to prev one
        //如果连续三次都是读取到对应的新的边沿
        if(++(handle->debounce_cnt) >= DEBOUNCE_TICKS) {
            handle->button_level = read_gpio_level;
            handle->debounce_cnt = 0;
        }
    } else { 连续读取三次未改变,则复位抖动计数变量
        handle->debounce_cnt = 0;
    }

    /*-----------------以下是实现按键功能的和状态机,说白了就是层层递进,更新按键状态-------------------*/
    switch (handle->state) {
    case 0:
        if(handle->button_level == handle->active_level) {  //start press down
            handle->event = (uint8_t)PRESS_DOWN;
            EVENT_CB(PRESS_DOWN);
            handle->ticks = 0;
            handle->repeat = 1;
            handle->state = 1;
        } else {
            handle->event = (uint8_t)NONE_PRESS;
        }
        break;

    case 1:
        //当检测到一次按键按下后才会到这里
        //此时按键要么松开-单击/双击?、要么继续按下-长按
        if(handle->button_level != handle->active_level) { //released press up
            handle->event = (uint8_t)PRESS_UP;
            EVENT_CB(PRESS_UP);
            handle->ticks = 0;
            handle->state = 2;

        } else if(handle->ticks > LONG_TICKS) {
            handle->event = (uint8_t)LONG_PRESS_START;
            EVENT_CB(LONG_PRESS_START);
            handle->state = 5;
        }
        break;

    case 2:
        //当按键有按下松开操作后才回到这
        //要么再次按下
        //要么松开,按照ticks和repet判断是单击还是双击
        if(handle->button_level == handle->active_level) { //press down again
            handle->event = (uint8_t)PRESS_DOWN;
            EVENT_CB(PRESS_DOWN);
            handle->repeat++;
            EVENT_CB(PRESS_REPEAT); // repeat hit
            handle->ticks = 0;
            handle->state = 3;
        } else if(handle->ticks > SHORT_TICKS) { //released timeout
            if(handle->repeat == 1) {
                handle->event = (uint8_t)SINGLE_CLICK;
                EVENT_CB(SINGLE_CLICK);
            } else if(handle->repeat == 2) {
                handle->event = (uint8_t)DOUBLE_CLICK;
                EVENT_CB(DOUBLE_CLICK); // repeat hit
            }
            handle->state = 0;
        }
        break;

    case 3:
        if(handle->button_level != handle->active_level) { //released press up
            handle->event = (uint8_t)PRESS_UP;
            EVENT_CB(PRESS_UP);
            if(handle->ticks < SHORT_TICKS) {
                handle->ticks = 0;
                handle->state = 2; //repeat press
            } else {
                handle->state = 0;
            }
        }else if(handle->ticks > SHORT_TICKS){ // long press up
            handle->state = 0;
        }
        break;

    case 5://长按
        if(handle->button_level == handle->active_level) {
            //continue hold trigger
            handle->event = (uint8_t)LONG_PRESS_HOLD;
            EVENT_CB(LONG_PRESS_HOLD);

        } else { //releasd
            handle->event = (uint8_t)PRESS_UP;
            EVENT_CB(PRESS_UP);
            handle->state = 0; //reset
        }
        break;
    }
}

  • 添加按键

/**
  * @brief  将按键对象添加到按键遍历链表中.
  * @param  handle: 需要添加的按键目标.
  * @retval 0: succeed. -1: already exist.
  */
int button_start(struct Button* handle)
{
    struct Button* target = head_handle;
    while(target) {
        //遍历链表中是否添加过该对象
        if(target == handle) return -1; //already exist.
        target = target->next;
    }
    handle->next = head_handle;//指针于指向表头
    head_handle = handle;//表头指向新的连接的对象
    return 0;
}

  • 删除按键

/**
  * @brief  将按键从链表中删除,不再参与遍历.
  * @param  handle: 按键目标.
  * @retval None
  */
void button_stop(struct Button* handle)
{
    struct Button** curr;
    for(curr = &head_handle; *curr; ) {
        struct Button* entry = *curr;
        if (entry == handle) {
            *curr = entry->next;
//            free(entry);
            return;//glacier add 2021-8-18
        } else
            curr = &entry->next;
    }
}

  • 按键时基

/**
  * @brief  通过定时器每5ms调用一次作为按键识别时基.
  * @param  None.
  * @retval None
  */
void button_ticks()
{
    struct Button* target;
    for(target=head_handle; target; target=target->next) {
        button_handler(target);
    }
}
3.Mu

3.MultiButton移植

1)移植需要准备一份带有按键驱动的代码

2)将MultiButton源码直接拷贝到工程目录下

3)将源码添加到工程中

4)修改struct Button中的函数指针hal_button_Level

由于我们实际就只需要传递一个读取管脚的状态通过返回值返回就行,不需要传递参数,所以按需将struct Button中的函数指针hal_button_Level修改为uint8_t (*hal_button_Level)(void)

 5)修改button_init函数

void button_init(struct Button* handle, uint8_t(*pin_level)(), uint8_t active_level, uint8_t button_id)
{
    memset(handle, 0, sizeof(struct Button));
    handle->event = (uint8_t)NONE_PRESS;
    handle->hal_button_Level = pin_level;
    handle->button_level = handle->hal_button_Level( );//改这里即可
    handle->active_level = active_level;
    handle->button_id = button_id;
}

6)注册按键

Button button_array[4];

static uint8_t GetButton_1_Level(void)
{
    GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
}
static uint8_t GetButton_2_Level(void)
{
    GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_2);
}
static uint8_t GetButton_3_Level(void)
{
    GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_3);
}
static uint8_t GetButton_4_Level(void)
{
    GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_4);
}

static void myButton_Init(void)
{
    button_init(&button_array[0], GetButton_1_Level, ACTIVE_LEVEL_HIGH, KEY_1);
    button_start(&button_array[0]);
    button_init(&button_array[1], GetButton_2_Level, ACTIVE_LEVEL_LOW, KEY_2);
    button_start(&button_array[1]);
    button_init(&button_array[2], GetButton_3_Level, ACTIVE_LEVEL_LOW, KEY_3);
    button_start(&button_array[2]);
    button_init(&button_array[3], GetButton_4_Level, ACTIVE_LEVEL_LOW, KEY_2);
    button_start(&button_array[3]);
}

7)系统滴答产生5ms中断,在中断服务函数中处理按键时基

static u8 multiButtonTicks;

void SysTick_Handler(void)
{
    if(SysTick->CTRL & (0x1 << 1))//读标志清零
    {
        multiButtonTicks++;
        if(multiButtonTicks == 5)
        {
            multiButtonTicks = 0;
            button_ticks();
        }
    }
}

8)主函数中遍历按键事件

当然这只是笔者的一种测试手段,大家同样可以利用回调函数或者其他方案去解决,实际项目中使用肯定要针对各种实时性问题综合进行运用

while(1)
{
    for(i = 0; i < 4; i++)
    {
        button_event = get_button_event(&button_array[i]);
        if((button_event > PRESS_REPEAT) && (button_event < number_of_event))
        {
            printf("按键<%d>发生了 ", button_array[i].button_id);
            switch((int)button_event)
            {
                case SINGLE_CLICK: printf("单击事件");break;
                case DOUBLE_CLICK: printf("双击击事件");break;
                case LONG_PRESS_START: printf("长按事件");break;
            }
            printf("\r\n");
            while(button_array[i].event == LONG_PRESS_START);
            button_array[i].event = NONE_PRESS;
            break;
        }
    }
}

9)测试结果如下

 

本文转自微信公众号 “嗨玩嵌入式”, 关注原作者可移步公众号。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐