STM32按键设计三之两个按键操控整个系统十几乃至几十种功能

本文详细介绍了如何在STM32平台使用两个按键实现多种状态控制,通过软件定时器优化IO资源并处理按键时间逻辑。通过实例展示了按键松开、按键按下处理函数和状态机设计,以及如何利用弱连接实现灵活的功能接口。
摘要由CSDN通过智能技术生成

本篇任务

前言:
按键,基本上所有设备的必备,可以毫不夸张的说:目之所及,皆有按键。
几个按键合适?根据系统控制需要,量才而用。按键是大才,能轻易的控制整个系统进行不同状态或者行为的切换,同时也是耗材,浪费IO口,占据大量体积。所以,需要量才而用,买足系统需求的前提下,越精简越好。

本篇将在上一篇按键中断的基础上,实现两个按键控制系统十几乃至几十中状态,节省器件,节约空间,节约IO口,同时又能实现复杂功能,目标就两个字”节约精简“。

按键实现

需要了解本篇,需要对上篇有个大致了解。只需要知道上篇按键中断里面预留的接口函数,本篇将构建与上篇的接口之上。上一篇传送门:STM32按键设计二之按键中断

  1. 按照质量守恒定律,如果按键即是质量中的量,软件为质量中的质。在减少了按键的数量的同时需要想要实现复杂按键的功能,需要提高软件的质。计算机中的代码优化和很多算法优化大多也是如此,都是在空间复杂度和时间复杂度中做权衡。
  2. 多功能按键需要在软件中将底层的按键设备抽离到软件层面,封装为一个大的结构体类型,整体上进行思考。
  3. 需要抽离出一个软件定时器类型,用于调度整个过程,甚至控制整个系统。
  4. 编写软件的过程实际是在反复验证逻辑的缜密性,很多时候需要大脑对整个过程进行模拟。

多功能按键实现基本逻辑图

在这里插入图片描述

  • 以上是按键实现的基本逻辑图,基本有两点:
  1. 需要在按键按下到松开的时候,进行按键按下时间长度的采集。用作分别设置短按,长按和复位按多种形式。
  2. 在本次按键和下次按键按下之间进行时间判定,如果超时,本次连续按键事件结束,系统控制程序运行。需要设置连续按键间隔时间,可以根据各自的系统按键情况进行设置。一般连按时间多很小。

按键软件实现

实现按键前,需要明白两个接口函数KEY0_Down_callback和KEY0_Up_callback,这是在硬件按键层设置的按键按下和松开接口处理函数,如此实现抽象出来了接口,为了更加容易维护和复用。实现方式和上篇中的传送门:STM32按键设计二之按键中断一模一样,没有做任何更改,可以直接使用。(整个代码篇幅太长,很多不是重点的内容或者重复内容不展示了)

软件定时器实现全局调度功能

  • 通过以上逻辑图,可以看出整个过程需要对时间进行计时,本篇采用软件定时器实现。也可以通过基本硬件定时器实现,控制方式都是一样的,但实现原理上软件定时器稍微复制一些,但是可以提供给整个系统使用,方便万能。直接贴代码:
  • 软件定时器结构体定义
/* 软件定时器结构定义 ---------------- */
STRUCT(Timer_t)
{
    Timer_Sta_t state;  /* 是否在使用 */
    int32_t val;        /* 下次触发值 */
    int32_t arr_val;    /* 重装载值 */
    u32 cycles;         /* 循环次数: 0xFF: 永远循环,1 ~ 0xFE: 循环次数 */
    TimerCallback_t callback;
    Callback_Block_t is_block;
};
  • 首先,定义一个软件定时器列表,用于
Timer_t Timers[TIMERS_SIZE];
  • 软件定时器设置函数,提供给系统任务,每个任务都可以为自己或者其他任务设置软件定时器
/* 函数定义 ------------------------------------------------------- */
/**
 * @name: Timer_SetAlarm
 * @description: 添加定时器
 * @param {u32} cycles
 * @param {u32} arr
 * @param {TimerCallback_t} callback
 * @return {*}
 */
int8_t Timer_SetAlarm(u32 cycles, u32 arr, TimerCallback_t callback, Callback_Block_t is_block)
{
    Timer_t *timer;
    int8_t timer_index;
    int8_t ret = TIMER_NONE;

    for(timer_index = 0, timer = Timers; timer_index <= timers_ptr + 1 && timer_index < (sizeof(Timers) / sizeof(Timers[0])); timer_index++, timer++)
    {
        if((timer->state == TIMER_FREE) && callback)
        {
            uint16_t next_time;

            /* 设置软件定时器重装载值 */
            timer->arr_val = arr;

            /* Timers指针增加条件 */
            if(timer_index == timers_ptr + 1) timers_ptr++;

            next_time = getNextTimerInterrupt();
            if(arr < next_time)
            {
                total_sleep_time += next_wakeup - next_time;
                next_wakeup = arr + next_wakeup - next_time;
                setTimer(arr);
            }
            /* 设置定时时间和状态 */
            timer->state = TIMER_WORKING;
            timer->cycles = cycles;
            timer->val = arr + next_wakeup - next_time;
            timer->callback = callback;
            timer->is_block = is_block;
            
            ret = timer_index;
            break;
        }
    }  
    return ret;
}
  • 软件定时器实现基本调度,在硬件定时器中断中进行调用,本篇硬件定时器配置的时钟分频为10us。
/**
 * @name: Timer_Dispatch
 * @description: 时间调度,可以在中断中进行
 * @param {*}
 * @return {*}
 */
void Timer_Dispatch(void)
{
    Timer_t *timer;
    int8_t timer_index;
    u32 temp = 0xffffffff;
    
    for(timer_index = 0, timer = Timers; timer_index <= timers_ptr && timer_index < sizeof(Timers) / sizeof(Timers[0]); timer_index++, timer++)
    {
        if(timer->state == TIMER_WORKING)
        {
            timer->val -= next_wakeup;
            if(timer->val <= 0)
            {                
                if(timer->cycles == 0xFF)
                {
                    timer->val = timer->arr_val;
                }
                else if(timer->cycles > 1)
                {
                    timer->val = timer->arr_val;
                    timer->cycles--;
                }
                else if(timer->cycles == 1)
                {
                    /* 当软件定时器执行一次的时候,执行完成 */
                    timer->state = TIMER_FREE;
                }
                /* 将定时器时间到达,需要执行的函数加入回调函数列表 */
                if(timer->is_block == CALLBACK_NOBLOCK)
                {
                    Timers_Callback_noblock[++callback_noblock_ptr] = timer->callback;
                }
                else
                {
                    Timers_Callback_block[++callback_block_ptr] = timer->callback;
                }
            }
            else
            {
                if((u32)timer->val < temp)
                    temp = timer->val;
            }
        }
    }
    /* 设置下次定时器时间 */
    next_wakeup = temp > TIMn_ARR ? TIMn_ARR : ((uint16_t)temp);

    setTimer(next_wakeup);

}

由于软件定时器不是本篇重点,所以很多内容从简(主要由于写完篇幅过长)。

按键实现

  • 在本篇中依然调用了这两个接口函数进行。
/* 任务回调函数,按键key.c中的接口回调 */
void KEY0_Down_callback(void)
{
    KEY_Down(KEY0_DEV);
}

/* 任务回调函数,按键key.c中的接口回调 */
void KEY0_Up_callback(void)
{
    KEY_Up(KEY0_DEV);
}

KEY_Down和KEY_Up是具体的处理函数,后续有源码。

  • 使用一个位段,对按键结构进行状态设置,根据状态进行相应的操作
typedef struct _tag_KEY_STA_BIT_t KEY_STA_BIT_t;
struct _tag_KEY_STA_BIT_t
{
    uint8_t using: 1;           /* 正在采集标志位 */
    uint8_t processing: 1;      /* 采集完成需要处理标志位 */  
    uint8_t press_long: 1;    /* 长按标志位 */
    uint8_t press_3s: 1;    /* 长按标志位 */
    uint8_t times :4;           /* 按键按下次数 */   
};
  • 同时定义一个共用体,方便对状态位进行一次性操作
typedef union _tag_KEY_STA_t KEY_STA_t;
union _tag_KEY_STA_t
{
    uint8_t flag;   /* 通过flag操作state,方便清0 */
    KEY_STA_BIT_t state;
};
  • 定义按键结构体,进行按键的操作,此后基本的按键都基于此
/* 实际按键设备编号 */
typedef enum _tag_KEY_Dev_t KEY_Dev_t;
enum _tag_KEY_Dev_t
{
    KEY0_DEV = 1,
    KEY1_DEV,   
};

/* 软件key结构体 */
STRUCT(KEY_t)
{
    KEY_STA_t key_state;                        /* KEY状态值 */
    KEY_Dev_t keys_table[KEY_TABLE_SIZE];     /* 按键列表 */
    u32 time_val;                               /* 按下时的系统时间 */
};
  • 下面就是对开头的函数接口中函数的实现了,按键松开处理函数
/**
 * @name: KEY_Up
 * @description: 按键松开处理函数
 * @param {KEY_Dev_t} key_dev 按键设备号
 * @return {*}
 */
static void KEY_Up(KEY_Dev_t key_dev)
{
    u32 temp;
    if(!Key.key_state.state.processing) /* 判断没有按键采集完成,未处理 */
    {
        temp = get_SystemTime();
        if(!Key.key_state.state.using)  /* 没开始采集 */
        {
            /* 没采集的操作 */
        }
        else
        {
            temp = temp > Key.time_val ? temp : (temp + TOTAL_SLEEP_TIME_MAX);
            if(temp - Key.time_val > KEY_PRESS_3sLONG) /* 长按键3s */
            {
                /* 长按键只能单独使用 */
                Key.key_state.state.processing = 1; /* 结束按键采集 */
                Key.key_state.state.press_3s = 1;
                Key.key_state.state.times = 1;
            }
            else if(temp - Key.time_val > KEY_PRESS_LONG) /* 长按键 */
            {
                /* 长按键只能单独使用 */
                Key.key_state.state.processing = 1; /* 结束按键采集 */
                Key.key_state.state.press_long = 1;
                Key.key_state.state.times = 1;
            }
            else    /* 短按键 */
            {
                /* 短按键可以叠加 */
                if(Key.key_state.state.times > KEY_COLLECTION_TIMES)
                {
                    Key.key_state.state.processing = 1; /* 结束按键采集 */
                }
                else
                {
                    Key.key_state.state.times++;    /* 按键次数记录 */
                    callback_num = Timer_SetAlarm(1, NEXT_KEY_SLICE, KEY_Timer_Callback, CALLBACK_NOBLOCK);
                }
            }
            Key.keys_table[Key.key_state.state.times - 1] = key_dev;    /* 按键设备号记录 */
        }
    }
    else
    {
        /* 按键未处理,新按键按下,处理程序 */
    }
}
  1. 首先说明:本系统中长按和复位按(3s按)不会进行连按设置,也就是这两种不支持连按。只有短按(接触按)可以使用连按,主要是考虑到实际当中长按用户的一般使用和体验,如果需要也可能实现其逻辑。
  2. KEY_Up进行了按键事件的采集,并且和按下时间进行计算,分析长按和短按模式。长按和复位按不支持连按会设置按键采集完成,需要处理标志位;短按则会判断按键次数和设置一个定时器时间,用于判断是否一次有效的连续采集。
  3. 最后记录了按键的物理设备按键码。
  • 注意:在写代码的过程中,难免手上动作和思维不一样,想清楚而没写对型。callback_num = Timer_SetAlarm(1, NEXT_KEY_SLICE, KEY_Timer_Callback, CALLBACK_NOBLOCK);只写了Timer_SetAlarm(1, NEXT_KEY_SLICE, KEY_Timer_Callback, CALLBACK_NOBLOCK);。编译,运行都可以,出现了逻辑错误,差点没要老命啊!逻辑错误最难找,后面有图示。
  • 定时器调用的函数,简短
/**
 * @name: KEY_Timer_Callback
 * @description: 如果在下次之间没有按键,进行按键收尾设置
 * @param {*}
 * @return {*}
 */
static void KEY_Timer_Callback(void) 
{
    Key.key_state.state.processing = 1;
}
  • 按键按下处理函数
/**
 * @name: KEY_Down
 * @description: 按键按下处理函数
 * @param {KEY_Dev_t} key_dev 按键设备号
 * @return {*}
 */
static void KEY_Down(KEY_Dev_t key_dev)
{
    if(!Key.key_state.state.processing) /* 判断没有按键采集完成,未处理 */
    {

        printf("key %d down ! \n", key_dev);
        Key.time_val = get_SystemTime();
        if(!Key.key_state.state.using)  /* 没开始采集 */
        {
            Key.key_state.state.using = 1;  /* 第一次采集 */
        }
        else
        {
            Timer_DelAlarm(callback_num);
        }
    }
    else
    {
        /* 按键未处理,新按键按下,处理程序 */
    }
}
  1. 首先判断是否上次的按键事件没有正在处理的。
  2. 记录按键按下时间。
  3. 如果是第一次按键按下,将按键使用中标志位置1;如果是连续按键,则需要将软件定时器清除。
  • 主要部分完成。

按键事件状态机实现

多功能按键事件相对简单,可以使用简单的if/else或者switch/case进行实现。对各个状态进行不同的操作可以提供一个函数接口,根据需要对其接口进行实现就可以实现具体的功能。

  • 按键状态机实现代码
/**
 * @name: KEY_Event_Handler
 * @description: 按键事件处理函数
 * @param {*}
 * @return {*}
 */
void KEY_Event_Handler(void)
{
    u32 short_press_val = 0;

    if(1 == Key.key_state.state.processing) /* 按键采集完成,需要处理 */
    {
		// printf("processing: %d \n", Key.key_state.state.processing);
        if(1 == Key.key_state.state.press_3s)   /* 按键3s */
        {
            KEY_Press_3sLong();
        } 
        else if(1 == Key.key_state.state.press_long)    /* 长按键处理 */
        {
            KEY_Press_Long();
        }
        else
        {   /* 短按键处理 */
            
            for(int8_t i = 0; i < Key.key_state.state.times; i++)
            {
                /* 短按键计算按键结果 */
                short_press_val += (u32)pow(10, Key.key_state.state.times-1-i) * Key.keys_table[i];
            }
            // printf("press short val: %d \n", short_press_val);
            Short_Press_Handler(short_press_val);
        }
        KEY_State_Clear();
    }
}

/**
 * @name: Short_Press_Handler
 * @description: 短按键状态机,连按处理方式
 * @param {u32} val
 * @return {*}
 */
static void Short_Press_Handler(u32 val)
{
    switch(val)
    {
        case PRESS_1:
            KEY_Press_1();
            break;

        case PRESS_2:
            KEY_Press_2();
            break;

        case PRESS_11:
            KEY_Press_11();
            break;

        case PRESS_12:
            KEY_Press_12();
            break;

        case PRESS_21:
            KEY_Press_21();
            break;

        case PRESS_22:
            KEY_Press_22();
            break;

        case PRESS_111:
            KEY_Press_111();
            break;

        default:
            Press_Err_Handler();
            break;
    }
}
  1. KEY_Event_Handler 和 Short_Press_Handler一起实现了按键事件的整个状态机,Short_Press_Handler为一个内部函数,只在KEY_Event_Handler 中进行调用,同时又为段案件时间提供了可以编程的接口函数,用于外部实现不同的功能。
  2. 短按键状态判断,根据其按键顺序进行,比如先按1,再按2,则其值为12,调用KEY_Press_12接口进行处理;在Press_Err_Handler未做任何处理。
  3. 所有的函数已经内部实现,但是不影响外部使用,因为内部使用了weak连接进行编译链接,如下放上全部,利于查看调试结果。
/* 按键事件函数接口,对外提供调用接口 ----------------------------------- */
__attribute__((weak)) void Press_Err_Handler(void)
{
    printf("weak: key press error \n");
}

__attribute__((weak)) void KEY_Press_3sLong(void)
{
    printf("weak: press 3s handle func, system reset! \n");
}

__attribute__((weak)) void KEY_Press_Long(void)
{
    printf("weak: press Long handle func, ! \n");
}

__attribute__((weak)) void KEY_Press_1(void)
{
    printf("weak: KEY_Press_1 \n");
}

__attribute__((weak)) void KEY_Press_2(void)
{
    printf("weak: KEY_Press_2 \n");
}

__attribute__((weak)) void KEY_Press_11(void)
{
    printf("weak: KEY_Press_11 \n");
}

__attribute__((weak)) void KEY_Press_12(void)
{
    printf("weak: KEY_Press_12 \n");
}

__attribute__((weak)) void KEY_Press_21(void)
{
    printf("weak: KEY_Press_21 \n");
}

__attribute__((weak)) void KEY_Press_22(void)
{
    printf("weak: KEY_Press_22 \n");
}

__attribute__((weak)) void KEY_Press_111(void)
{
    printf("weak: KEY_Press_111 \n");
}
  • 同时在APP中对,需要调用接口函数进行了再次实现
void KEY_Press_3sLong(void)
{
    printf("In APP, 3s press! reset now ... \n");
}

void KEY_Press_11(void)
{
    printf("In APP, press_11 ... \n");
}

结果分析

  • 终于对了
    在这里插入图片描述

以上进行了简单测试

  • 1为只按了一次按键1,调用了默认的接口函数KEY_Press_1 ;
  • 3为连续短按了按键1和按键2,调用了默认的接口函数KEY_Press_12;
  • 4为按下两次按键1,调用了外部实现的任务函数接口KEY_Press_11,打印结果为In APP,说明不是默认函数实现;
  • 5按了3s的按键1,调用系统复位;
  • 6为按下三次按键2,由于此按键事件没有定义和实现,所以调用了默认Press_Err_Handler错误处理函数。
  • 在终于对了之前,出现的问题:
    在这里插入图片描述

多出三只鬼,消灭三只 …

尾声

  • 至此,多功能按键基本完了。
  • 2个按键的多功能实现,按键最多按一次,可以有6种状态,分别为:4+2 = 6
    按键1,按键2,按键1长按,按键2长按,按键1复位按,按键2复位按
  • 2个按键,按键次数设置位2,可以实现10种状态;按键次数设置位3,可以实现18种状态;
  • 总公式为 : 4 + 21 + 22+ … + 2n
  • 推荐使用按键次数设置为2或者3,同时保留长按或者复位按中的一个,这样可以使用的状态有8~~16种之多,如果不够用可以使用状态机,设置不同系统状态下,按键状态对应的功能不一样,有限无穷,周而复始。
  • 按键此时判断在上面KEY_Up函数中,KEY_COLLECTION_TIMES宏定义了按键次数,可以方便更改。


码字不易,请点个赞哦,谢谢!

  • 19
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值