【源码详解~按键状态机~简洁易懂】1,系列篇

114 阅读7分钟

定时器初始化:

void TIM3\_Int\_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC\_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
	
	//定时器TIM3初始化
	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck\_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM\_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
 
	TIM\_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断

	//中断优先级NVIC设置
	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级0级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级3级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC\_Init(&NVIC_InitStructure);  //初始化NVIC寄存器


	TIM\_Cmd(TIM3, ENABLE);  //使能TIMx 
}

初始化定时器20ms:TIM3_Int_Init(200-1,7200-1);

4.1.2 按键:

原理图:使用PA0引脚
在这里插入图片描述
注意:这里按键按下接地,故配置为上拉输入。
代码:

//按键初始化函数,配置IO口
void KEY\_Init(void) //IO初始化
{ 
 	GPIO_InitTypeDef GPIO_InitStructure;
 
 	RCC\_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能PORTA,PORTB时钟

	//初始化 WK\_UP-->GPIOA.0 
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
	GPIO\_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0

}

4.1.3 串口打印*

这部分可用不用,我这里是为了方便查看实验结果。
不使用的话,读者可配置自己的验证方式。(后文的printf相关的注释即可)
参考代码:

#include "sys.h"
#include "usart.h" 
// 
//如果使用ucos,则包括下面的头文件即可.
#if SYSTEM\_SUPPORT\_OS
#include "includes.h" //ucos 使用 
#endif

//
//加入以下代码,支持printf函数,而不需要选择use MicroLIB 
#if 1
#pragma import(\_\_use\_no\_semihosting) 
//标准库需要的支持函数 
struct \_\_FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义\_sys\_exit()以避免使用半主机模式 
\_sys\_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE \*f)
{      
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕 
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 

/\*使用microLib的方法\*/
 /\* 
int fputc(int ch, FILE \*f)
{
 USART\_SendData(USART1, (uint8\_t) ch);

 while (USART\_GetFlagStatus(USART1, USART\_FLAG\_TC) == RESET) {} 
 
 return ch;
}
int GetKey (void) { 

 while (!(USART1->SR & USART\_FLAG\_RXNE));

 return ((int)(USART1->DR & 0x1FF));
}
\*/
 
#if EN\_USART1\_RX //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误 
u8 USART_RX_BUF[USART_REC_LEN];     //接收缓冲,最大USART\_REC\_LEN个字节.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0;       //接收状态标记 
  
void uart\_init(u32 bound){
  //GPIO端口设置
  GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	 
	RCC\_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
  
	//USART1\_TX GPIOA.9
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
  GPIO\_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
  //USART1\_RX GPIOA.10初始化
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
  GPIO\_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10 

  //Usart1 NVIC 配置
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC\_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
  
   //USART 初始化设置

	USART_InitStructure.USART_BaudRate = bound;//串口波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

  USART\_Init(USART1, &USART_InitStructure); //初始化串口1
  USART\_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
  USART\_Cmd(USART1, ENABLE);                    //使能串口1 

}

void USART1\_IRQHandler(void)                	//串口1中断服务程序
	{
	u8 Res;
#if SYSTEM\_SUPPORT\_OS //如果SYSTEM\_SUPPORT\_OS为真,则需要支持OS.
	OSIntEnter();    
#endif
	if(USART\_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
		Res =USART\_ReceiveData(USART1);	//读取接收到的数据
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
			{
			if(USART_RX_STA&0x4000)//接收到了0x0d
				{
				if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了 
				}
			else //还没收到0X0D
				{	
				if(Res==0x0d)USART_RX_STA|=0x4000;
				else
					{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 
					}		 
				}
			}   		 
     } 
#if SYSTEM\_SUPPORT\_OS //如果SYSTEM\_SUPPORT\_OS为真,则需要支持OS.
	OSIntExit();  											 
#endif
} 
#endif 



4.2 类型、变量定义
4.2.1按键事件

枚举型名称:KEY_EventList_TypeDef
本文实现短按长按功能,故只有三种事件:无事件、短按事件、长按事件
对应代码,即:空闲、单击、长按

typedef enum \_KEY\_EventList\_TypeDef 
{
	KEY_Event_Null 		   = 0x00, // 空闲
	KEY_Event_SingleClick  = 0x01, // 单击
	KEY_Event_LongPress    = 0x02 // 长按
}KEY_EventList_TypeDef;

4.2.2 按键电平、动作

因为有的电路按下时,引脚为低电平;有的按下时,引脚却为高电平。这里将电平、动作分开,更方便移植。
枚举型名称:KEY_PinLevel_TypeDef
即:高、低电平。

// 按键引脚的电平
typedef enum
{ 
	KKEY_PinLevel_Low = 0,
	KEY_PinLevel_High
}KEY_PinLevel_TypeDef;

枚举型名称:KEY_Action_TypeDef
按键只有按下和没按下俩个动作:
即:按下、释放

// 按键动作,
typedef enum
{ 
	KEY_Action_Press = 0,
	KEY_Action_Release
}KEY_Action_TypeDef;

4.2.3 按键状态

枚举型名称:KEY_StatusList_TypeDef
思路图解的分析,分为如下几个状态。
即:空闲、消抖、确认按下、确认长按

// 按键状态
typedef enum \_KEY\_StatusList\_TypeDef 
{
	KEY_Status_Idle 	= 0, // 空闲
	KEY_Status_Debounce ,    // 消抖
	KEY_Status_ConfirmPress	,    // 确认按下 
	KEY_Status_ConfirmPressLong,    // 确认长按
}KEY_StatusList_TypeDef;

4.2.4 按键配置结构体

按键配置信息的结构体名称:KEY_Configure_TypeDef
打包好按键的基本属性。

typedef struct \_KEY\_Configure\_TypeDef 
{
	uint16\_t                        KEY_Count;                 // 按键长按时长计数
	KEY_Action_TypeDef             KEY_Action;                // 按键动作,按下或释放
	KEY_StatusList_TypeDef         KEY_Status;                // 按键状态
	KEY_EventList_TypeDef          KEY_Event;                 // 按键事件
	KEY\_PinLevel\_TypeDef           (\*KEY_ReadPin_Fcn)(void);  // 读引脚电平函数
}KEY_Configure_TypeDef;


成员解释:

  • KEY_Count:计数,记一个数为20ms。
  • KEY_Action:按键动作,按下或者释放。
  • KEY_Status:记录按键的状态值。
  • KEY_Event:记录按键触发的事件。
  • KEY_ReadPin_Fcn:读取按键电平值的函数指针。方便移植。
4.3 变量、函数、宏定义
4.3.1宏定义
  • KEY_LONG_PRESS_TIME :
    短按、长按的时长分界线。大于–>长按,小于–>短按。
  • KEY_PRESSED_LEVEL:
    按键被按下的实际电平,我的电路里,按键按下引脚接地。所以为低电平。
/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 
\* 长按、单击 定义
\* 长按事件:按下时间大于 KEY\_LONG\_PRESS\_TIME,释放后响应。(不支持连按,需要连按响应可自己配置)
\* 单击事件:按下时间小于 KEY\_LONG\_PRESS\_TIME 释放后响应。
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
// 长按时长的宏定义
#define KEY\_LONG\_PRESS\_TIME 50 // 20ms\*50 = 1s
#define KEY\_PRESSED\_LEVEL 0 // 按键被按下时的电平

4.3.2 变量定义

KeyCfg:全局变量,打包了按键的各个属性。

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 
\* 按键配置信息的全局结构体变量
\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
KEY_Configure_TypeDef KeyCfg={
		
		0,						// 按键长按时长计数
		KEY_Action_Release,		// 按键动作,按下或释放 
		KEY_Status_Idle,        // 按键状态
		KEY_Event_Null,         // 按键事件
		KEY_ReadPin             // 读引脚电平函数
};

4.3.3函数定义

局部函数:

//读取引脚的电平
static KEY_Action_TypeDef KEY\_ReadPin(void) 
{
  return (KEY_Action_TypeDef)GPIO\_ReadInputDataBit(GPIOA,GPIO_Pin_0);
}

// 获取按键动作,按下或释放,保存到结构体
static void KEY\_GetAction(void) 
{
	if(KeyCfg.KEY\_ReadPin\_Fcn() == KEY_PRESSED_LEVEL)
	{
		KeyCfg.KEY_Action = KEY_Action_Press;
	}
	else
	{
		KeyCfg.KEY_Action =  KEY_Action_Release;
	}
 
}

KEY_ReadPin:读取PA_0的电平状态。
KEY_GetAction:将读取到的电平状态转换为实际的按下或释放的动作。
状态处理函数
KEY_ReadStateMachine:编写,思路完全按照前文手绘的思路图像
首先读取按键的动作,再在switch case 里面匹配引脚的状态,case下用if判断按键动作或按下的时长,对状态、事件进行赋值。

void KEY\_ReadStateMachine(void)
{

	
    KEY\_GetAction();
	
	switch(KeyCfg.KEY_Status)
	{
		//状态:没有按键按下
		case KEY_Status_Idle:
			if(KeyCfg.KEY_Action == KEY_Action_Press)
			{
				KeyCfg.KEY_Status = KEY_Status_Debounce;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			else
			{
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			break;
			
		//状态:消抖
		case KEY_Status_Debounce:
			if(KeyCfg.KEY_Action == KEY_Action_Press)
			{
				KeyCfg.KEY_Status = KEY_Status_ConfirmPress;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			else
			{
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			break;	
			
		//状态:确认按下
		case KEY_Status_ConfirmPress:
			if( (KeyCfg.KEY_Action == KEY_Action_Press) && ( KeyCfg.KEY_Count >= KEY_LONG_PRESS_TIME))
			{
				printf("KEY\_Status\_ConfirmPressLong\r\n");
				KeyCfg.KEY_Count = 0;
				KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;
				KeyCfg.KEY_Event = KEY_Event_Null;
				
			}
			else if( (KeyCfg.KEY_Action == KEY_Action_Press) && (KeyCfg.KEY_Count < KEY_LONG_PRESS_TIME))
			{
				printf("继续按下 %d\r\n",KeyCfg.KEY_Count);
				KeyCfg.KEY_Count++;
				KeyCfg.KEY_Status = KEY_Status_ConfirmPress;
				KeyCfg.KEY_Event = KEY_Event_Null;
			}
			else
			{
				printf("突然gg,按短了 %d\r\n",KeyCfg.KEY_Count);
				KeyCfg.KEY_Count = 0;
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_SingleClick;

			}
			break;	

			
		//状态:确认长按
		case KEY_Status_ConfirmPressLong:
			printf("KEY\_Status\_ConfirmPressLong\r\n");
			if(KeyCfg.KEY_Action == KEY_Action_Press) 
			{   // 一直等待其放开
				printf("一直按着 KEY\_Status\_ConfirmPressLong\r\n");
				KeyCfg.KEY_Status = KEY_Status_ConfirmPressLong;
				KeyCfg.KEY_Event = KEY_Event_Null;
				KeyCfg.KEY_Count = 0;
			}
			else
			{
				KeyCfg.KEY_Status = KEY_Status_Idle;
				KeyCfg.KEY_Event = KEY_Event_LongPress;
				KeyCfg.KEY_Count = 0;
			}
			break;	
		default:
			break;
	}

}

后记:
这里面的printf用于打印查看。
为了读起来轻松些(对称美bushi),多写了一些没必要的赋值操作。实际应用时可以注释掉,以提高效率。

4.4 开源

复制粘贴这些代码就可以跑起来的,若仍想看Keil工程文件,这里提供了下载方式。

参考工程代码下载

获取方式
1.打包下载。积分够的用户支持一下。
2.GitHub仓库:github.com/nanshoui163…

5.实验验证

5.1定时器中断:

直接调用KEY_ReadStateMachine()函数即可,将读取到的事件保存到KeyCfg.KEY_Event变量。

extern KEY_Configure_TypeDef KeyCfg;
//定时器3中断服务程序
void TIM3\_IRQHandler(void)   //TIM3中断
{

	if (TIM\_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
	{
		TIM\_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 
            KEY\_ReadStateMachine();  //调用状态机
			
			if(KeyCfg.KEY_Event == KEY_Event_SingleClick)
			{
				printf("单击\r\n");//事件处理
			}
			if(KeyCfg.KEY_Event == KEY_Event_LongPress)
			{
				printf("长按\r\n");//事件处理
			}
		}


![img](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/98f82228fa1941b9ae2356bbc0b90b73~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1772461973&x-signature=wtTtJk7w%2FjIvbNd6yL8mSQnnzSs%3D)
![img](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/000dcec9f6c34d639731beefebeb30f6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1772461973&x-signature=uixfiGJ1qLFpXPJDVqs5nzMok64%3D)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://gitee.com/vip204888)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**