按键作为人机交互中重要的一环,是必需掌握的部分。比赛板子上一共有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