文章目录
前言:本思路方法,参考了B站UP主江协科技,的非阻塞按键的写法。在此基础上采用SysTick系统定时器实现
一、基础知识
1.1 SysTick
1.1.1 SysTick 简介
SysTick—系统定时器是属于CM3 内核中的一个外设,内嵌在NVIC 中。系统定时器是一个24bit的向下递减的计数器,计数器每计数一次的时间为1/SYSCLK,一般我们设置系统时钟SYSCLK等于72M。当重装载数值寄存器的值递减到0 的时候,系统定时器就产生一次中断,以此循环往复。
因为SysTick 是属于CM3 内核的外设,所以所有基于CM3 内核的单片机都具有这个系统定时器,使得软件CM3 单片机中可以很容易的移植。系统定时器一般用于操作系统,用于产生时基,维持操作系统的心跳。
1.1.2 SysTick 寄存器介绍
SysTick—系统定时器有4 个寄存器,简要介绍如下。在使用SysTick 产生定时的时候,只需要配置前三个寄存器,最后一个校准寄存器不需要使用,今天我们只使用最小时基,直接调用函数即可,以下寄存器了解即可。
寄存器名称 | 寄存器描述 |
---|---|
CTRL | SysTick控制及状态寄存器 |
LOAD | SysTick重装载数值寄存器 |
VAL | SysTick当前数值寄存器 |
CALIB | SysTick校准数值寄存器 |
1.2 按键
1.2.1 按键抖动
按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。
按键消抖,一般分为硬件消抖和软件消抖,硬件消抖分为,采用电容滤波消抖和采用RS触发器消抖,好处是电路稳定,缺点是由于需要硬件介入,会增加成本,所以使用场合,很少。用途更广的是采用软件消抖。
1.2.2 软件消抖
Delay 延时函数按键消抖(采用阻塞方式)
好处:结构简单,方便理解和编写
坏处:过度的消耗CPU资源,时间不够准确
int8_t KEY_EX_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
int8_t key=0;
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)==KEY_EX_ON)
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)==KEY_EX_ON);
Delay_ms(20);
key=1;
}
return key;
}
分析函数,知道程序会很长时间阻塞在Delay_ms(20),这个延时函数,如果此时,主函数需要一个灯不停闪烁时,当按键按下后,大概几率LED灯将停止闪烁,此时程序发生阻塞,CPU无法执行LED灯闪烁程序。所以在实时性要求很高的场合或者需要多个设备同时响应时,这样的写法将不在适用。
定时器按键消抖(非阻塞方式)
原理:使用定时器,代替原来的Delay_ms(20),完成延时的目的,具体过程,分为三步
第一:初始化定时器,确实最小时基
第二:编写按键处理函数,按键检测判断逻辑函数
第三:设立按键标志位,进行20ms定时分频操作,编写中断处理函数
[!CAUTION]
注意:逻辑说明,按键如果本次是高电平,上次是低电平,就表明按键已经按下,且目前是松手状态,返回按键码值,并复位标志位。
二、具体程序编写
2.1 初始化 SysTick定时器
编写SysTick.c、SysTick.h文件
SysTick.c、SysTick.h
#include "stm32f10x.h"
#include "SysTick.h"
void SysTick_Init(void)
{
/* SystemFrequency / 1000 1ms 中断一次
9 * SystemFrequency / 100000 10us 中断一次
10 * SystemFrequency / 1000000 1us 中断一次
*/
if(SysTick_Config(SystemCoreClock/1000))
{
while(1);
}
}
#ifndef SYSTICK_H
#define SYSTICK_H
void SysTick_Init(void);
#endif //SYSTICK_DELAY_H
SysTick_Init(),函数确定了,中断的最小时基,1ms中断一次,为后面分频做准备。
2.2 编写按键处理函数
编写app_key.c、app_key.h文件,在app_key.h文件,定义按键结构体变量和枚举变量
app_key.h
#ifndef APP_KEY_H
#define APP_KEY_H
#include "stm32f10x.h"
typedef enum
{
KEY_Flag_OFF=0
,KEY_Flag_ON
}KEYMode;//定义按键模式
typedef struct
{
uint32_t KEY_Task_Cycle; //按键扫描周期
uint32_t KEY_Task_Time; //按键消抖时间
KEYMode KEY_Task_Flag; //按键标志位,这里数据类型采用的上面的定义的枚举结构体,不理解的可以分开写
}KEY_Info;
extern KEY_Info KEY1_info;//全局变量
extern KEY_Info KEY2_info;
//函数声明
void KEY_Task_Init(uint32_t Light_LED_Task_Cycles); //按键初始化,消抖扫描周期
uint16_t KEY_GetNum_Task(void); //按键检测,返回键值
uint16_t KEY_GetState(void); ////获取按键,键值
#endif //APP_KEY_H
app_key.c
#include "app_key.h"
#include "key/bsp_key.h"
#include "key/bsp_key_EX.h"
KEY_Info KEY1_info;//结构体全局变量
KEY_Info KEY2_info;
/**
* @brief 按键检测任务功能初始化函数
* @param KEY_Task_Cycles 任务周期,20ms轮询一次
* @note 无
* @retval 无
*/
void KEY_Task_Init(uint32_t KEY_Task_Cycles)
{
KEY1_info.KEY_Task_Cycle = KEY_Task_Cycles; //任务周期
KEY1_info.KEY_Task_Flag=KEY_Flag_OFF; //清除标志位
}
/**
* @brief 按键检测任务函数
* @param 无
* @note 无
* @retval 返回当前键值
*/
uint16_t KEY_GetNum_Task(void)
{
static uint16_t Key_CurrState,Key_PervState;
//*** 重要内容,定义两个静态变量,分别存贮,按键的本次值,和上次值
if(KEY1_info.KEY_Task_Flag==KEY_Flag_ON)
{
Key_PervState=Key_CurrState; //首次时,先将本次值赋给上次值,上一次,就是上一次的本次
Key_CurrState=KEY_GetState(); //获取本次值
if(Key_CurrState==0&&Key_PervState!=0) //判断本次值为0,而上次值非0,时表示按键按下
{
KEY1_info.KEY_Task_Flag=KEY_Flag_OFF;//清除标志位
return Key_PervState; //返回当前键值
}
}
return 0;
}
/**
* @brief 按键处理函数
* @param 无
* @note 无
* @retval 返回当前编码后的按键,键值
*/
uint16_t KEY_GetState(void)
{
if(GPIO_ReadInputDataBit(KEY3_GPIO_PORT,KEY3_GPIO_PIN)==KEY_EX_ON)
{
return 1; //按键1,按下,返回编码1
}
if(GPIO_ReadInputDataBit(KEY4_GPIO_PORT,KEY4_GPIO_PIN)==KEY_EX_ON)
{
return 2;//按键2,按下,返回编码2
}
return 0;//否则返编码0
}
[!CAUTION]
在实际使用按键检测时,只需要调用,void
KEY_Task_Init
(uint32_t KEY_Task_Cycles)和uint16_tKEY_GetNum_Task
(void),这两个函数。这样做到了函数的高度封装和低耦合
static
uint16_tKey_CurrState
,Key_PervState
,这两个变量一定要是,静态变量,这样退出函数后,数据才不会丢失
2.3 编写中断处理函数
在
stm32f10x_it
.c 文件中,找到voidSysTick_Handler
(void),此函数为该.c文件自带函数
#include "app_key.h"
void SysTick_Handler(void)
{
if(KEY1_info.KEY_Task_Time==KEY1_info.KEY_Task_Cycle) //时间计数到达指定周期后,标志位置位,时间清零
{
KEY1_info.KEY_Task_Flag=KEY_Flag_ON;
KEY1_info.KEY_Task_Time=0;
}
else
{
KEY1_info.KEY_Task_Time++;
}
//其他情况,每中断一次时间自增一
}
[!TIP]
也可以通过,将按键逻辑,写在中断里面,但是中断要求的是快进快出,所以尽可能的少最好,切记中断里面不要有普通的延时函数,否则中断处理函数,将堵塞。
三、总结
采用Systick系统定时器,产生最小时基的方法,来进行按键消抖检测,极大的提高了按键的准确性和实时性。避免了程序造成堵塞的弊端。极大的提高了,cpu的利用率,下面用一个例子,来展现这种按键的好处。
例子:功能1板载LED绿灯,以50ms为周期,心跳闪烁,循环执行,功能2外部按键KEY1,控制板载蓝灯的亮灭,要求,在这个过程不能干扰到心跳灯运行
主函数
#include "main.h"
#include "stm32f10x.h"
#include "NTC/bsp_ntc.h"
/**
* @brief 主函数
* @param 无
* @note 无
* @retval 无
*/
int main(void)
{
uint16_t keynum,count1=0
LED_GPIO_config();
KEY_GPIO_config();
SysTick_Init();
Heartbeat1_LED_Task_Init(100);
KEY_Task_Init(20);//按键周期设置为20ms
while (1)
{
Heartbeat1_LED_Task();
keynum=KEY_GetNum_Task();
if(keynum==1)
{
B_LED_TOGGLE;//取反
}
}
}
led.c、key.c
/**
******************************************************************************
* @file app_led.c
* @author huangwei
* @version V1.0
* @date 2024年11月6日
* @brief 心跳灯1应用层功能接口
******************************************************************************
**/
#include "app_led.h"
#include "LED/bsp_led.h"
uint16_t Heartbeat1_Led_Task_Flag;
uint32_t Heartbeat1_Led_Task_Cycle;
uint32_t Heartbeat1_Led_Task_Time;
/**
* @brief 心跳灯任务函数
* @param 无
* @note 无
* @retval 无
*/
void Heartbeat1_LED_Task()
{
if(Heartbeat1_Led_Task_Flag==1)
{
G_LED_TOGGLE;
//Heartbeat1_LED_Task_Rest();
Heartbeat1_Led_Task_Flag=0;
}
}
/**
* @brief 心跳灯任务计数复位函数
* @param 无
* @note 无
* @retval 无
*/
void Heartbeat1_LED_Task_Rest()
{
Heartbeat1_Led_Task_Time=Heartbeat1_Led_Task_Cycle;
Heartbeat1_Led_Task_Flag=0;
}
/**
* @brief 心跳灯任务初始化函数
* @param Heartbeat1_LED_Cycles: 任务周期 单位ms(可修改系统节拍定时器)
* @note 无
* @retval 无
*/
void Heartbeat1_LED_Task_Init(uint32_t Heartbeat1_LED_Cycles)
{
Heartbeat1_Led_Task_Cycle=Heartbeat1_LED_Cycles;
//Heartbeat1_LED_Task_Rest();
Heartbeat1_Led_Task_Flag=0;
}
/**
******************************************************************************
* @file app_key.c
* @author huangwei
* @version V1.0
* @date 2024年11月6日
* @brief 按键应用层功能接口
******************************************************************************
**/
#include "app_key.h"
#include "key/bsp_key.h"
#include "key/bsp_key_EX.h"
KEY_Info KEY1_info;//结构体全局变量
KEY_Info KEY2_info;
/**
* @brief 按键模块任务功能初始化函数
* @param KEY_Task_Cycles 任务周期,100ms轮训一次
* @note 无
* @retval 无
*/
void KEY_Task_Init(uint32_t KEY_Task_Cycles)
{
KEY1_info.KEY_Task_Cycle = KEY_Task_Cycles;
//KEY_Task_Rest();
KEY1_info.KEY_Task_Flag=KEY_Flag_OFF;
}
/**
* @brief 按键任务计数复位函数
* @param 无
* @note 无
* @retval 无
*/
void KEY_Task_Rest(void)
{
KEY1_info.KEY_Task_Time=KEY1_info.KEY_Task_Cycle;
KEY1_info.KEY_Task_Flag=KEY_Flag_OFF;
}
/**
* @brief 按键任务函数
* @param 无
* @note 无
* @retval 返回当前按键值
*/
uint16_t KEY_GetNum_Task(void)
{
static uint16_t Key_CurrState,Key_PervState;
if(KEY1_info.KEY_Task_Flag==KEY_Flag_ON)
{
Key_PervState=Key_CurrState;
Key_CurrState=KEY_GetState();
if(Key_CurrState==0&&Key_PervState!=0)
{
KEY1_info.KEY_Task_Flag=KEY_Flag_OFF;
return Key_PervState;
}
//KEY_Task_Rest();
}
return 0;
}
/**
* @brief 按处理键编码函数
* @param 无
* @note 无
* @retval 返回按键编码值
*/
uint16_t KEY_GetState(void)
{
if(GPIO_ReadInputDataBit(KEY3_GPIO_PORT,KEY3_GPIO_PIN)==KEY_EX_ON)
{
return 1;
}
if(GPIO_ReadInputDataBit(KEY4_GPIO_PORT,KEY4_GPIO_PIN)==KEY_EX_ON)
{
return 2;
}
return 0;
}
Systick 中断处理函数
void SysTick_Handler(void)
{
if(Heartbeat1_Led_Task_Time==Heartbeat1_Led_Task_Cycle)
{
Heartbeat1_Led_Task_Flag=1;
Heartbeat1_Led_Task_Time=0;
}
else
{
Heartbeat1_Led_Task_Time++;
//Heartbeat1_Led_Task_Flag=0;
}
if(KEY1_info.KEY_Task_Time==KEY1_info.KEY_Task_Cycle)
{
KEY1_info.KEY_Task_Flag=KEY_Flag_ON;
KEY1_info.KEY_Task_Time=0;
}
else
{
KEY1_info.KEY_Task_Time++;
//KEY1_info.KEY_Task_Flag=KEY_Flag_OFF;
}
}
程序现象
可以看出,按键并没有影响到心跳灯,表明此方法完成了,按键的非阻塞检测扫描,并且将按键周期设置,进行打包封装,展现出了很高的灵活性。