【蓝桥杯嵌入式】按键控制LED与LCD(必考三件套)
- 前言
- LED相关功能的实现
- LED基础功能函数(点亮、全熄灭、翻转)
- LED的闪烁与定时点亮熄灭
- 流水灯的实现
- 按键的扫描及长短按、双击的实现
- 按键的短按
- 按键业务逻辑程序进程
- 按键的长短按
- 长短按与双击
- LCD移植与显示
- LCD的移植与进程函数
- LCD与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灯elseHAL_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);elseled_show(1,0);//5s计时启动,LED8亮if(sec5_flag == 1)led_show(8,1);elseled_show(8,0);
}
led闪烁的定时器中断底层设计
通过系统滴答定时器计时,滴答定时器的定时时间为1ms
uint shake_tick = 0;
u32 sec5_tick = 0;
void SysTick_Handler(void)
{//控制led闪烁if(shake_flag){shake_tick++;//led闪烁频率为1sif(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: //消抖 10msif(key[i].key_sta == 0)key[i].judge_sta = 2;elsekey[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: //消抖 10msif(key[i].key_sta == 0)key[i].judge_sta = 2;elsekey[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: //消抖 10msif(key[i].key_sta == 0)key[i].judge_sta = 2;elsekey[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; }}elsekey[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;elsekey[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); //特定位置设置背景颜色为绿色 elseLCD_SetBackColor(Black); //其他位置保持背景颜色为黑色 LCD_DisplayChar(Line, refcolumn, *ptr);refcolumn -= 16;ptr++;i++;}LCD_SetBackColor(Black); //其他位置保持背景颜色为黑色
}
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;}}
}
实际效果: