17届蓝桥杯嵌入式赛道开发板外设使用教程——按键、蜂鸣器、LCD屏幕

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屏幕都必须调用当前函数

    c 复制代码
    void LCD_Init(void);
  • 设置文本颜色函数:传参时传入对应的宏定义即可

    c 复制代码
    void 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    
  • 设置背景颜色:传参时同样传入对应的颜色宏定义即可

    c 复制代码
    void LCD_SetBackColor(vu16 Color)
  • 清除指定行内容:传入指定行数,随后便会将指定行数内容清空,传参时同样传入对应的行数宏定义即可

    c 复制代码
    void 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屏幕中的内容

    c 复制代码
    void LCD_Clear(u16 Color)
  • 设置光标位置:传入目标位置的x和y坐标即可

    c 复制代码
    void LCD_SetCursor(u8 Xpos, u16 Ypos)
  • LCD屏幕上绘制字符:传入绘制字符的位置信息以及字符内容

    c 复制代码
    void LCD_DrawChar(u8 Xpos, u16 Ypos, uc16 *c)
  • LCD上显示字符:传入行列信息即可,当前函数通过调用LCD_DrawChar函数实现

    c 复制代码
    void LCD_DisplayChar(u8 Line, u16 Column, u8 Ascii)
  • LCD上显示字符串:直接传入对应的字符串即可在LCD上显示对应信息,该函数底层通过调用LCD_DisplayChar函数实现

    c 复制代码
    void 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屏幕的使用有了大体的了解,但是项目中的按键检测逻辑较为繁琐还是建议大家去看一下另一篇介绍思路的文章
文章链接

相关推荐
逆境不可逃2 小时前
LeetCode 热题 100 之 763.划分字母区间
算法·leetcode·职场和发展
wuqingshun3141592 小时前
蓝桥杯 无影之谜
算法·职场和发展·蓝桥杯
逆境不可逃2 小时前
【从零入门23种设计模式17】行为型之中介者模式
java·leetcode·microsoft·设计模式·职场和发展·中介者模式
爬山算法3 小时前
MongoDB(32)如何查看集合中的索引?
数据库·mongodb
筱昕~呀3 小时前
冲刺蓝桥杯-BFS板块(第八天)
职场和发展·蓝桥杯·宽度优先
仰泳的熊猫3 小时前
题目2086:蓝桥杯算法提高VIP-最长公共子序列
数据结构·c++·算法·蓝桥杯·动态规划
数据知道11 小时前
MongoDB复制集架构原理:Primary、Secondary 与 Arbiter 的角色分工
数据库·mongodb·架构
修行者Java11 小时前
(七)从 “非结构化数据难存储” 到 “MongoDB 灵活赋能”——MongoDB 实战进阶指南
数据库·mongodb
2301_8008951011 小时前
BFS--备战蓝桥杯版h
算法·蓝桥杯·宽度优先