文章目录
前言
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;
}
}