【蓝桥杯嵌入式】按键控制LED与LCD(必考三件套)

前言

按键、LED以及LCD是蓝桥杯每年必考的三个知识点,也作为工程建立的基础与突破口,因此熟练掌握该三个板块内容及其重要:

  1. 本人习惯自建user.c函数,将各种程序放在该文件内,方便程序编写

  2. LCD的实现不需要配置相关IO口,只需要对工程进行移植即可

    LCD_Init();
    LCD_SetBackColor(Black);	//设置背景颜色
    LCD_SetTextColor(White);	//设置字体颜色
    LCD_Clear(Black);			//清屏
    LCD_DisplayStringLine(Line4, (unsigned char *)"    Hello,world.   ");	//LCD显示函数
    
  3. 按键分为短按,长按和双击,长按考频率不高,双击至今还未考过

  4. LCD与LED共用引脚,需要对LCD相关函数进行优化

    u32 temp = GPIOC->ODR;
    
    GPIOC->ODR = temp;
    
  5. 业务逻辑在三个进程函数内实现,进程函数在while(1)中运行

  6. 亘古不变的变量

    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};

其中短按的程序设计逻辑为:

  1. IO口电平为0,按下
  2. 软件消抖(判断是否真实按下)
  3. 松手检测,短按标志置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};

长按的程序设计逻辑为:

  1. IO口电平为0,按下,启动计时
  2. 软件消抖(判断是否真实按下)
  3. 计时,若按下时间超过0.8s,长按标志置1
  4. 松手检测,若按下时间小于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};

双击的程序设计逻辑为:

  1. IO口电平为0,按下,启动第一次按下计时
  2. 软件消抖(判断是否真实按下)
  3. 计时按下时间
  4. 松手,开始计时松手时间,若再次按下,则进入双击
  5. 松手超过300ms,判断为长短按,结束
  6. 进入双击,消抖
  7. 检测松手,双击标志置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);	//其他位置保持背景颜色为黑色 
}

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;
		}
	}
}

实际效果:

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会编程的小江江

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值