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

本文主要是介绍STM32按键设计三之两个按键操控整个系统十几乃至几十种功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本篇任务

前言:
按键,基本上所有设备的必备,可以毫不夸张的说:目之所及,皆有按键。
几个按键合适?根据系统控制需要,量才而用。按键是大才,能轻易的控制整个系统进行不同状态或者行为的切换,同时也是耗材,浪费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宏定义了按键次数,可以方便更改。


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

这篇关于STM32按键设计三之两个按键操控整个系统十几乃至几十种功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/768546

相关文章

Mac系统下卸载JAVA和JDK的步骤

《Mac系统下卸载JAVA和JDK的步骤》JDK是Java语言的软件开发工具包,它提供了开发和运行Java应用程序所需的工具、库和资源,:本文主要介绍Mac系统下卸载JAVA和JDK的相关资料,需... 目录1. 卸载系统自带的 Java 版本检查当前 Java 版本通过命令卸载系统 Java2. 卸载自定

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

苹果macOS 26 Tahoe主题功能大升级:可定制图标/高亮文本/文件夹颜色

《苹果macOS26Tahoe主题功能大升级:可定制图标/高亮文本/文件夹颜色》在整体系统设计方面,macOS26采用了全新的玻璃质感视觉风格,应用于Dock栏、应用图标以及桌面小部件等多个界面... 科技媒体 MACRumors 昨日(6 月 13 日)发布博文,报道称在 macOS 26 Tahoe 中

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

MybatisPlus service接口功能介绍

《MybatisPlusservice接口功能介绍》:本文主要介绍MybatisPlusservice接口功能介绍,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友... 目录Service接口基本用法进阶用法总结:Lambda方法Service接口基本用法MyBATisP

基于Python实现一个简单的题库与在线考试系统

《基于Python实现一个简单的题库与在线考试系统》在当今信息化教育时代,在线学习与考试系统已成为教育技术领域的重要组成部分,本文就来介绍一下如何使用Python和PyQt5框架开发一个名为白泽题库系... 目录概述功能特点界面展示系统架构设计类结构图Excel题库填写格式模板题库题目填写格式表核心数据结构

Linux系统中的firewall-offline-cmd详解(收藏版)

《Linux系统中的firewall-offline-cmd详解(收藏版)》firewall-offline-cmd是firewalld的一个命令行工具,专门设计用于在没有运行firewalld服务的... 目录主要用途基本语法选项1. 状态管理2. 区域管理3. 服务管理4. 端口管理5. ICMP 阻断

MyBatis设计SQL返回布尔值(Boolean)的常见方法

《MyBatis设计SQL返回布尔值(Boolean)的常见方法》这篇文章主要为大家详细介绍了MyBatis设计SQL返回布尔值(Boolean)的几种常见方法,文中的示例代码讲解详细,感兴趣的小伙伴... 目录方案一:使用COUNT查询存在性(推荐)方案二:条件表达式直接返回布尔方案三:存在性检查(EXI

Java反射实现多属性去重与分组功能

《Java反射实现多属性去重与分组功能》在Java开发中,​​List是一种非常常用的数据结构,通常我们会遇到这样的问题:如何处理​​List​​​中的相同字段?无论是去重还是分组,合理的操作可以提高... 目录一、开发环境与基础组件准备1.环境配置:2. 代码结构说明:二、基础反射工具:BeanUtils

Windows 系统下 Nginx 的配置步骤详解

《Windows系统下Nginx的配置步骤详解》Nginx是一款功能强大的软件,在互联网领域有广泛应用,简单来说,它就像一个聪明的交通指挥员,能让网站运行得更高效、更稳定,:本文主要介绍W... 目录一、为什么要用 Nginx二、Windows 系统下 Nginx 的配置步骤1. 下载 Nginx2. 解压