文章目录
- 原题展示
- 原题分析
- 原题题解
- LED相关
- LCD相关
- 按键相关
- ADC相关
- 定时器相关
- PWM
- 输入捕获
- 小结
- 文章福利
原题展示
原题分析
今年的第一场比赛绝对np,官方将串口直接省掉了,将其替换成很多小功能,如:切换计时、频率均匀变化、锁机制等等,总的来说本届赛题的难度提升了不少。
本届试题需要用到的功能模块有LCD、LED、按键、定时器输入捕获、定时器PWM输出、ADC获取,虽然这届试题模块简单,但是功能实现一点也不简单,感觉跟十二届省赛一样😂😂😂。
还值得注意的是:本届试题有三个地方需要计时,即模式切换、LED闪烁与长按键,,这可能是蓝桥杯为了提升难度的一个方向。(小编感觉这计时真的是恶心🤣🤣🤣)
原题题解
LED相关
通过查询产品手册知,LED的引脚为PC8~PC15,外加锁存器74HC573需要用到的引脚PD2。(由于题目要求除题目要求需要使用的LED外其他LED都处于熄灭状态,此处特意将所有的LED都初始化以便于管理其他的LED灯)
CubeMX配置:
代码样例
由于G431的所有LED都跟锁存器74HC573连接,因此每次更改LED状态时都需要先打开锁存器,写入数据后再关闭锁存器。
/*****************************************************
* 函数功能:改变所有LED的状态
* 函数参数:
* char LEDSTATE: 0-表示关闭 1-表示打开
* 函数返回值:无
******************************************************/
void changeAllLedByStateNumber(char LEDSTATE)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));
//打开锁存器 准备写入数据
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
//关闭锁存器 锁存器的作用为 使得锁存器输出端的电平一直维持在一个固定的状态
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
/*****************************************************
* 函数功能:根据LED的位置打开或者是关闭LED
* 函数参数:
* uint16_t LEDLOCATION:需要操作LED的位置
* char LEDSTATE: 0-表示关闭 1-表示打开
* 函数返回值:无
******************************************************/
void changeLedStateByLocation(uint16_t LEDLOCATION,char LEDSTATE)
{
HAL_GPIO_WritePin(GPIOC,LEDLOCATION,(LEDSTATE==1?GPIO_PIN_RESET:GPIO_PIN_SET));
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
试题要求的LED显示其条件都比较单一,在满足点亮条件时直接点亮,否则,就直接熄灭即可,至于闪烁的周期控制,可以借助与sysTick中断实现,如果就此再开一个定时器就有点浪费资源了。(虽然小编以前经常这样子干🤣🤣🤣)
小编写的LED工作逻辑函数:
/***************************************
* 函数功能:LED显示逻辑函数
* 函数参数:无
* 函数返回值:无
***************************************/
static void ledWork(void)
{
// 数据界面LED1电量
if(mod == 0)
changeLedStateByLocation(LED1,1);
else
changeLedStateByLocation(LED1,0);
// 切换期间 LED2闪烁
if(LED2Flag && sysCount[1] >= 100)
{
rollbackLedByLocation(LED2);
sysCount[1] = 0;
}
else if(!LED2Flag)
changeLedStateByLocation(LED2,0);
// 锁定模式下 LED3电量
if(lock)
changeLedStateByLocation(LED3,1);
else
changeLedStateByLocation(LED3,0);
}
LCD相关
样例代码
由于LCD的相关代码在官方给的比赛资源数据包中存在,因此,可以直接调用资源包中的.c、.h文件来完成LCD的相关初始化以及显示。这是一个简单的LCD初始化函数,其功能是将LCD显示屏初始化为一个背景色为黑色、字体颜色为白色的屏幕,具体代码如下:
/******************************************************************************
* 函数功能:LCD初始化
* 函数参数:无
* 函数返回值:无
*******************************************************************************/
void lcdInit(void)
{
//HAL库的初始化
LCD_Init();
//设置LCD的背景色
LCD_Clear(Black);
//设置LCD字体颜色
LCD_SetTextColor(White);
//设置LCD字体的背景色
LCD_SetBackColor(Black);
}
在显示时,可以借助于sprintf()
函数将需要显示的数据格式成一个字符串,再在LCD上显示这个字符串。
char temp[20];
sprintf(temp," M=%c ",mTable[M]);
LCD_DisplayStringLine(Line3,(u8*)temp);
为了操作LED与LCD显示方便,不让其相互干扰,小编这里对LCD进行了部分源码改写,使得每次LCD显示时不改变LED的显示状态,具体的方法各位可以点击查看【蓝桥杯】一文解决蓝桥杯嵌入式开发板(STM32G431RBT6)LCD与LED显示冲突问题,并讲述LCD翻转显示。
下面附上小编完成的LCD部分的详细代码:
/***************************************
* 函数功能:LCD显示数据
* 函数参数:无
* 函数返回值:无
***************************************/
static void lcdDisplay()
{
char temp[20];
extern uint32_t cclValue ;
// 数据显示界面
if(mod == 0)
{
LCD_DisplayStringLine(Line1,(u8*)" DATA ");
sprintf(temp," M=%c ",mTable[M]);
LCD_DisplayStringLine(Line3,(u8*)temp);
sprintf(temp," P=%d%% ",pa1Zhan[1]);
LCD_DisplayStringLine(Line4,(u8*)temp);
sprintf(temp," V=%.1f ",V);
LCD_DisplayStringLine(Line5,(u8*)temp);
}
// 参数显示界面
else if(mod == 1)
{
LCD_DisplayStringLine(Line1,(u8*)" PARA ");
sprintf(temp," R=%d ",RK[0]);
LCD_DisplayStringLine(Line3,(u8*)temp);
sprintf(temp," K=%d ",RK[1]);
LCD_DisplayStringLine(Line4,(u8*)temp);
LCD_ClearLine(Line5);
}
// 统计界面
else if(mod == 2)
{
LCD_DisplayStringLine(Line1,(u8*)" RECD ");
sprintf(temp," N=%d ",N);
LCD_DisplayStringLine(Line3,(u8*)temp);
sprintf(temp," MH=%.1f ",MH);
LCD_DisplayStringLine(Line4,(u8*)temp);
sprintf(temp," ML=%.1f ",ML);
LCD_DisplayStringLine(Line5,(u8*)temp);
}
}
(是不是非常简单粗暴。哈哈哈哈)
按键相关
通过查询产品手册知,开发板上的四个按键引脚为PB0~PB2、PA0。
CubeMX配置
代码样例
由于题中涉及到长按键,因此此处将不再使用延时消抖,可以使用三行按键完成长按键与短按键设计,其核心代码就是三行逻辑运算完成消抖等一系列操作,但是在轮询系统中可能会存在漏检的问题。其完整代码如下:
// 声明获取按键的状态值
#define getKeysState() ( HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0) << 0 | HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1) << 1 | \
HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) << 2 | HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) << 3 )
/**** 留给按键外部调用的值 ****/
// 用于判断按键是否处于按下状态 非0-表示按下 0-表示松开 (keyOldState取值有:1-表示B1 2-表示B2 4-表示B3 8-表示B4)
uint8_t keyOldState = 0;
// 用于判断按键是否按下过 非0-表示按下 0-表示松开
uint8_t keyFalling = 0;
// 用于判断按键是否松开 非0-表示按下 0-表示松开
uint8_t keyRising = 0;
/**************************************************************************
* 函数功能:通过逻辑运算获取按键的值 (这玩意可以设计多按键控制某个功能)
* 函数参数:无
* 函数返回值:无
***************************************************************************/
void keyRefresh(void)
{
// 获取按键状态
uint16_t state = getKeysState();
// 对按键的值进行异或处理 一次判断
uint8_t key_temp = 0xFF ^ (0xF0 | state);
/*** 通过逻辑运算处理消抖 **/
// 按下状态
keyFalling = key_temp & (key_temp ^ keyOldState);
// 松开状态
keyRising = ~key_temp & (key_temp ^ keyOldState);
// 保存本次按键的值
keyOldState = key_temp;
}
上述设计只完成了按键的短按设计,但是长按键未涉及到。
下边小编就给大家一个详细的长按键设计:
- 步骤一:判断按键是否按下,如果按下,则获取当前时间
Tstart
,否则函数不做任何处理;- 步骤二:判断按键是否松开,如果松开,则获取当前时间
Tend
,再判断Tend - Tstart
是否符合长按键的条件。如果符合就执行长按键逻辑,否则就执行短按键逻辑。
代码实现如下:
// 按键扫描
keyRefresh();
// 按键按下 并且按键的值等于8也就是B4
if(keyFalling)
uwKeyTick = HAL_GetTick();
if(keyRising && HAL_GetTick()-uwKeyTick>2000)
;// 长按键逻辑
else
;// 短按键逻辑
结合试题的要求,我们可以得到以下的按键逻辑函数:
/**************************************************************************
* 函数功能:按键逻辑函数
* 函数参数:无
* 函数返回值:无
***************************************************************************/
static void keyPro(void)
{
signed char i = 0;
// 按键扫描
keyRefresh();
// 按键按下 并且按键的值等于8也就是B4
if(keyFalling == 8)
uwKeyTick = HAL_GetTick();
switch(keyRising)
{
// 按键B1
case 1:
mod++;
// 每次进去参数界面默认参数为R
if(mod == 1) rkCount = 0;
// 退出参数界面
if(mod != 1 )
{
// 遍历参数 并且刷新
for(i=0;i<2;++i)
if(RKOld[i] != RK[i])
RK[i] = RKOld[i];
}
if(mod == 3) mod = 0;
break;
// 按键B2
case 2:
// 数据界面
if(mod == 0 && LED2Flag == 0 && sysCount[0] >= 5000)
{
sysCount[0] = 0;
LED2Flag = 1;
}
// 参数界面
if(mod == 1)
rkCount ^= 1;
break;
// 按键B3
case 4:
// 参数界面 加1
if(mod == 1)
if(++RKOld[rkCount] == 11) RKOld[rkCount] = 1;
break;
// 按键B4
case 8:
// 数据界面
if(mod == 0 )
// 长按键锁住
if(HAL_GetTick() - uwKeyTick > 2000)
lock = 1;
// 短按键解锁
else
lock = 0;
// 参数界面 减1
else if(mod == 1)
if(--RKOld[rkCount] == 0) RKOld[rkCount] = 10;
break;
// 其他
default:
break;
}
// 延时5秒切换
if(LED2Flag&& sysCount[0]>=5000)
{
M ^= 1;
// 切换次数增加
N++;
LED2Flag = 0;
}
}
ADC相关
CubeMX配置
ADC配置非常简单,大家一起来看看ADC的CubeMX配置方式吧!
样例代码
ADC获取数据时,为了获取ADC数据更加准确,小编采用连续读取10次数据然后取平均值作为本轮ADC数据采集的值!😉😉😉
/*******************************************************************
* 函数功能:获取ADC的值
* 函数参数:
* ADC_HandleTypeDef *hadc:ADC的通道值
* 函数返回值:
* double:转换后的ADC值
*******************************************************************/
double getADC(ADC_HandleTypeDef *hadc)
{
unsigned int value = 0,i = 0;
//开启转换ADC并且获取值
HAL_ADC_Start(hadc);
for(i=0;i<10;++i)
{
HAL_ADC_PollForConversion(hadc,10);
value += HAL_ADC_GetValue(hadc);
}
//ADC值的转换 3.3V是电压 4096是ADC的精度为12位也就是2^12=4096
return value/10*3.3/4096;
}
获取到ADC值后,那么题中折线图的转化问题。折线图分为三段,根据数学知识,我们可以将每一段转换成如下关系:
- 0 < advValue < 1 时,PA1频率为10;
- 1 < advValue < 3 时,PA1频率为 char((adcValue*75- 55)/2+0.5);(因为这里占空比需要取整,因此这里加上一个0.5)
- 3 < advValue 时, PA1频率为85;
具体的代码实现如下:
// ADC获取R37 与 PA1输出的转换
adcV = getADC(&hadc2);
// 处于解锁模式
if(lock == 0)
{
if(0<= adcV && adcV < 1)
pa1Zhan[1] = 10;
else if(3<= adcV)
pa1Zhan[1] = 85;
else
pa1Zhan[1] = (char)((adcV*75- 55)/2+0.5);
// 占空比发生改变应该调整
if(pa1Zhan[0] != pa1Zhan[1] && LED2Flag == 0)
{
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,pa1F[M]*pa1Zhan[1]/100);
pa1Zhan[0] = pa1Zhan[1];
}
}
定时器相关
PWM
CubeMX配置
注意:
- 配置完成后还需要开启中断并且设置NVIC优先级。
- 在系统正式工作前,还需要使用函数
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2)
启动定时器2通道2的PWM功能。
由于题目中还涉及到修改PWM的占空比以及频率,修改的示例代码如下:
- 修改占空比:
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,pa1F[M]*pa1Zhan[1]/100);
pa1F[M]
表示频率,由于题目中的频率也是一个变量,因此每次修改占空比时最好代入频率计算得到。
- 修改频率:
__HAL_TIM_SetAutoreload(&htim2,pa1F[M]);
HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE);
由于频率 = 主频 / (预分频系数+1) / (重装载值+1)
,因此,此处通过修改重装载值来完成频率的修改工作。经过小编测试,每次修改完成定时器的重装载值后最好使用函数HAL_TIM_GenerateEvent(&htim2, TIM_EVENTSOURCE_UPDATE)
更新一个次定时器,函数的参数二表示触发源。
输入捕获
CubeMx配置
在完成定时输入捕获的配置后,还需要在系统正式工作前使用函数HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2)
开启 “定时器的输入捕获功能” ,然后就需要将注意力集中在定时器中断函数中。
样例代码
定时器输入捕获功能关键在于定时器中断,即我们捕获到输入后的处理,下面就给大家看看小编的中断处理函数吧!😁😁😁
/**********************************************定时器输入捕获相关************************************/
uint16_t f = 0;
// 定时器的回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
// 保存TIMx_CCR的值
uint32_t cclValue = 0;
// 定时器3时执行该段
if(htim->Instance == TIM3)
{
cclValue = __HAL_TIM_GET_COUNTER(&htim3);
__HAL_TIM_SetCounter(&htim3, 0);
f = 1000000 / cclValue;
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2);
}
}
中断处理思想如下:
- 将中断执行时定时器的计数值 作为 输入通道的重装载值;
- 通过 频率 与 重装载值 的公式来计算频率;
题目中还要求有频率与速度的关系转换,经过小编的转换,经过转换二者关系可得到下面的代码:
// PA7频率 与 速度V 相关的转换
if(f != fOld)
{
V = (f*2*3.14*RK[0]*1.0)/(100*RK[1]);
fOld = f;
sysCount[2] = 0;
}
( 纯纯的代数转换😁😁😁)
(注:文章中所有的sysCount[]均表示一个毫秒级计数值,计数逻辑函数在sysTick的中断里)
小结
总的来说,虽然这届试题相对比较难,主要难在小功能太多,而且不好实现,如果像13届一样来个串口替换这些小功能,那么串口逻辑比这些就好些很多啊!但是呢,这玩意实现起来逻辑并不复杂,主要是看平常的熟练度 以及 出现bug时的处理能力。
最后,希望各位有打蓝桥杯意愿的同学都能够得偿所愿。🎉🎉🎉
文章福利
下边是小编个人整理出来免费的蓝桥杯嵌入式福利,有需要的童鞋可以自取哟!🤤🤤🤤
省赛:
- 【蓝桥杯嵌入式】第十一届蓝桥杯嵌入式省赛(第二场)程序设计试题及其题解
- 【蓝桥杯嵌入式】第十二届蓝桥杯嵌入式省赛程序设计试题以及详细题解
- 【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式省赛程序设计试题及其详细题解
- 【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式省赛(第二场)程序设计试题及其题解
- 【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式省赛客观题以及详细题解
- 【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式省赛(第二场)客观题以及详细题解
- 【蓝桥杯嵌入式】第十二届蓝桥杯嵌入式省赛客观题及详细题解
国赛:
- 【蓝桥杯嵌入式】第十二届蓝桥杯嵌入式国赛程序设计试题以及详细题解
- 【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式国赛客观题以及详细题解
- 【蓝桥杯嵌入式】第十二届蓝桥杯嵌入式国赛客观题及详细题解
其他:
- 【蓝桥杯嵌入式】第十二届蓝桥杯嵌入式省赛(模拟赛)程序设计题以及详细题解
- 【蓝桥杯嵌入式】第十四届蓝桥杯嵌入式(模拟赛1)客观题及详细题解
- 【蓝桥杯嵌入式】第十四届蓝桥杯嵌入式(模拟赛1)程序设计试题及详细题解
- 【蓝桥杯嵌入式】第十四届蓝桥杯嵌入式(模拟赛2)客观题及详细题解
- 【蓝桥杯嵌入式】第十四届蓝桥杯嵌入式(模拟赛2)程序设计试题及详细题解
- 【蓝桥杯】一文解决蓝桥杯嵌入式开发板(STM32G431RBT6)LCD与LED显示冲突问题,并讲述LCD翻转显示
也欢迎大家留言或私信交流,共同进步哟!😉😉😉