基于STM32F103C8T6的多按键检测 | 有限状态机短按、长按识别 | 标准库函数实现方法

前言

制作航模遥控器需要用到多按键检测,实现过程中主要参考了以下两篇文章,尤其是第一篇收获最大,作者的代码思想很好,但文中部分代码有误,实际运行时检测到的IO电平是错误的,花费了一天时间才调通,简单记录一下。

https://blog.csdn.net/qq_42679566/article/details/105892105

https://www.cnblogs.com/ZzJan/p/11334869.html

1.电路连接

使用STM32F103C8T6蓝色板,按键采用共阴极连接。

6个按键:
                CH1Left     接PB5
                CH1Right   接PB4
                CH2Up      接PA15
                CH2Down  接PB3
                CH4Left     接PA12
                CH4Right  接PA11

串口USB-TTL接法:    
                GND       电源地
                3V3         接3.3V
                TXD        接PB7
                RXD       接PB6
        ST-LINK V2接法:
                GND       电源地
                3V3         接3.3V
                SWCLK   接DCLK
                SWDIO    接DIO

基于STM32F103C8T6的多按键检测 | 有限状态机短按、长按识别 | 标准库函数实现方法_第1张图片

2.程序实现

key.h - 主要定义结构体和函数预定义

#ifndef __KEY_H
#define __KEY_H	 
#include "stm32f10x.h"
#include "stm32f10x_gpio.h" 
typedef struct // 构造按键初始化类
{
	GPIOMode_TypeDef GPIO_Mode; // 初始化按键模式
	GPIO_TypeDef* GPIOx; // 初始化按键口
	uint16_t GPIO_Pin_x; // 初始化按键引脚好
	uint32_t RCC_APB2Periph_GPIOx; // 初始化时钟
}Key_Init;

typedef enum _KEY_STATUS_LIST // 按键状态
{
	KEY_NULL = 0x00, // 无动作
	KEY_SURE = 0x01, // 确认状态
	KEY_UP   = 0x02, // 按键抬起
	KEY_DOWN = 0x04, // 按键按下
	KEY_LONG = 0x08, // 长按
}KEY_STATUS_LIST;

typedef struct _KEY_COMPONENTS // 状态机类
{
    FunctionalState KEY_SHIELD; //按键屏蔽,DISABLE(0):屏蔽,ENABLE(1):不屏蔽
	uint8_t KEY_COUNT;        	//按键长按计数
    BitAction KEY_LEVEL;        //最终按键状态,按下Bit_SET(1),抬起Bit_RESET(0)
    BitAction KEY_DOWN_LEVEL;   //按下时,按键IO实际的电平
    KEY_STATUS_LIST KEY_STATUS;       //按键状态
    KEY_STATUS_LIST KEY_EVENT;        //按键事件
    BitAction (*READ_PIN)(Key_Init Key);//读IO电平函数
}KEY_COMPONENTS;


typedef struct // 按键类
{
	Key_Init Key; // 继承初始化父类
	KEY_COMPONENTS Status; // 继承状态机父类
}Key_Config;


typedef enum // 按键注册表
{
	CH1Left,
	CH1Right,
	CH2Up,
	CH2Down,
	CH4Left,
	CH4Right,// 用户添加的按钮名称
	KEY_NUM, // 必须要有的记录按钮数量,必须在最后
}KEY_LIST;


void KEY_Init(void);//IO初始化
void Creat_Key(Key_Init* Init); // 初始化按钮函数
void ReadKeyStatus(void); // 状态机函数
void TIM3_Init(u16 arr,u16 psc);
#endif

 原文中Key用的是指针,结果导致读电平函数GPIO_ReadInputDataBit()寻址错误,才使得读出的电平有误。

typedef struct // 按键类
{
	Key_Init *Key; // 继承初始化父类
	KEY_COMPONENTS Status; // 继承状态机父类
}Key_Config;

key.c - TIM3定时器初始化,定时检测按键状态;有限状态机实现 

#include "stm32f10x.h"
#include "key.h"
#include "sys.h" 
#include "delay.h"
#include "usart.h"

//参考链接https://blog.csdn.net/qq_42679566/article/details/105892105,原文错误已修正

Key_Config Key_Buf[KEY_NUM];	// 创建按键数组
#define KEY_LONG_DOWN_DELAY 30 	// 设置30个TIM3定时器中断=600ms算长按	
#define DBGMCU_CR  (*((volatile u32 *)0xE0042004))
	
/*通用定时器3中断初始化,使用TIM3控制按键定时检测
  时钟选择为APB1的2倍,而APB1为36M
* 参数:arr:自动重装值。
		psc:时钟预分频数
*/
void TIM3_Init(u16 arr,u16 psc)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); 		//时钟使能

	TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载寄存器周期的值
	TIM_TimeBaseInitStructure.TIM_Prescaler=psc;  //预分频值
	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 向上计数
	TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分割为0,仍然使用72MHz
	TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);//允许更新中断
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
	
	
	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
	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);
}

void TIM3_IRQHandler(void)   //TIM3中断
{
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
	{
		// 中断处理代码
		ReadKeyStatus();  //调用状态机
		u8 i,status;
		for(i = 0;i < KEY_NUM;i++)
    	{
			status = Key_Buf[i].Status.KEY_EVENT;
			//if(status!=KEY_NULL) printf("%d,%d\n",i,status);//事件处理
			if(status==KEY_DOWN) printf("%d短按\n",i);
			if(status==KEY_LONG) printf("%d长按\n",i);
		}
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);  //清除TIMx更新中断标志 
	}
}

//按键初始化函数
void KEY_Init(void) //IO初始化
{ 
	Key_Init KeyInit[KEY_NUM]=
	{ 
		{GPIO_Mode_IPU, GPIOB, GPIO_Pin_5, RCC_APB2Periph_GPIOB}, 	// 初始化按键CH1Left
		{GPIO_Mode_IPU, GPIOB, GPIO_Pin_4, RCC_APB2Periph_GPIOB}, 	// 初始化按键CH1Right
		{GPIO_Mode_IPU, GPIOA, GPIO_Pin_15, RCC_APB2Periph_GPIOA}, 	// 初始化按键CH2Up
		{GPIO_Mode_IPU, GPIOB, GPIO_Pin_3, RCC_APB2Periph_GPIOB}, 	// 初始化按键CH2Down
		{GPIO_Mode_IPU, GPIOA, GPIO_Pin_12, RCC_APB2Periph_GPIOA}, 	// 初始化按键CH4Left
		{GPIO_Mode_IPU, GPIOA, GPIO_Pin_11, RCC_APB2Periph_GPIOA}, 	// 初始化按键CH4Right
	};
	Creat_Key(KeyInit); // 调用按键初始化函数
	
	//STM32没有彻底释放PB3作为普通IO口使用,切换到SW调试可释放PB3、PB4、PA15
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
	DBGMCU_CR &=0xFFFFFFDF;  //如果没有这段代码,PB3就会一直是低电平
}

static BitAction KEY_ReadPin(Key_Init Key) //按键读取函数
{
  return (BitAction)GPIO_ReadInputDataBit(Key.GPIOx,Key.GPIO_Pin_x);
}

void Creat_Key(Key_Init* Init)
{
	uint8_t i; 
	GPIO_InitTypeDef  GPIO_InitStructure[KEY_NUM];
  	for(i = 0;i < KEY_NUM;i++)
	{
		Key_Buf[i].Key = Init[i]; // 按钮对象的初始化属性赋值
		RCC_APB2PeriphClockCmd(Key_Buf[i].Key.RCC_APB2Periph_GPIOx, ENABLE);//使能相应时钟
		GPIO_InitStructure[i].GPIO_Pin = Key_Buf[i].Key.GPIO_Pin_x;	//设定引脚			
		GPIO_InitStructure[i].GPIO_Mode = Key_Buf[i].Key.GPIO_Mode; 	//设定模式		
		GPIO_Init(Key_Buf[i].Key.GPIOx, &GPIO_InitStructure[i]);       //初始化引脚
		// 初始化按钮对象的状态机属性
		Key_Buf[i].Status.KEY_SHIELD = ENABLE;
		Key_Buf[i].Status.KEY_COUNT = 0;
		Key_Buf[i].Status.KEY_LEVEL = Bit_RESET;
		if(Key_Buf[i].Key.GPIO_Mode == GPIO_Mode_IPU) // 根据模式进行赋值
			Key_Buf[i].Status.KEY_DOWN_LEVEL = Bit_RESET;
		else
			Key_Buf[i].Status.KEY_DOWN_LEVEL = Bit_SET;
		Key_Buf[i].Status.KEY_STATUS = KEY_NULL;
		Key_Buf[i].Status.KEY_EVENT = KEY_NULL;
		Key_Buf[i].Status.READ_PIN = KEY_ReadPin;	//赋值按键读取函数
	}
}

static void Get_Key_Level(void) // 根据实际按下按钮的电平去把它换算成虚拟的结果
{
    uint8_t i;
    
    for(i = 0;i < KEY_NUM;i++)
    {
        if(Key_Buf[i].Status.KEY_SHIELD == DISABLE)
            continue;
        if(Key_Buf[i].Status.READ_PIN(Key_Buf[i].Key) == Key_Buf[i].Status.KEY_DOWN_LEVEL)
            Key_Buf[i].Status.KEY_LEVEL = Bit_SET;
        else
            Key_Buf[i].Status.KEY_LEVEL = Bit_RESET;
    }
}

void ReadKeyStatus(void)
{
    uint8_t i;
	
    Get_Key_Level();
	
    for(i = 0;i < KEY_NUM;i++)
    {
        switch(Key_Buf[i].Status.KEY_STATUS)
        {
            //状态0:没有按键按下
            case KEY_NULL:
                if(Key_Buf[i].Status.KEY_LEVEL == Bit_SET)//有按键按下
                {
                    Key_Buf[i].Status.KEY_STATUS = KEY_SURE;//转入状态1
					Key_Buf[i].Status.KEY_EVENT = KEY_NULL;//空事件
                }
                else
                {
                    Key_Buf[i].Status.KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //状态1:按键按下确认
            case KEY_SURE:
                if(Key_Buf[i].Status.KEY_LEVEL == Bit_SET)//确认和上次相同
                {
                    Key_Buf[i].Status.KEY_STATUS = KEY_DOWN;//转入状态2
					Key_Buf[i].Status.KEY_EVENT = KEY_DOWN;//按下事件
                    Key_Buf[i].Status.KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].Status.KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].Status.KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //状态2:按键按下
            case KEY_DOWN:
                if(Key_Buf[i].Status.KEY_LEVEL != Bit_SET)//按键释放,端口高电平
                {
                    Key_Buf[i].Status.KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].Status.KEY_EVENT = KEY_UP;//松开事件
                }
                else if((Key_Buf[i].Status.KEY_LEVEL == Bit_SET)
					&& (++Key_Buf[i].Status.KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].Status.KEY_STATUS = KEY_LONG;//转入状态3
                    Key_Buf[i].Status.KEY_EVENT = KEY_LONG;//长按事件
					Key_Buf[i].Status.KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].Status.KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //状态3:按键连续按下
            case KEY_LONG:
                if(Key_Buf[i].Status.KEY_LEVEL != Bit_SET)//按键释放,端口高电平
                {
                    Key_Buf[i].Status.KEY_STATUS = KEY_NULL;//转入状态0
                    Key_Buf[i].Status.KEY_EVENT = KEY_UP;//松开事件
                }
                else if((Key_Buf[i].Status.KEY_LEVEL == Bit_SET) 
                && (++Key_Buf[i].Status.KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放
                {
                    Key_Buf[i].Status.KEY_EVENT = KEY_LONG;//长按事件
                    Key_Buf[i].Status.KEY_COUNT = 0;//计数器清零
                }
                else
                {
                    Key_Buf[i].Status.KEY_EVENT = KEY_NULL;//空事件
                }
                break;
			default:
				break;
        }
	}
}


main.c - 主函数调用TIM3初始化 

#include "delay.h"
#include "usart.h"
#include "stm32f10x.h"
#include "key.h"
int main()
{
    delay_init();//初始化延时函数
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2,2位抢占优先级和2位子优先级
    usart_init(115200);//初始化串口1,波特率为115200
    TIM3_Init(19999,71);//1MHz,每20ms检测按键一次;
    KEY_Init();		//KEY初始化
    while(1){
        delay_ms(1);
    }
}

3.实现效果

基于STM32F103C8T6的多按键检测 | 有限状态机短按、长按识别 | 标准库函数实现方法_第2张图片

你可能感兴趣的:(STM32,stm32,单片机,按键检测,有限状态机)