摘要:本文为作者对第十三届蓝桥杯嵌入式设计与开发项目省赛(第二场)——程序设计试题的解析。本文包括“题目要求”、“程序设计”、“效果展示”3个部分。供复盘使用。若发现错误之处,请不吝赐教。

链接:蓝桥杯嵌入式方向备赛记录(STM32G431)为作者备赛蓝桥杯嵌入式过程中,整理的学习总结。包括各模块使用要点、各模块程序等,基本搭建好工程框架,给出了各模块处理程序。

 写在前面:本道题在EEPROM上稍有难度,其他部分在内容和难度上和第一场差不多。

目        录

一、 题目要求

二、程序设计

(一)思路分析

1、涉及考点

2、几条主线

3、注意点

(二)程序设计

1、按键

2、LED

3、Systic

4、TIM

5、串口

6、EEPROM

7、LCD

8、整合

三、效果展示


一、 题目要求

二、程序设计

(一)思路分析

前期已准备好模块程序、搭建好工程框架,下面结合本道题,实际分析工程思路:

1、涉及考点

      按键、LED、EEPROM、定时器、串口、LCD等。

2、几条主线

(1)按键:要完成按键检测+功能处理。“按键检测”使用模块程序(非阻塞+移位消抖)即可。下面分析具体功能:B1:界面切换按键,程序上改变界面状态标志位即可;B2、B3:分别对应商品X、Y,在不同界面下按下按键,改变对应商品的购买数量、单价、库存,通过运算符对变量处理即可;B4:确认按键,处理好变量关系即可。

(2)LED

LED指示灯要求

主要是LD1亮5秒,LD2以0.1秒为间隔闪:LD1点亮5秒后熄灭,可以通过Systic秒计数实现;LD2以0.1秒为间隔闪烁,可以通过设置LED处理函数刷新时间为0.1秒来实现(在LED处理函数中,if语句,库存量均为0时,翻转对应引脚。该函数0.1秒刷新一次,即可实现LD2以0.1秒为间隔闪烁)。

(3)定时器:一个脚,两个状态,输出“1路相同频率、不同占空比”的PWM。

PWM要求

本题,修改占空比(改变CCR值)即可。设置参数如下表所示:

波形 PSC ARR CCR
2KHz 5% 400 100 5
2KHz 30% 400 100 5

(4)串口:通过串口查询单价,显示价格。STM32接收:中断、1位数据,发送:串口重定向printf。

(5)LCD:很常规,不再赘述。

(6)EEPROM

EEPROM要求

一般拿到题目,要分析注意以下2点:

  • 题目明确初始状态(上电后为默认值、且题目未要求EEPROM)——不要EEPROM,直接变量赋初值就够了;
  • 初次上电默认值(掉电保存:第一次上电为默认值,后面需要保存)——要EEPROM,要判断是否第一次上电。

本题就属于第二种情况,注意以下3点,待下文详细说明:

  • (题目要求)……发生变动时,写入;无变化不写入;
  • (题目要求)设备重新上电,能从EEPROM相应地址载入……须判断设备第一次上电?
  • (EEPROM使用)连续读、写,需要延时。

3、注意点

(1)EEPROM的使用;

(2)IIC:硬件和软件,待下文详细说明;

(3)模块化编程:前几篇博客没有说,但实际工程都是模块化的。从目录、程序里可以清晰看到。

(二)程序设计

1、按键

uint8_t ucConfirm = 0;//商品购买界面下,按下B4确认Flag
uint8_t  ucLcd[21];//LCD值(\0结束) */  //在写函数内,从0开始按顺序存储到1~3地址
uint8_t ucX_SHOP = 0;//X购买数量
uint8_t ucY_SHOP = 0;//Y购买数量
uint8_t ucX_PRICE=10, ucY_PRICE=10;	//商品价格(*10) 
uint8_t ucX_REP = 10;//X库存
uint8_t ucY_REP = 10;//Y库存

void KEY_Proc(void)		//按键处理函数		注意要清除标志位
{
	if(key[0].ucJudgeKeyState == 1)	//如果K1短按	
	{
		key[0].ucJudgeKeyState = 0;
		if(++ucState == 3)
		{
			ucState = 0;//  0~2  循环
		}
	}
	if(key[1].ucJudgeKeyState == 1)	//如果K2短按 
	{
		key[1].ucJudgeKeyState = 0;
		switch(ucState)
		{
			case 0:
				{
					if(++ucX_SHOP == ucX_REP + 1) 
						ucX_SHOP = 0;//  0~库存数量	循环
				}
				break;
			case 1:
				{
					if(++ucX_PRICE == 21) 
						ucX_PRICE = 10;// 	10~20
					ucLcd[0] = ucX_PRICE ;
					MEM_Write(ucLcd,2,1); //保存X价格
				}
				break;
			case 2:
				{
					++ucX_REP ; 
					ucLcd[0] = ucX_REP ;
					MEM_Write(ucLcd,0,1); //保存X库存
				}
				break;
			default : break;
		}
	}
	if(key[2].ucJudgeKeyState == 1)	//如果K3短按       
	{
		key[2].ucJudgeKeyState = 0;
		switch(ucState)
		{
			case 0:
				{
					if(++ucY_SHOP == ucY_REP + 1)
						ucY_SHOP = 0;  //0~库存数量	循环
				}
				break;
			case 1:
				{
					if(++ucY_PRICE ==  21)
						ucY_PRICE = 10;// 10~20  循环
					ucLcd[0] = ucY_PRICE ;
					MEM_Write(ucLcd,3,1); //保存X价格
				}
				break;
			case 2:
				{
					++ucY_REP;
					ucLcd[0] = ucY_REP ;
					MEM_Write(ucLcd,1,1); //保存Y库存
				}
				break;
			default : break;
		}
	}
	if(key[3].ucJudgeKeyState == 1)	//如果K4短按       
	{
		key[3].ucJudgeKeyState = 0;
		
		if(ucState == 0)
		{
			ucConfirm = 1;//确认标志位

			ucX_REP -= ucX_SHOP;//新库存量 = 库存量 - 购买量 
			ucY_REP -= ucY_SHOP;

			ucLcd[0] = ucX_REP ; //X库存
			ucLcd[1] = ucY_REP ; //Y库存
			ucLcd[2] = ucX_PRICE ; //X单价
			ucLcd[3] = ucY_PRICE ; //Y单价
			MEM_Write(ucLcd,0,2); //保存库存数量

			printf("X:%u,Y:%u,Z:%2.1f\r\n", ucX_SHOP, ucY_SHOP,(ucX_SHOP*ucX_PRICE+ucY_SHOP*ucY_PRICE)/10.0); //打印输出总价及购买信息
			
			ucX_SHOP = 0;//购买量清0
			ucY_SHOP = 0;
		}
		ucSec = 0;//B4按下,秒计数清0,重新开始计数
	}
}

2、LED

uint8_t ucLed=0;//LED状态

void LED_Proc(void) //LED处理函数   //逻辑/处理/重点
{	
	//0.1s刷新,刚好下面LD2闪烁,下面不作处理了(实际效果确实可以)
	//这也提供了0.1s闪烁的一种思路
	if (ucTblk < 100)
    return;
	ucTblk = 0;  
	
	//LD1   SHOP界面按下B4 (ucConfirm)、且在5s内	点亮LD1
	if( ucConfirm && (ucSec < 6) ) // 5/6?
		ucLed |= 1;//点亮
	else
		ucLed &=~ 1;//熄灭LD1
	 
	//5s时间到 熄灭LD1
	if( ucConfirm && (ucSec >5 ) )
	{
		ucConfirm = 0;
		ucLed &=~ 1;//熄灭LD1
	}
	
	//LD2   库存均为0,指示灯 LD2 以 0.1 秒为间隔亮、灭闪烁报警
	if ((ucX_REP == 0) && (ucY_REP ==0)  ) //不能连等判断,否则没用
		ucLed ^= 2;//设置LED处理函数0.1s刷新即可
    else
		ucLed &= ~2; 
	
	Led_Disp(ucLed);
}

3、Systic

uint16_t usTms; //ms计数
uint8_t ucSec; //秒计时
uint8_t ucTblk; //LED刷新
uint16_t usTlcd;//LCD刷新

void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */
  extern uint16_t usTms;       		   	/* 毫秒计时 */
  extern uint8_t ucSec;             	/* 秒计时 */
  extern uint8_t ucTblk; //LED刷新
  extern uint16_t usTlcd;//LCD刷新
  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
	if (++usTms == 1000)               	/* 1s到 */
	  {
		usTms = 0;
		ucSec++;                         	/* 秒加1 */
	  }
	  ucTblk++;
	  usTlcd++;
  /* USER CODE END SysTick_IRQn 1 */
}

4、TIM

void TIM_Proc(void)    //定时器处理函数
{
	if(ucConfirm == 0) //2k 5%
	{
		__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,5);
	}
	else	//2k 30%
	{
		__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,30);
	}
}

5、串口

uint8_t Rxindex;
uint8_t RxDate;
uint8_t RxBuffer[7];//接收缓冲区

int fputc(int ch, FILE *f)    //串口重定向
{
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);  //注意,这里使用的是串口1,采用轮询方式发送1字节数据,HAL_MAX_DELAY表示超时时间为无限等待

	return ch;
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)   //串口接收中断回调函数
{
	if( huart->Instance == USART1 )
	{
		RxBuffer[Rxindex++] = RxDate; //接收1个:先RxBuffer[0] =  RxDate  然后Rxindex = 1  //再接收一个:RxBuffer[1] =  RxDate  然后Rxindex = 2 。。。。。。 
		HAL_UART_Receive_IT(&huart1,&RxDate,1);//使能接收中断
	}
}

uint8_t isRxlegal() //判断接收到的数据是否合法 //返回1
{
	unsigned char i;
	
	if(Rxindex==0)    //未接收到数据
		return 0;
	if(Rxindex!=1)    //接收数据不为1个ASCII时不合法
		return 2;
	if(RxBuffer[0]=='?')    //接收数据为'?'时 //或者这样:RxBuffer[0]=='?'
		return 1;
	else    //接收数据不为'?'时不合法
		return 2;
}

void UART_Proc(void)    //串口处理函数
{
	if(isRxlegal()==1)//接收的数据合法
	{
		printf("X:%2.1f,Y:%2.1f\r\n", ucX_PRICE/10.0, ucY_PRICE/10.0); 设备返回当前各类商品单价
	}
	Rxindex = 0; //索引清0
}

这一点切记,血与泪的教训啊!!!

使用串口重定向,一定要勾选“魔术棒”里的:“Use MicroLlB”,否则程序不能运行。

6、EEPROM

要点1:EEPROM的读写

  • 说明1:(官方教程1)STM32G431板子上的24C02连接的引脚是PB6、PB7。而CT117E-M4 I2C接口的PB6没有SCL功能,可以用并口仿真实现(即软件IIC:参见竞赛资源包中的底层驱动代码),也可以用硬件I2C实现(PA15-SCL),具体步骤如下:① 拔掉J10和J19上的短路块。② 用短路块连接J10_1(PA15)和J19_1(SCL)。然后程序上:
/* EEPROM读 */
void EEPROM_Read(uint8_t *ucBuf, uint8_t ucAddr, uint8_t ucNum)
{
  HAL_I2C_Mem_Read(&hi2c1, 0xa0, ucAddr, 1, ucBuf, ucNum, 100);
}
/* EEPROM写 */
void EEPROM_Write(uint8_t *ucBuf, uint8_t ucAddr, uint8_t ucNum)
{
  HAL_I2C_Mem_Write(&hi2c1, 0xa0, ucAddr, 1, ucBuf, ucNum, 100);  
}
/* MCP写 */
void MCP_Write(uint8_t ucVal)
{
  HAL_I2C_Master_Transmit(&hi2c1, 0x5e, &ucVal, 1, 100);
}
  • 以上是PPT教程里的说明,但我手头的板子上没有找到J10和J19,所以这种方法无法进行。

  • 说明2:PB6、PB7软件模拟IIC实现,有2套模块程序,使用时按需选择即可,如下:

       选择1:

//选择1

//E2PROM读函数:通过IIC通信,读取E2PROM的address位置处的值
uint8_t x24c02_read(uint8_t address)
{
	unsigned char val;
	
	I2CStart(); 
	I2CSendByte(0xa0);
	I2CWaitAck(); 
	
	I2CSendByte(address);
	I2CWaitAck(); 
	
	I2CStart();
	I2CSendByte(0xa1); 
	I2CWaitAck();
	val = I2CReceiveByte(); 
	I2CWaitAck();
	I2CStop();
	
	return(val);
}

//E2PROM写函数:通过IIC通信,将某值info写进E2PROM内指定的地址address
void x24c02_write(unsigned char address,unsigned char info)
{
	I2CStart(); 
	I2CSendByte(0xa0); 
	I2CWaitAck(); 
	
	I2CSendByte(address);	
	I2CWaitAck(); 
	I2CSendByte(info); 
	I2CWaitAck(); 
	I2CStop();
}

      选择2:

//选择2

//在i2c_hal.c里添加
/* 存储器读 */
void MEM_Read(unsigned char* ucBuf, unsigned char ucAddr,
  unsigned char ucNum)
{
  I2CStart(); 
  I2CSendByte(0xa0);
  I2CWaitAck(); 

  I2CSendByte(ucAddr);
  I2CWaitAck(); 

  I2CStart();
  I2CSendByte(0xa1); 
  I2CWaitAck();

  while (ucNum--) {
    *ucBuf++ = I2CReceiveByte();
    if (ucNum)
      I2CSendAck();
    else
      I2CSendNotAck();
  }
  I2CStop();
}
/* 存储器写 */
void MEM_Write(unsigned char* ucBuf, unsigned char ucAddr,
  unsigned char ucNum)
{
  I2CStart(); 
  I2CSendByte(0xa0); 
  I2CWaitAck(); 

  I2CSendByte(ucAddr);	
  I2CWaitAck();

  while (ucNum--) {
    I2CSendByte(*ucBuf++); 
    I2CWaitAck(); 
  }
  I2CStop();
  delay1(500);
}

void MCP_Write(unsigned char ucVal)
{
  I2CStart();
  I2CSendByte(0x5e);
  I2CWaitAck();

  I2CSendByte(ucVal);
  I2CWaitAck();
  I2CStop();
}

要点2:EEPROM连续读写注意点

  • 每两个读取或者写入函数之间,必须加5毫秒延时。原因是,EEPROM外设的读取速度是跟不上MCU的运行速度的,需要让MCU停下来等待一会儿。
  • 这里参考其他博主文章:蓝桥杯嵌入式——EEPROM避坑指南(干货)。同时里面还提供了判断设备是否第一次上电的另一种方法。

要点3:判断设备是否第一次上电

  • 方法:在EEPROM里的某些个地址存储特定的值,每次启动时都读取这这些地址里的值。如果读取出的数值不是自己设定的值,则可判断设备第一次上电,再将特定值写入;否则(说明这些值已经写入),就不是第一次上电,那么就从EEPROM中载入变量值
  • 程序:如下所示。
//在main函数while前
I2CInit();
	
//设备重新上电,能够从EEPROM相应地址中载入商品库存数量和价格
MEM_Read(ucLcd,0,7);//读出E2PROM存储的商品信息和是否第一次运行标志位
HAL_Delay(100);
if((ucLcd[4]==0x77)&&(ucLcd[5]==0x7A)&&(ucLcd[6]==0x64))//设备不是第一次运行
{
	ucX_REP	   =  ucLcd[0];
	ucY_REP	   =  ucLcd[1];
	ucX_PRICE  =  ucLcd[2];
	ucY_PRICE  =  ucLcd[3];
}
else//设备第一次运行
{
	ucLcd[4]=0x77;
	ucLcd[5]=0x7A;
	ucLcd[6]=0x64;//自定义的数据
		
	ucLcd[0]=10;
	ucLcd[1]=10;
    ucLcd[2]=10;
	ucLcd[3]=10;//将初始的商品库存和价格按规定的位置写入E2PROM
	MEM_Write(ucLcd,0,7);
}

7、LCD

uint8_t ucState=0;//界面状态:0—SHOP界面   1—PRICE界面   2-REP界面
char buf1[20];

void LCD_Proc(void)    //LCD处理函数
{
	if(usTlcd < 100)
		return;
	usTlcd = 0;
	
	switch(ucState)
	{
		case 0:
			{
				LCD_DisplayStringLine(Line0,(unsigned char*)"                     ");
				LCD_DisplayStringLine(Line1,(unsigned char*)"        SHOP         ");
				LCD_DisplayStringLine(Line2,(unsigned char*)"                     ");
				sprintf(buf1,"     X:%d         ",ucX_SHOP);
				LCD_DisplayStringLine(Line3,(unsigned char*)buf1);
				sprintf(buf1,"     Y:%d         ",ucY_SHOP);
				LCD_DisplayStringLine(Line4,(unsigned char*)buf1);
	     	}
			break;
		case 1:
			{
				LCD_DisplayStringLine(Line0,(unsigned char*)"                     ");
				LCD_DisplayStringLine(Line1,(unsigned char*)"        PRICE        ");
				LCD_DisplayStringLine(Line2,(unsigned char*)"                     ");
				sprintf(buf1,"     X:%2.1f         ",ucX_PRICE/10.0);
				LCD_DisplayStringLine(Line3,(unsigned char*)buf1);
				sprintf(buf1,"     Y:%2.1f         ",ucY_PRICE/10.0);
				LCD_DisplayStringLine(Line4,(unsigned char*)buf1);
	     	}
			break;
		case 2:
			{
				LCD_DisplayStringLine(Line0,(unsigned char*)"                     ");
				LCD_DisplayStringLine(Line1,(unsigned char*)"         REP         ");
				LCD_DisplayStringLine(Line2,(unsigned char*)"                     ");
				sprintf(buf1,"     X:%d         ",ucX_REP);
				LCD_DisplayStringLine(Line3,(unsigned char*)buf1);
				sprintf(buf1,"     X:%d         ",ucY_REP);
				LCD_DisplayStringLine(Line4,(unsigned char*)buf1);
	     	}
			break;
		default : break;
	}
		LCD_DisplayStringLine(Line5,(unsigned char*)"                     ");
		LCD_DisplayStringLine(Line6,(unsigned char*)"                     ");
		LCD_DisplayStringLine(Line7,(unsigned char*)"                     ");
		LCD_DisplayStringLine(Line8,(unsigned char*)"                     ");
		LCD_DisplayStringLine(Line9,(unsigned char*)"                     ");
}

8、整合

//main里部分
/* USER CODE BEGIN 2 */
	HAL_TIM_Base_Start_IT(&htim4); //按键定时器	
	HAL_TIM_PWM_Start_IT(&htim2,TIM_CHANNEL_2);//PA1 2kHz 5% : psc400  ARR100  CCR5  ; 	2kHz 30% : psc 400  ARR 100  CCR  30
	HAL_UART_Receive_IT(&huart1,&RxDate,1);//使能串口接收中断 
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
	LCD_Init();
    LCD_Clear(Black);
    LCD_SetBackColor(Black);
    LCD_SetTextColor(White);

	I2CInit();
	
	//设备重新上电,能够从EEPROM相应地址中载入商品库存数量和价格
	MEM_Read(ucLcd,0,7);//读出E2PROM存储的商品信息和是否第一次运行标志位
    HAL_Delay(100);
    if((ucLcd[4]==0x77)&&(ucLcd[5]==0x7A)&&(ucLcd[6]==0x64))//设备不是第一次运行
    {
		 ucX_REP	=	ucLcd[0];
		 ucY_REP	=	ucLcd[1];
		 ucX_PRICE  =	ucLcd[2];
		 ucY_PRICE  =	ucLcd[3];
    }
    else//设备第一次运行
    {
	 ucLcd[4]=0x77;
	 ucLcd[5]=0x7A;
	 ucLcd[6]=0x64;//自定义的信息,
		
	 ucLcd[0]=10;
	 ucLcd[1]=10;
	 ucLcd[2]=10;
	 ucLcd[3]=10;//将初始的商品库存和价格按规定的位置写入E2PROM
	 MEM_Write(ucLcd,0,7);
    }
	
	ucLed = 0;
	Led_Disp(ucLed);
    while (1)
    {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
	   KEY_Proc();
	   LCD_Proc();
	   LED_Proc();
	   TIM_Proc();
	   UART_Proc();
    }
  /* USER CODE END 3 */

三、效果展示

        视频后续上传。 

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐