【蓝桥杯嵌入式】状态机按键

        按键作为人机交互中重要的一环,是必需掌握的部分。比赛板子上一共有4个按键,分别连接到不同的IO口上(B1----PB0,B2----PB1,B3----PB2,B4----PA0)。如下图所示,按键在松开(空闲)状态时,IO口检测到电平为高电平,按键按下时,IO口检测到的为低电平。我们把相应IO口设置为输入模式,那么捕捉到低电平,我们就可以认为按键被按下了。由于物理按键存在一些抖动、振动等不确定因素,所以需要添加软件消抖(在检测到按下信号后,隔10~20ms后再次确认状态,排除干扰)。我们选择结合定时器轮询,同时使用状态机检测方法(将在下面的编程部分进行介绍),这样既可以实现消抖、多按键检测、单次起效,还可以拓展实现长短按、双按等等。

一、STM32Cube 按键的设置

复制上次的2_TIM工程文件夹,并命名为“3_KEY”,打开TEST.ioc。

Cube中将PB0,PB1,PB2,PA0设置为输入模式,上拉(Pull-up),并进行命名。按键外接上拉电阻,在空闲状态下,IO口被拉至高电平,所以选择上拉模式。

轮询用的定时器我们沿用上次设置的总控定时器TIM4,这样基本设置就好了,按“GENERATE CODE”更新工程。

二、按键(状态机)程序的实现

用Keil 5打开TEST.uvprojx,再打开USER中用于中断的interrupt.c。

我们计划通过按键来控制按钮,各按键效果:

B1:亮LD1、LD2

B2:亮LD3、LD4

B3:LD5、LD6状态翻转

B4:熄灭LD1~LD6

其他LED灯效果:LD7始终熄灭,LD8每0.5s翻转一次状态。

1、基本的状态机编程

首先,我们为状态机定义三个状态:空闲状态,未按下(IDLE)、按下待执行相应功能(PRESSED_READY)、按下功能已执行等待松开按键回到空闲状态(PRESSED_FINISH)。

我们定义了一个枚举类型变量 ButtonState,来记录这些状态。

typedef enum 
{
	IDLE,						//未按下,空闲状态
	PRESSED_READY,	//按下,待执行相应功能
	PRESSED_FINISH,	//按下,功能已执行,等待松开按键回到空闲状态
	//PRESSED_COUNT		//按下,处于计时状态(用于长短按)
}ButtonState;			//状态机状态

枚举类型的用法:

enum 枚举类型名 {
    枚举常量1,
    枚举常量2,
    枚举常量3,
    ...
};

或者
typedef enum {
    枚举常量1,
    枚举常量2,
    枚举常量3,
    ...
} 枚举类型名;

 由于按键所对应的IO口不是连续(PB0,PB1,PB2,PA0),我们需要分开读取IO口电平,同时为了代码简洁,我们用结构体记录IO口信息,for循环进行轮流查询。

typedef struct
{
	GPIO_TypeDef *port;
	uint16_t pin;
}key_GPIO;						//按键对应IO口信息
key_GPIO key[4] = {	{GPIOB,GPIO_PIN_0},
					{GPIOB,GPIO_PIN_1},
					{GPIOB,GPIO_PIN_2},
					{GPIOA,GPIO_PIN_0}};

unsigned int key_value = 0x0000;

for(i = 0 ; i<4 ; i++)
{
    if(HAL_GPIO_ReadPin(key[i].port,key[i].pin) == GPIO_PIN_RESET)
    {
		key_value = key_value | (0X1000 >> (i*4));    //将记录到的信息储存在一个char型变量key_value中,后续用于判断按键状况
    }
}

结构体(struct)是C语言中一种用户自定义的数据类型,它可以存储不同类型的数据成员,并将这些数据成员组合在一起,形成一个逻辑上相关的数据单元。结构体允许程序员定义一种新的复合数据类型,使得能够在单个变量中存储多个相关的数据。

struct 结构体名 {
    数据类型 成员1;
    数据类型 成员2;
    ...
};

或
typedef struct {
    数据类型 成员1;
    数据类型 成员2;
    ...
}结构体名;
状态机的基本工作流程:

这里由于每次检测都在定时器的中断中,也即周期性检测,顺便实现了消抖功能(检测到按键按下,隔一个时间周期(10~20ms),进入“PRESSED_READY”状态再次检测,确认有按下,判断情况并执行相关功能,若无按下,说明是抖动,误测,返回“IDLE”状态)。

状态机实现代码:

// 声明变量 key_value 和 i,分别用于存储按键值和循环计数
unsigned int key_value = 0x0000;
unsigned int i = 0;

// 根据按钮状态进行不同的操作
switch(key_state)
{
	case IDLE:
		// 检查每个按键是否被按下,若有按下则更新 key_value
		for(i = 0 ; i < 4 ; i++)
		{
			if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
			{
				key_value = key_value | (0X1000 >> (i * 4)); // 更新按键值
			}
		}	
		// 若有按键被按下,则将状态切换为 PRESSED_READY
		if(key_value != 0x0000)
		{
			key_state = PRESSED_READY;
		}
	break;
	
	case PRESSED_READY:
		// 再次检查每个按键是否被按下,若有按下则更新 key_value
		for(i = 0 ; i < 4 ; i++)
		{
			if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
			{
				key_value = key_value | (0X1000 >> (i * 4)); // 更新按键值
			}
		}	
		// 若没有按键被按下,则将状态切换为 IDLE;否则根据按键值进行相应操作
		if(key_value == 0x0000)
		{
			key_state = IDLE;
		}
		else 
		{
			switch(key_value)
			{
				case 0x1000:
					// 按键B1被按下
					led_addr = led_addr | 0x03; // 更新LED位置
					led_disp(led_addr); // 控制LED显示
					key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
				break;
				
				case 0x0100:
					// 按键B2被按下
					led_addr = led_addr | 0x0C; // 更新LED位置
					led_disp(led_addr); // 控制LED显示
					key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
				break;
				
				case 0x0010:
					// 按键B3被按下
					led_addr = led_addr ^ 0x30; // 更新LED位置
					led_disp(led_addr); // 控制LED显示
					key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
				break;
				
				case 0x0001:
					// 按键B4被按下
					led_addr = led_addr & 0x80; // 更新LED位置
					led_disp(led_addr); // 控制LED显示
					key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
				break;
				
				default:
					key_state = IDLE; // 若按键值无效,则切换状态为 IDLE
				break;
			}
		}
	break;
	
	case PRESSED_FINISH:
		// 检查每个按键是否仍然被按下,若没有则将状态切换为 IDLE
		for(i = 0 ; i < 4 ; i++)
		{
			if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
			{
				key_value = key_value | (0X1000 >> (i * 4)); // 更新按键值
			}
		}	
		if(key_value == 0x0000)
		{
			key_state = IDLE; // 若没有按键被按下,则切换状态为 IDLE
		}
	break;
}	

点亮某几个灯:

led_addr = 0x00 = 0000 0000B,表示灯全灭

led_addr = 0x03 = 0000 0011B,表示右两颗灯亮起

0x00 | 0x03 = 0x03,即或( | )操作可以点亮灯

0x03 & 0x00 = 0x00,与( & )操作可以熄灭灯

而异或( ^ )则可以实现翻转

1^0 = 1        1^1 = 0

0^0 = 0        0^1 = 1

0x03 ^ 0x03 = 0x00

0x00 ^ 0x03 = 0x03

interrupt中完整代码如下:

#include "interrupt.h"
#include "led.h"

typedef enum 
{
	IDLE,						//未按下,空闲状态
	PRESSED_READY,	//按下,待执行相应功能
	PRESSED_FINISH,	//按下,功能已执行,等待松开按键回到空闲状态
	//PRESSED_COUNT		//按下,处于计时状态(用于长短按)
}ButtonState;			//状态机状态

typedef struct
{
	GPIO_TypeDef *port;
	uint16_t pin;
}key_GPIO;						//按键对应IO口信息
key_GPIO key[4] = {	{GPIOB,GPIO_PIN_0},
										{GPIOB,GPIO_PIN_1},
										{GPIOB,GPIO_PIN_2},
										{GPIOA,GPIO_PIN_0}};

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{//10ms
    // 声明静态变量用于计时和记录LED位置
    static unsigned int tim4_count = 0; 		// 用于TIM4计数,实现分时复用
    static unsigned char led_addr = 0x00;  // 用于记录亮灯的位置
    static ButtonState key_state = IDLE;
    // 判断中断来源是否是TIM4
    if(htim->Instance == TIM4)
    {

			// 声明变量 key_value 和 i,分别用于存储按键值和循环计数
			unsigned int key_value = 0x0000;
			unsigned int i = 0;

			// 根据按钮状态进行不同的操作
			switch(key_state)
			{
				case IDLE:
					// 检查每个按键是否被按下,若有按下则更新 key_value
					for(i = 0 ; i < 4 ; i++)
					{
						if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
						{
							key_value = key_value | (0X1000 >> (i * 4)); // 更新按键值
						}
					}	
					// 若有按键被按下,则将状态切换为 PRESSED_READY
					if(key_value != 0x0000)
					{
						key_state = PRESSED_READY;
					}
				break;
				
				case PRESSED_READY:
					// 再次检查每个按键是否被按下,若有按下则更新 key_value
					for(i = 0 ; i < 4 ; i++)
					{
						if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
						{
							key_value = key_value | (0X1000 >> (i * 4)); // 更新按键值
						}
					}	
					// 若没有按键被按下,则将状态切换为 IDLE;否则根据按键值进行相应操作
					if(key_value == 0x0000)
					{
						key_state = IDLE;
					}
					else 
					{
						switch(key_value)
						{
							case 0x1000:
								// 按键B1被按下
								led_addr = led_addr | 0x03; // 更新LED位置
								led_disp(led_addr); // 控制LED显示
								key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
							break;
							
							case 0x0100:
								// 按键B2被按下
								led_addr = led_addr | 0x0C; // 更新LED位置
								led_disp(led_addr); // 控制LED显示
								key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
							break;
							
							case 0x0010:
								// 按键B3被按下
								led_addr = led_addr ^ 0x30; // 更新LED位置
								led_disp(led_addr); // 控制LED显示
								key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
							break;
							
							case 0x0001:
								// 按键B4被按下
								led_addr = led_addr & 0x80; // 更新LED位置
								led_disp(led_addr); // 控制LED显示
								key_state = PRESSED_FINISH; // 切换状态为 PRESSED_FINISH
							break;
							
							default:
								key_state = IDLE; // 若按键值无效,则切换状态为 IDLE
							break;
						}
					}
				break;
				
				case PRESSED_FINISH:
					// 检查每个按键是否仍然被按下,若没有则将状态切换为 IDLE
					for(i = 0 ; i < 4 ; i++)
					{
						if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
						{
							key_value = key_value | (0X1000 >> (i * 4)); // 更新按键值
						}
					}	
					if(key_value == 0x0000)
					{
						key_state = IDLE; // 若没有按键被按下,则切换状态为 IDLE
					}
				break;
			}	

				
			tim4_count++; // TIM4计数递增	
			// 每500ms进行一次LD8的翻转
			if(tim4_count == 50)
			{
					tim4_count = 0; // 重置计数变量,很重要
					led_addr = led_addr ^ 0x80; // 异或操作
					led_disp(led_addr); // LED显示函数
			}
    }
}

三、效果演示

状态机按键控制LED

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值