【蓝桥杯嵌入式】按键控制LED与LCD(必考三件套)
前言
按键、LED以及LCD是蓝桥杯每年必考的三个知识点,也作为工程建立的基础与突破口,因此熟练掌握该三个板块内容及其重要:
-
本人习惯自建user.c函数,将各种程序放在该文件内,方便程序编写
-
LCD的实现不需要配置相关IO口,只需要对工程进行移植即可
LCD_Init(); LCD_SetBackColor(Black); //设置背景颜色 LCD_SetTextColor(White); //设置字体颜色 LCD_Clear(Black); //清屏 LCD_DisplayStringLine(Line4, (unsigned char *)" Hello,world. "); //LCD显示函数
-
按键分为短按,长按和双击,长按考频率不高,双击至今还未考过
-
LCD与LED共用引脚,需要对LCD相关函数进行优化
u32 temp = GPIOC->ODR; GPIOC->ODR = temp;
-
业务逻辑在三个进程函数内实现,进程函数在while(1)中运行
-
亘古不变的变量
uchar ui = 0; //lcd显示的界面号 char text[20]; //lcd的显存buf struct keys key[4] = {0,0,0}; //按键结构体变量
注: 本文内容主要实现按键、LCD与LED的底层工程函数与配合使用功能的程序设计,相关cubemx工程配置请参考:【蓝桥杯嵌入式】Cubemx新建工程引脚配置与点亮LED
LED相关功能的实现
LED基础功能函数(点亮、全熄灭、翻转)
点亮一个LED灯
void led_show(uchar led, bool mode)
{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET); //打开锁存器
if(mode)
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<(led-1),GPIO_PIN_RESET); //点亮一个LED灯
else
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_8<<(led-1),GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET); //关闭锁存器
}
关闭所有LED灯(用于初始化熄灭全部LED,在main.c的while(1)之前调用)
void led_offAll(void)
{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
翻转LED
void led_toggle(uchar led)
{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_8<<(led-1));
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
LED的闪烁与定时点亮熄灭
led的闪烁通过定时器在特定的时间内改变led的亮灭来实现闪烁效果,其中led的闪烁采用系统滴答定时器实现,本人习惯将stm32g4xx_it.c中的滴答定时器中断服务函数SysTick_Handler
剪切至user.c中用于控制led的闪烁。
全局变量
bool shake_flag = 0; //闪烁标志位
bool led_mode = 1; //led状态
bool sec5_flag = 0; //5s计时器
led进程函数
void led_process(void)
{
//led闪烁
if(shake_flag)
led_show(1,led_mode);
else
led_show(1,0);
//5s计时启动,LED8亮
if(sec5_flag == 1)
led_show(8,1);
else
led_show(8,0);
}
led闪烁的定时器中断底层设计
通过系统滴答定时器计时,滴答定时器的定时时间为1ms
uint shake_tick = 0;
u32 sec5_tick = 0;
void SysTick_Handler(void)
{
//控制led闪烁
if(shake_flag)
{
shake_tick++;
//led闪烁频率为1s
if(shake_tick >= 500)
{
shake_tick = 0;
led_mode = !led_mode;
}
}
//5s计时器
if(sec5_flag)
{
sec5_tick++;
if(sec5_tick >= 5000)
{
sec5_flag = 0;
sec5_tick = 0;
}
}
HAL_IncTick();
}
流水灯的实现
/**********************全局变量*******************/
//流水灯标志位
bool flue_flag = 0;
bool flue_cnt = 0;
u32 led_tick = 0;
void led_process(void)
{
//控制进入led的时间 用于控制流水灯速度
if(uwTick - led_tick < 150)
return;
led_tick = uwTick;
if(flue_flag)
{
static uchar i = 1;
if(i > 4) //流水灯的范围
{
i = 1;
led_show(4,0);
}
led_show(i,1); //点亮流水灯
led_show(i-1,0); //熄灭之前的灯
i++; //流水
flue_cnt = 1; //用于只关闭流水范围内的灯一次
}
else if(flue_cnt && flue_cnt == 1) //流水结束 关闭流水范围内的灯 关一次
{
for(uchar i = 1; i <= 4; i++)
led_show(i,0);
flue_cnt = 0;
}
}
按键的扫描及长短按、双击的实现
开发板的按键四颗按键分别接在PB0~PB2以及PA0引脚,当按下按键时,IO口被拉低,通过定时器扫描按键IO口电平状态来检测按键是否被按下,按键原理图如下图所示:
定时器中断回调函数
记得在main.c函数的初始化中打开定时器中断!!!
HAL_TIM_Base_Start_IT(&htim6);
重写回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);
直接去tim.c底下的stm32g4xx_hal_tim.h里面找即可,直接拖到文件末尾,倒数第三个板块的第一个函数。
按键的短按
按键结构体定义:
struct keys{
uchar judge_sta; //状态集
bool key_sta; //IO口电平
bool single_flag; //短按标志位
};
按键变量的定义
struct keys key[4] = {0,0,0};
其中短按的程序设计逻辑为:
- IO口电平为0,按下
- 软件消抖(判断是否真实按下)
- 松手检测,短按标志置1
短按的定时器扫描实现:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6) //对应的定时器中断 10ms
{
//读取IO口电平
key[0].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].key_sta = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(uchar i = 0; i < 4; i++)
{
switch(key[i].judge_sta)
{
case 0:
if(key[i].key_sta == 0) //按键按下
key[i].judge_sta = 1;
break;
case 1: //消抖 10ms
if(key[i].key_sta == 0)
key[i].judge_sta = 2;
else
key[i].judge_sta = 0;
break;
case 2:
if(key[i].key_sta == 1) //松手 短按标志置1
{
key[i].single_flag = 1;
key[i].judge_sta = 0;
}
break;
}
}
}
}
按键业务逻辑程序进程
void key_process(void)
{
if(key[0].single_flag == 1) //按键1按下
{
//按键1短按业务逻辑程序
led_toggle(1);
ui = (ui + 1) % 3; //按键1通常为切换界面
LCD_Clear(Black); //☆切换界面记得需要清屏
key[0].single_flag = 0; //清空按下标志位
}
if(key[1].single_flag == 1) //按键2按下
{
//按键2短按业务逻辑程序
led_toggle(2);
key[1].single_flag = 0;
}
if(key[2].single_flag == 1) //按键3按下
{
//按键3短按业务逻辑程序
led_toggle(3);
key[2].single_flag = 0;
}
if(key[3].single_flag == 1) //按键4按下
{
//按键4短按业务逻辑程序
led_toggle(4);
key[3].single_flag = 0;
}
/*******以下是有长短按时的业务逻辑,其他按键同理******/
if(key[3].long_flag == 1) //按键4长按
{
//按键4短按业务逻辑程序
led_show(5,1);
key[3].long_flag = 0;
}
if(key[3].double_flag == 1) //按键4双击
{
//按键4短按业务逻辑程序
led_show(5,0);
key[3].double_flag = 0;
}
}
按键的长短按
按键结构体的定义
struct keys{
uchar judge_sta; //状态集
bool key_sta; //IO口电平
bool single_flag; //短按按下标志位
uint key_time; //按键按下时间
bool long_flag; //长按标志位
};
按键变量的定义
struct keys key[4] = {0,0,0,0,0};
长按的程序设计逻辑为:
- IO口电平为0,按下,启动计时
- 软件消抖(判断是否真实按下)
- 计时,若按下时间超过0.8s,长按标志置1
- 松手检测,若按下时间小于0.8s,短按标志置1
按键的长短按定时器扫描实现
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6) //对应的定时器中断 10ms
{
//读取IO口电平
key[0].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].key_sta = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(uchar i = 0; i < 4; i++)
{
switch(key[i].judge_sta)
{
case 0:
if(key[i].key_sta == 0) //按键按下 时间置0
{
key[i].judge_sta = 1;
key[i].key_time = 0;
}
break;
case 1: //消抖 10ms
if(key[i].key_sta == 0)
key[i].judge_sta = 2;
else
key[i].judge_sta = 0;
break;
case 2:
if(key[i].key_sta == 1) //松手
{
if(key[i].key_time < 80) //按下时间小于800ms 短按
key[i].single_flag = 1;
key[i].judge_sta = 0;
}
else
{
key[i].key_time++;
if(key[i].key_time >= 80) //按下时间一旦大于800ms 长按
key[i].long_flag = 1;
}
break;
}
}
}
}
长短按与双击
按键结构体的定义
struct keys{
uchar judge_sta; //状态集
bool key_sta; //IO口电平
uint key_time1; //第一次按下时间
uint key_time2; //松手后的时间
bool single_flag; //短按标志
bool long_flag; //长按标志
bool double_flag; //双击标志
};
按键变量的定义
struct keys key[4] = {0,0,0,0,0,0,0};
双击的程序设计逻辑为:
- IO口电平为0,按下,启动第一次按下计时
- 软件消抖(判断是否真实按下)
- 计时按下时间
- 松手,开始计时松手时间,若再次按下,则进入双击
- 松手超过300ms,判断为长短按,结束
- 进入双击,消抖
- 检测松手,双击标志置1
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6) //对应的定时器中断 10ms
{
//读取IO口电平
key[0].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].key_sta = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].key_sta = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(uchar i = 0; i < 4; i++)
{
switch(key[i].judge_sta)
{
case 0:
if(key[i].key_sta == 0) //按键按下 时间置0
{
key[i].judge_sta = 1;
key[i].key_time1 = 0;
key[i].key_time2 = 0;
}
break;
case 1: //消抖 10ms
if(key[i].key_sta == 0)
key[i].judge_sta = 2;
else
key[i].judge_sta = 0;
break;
case 2:
if(key[i].key_sta == 1) //松手
key[i].judge_sta = 3;
else
{
key[i].key_time1++;
if(key[i].key_time1 >= 80)
key[i].long_flag = 1;
}
break;
case 3:
if(key[i].key_sta == 1)
{
key[i].key_time2++;
if(key[i].key_time2 >= 30) //长短按
{
if(key[i].key_time1 < 80)
key[i].single_flag = 1;
key[i].judge_sta = 0;
}
}
else
key[i].judge_sta = 4; //双击了
break;
case 4:
if(key[i].key_sta == 0)
key[i].judge_sta = 5; //消抖
else
{
if(key[i].key_time1 >= 80)
key[i].long_flag = 1;
else
key[i].single_flag = 1;
key[i].judge_sta = 0;
}
break;
case 5:
if(key[i].key_sta == 1) //双击松手
{
key[i].double_flag = 1;
key[i].judge_sta = 0;
}
break;
}
}
}
}
LCD移植与显示
LCD的移植与进程函数
LCD的相关引脚配置无需在cubemx中进行配置,只需要将官方提供的lcd.c复制到src文件夹,将lcd.h,fonts.h复制到ins文件夹中即可,并将lcd.c文件添加至工程中,复制例程中main.c的相关配置即可
初始化lcd
LCD_Init();
lcd相关配置
LCD_SetBackColor(Black); //设置背景颜色
LCD_SetTextColor(White); //设置字体颜色
LCD_Clear(Black); //清屏
lcd进程函数
void lcd_process(void)
{
if(ui == 0) //第一个界面显示的内容
{
sprintf(text," Title1 ");
LCD_DisplayStringLine(Line1, (unsigned char *)text); //LCD显示函数
/******************其他显示的内如下****************/
}
else if(ui == 1) //第二个界面显示的内容
{
sprintf(text," Title2 ");
LCD_DisplayStringLine(Line1, (unsigned char *)text); //LCD显示函数
/******************其他显示的内如下****************/
}
else if(ui == 2) //第三个界面显示的内容
{
sprintf(text," Title3 ");
LCD_DisplayStringLine(Line1, (unsigned char *)text); //LCD显示函数
/******************其他显示的内如下****************/
}
}
LCD与LED冲突的问题解决
对于蓝桥杯的开发板,几乎每题都会遇到LED与LCD显示冲突的情况,这是因为LCD与LED共用了PC8 ~ PC15的引脚,这使得LCD更新显示,PC的引脚电平就无法确定了,使得LCD显示与LED会冲突。
解决办法: 操作LCD之前保存GPIOC相关寄存器的值,对LCD操作结束后,重新恢复原值,对LCD的高亮显示
void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue);
void LCD_WriteRAM_Prepare(void);
void LCD_WriteRAM(u16 RGB_Code);
这三个函数进行处理,即,首行都加上u32 temp = GPIOC->ODR;
,尾行都加上GPIO->ODR = temp;
即可
如下所示,其他两个函数进行同理操作
void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue)
{
u32 temp = GPIOC->ODR;
GPIOB->BRR |= GPIO_PIN_9;
GPIOB->BRR |= GPIO_PIN_8;
GPIOB->BSRR |= GPIO_PIN_5;
GPIOC->ODR = LCD_Reg;
GPIOB->BRR |= GPIO_PIN_5;
__nop();
__nop();
__nop();
GPIOB->BSRR |= GPIO_PIN_5;
GPIOB->BSRR |= GPIO_PIN_8;
GPIOC->ODR = LCD_RegValue;
GPIOB->BRR |= GPIO_PIN_5;
__nop();
__nop();
__nop();
GPIOB->BSRR |= GPIO_PIN_5;
GPIOB->BSRR |= GPIO_PIN_8;
GPIOC->ODR = temp;
}
LCD的高亮显示
LCD的高亮显示即设置LCD的背景颜色,若直接设置LCD的背景颜色则是对整个界面设置,因此需要再LCD_DisplayStringLine
的内部设置颜色,从而确定高亮显示的位置,改写LCD_DisplayStringLine函数为LCD_DisplayStringLineHight代表高亮显示的函数,其中添加一个参数表示高亮显示的起点,高亮显示的终点或长度可自行决定设置,这里以第九届赛题为例:起始位置为参数,每次高亮长度为2个位置
高亮函数的实现
void LCD_DisplayStringLineHight(u8 Line, u8 *ptr,uint8_t start)
{
u32 i = 0;
u16 refcolumn = 319;//319;
LCD_SetBackColor(Black); //其他位置保持背景颜色为黑色
while ((*ptr != 0) && (i < 20)) // 20
{
if(i >= start && i < (start + 2))
LCD_SetBackColor(Green); //特定位置设置背景颜色为绿色
else
LCD_SetBackColor(Black); //其他位置保持背景颜色为黑色
LCD_DisplayChar(Line, refcolumn, *ptr);
refcolumn -= 16;
ptr++;
i++;
}
LCD_SetBackColor(Black); //其他位置保持背景颜色为黑色
}
![](https://i-blog.csdnimg.cn/blog_migrate/f9b6a87e786f8ad0d876f58e37dd7e7e.png)
LCD进程函数中调用高亮函数,实现特定位置的高亮显示
void lcd_process(void)
{
if(ui == 0) //第一个界面显示的内容
{
sprintf(text," Title1 ");
LCD_DisplayStringLine(Line1, (unsigned char *)text); //LCD显示函数
/******************其他显示的内如下****************/
sprintf(text," %02d:%02d:%02d ",12,2,2);
switch(choice)
{
case 0: LCD_DisplayStringLine(Line3,(unsigned char *)text);break;
case 1: LCD_DisplayStringLineHight(Line3,(unsigned char *)text,5);break;
case 2: LCD_DisplayStringLineHight(Line3,(unsigned char *)text,8);break;
case 3: LCD_DisplayStringLineHight(Line3,(unsigned char *)text,11);break;
default:break;
}
}
}
实际效果: