3.蓝桥杯嵌入式速通之LCD显示,按键控制&LED指示

前言

LCD显示,按键控制和LED指示是历届蓝桥杯嵌入式程序设计的必考点.
其中,LCD显示的考点为:

  • 整行显示(刷新时预防重影,整行高亮)
  • 局部高亮
  • 屏幕翻转

按键控制的考点为:

  • 单击(短按)
  • 长按
  • 双击

LED指示的考点为:

  • 局部控灯
  • LED闪烁

一.LCD显示

1.CubeMX中的配置

在这里插入图片描述
根据LCD模块在板子上的原理图,本来应该将对应引脚设置为推挽输出模式,但是由于在官方提供的lcd.h中已经完成了相关的初始化配置操作,因此在CubeMx中无需再配置相应引脚.

2.LCD显示的准备工作

导入lcd的头文件

在主循环前:
1.设置全局缓存区用以存储需要显示的内容.
2.使LCD进入就绪状态.
3.在主循环前以背景颜色清一次屏.
4.清屏以后,设置初始显示时字符(串)的背景色和前景色.

在主循环内:
对于整个lcd进程务必设置减速函数以防止lcd刷新过快导致评审系统检测出错.
#include "lcd.h"

char text[99];//存储用于lcd显示的内容
volatile uint32_t lcd_tick = 0;//防止编译器优化的lcd计时器

LCD_Init();//使lcd进入就绪状态
LCD_Clear(Black);//设置背景颜色为黑色
LCD_SetBackColor(Black);//设置字符(串)显示的背景色
LCD_SetTextColor(White);//设置字符(串)显示的前景色

void lcd_proc()
{
	if(uwTick-lcd_tick<100) return; //每100毫秒刷新一次lcd
	else lcd_tick = uwTick; 
}

3.LCD整行显示的算法逻辑

在主循环内:
1.将需显示的内容(在有效内容后敲空格以防刷新时产生重影)存入缓存区.
2.如需整行高亮,则在显示整行内容前重新设置背景色和前景色,
否则直接显示.
3.若重设背景色和前景色,则在显示后恢复原先的背景色和前景色.
float PA4 = 0;    //经PA4端口转换的电压
float PA5 = 0;    //经PA5端口转换的电压

//高亮显示
//高亮前刷新背景色和前景色
LCD_SetBackColor(Yellow);
LCD_SetTextColor(Magenta);
sprintf(text,"     PA4=%.2f          ",PA4);//存储显示内容
LCD_DisplayStringLine(Line3,(u8*)text);  //整行显示
//恢复原先的背景色和前景色 
LCD_SetBackColor(Black);
LCD_SetTextColor(White); 

//正常显示
sprintf(text,"     PA5=%.2f  ",PA5);//存储显示内容
LCD_DisplayStringLine(Line4,(u8*)text); //整行显示

4.LCD局部高亮的算法逻辑

在主循环内:
1.重新设置前景色和背景色.
2.在对应位置显示字符.
注:显示屏共10行20列,对应分辨率为240×320,即对于每一行,每列
被分配了320/20=16个像素点,因为填入320时,数据显示在第一列,
因此320-(x-1)*16能让字符精准地落入该行的x列中显示.
3.在显示后恢复原先的背景色和前景色.

注:若题目未强调需要采用局部高亮,则尽量不使用以防评审系统检测出错.
uint8_t a1= 1;
uint8_t a2= 2;
//在第四行局部高亮显示1.2
//高亮前刷新背景色和前景色
LCD_SetBackColor(Yellow);
LCD_SetTextColor(Magenta);
//转为ASCII码显示字符
LCD_DisplayChar(Line3,320-(9*16),a1+'0');//第3行第10列
LCD_DisplayChar(Line3,320-(10*16),'.');//第3行第11列
LCD_DisplayChar(Line3,320-(11*16),a2+'0');//第3行第12列
//恢复原先的背景色和前景色 
LCD_SetBackColor(Black);
LCD_SetTextColor(White);

5.LCD屏幕翻转的寄存器逻辑

由于这是寄存器逻辑,所以得查LCD的手册.
手册中谈到LCD的驱动电路由720输出的源极驱动器和320输出的栅极驱动器组成。当第720位的数据被输入的时候,显示模式的数据就被锁住,然后控制源极驱动器产生驱动波形,以此驱动电路工作.源极驱动器的720个源极输出移动方向由SS比特位设置,栅极驱动器的320个栅极输出移动方向由GS比特位设置.
在这里插入图片描述

手册中R01h寄存器的D8位是SS,其为0则扫描顺序从S1到S720,是1则扫描方向反转.
在这里插入图片描述
手册中R60h寄存器的D15位是GS,其为0则扫描方向从G1到G320,是1则扫描方向反转.
在这里插入图片描述
在这里插入图片描述
明白了寄存器逻辑,我们进入lcd.c中找到写寄存器的函数,找到与R01h(R1)和R60h(R96)相关的内容.
由于SS是R1的D8,也就是对应二进制位中的第9位,原先是0,现在为实现翻转显示,应将其置1,所以应将原先的0x0000改为0x0100.
在这里插入图片描述
由于GS是R96的D15,也就是对应二进制位中的第16位,原先是0,现在为实现翻转显示,应将其置1,所以应将原先的0x2700改为0xA700.
在这里插入图片描述

bool lcd_rvs_en = 0;    //lcd翻转使能位

void lcd_rvs(void);     //lcd翻转函数 

void lcd_rvs()
{
  if(lcd_rvs_en){
    lcd_rvs_en = 0;                //清零翻转使能位
    LCD_Clear(Black);           //翻转前清屏消重影
    static bool lcd_rvs_time = 0; //翻转计数器,每翻转两次清零
    if(!lcd_rvs_time){            //奇数次从正往反翻转
      //反向显示的寄存器配置
      LCD_WriteReg(R1,0x0100);
      LCD_WriteReg(R96,0xA700);  
    }
    else{                         //偶数次从反往正翻转
      //正向显示的寄存器配置
      LCD_WriteReg(R1,0x0000);
      LCD_WriteReg(R96,0x2700);
    }
    lcd_rvs_time = !lcd_rvs_time;//刷新翻转计数器
  }
}

二.按键控制

1.CubeMX中的配置

根据板子的原理图可知,按键B1——B4对应着单片机引脚PB0、PB1、PB2、PA0.
因此需要把这几个引脚设置为GPIO输入模式,且由于原理图中有接上拉电阻,因此配置为默认的浮空输入即可.同时由于上拉电阻的存在,按键断开时输入为高电平,按键按下时输入为低电平.
在这里插入图片描述
在这里插入图片描述
又因为按键存在5-10ms的按键抖动,所以要考虑延时消抖方案,即每次读取到按键被按下,过一小会(>10ms)再读一次按键值以消除抖动对按键值的影响.
此处涉及到延时,又为避免使用延时函数对程序造成停滞的影响,我们需要配置一个基本定时器来计时.为方便起见,我们延时20ms.
在这里插入图片描述
配置要点说明:

  • 首先使能基本定时器6.
  • 然后我们做80分频,由于PSC的0表示不分频(也就是1分频),因此实现80分频应填入80-1.
    在这里插入图片描述
  • 根据时钟树我们知道,PCLK1上的时钟脉冲是80MHZ,做了80分频以后是1MHZ,因此
    计数器是对1MHZ的时钟脉冲计数,每计一个数对应时间是1微秒,为实现20ms的定时,需计
    20000个数,由于自动重装载寄存器的0表示计一个数触发1次计数器更新中断,所以我们应填入20000-1.
  • 使能影子寄存器,这是为了防止我们在计数中途改动自动重装载寄存器时,定时器的计数大于我们给定的值,导致计数器迟迟无法触发更新中断.
    在这里插入图片描述
  • 开启定时器中断,否则不会触发中断回调.

2.按键控制的准备工作

1.在bsp中新建一对keys.h/.c文件作为按键驱动文件

2.在main.c中导入keys.h并导入keys.h中定义的变量

3.打开基本定时器中断

4.重新定义定时中断回调函数
//main前
#include "keys.h"

extern keys key[4];     //导入按键结构体

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim)
{
  keys_scan();//按键扫描函数
  keys_func();//按键功能函数
}

//main中
HAL_TIM_Base_Start_IT(&htim6);//打开基本定时器中断

keys.h的写法

1.使用条件编译防止多次编译

2.导入main.h以调用数据类型重定义和HAL库函数

3.定义按键结构体,包含:
- 按键值value:0代表按键按下,1代表按键未按下;
- 按键按下计数器time_down:在按键按下以后到抬起之前,每隔一定的时间自增,按键抬起后清零;
- 按键抬起计数器time_up:在按键按下并抬起以后,每隔一定的时间自增,按键再次按下清零;
- 按压模式标志位:未按压状态下为0,单击按压状态为1,长按按压状态为2,双击按压状态为3.
- 按键短按(单击)标志位short_flage:0代表按键未被短按(单击),1代表按键已被短按(单击);
- 按键长按标志位long_flage:0代表按键未被长按,1代表按键已被长按;
- 按键双击标志位double_flage:0代表按键未被双击,1代表按键已被双击.

4.声明按键扫描函数&按键功能函数
#ifndef __KEYS_H__
#define __KEYS_H__

#include "main.h"       
#include "stdbool.h"    

typedef struct 
{
    bool        value           ;//按键值
    uint16_t    time_down       ;//按键按下计数器
    uint16_t    time_up         ;//按键抬起计数器
    uint8_t     press_flag      ;//按压确认标志位
    bool        short_flag      ;//短按标志位
    bool        long_flag       ;//长按标志位
    bool        double_flag     ;//双击标志位  
} keys;

void        keys_scan(void)     ;//按键扫描函数
void        keys_func(void)     ;//按键功能函数

#endif

keys.c的写法

1.导入keys.h

2.初始化按键结构体

3.定义按键扫描函数和按键功能函数
#include "keys.h"

#define short_press         1
#define long_press          2
#define double_press        3

//初始化按键值为1(未按下),各计数器和标志位为0
keys key[4] = {1,0,0,0,0,0,0};  

void keys_scan()
{
    key[0].value = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);//按键1值
    key[1].value = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);//按键2值
    key[2].value = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);//按键3值
    key[3].value = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//按键4值
}

void keys_func()
{

}

然后我们根据要实现的按键功能,设计按键功能函数.

3.按键单击的算法逻辑

1.遍历所有按键,检测是否有按键按下.

2.检测到有按键按下后,清零抬起计数器,否则清零按键按下计数器.

3.从该按键按下开始至其抬起,按键按下计数器开始计数.
若按键按下计数器计数为2时,按压标志位置为1.

4.松手后按键抬起计数器开始计数.
若按键抬起计数器为15,则将单击标志位置1,同时清零按压标志位.

4.按键长按的算法逻辑

基于上述按键单击算法逻辑,按键长按的算法逻辑如下:

若按键按下计数器为50,则将长按标志位置1同时将按压标志位置为长按按压状态.
松手后将长按标志位和按压标志位置0.

5.按键双击的算法逻辑

基于上述按键单击的算法逻辑,按键双击的算法逻辑如下:

1.遍历所有按键,检测是否有按键按下.

2.检测到有按键按下后,清零抬起计数器,否则清零按键按下计数器.

3.从该按键按下开始至其抬起,按键按下计数器开始计数.
按键按下计数器计数为2时,若按压状态标志位为短按按压状态
则将按压状态标志位置为双击按压状态

4.若按压状态标志位为双击按压状态,则松手后将双击标志位置1同时清零按压标志位.

6.集按键单击、长按和双击于一体的按键功能函数

void keys_func()
{
    for(uint8_t i=0;i<4;i++)//遍历所有按键
    {
        if(key[i].value==0)//有按键按下
        {
            key[i].time_up = 0;//抬起计数器清零
            key[i].time_down++;//按下计数器累计
            switch (key[i].time_down)
            {
                case 2: 
                {
                    if(key[i].press_flag == short_press)
                    {
                        key[i].press_flag = double_press;//双击按压状态
                    }
                    else
                    {
                        key[i].press_flag = short_press;//单击按压状态
                    }
                } 
                break;

                case 50:{
                    key[i].long_flag = 1; //长按按压标志位置1
                    key[i].press_flag = long_press; //长按按压状态
                }
                break;
            }
        }
        else
        {
            key[i].time_down = 0;//按下计数器清零              
            switch (key[i].press_flag)
            {
                case short_press:
                {
                    key[i].time_up++;//抬起计数器累计
                    if(key[i].time_up==15)//松手后不再按下
                    {
                            key[i].short_flag = 1;//确认是单击
                            key[i].press_flag = 0;//按压标志位清零
                    }
                }
                break;

                case long_press:
                {
                    key[i].long_flag = 0;//长按标志位置0
                    key[i].press_flag = 0;//清零按压标志位
                }
                break;

                 case double_press:
                {
                    key[i].double_flag = 1;//确认是双击
                    key[i].press_flag = 0;//按压标志位清零
                }
                break;
            }
        }
    }
}

至此我们设计出了集单击、长按和双击功能于一体的按键功能函数.

三.LED指示

1.CubeMX中的配置

由图可知需配置引脚PC8—PC15和PD2,由于lcd.c文件中对PC8—PC15进行了配置,因此这里只需要配置PD2.
在这里插入图片描述
PD2的配置如图所示.
在这里插入图片描述

2.LED指示的准备工作

1.在bsp新建一对led.h/.c文件作为LED的驱动文件

2.在main.c中导入led.h

3.在main函数前定义全局变量led并初始化为0x00以供调用

4.调用led驱动函数进行led的初始化.
#include "led.h"

uint8_t     led = 0x00      ;//led指示灯    

led_driver(led);//调用led驱动函数初始化led

led.h文件的写法

1.条件编译

2.导入main.h以调用经重定义后的数据类型和HAL库函数

3.声明led驱动函数
#ifndef __LED_H__
#define __LED_H__

#include "main.h"

void led_driver(uint8_t led);

#endif

led.c文件的写法

1.导入led.h文件

2.定义led驱动函数
- 点灯准备阶段:还原之前的置位(输入信号全置1)
-点灯操作阶段:
    对应灯亮(对应输入信号置0)
    将输入信号送至输出端
-点灯结束阶段:锁住输入信号,防止lcd输入信号的干扰
#include "led.h"

void led_driver(uint8_t led)
{
    //点灯准备阶段:还原之前的置位(输入信号全置1)
    HAL_GPIO_WritePin(GPIOC, 0xff<<8, GPIO_PIN_SET);

    //点灯操作阶段:
    //对应灯亮(对应输入信号置0)
    HAL_GPIO_WritePin(GPIOC,led<<8,GPIO_PIN_RESET);
    //将输入信号送至输出端
    HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);

    //点灯结束阶段:锁住led输入信号,防止干扰lcd工作
    HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}

3.局部控灯的算法逻辑

通过按位或运算使能某盏led,然后调用led驱动函数使之点亮;
通过按位与运算屏蔽某盏led,然后调用led驱动函数使之熄灭;
注:这种算法仅改变被选择的led灯而不影响其他led灯
led = led |0x01; //使能第一盏led
led_driver(led);//点亮第一盏led

led = led &0xfb; //屏蔽第三盏led
led_driver(led);//熄灭第三盏led

4.LED闪烁的算法逻辑

定时一段时间,时间一到便将led某位的电平翻转
//该中断回调函数每20ms执行一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim)
{
	static uint8_t counter = 0;
	counter ++; 
	if(counter==50) //第一盏led每秒闪烁一次
	{
		counter = 0;
		static bool n=0;
		if(!n)
		{
			led = led | 0x01;
			led_driver(led); //点亮第一盏led
		}
		else
		{
			led = led &0xfe;
			led_driver(led);//熄灭第一盏led
		}
		n = !n;
	}
}	
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值