17届蓝桥杯嵌入式赛道开发板外设使用教程------按键、蜂鸣器、LCD屏幕,实现按键复杂状态检测
引言
上期视频我们介绍了开发板上最基础的外设------LED,接下来我会继续介绍单片机上的按键以及蜂鸣器外设,同时我会编写一个捕获按键状态的项目并通过LCD屏幕输出结果信息
硬件连接介绍
按键

通过观察原理图我们发现单片机上的四个按键与引脚之间的对应关系如下
| 按键编号 | 对应引脚 |
|---|---|
B1 |
PB0 |
B2 |
PB1 |
B3 |
PB2 |
B4 |
PA0 |
通过硬件连接图给出的信息我们总结出当按键按下时对应的引脚电平会变为低电平,当按键抬起的时候对应的引脚电平为高电平
蜂鸣器

由图可知蜂鸣器连接着单片机上的PB3引脚,且当对应引脚为低电平时蜂鸣器才会开始工作
LCD屏幕


由图可知LCD屏幕一共有30个引脚,我们观察之后可得引脚对应关系如下
| LCD编号 | 对应引脚 |
|---|---|
LCD_D0 |
PC0 |
LCD_D1 |
PC1 |
LCD_D2 |
PC2 |
LCD_D3 |
PC3 |
LCD_D4 |
PC4 |
LCD_D5 |
PC5 |
LCD_D6 |
PC6 |
LCD_D7 |
PC7 |
LCD_D8 |
PC8 |
LCD_D9 |
PC9 |
LCD_D10 |
PC10 |
LCD_D11 |
PC11 |
LCD_D12 |
PC12 |
LCD_D13 |
PC13 |
LCD_D14 |
PC14 |
LCD_D15 |
PC15 |
LCD_CS |
PB9 |
LCD_RS |
PB8 |
LCD_WR/SCL |
PB5 |
LCD_RD |
PA8 |
LCD_RESET |
RST |
乍一看这么多引脚是不是心里都在想:这么多引脚在CubeMX要配置多长时间才能使用?这你就多虑了,在实际的比赛过程中官方会给我们提供对应的LCD驱动项目的完整文件,我们只需要直接将其中的LCD驱动文件添加到我们对应的项目中即可,根本不要花时间和精力去进行LCD的配置操作
项目配置
基本配置
了解完底层的硬件连接之后我们就可以尝试去进行项目的配置操作了,由于开发板上搭载的按键以及蜂鸣器都较为常规,因此配置过程也较为简单
我们将蜂鸣器对应引脚设置为普通的推挽输出即可,由于在当前项目中需要利用状态机来检测按键的状态,因此我们需要将按键对应的引脚设置为下降沿中断触发模式

由于在本项目中我们需要定期的扫描按键的状态,因此为了减少对主逻辑的影响我们选择在定时器的中断回调函数中进行按键扫描操作,定时器配置如下

由于单片的主频为170Mhz,因此预分频器的值设置为170-1,这样定时器的计数频率就是一秒钟1,000,000次,随后我们选择将自动重装载器的值设置为10000-1,这样定时器就会每10ms产生一次中断

最后不要忘了检查一下按键中断是否成功开启,确认无误后就可以点击右上角的生成代码选项生成项目
LCD驱动导入
由于我们使用的是官方提供的驱动文件,因此我们只需要直接导入对应驱动文件到项目中即可导入流程如下
-
在
HardWare文件夹下创建LCD文件夹,并将对应驱动文件直接拷贝到该文件夹下
-
在
Keil软件中创建对应文件夹

-
添加对应的头文件收索路径

至此我们就成功的将LCD的驱动文件添加到我们项目中去了,在后续的代码编写过程中我们如果需要使用LCD显示屏,那么只需要直接引用相关头文件,然后直接调用相关的驱动函数即可
代码编写
项目配置完成后我们就可以尝试编写对应的外设驱动文件了
蜂鸣器
由于当前单片机上搭载的是有源蜂鸣器,因此只能操作其发声和关闭,对应的驱动文件编写也是最简单的
头文件
c
#ifndef __BUZZ_H__
#define __BUZZ_H__
#include "main.h"
// 开启蜂鸣器
void Open_buzz(void);
// 关闭蜂鸣器
void Close_buzz(void);
#endif
源文件
c
#include "buzz.h"
// 开启蜂鸣器
void Open_buzz(void)
{
//写入低电平即可开启蜂鸣器
HAL_GPIO_WritePin(Buzz_GPIO_Port, Buzz_Pin, GPIO_PIN_RESET);
}
// 关闭蜂鸣器
void Close_buzz(void)
{
//写入高电平关闭蜂鸣器
HAL_GPIO_WritePin(Buzz_GPIO_Port, Buzz_Pin, GPIO_PIN_SET);
}
按键
在当前项目中我们需要判断案件的三种不同状态:短按、长按和双击,因此我们需要用到状态机来实现状态转换以及状态检测。在之前的项目中我们就做过这个功能,这里再做一次只是为了演示一下开发板上按键和LCD屏幕的用法,大家也可以去那个文章中了解一下更多的细节和具体的实现思路
文章链接
头文件
首先我们需要存储按键的状态,根据前面的介绍我们了解到在当前项目中按键一共会有四种状态:空闲态、确认状态(用于按键消抖)、按下状态以及等待抬起状态(这里不理解的话建议看看上面推荐的文章)
c
typedef enum
{
KEY_STATE_IDLE = 0, //空闲状态
KEY_STATE_CONFIRM, //确认状态
KEY_STATE_PRESSED, //按下状态
KEY_STATE_WAIT_RELEASE, //等待抬起状态
} KeyState;
同时我们还需要存储对应的事件,以便当不同事件发生时我们去执行不同的操作,本项目中的事件也有四种:未发生事件、按键短按事件、按键长按事件以及按键双击事件,我们会根据状态的发生来更改按键的状态
c
typedef enum
{
KEY_EVENT_NONE = 0, //未发生事件
KEY_EVENT_SHORT, //短按事件
KEY_EVENT_LONG, //长按事件
KEY_EVENT_DOUBLE, //双击事件
} KeyEvent;
最后为了方便统一管理所有与按键有关的状态和事件,我们顶一个结构体来存储所有有关按键的变量,其中包含了按键状态枚举类型变量(state)、按键事件枚举类型变量(event)、记录按下持续时间变量(press_times)、记录两次按键按下间隔时间变量(gap_times)以及存储在一轮判断中按键按下的次数变量(click_count)
c
typedef struct
{
KeyState state;
uint16_t press_times; //记录当前这次按下持续了多少个周期
uint16_t gap_times; //第一次松开按键到第二次按键按键中间经历了多少个周期
uint8_t click_count; //记录在一轮操作中按下的次数
KeyEvent event;
} KeyEx;
随后就是一些循环参数以及外部函数的声明
c
//扫描周期
#define KEY_SCAN_PERIOD_MS 10
//长按时间判定,单按键按下时间超过1秒钟时说明按键处于长按状态
#define KEY_LONG_MS 1000
//双击时间间隔,当两次按键按下的时间间隔在300ms以内证明按键被双击了
#define KEY_DOUBLE_MS 300
//将时间转换为扫描的次数
#define KEY_LONG_TIMES (KEY_LONG_MS / KEY_SCAN_PERIOD_MS)
#define KEY_DOUBLE_TIMES (KEY_DOUBLE_MS / KEY_SCAN_PERIOD_MS)
// 按键事件标志位,在 Key.c 中定义,这里只做声明
extern volatile uint8_t key1_flag;
extern volatile uint8_t key2_flag;
extern volatile uint8_t key3_flag;
// 三个按键的扩展状态(在 Key.c 中定义),用于在 Bsp_loop 中读取事件
extern KeyEx key1;
extern KeyEx key2;
extern KeyEx key3;
// 单个按键扫描函数
void Key_scan_states_one(GPIO_TypeDef *port, uint16_t pin, KeyEx *k);
//全部按键扫描函数
void Key_scan_states_aLL(void);
源文件
在头文件中声明了对应的驱动函数之后就需要在源文件中实现对应的函数功能
首先初始化全局变量,设置好按键的初始状态
c
// 初始化按键为空闲状态,全局标志位只在本文件定义一次
volatile uint8_t key1_flag = 0;
volatile uint8_t key2_flag = 0;
volatile uint8_t key3_flag = 0;
// 三个按键的扩展状态(供其它模块读取事件)
KeyEx key1 = {KEY_STATE_IDLE};
KeyEx key2 = {KEY_STATE_IDLE};
KeyEx key3 = {KEY_STATE_IDLE};
随后实现单个按键的扫描函数
c
/**
* @brief 单个按键扫描函数
*
* @param port 按键对应的引脚组
* @param pin 按键对应的引脚编号
* @param k 按键的状态
*/
void Key_scan_states_one(GPIO_TypeDef *port, uint16_t pin, KeyEx *k)
{
//获取按键电平状态
GPIO_PinState level = HAL_GPIO_ReadPin(port, pin);
//根据按键的不同状态做出不同响应
switch (k->state)
{
//空闲状态
case KEY_STATE_IDLE:
//重置变量状态
k->event = KEY_EVENT_NONE;
k->press_times = 0;
k->gap_times = 0;
k->click_count = 0;
//电平发生变化,说明此时按键疑似按下,接下来需要进步验证
if (level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_CONFIRM;
}
break;
//接下来进入验证状态,在验证状态中需要检测按键是否真的已经按下
case KEY_STATE_CONFIRM:
//再次读取信息以确定按键状态
if(level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_PRESSED;
k->press_times = 0;
}
else
{
//此时说明前面的按下是抖动误触,此时将状态返回到空闲态等待下一次电平变化
k->state = KEY_STATE_IDLE;
}
break;
//当确定按键第一次按下后需要记录按下的持续时间以进行下一步操作
case KEY_STATE_PRESSED:
//记录按下状态的持续时间
k->press_times++;
//长按判定,达到指定时间则更改状态为长按状态
if (k->press_times == KEY_LONG_TIMES)
{
k->event = KEY_EVENT_LONG;
}
//状态变化说明按键抬起,表明此时不是长按状态
if (level == GPIO_PIN_SET)
{
//此时进入短按一次状态
if (k->press_times < KEY_LONG_TIMES)
{
k->click_count++;
//如果这是第一次短按则进入等待,尝试等待第二次按键按下
if (k->click_count == 1)
{
//状态更新为等待按键松开
k->state = KEY_STATE_WAIT_RELEASE;
//重置间隔时间
k->gap_times = 0;
}
else
{
//此时说明第二次短按也完成,则判定进入了双击状态
k->event = KEY_EVENT_DOUBLE;
//一轮判断完成,将状态恢复为空闲态
k->state = KEY_STATE_IDLE;
}
}
else
{
//此时已经触发过了长按,当不再短按时直接回到空闲态
k->state = KEY_STATE_IDLE;
}
}
break;
case KEY_STATE_WAIT_RELEASE:
//记录两次按键按下的时间间隔
k->gap_times++;
//按键按下时间超过了双击的间隔时间则进一步判断是否为长按状态
if (k->gap_times > KEY_DOUBLE_TIMES)
{
if (k->click_count == 1 && k->event != KEY_EVENT_LONG)
{
k->event = KEY_EVENT_SHORT;
}
k->state = KEY_STATE_IDLE;
}
else
{
if (level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_CONFIRM;
}
}
break;
default:
k->state = KEY_STATE_IDLE;
break;
}
}
实现了单个按键的扫描函数之后我们只需要调用三次按键扫描函数分别扫描三个按键的状态即可
c
/**
* @brief 扫描所有按键状态函数
*
*/
void Key_scan_states_aLL(void)
{
Key_scan_states_one(KEY1_GPIO_Port, KEY1_Pin, &key1);
Key_scan_states_one(KEY2_GPIO_Port, KEY2_Pin, &key2);
Key_scan_states_one(KEY3_GPIO_Port, KEY3_Pin, &key3);
}
最后我们需要重写TIM6的中断回调函数,并在中断回调函数中调用全键扫描函数即可
c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
// 判断中断信号源
if (htim->Instance == TIM6)
{
Key_scan_states_aLL();
}
}
LCD
由于LCD的驱动文件我们是直接使用官方提供的,因此在这里我们只需要了解一下官方给我们编写的驱动函数的用法即可,接下来只介绍常用的几个函数的用法及作用
-
初始化函数:用于初始化
LCD屏幕,每次使用LCD屏幕都必须调用当前函数cvoid LCD_Init(void); -
设置文本颜色函数:传参时传入对应的宏定义即可
cvoid LCD_SetTextColor(vu16 Color) // 颜色宏定义 #define White 0xFFFF #define Black 0x0000 #define Grey 0xF7DE #define Blue 0x001F #define Blue2 0x051F #define Red 0xF800 #define Magenta 0xF81F #define Green 0x07E0 #define Cyan 0x7FFF #define Yellow 0xFFE0 -
设置背景颜色:传参时同样传入对应的颜色宏定义即可
cvoid LCD_SetBackColor(vu16 Color) -
清除指定行内容:传入指定行数,随后便会将指定行数内容清空,传参时同样传入对应的行数宏定义即可
cvoid LCD_ClearLine(u8 Line) //行数宏定义 #define Line0 0 #define Line1 24 #define Line2 48 #define Line3 72 #define Line4 96 #define Line5 120 #define Line6 144 #define Line7 168 #define Line8 192 #define Line9 216 -
清除整个
LCD屏幕中的内容cvoid LCD_Clear(u16 Color) -
设置光标位置:传入目标位置的x和y坐标即可
cvoid LCD_SetCursor(u8 Xpos, u16 Ypos) -
在
LCD屏幕上绘制字符:传入绘制字符的位置信息以及字符内容cvoid LCD_DrawChar(u8 Xpos, u16 Ypos, uc16 *c) -
在
LCD上显示字符:传入行列信息即可,当前函数通过调用LCD_DrawChar函数实现cvoid LCD_DisplayChar(u8 Line, u16 Column, u8 Ascii) -
在
LCD上显示字符串:直接传入对应的字符串即可在LCD上显示对应信息,该函数底层通过调用LCD_DisplayChar函数实现cvoid LCD_DisplayStringLine(u8 Line, u8 *ptr)
至此有关LCD的驱动函数我们就介绍完毕了,接下来我们如果需要使用LCD屏幕,直接调用对应的驱动函数即可
函数调用
前面完成了所有驱动文件的编写以及介绍,接下来我就展示一下如何调用上述驱动函数实现按键检测的完整代码
c
void Bsp_loop(void)
{
LED_TurnOffALL();
// 处理按键1的事件(控制LED1)
KeyEvent e1 = key1.event;
key1.event = KEY_EVENT_NONE;
switch (e1)
{
case KEY_EVENT_SHORT: // 单击:切换LED1的亮灭
LED_Toggle(LED1);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key1 Pressed!");
break;
case KEY_EVENT_LONG: // 长按:熄灭LED1
LED_TurnOff(LED1);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key1 Long Pressed!");
break;
case KEY_EVENT_DOUBLE: // 双击:点亮LED1
LED_TurnOn(LED1);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key1 Double Pressed!");
break;
default:
break;
}
// 处理按键2的事件(控制LED2)
KeyEvent e2 = key2.event;
key2.event = KEY_EVENT_NONE;
switch (e2)
{
case KEY_EVENT_SHORT: // 单击:切换LED2的亮灭
LED_Toggle(LED2);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key2 Pressed!");
break;
case KEY_EVENT_LONG: // 长按:熄灭LED2
LED_TurnOff(LED2);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key2 Long Pressed!");
break;
case KEY_EVENT_DOUBLE: // 双击:点亮LED2
LED_TurnOn(LED2);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key2 Double Pressed!");
break;
default:
break;
}
// 处理按键3的事件(控制LED3)
KeyEvent e3 = key3.event;
key3.event = KEY_EVENT_NONE;
switch (e3)
{
case KEY_EVENT_SHORT: // 单击:切换LED3的亮灭
LED_Toggle(LED3);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key3 Pressed!");
break;
case KEY_EVENT_LONG: // 长按:熄灭LED3
LED_TurnOff(LED3);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key3 Long Pressed!");
break;
case KEY_EVENT_DOUBLE: // 双击:点亮LED3
LED_TurnOn(LED3);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key3 Double Pressed!");
break;
default:
break;
}
}
将上述函数直接在主循环中调用就可以实现对应的按键状态判断功能
结果展示
代码烧录后我们按下按键,LCD显示屏上就会以文字的形式显示按键的状态
总结
通过当前项目你应该就对开发板上对应的按键、蜂鸣器以及LCD屏幕的使用有了大体的了解,但是项目中的按键检测逻辑较为繁琐还是建议大家去看一下另一篇介绍思路的文章
文章链接