蓝桥杯嵌入式第十五届省赛解析
前言这是我写的第二篇文章。第一篇收到了很多反馈,也认识到了很多朋友。所以,我经过反思之后,决定改正一些问题。希望通过第十五届省赛题目解析,让大家都能明白解题思路。废话不多说,开搞!!第十五届省赛在4t测评网成绩如下:有一丢丢瑕疵,小编想不出bug出在哪了,希望各位多加指正,小编我就把问题也放出来了。一、思路讲解这套题目在我看来相对直观,主要是几个核心模块的综合运用,包括输入捕获双通道的采集、对两张
目录
前言
这是我写的第二篇文章。第一篇收到了很多反馈,也认识到了很多朋友。所以,我经过反思之后,决定改正一些问题。希望通过第十五届省赛题目解析,让大家都能明白解题思路。废话不多说,开搞!!
第十五届省赛在4t测评网成绩如下:![]()
有一丢丢瑕疵,小编想不出bug出在哪了,希望各位多加指正,小编我就把问题也放出来了。

一、思路讲解
这套题目在我看来相对直观,主要是几个核心模块的综合运用,包括输入捕获双通道的采集、对两张图片的理解,以及其他一些常规操作。解题思路清晰明了:
首先,利用STM32CubeMX完成所有必要的初始化配置,随后进行输入部分的编写,包括按键的初始化(特别注重消抖处理和长按功能的实现)以及双通道采集的设置。接着,转向输出部分,完成LCD屏的移植和LED的初始化,至此底层逻辑框架已搭建完毕。
接下来,根据题目要求,精心设计LCD显示屏的各个界面,确保界面布局合理、信息展示清晰。
然后,面对最具挑战性的部分——实现两张图片中的要求和效果。这一步需要深入理解图片背后的逻辑,并巧妙地将这些逻辑转化为代码,可能需要多次调试和优化,以达到最佳效果。
实现按键的逻辑处理,确保按键能够准确识别并响应用户的操作。
最后,利用LED收尾。
总的来说,这套题目主要考察了细节处理和周全考虑的能力,而提升这些能力的最佳途径就是多加练习。通过不断实践,我们可以更加熟练地掌握这些技能,从而在解题过程中游刃有余。
二、难点分析
1.双通道采集:
小编对频率采集其实也花了不少时间,频率和周期采集的实现逻辑,其逻辑是先在STM32CubeMX中将TIM8定时器及其通道1和TIM6定时器及其通道1已配置为输入捕获模式,然后在中断回调函数中捕获两次值,在计算时间差,他有两种情况:如果第二个捕获值大于第一个,直接相减得到时间差uwDiffCapture(周期)。;如果第二个捕获值小于第一个(由于计数器溢出),则使用定时器计数器的最大值0xFFFF(65535)进行计算,得到实际的时间差。这样就把周期计算出来了。
频率的计算:使用公式频率 = 1 / 周期,其中周期由时间差uwDiffCapture_PA15和定时器时钟频率决定。频率uwFrequency计算为1000000 / uwDiffCapture。(定时器时钟频率:80mkz/80)这样考虑了计数器溢出的情况。
通道采集代码:if(htim->Instance == TIM8){ if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { if(uhCaptureIndex_PA15 == 0) { /* 获取第一个输入捕获值 */ uwIC2Value1_PA15 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uhCaptureIndex_PA15 = 1; } else if(uhCaptureIndex_PA15 == 1){ /* 获取第二个输入捕获值 */ uwIC2Value2_PA15 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); /* 捕获值计算 */ if (uwIC2Value2_PA15 > uwIC2Value1_PA15){ uwDiffCapture_PA15 = (uwIC2Value2_PA15 - uwIC2Value1_PA15); } else if (uwIC2Value2_PA15 < uwIC2Value1_PA15) { /* 0xFFFF是TIM_CCRx的最大值 */ uwDiffCapture_PA15 = ((0xFFFF - uwIC2Value1_PA15) + uwIC2Value2_PA15) + 1; } /* 频率计算:本例中TIMx(TIM8)由APB2Clk时钟驱动 */ uwFrequency_PA15 = 1000000 / uwDiffCapture_PA15; uhCaptureIndex_PA15 = 0; } } }
2.频率超限:
要求:系统内置参数 PH,当通道 A 或 B 的频率大于 PH 时,对应的超
限次数 NHA/B 加 1。
解题实路:
通过题目分析得出他有两个条件:一频率大于 PH,二上升趋势才加一。所以可以这么写用取两次频率值,上一次频率的值小于PH,下一次值大于PH,这样就符合要求了。其代码附上:
频率超限:
/*系统内置参数 PH,当通道 A 或 B 当时频率大于 PH 时且上一次频率小于 PH,对应的超限次数 NHA/B+1*/ // PH_out函数定义,用于检测两个输出频率是否超过某个阈值,并更新相应的计数器 void PH_out(void) { // 检查uwFrequency_PA15_Out是否超过了Frequency_PH阈值, // 并且确保上一次的值uwFrequency_PA15_old没有超过该阈值 // 如果条件满足,说明频率从低于阈值变为了高于阈值,因此增加Frequency_NHA计数器 if(uwFrequency_PA15_Out > (long int)Frequency_PH && uwFrequency_PA15_old < (long int)Frequency_PH) Frequency_NHA++; // 更新uwFrequency_PA15_old为当前的值uwFrequency_PA15_Out, // 以便下次调用时可以比较新的频率变化 uwFrequency_PA15_old = uwFrequency_PA15_Out; // 检查uwFrequency_PB4_Out是否超过了Frequency_PH阈值, // 并且确保上一次的值uwFrequency_PB4_old没有超过该阈值 // 如果条件满足,同样增加Frequency_NHB计数器 if(uwFrequency_PB4_Out > (long int)Frequency_PH && uwFrequency_PB4_old < (long int)Frequency_PH) Frequency_NHB++; // 更新uwFrequency_PB4_old为当前的值uwFrequency_PB4_Out uwFrequency_PB4_old = uwFrequency_PB4_Out; }
3.频率突变:
要求:时间窗口内(3 秒),某个通道采集到的频率最大值 fMAX和最 小值 fMIN的差值大于 PD 时,该通道频率突变次数 NDA/B 加 1。

解题实路:要求在3秒内的时间段里面计算出最大值和最小值之差>PD;所以就用定时器定100ms的中断在中断回调函数定义一个30长的数组frq1_array[30],每次让frq1_array[索引]等与新的频率值,然后索引+1,当索引等于30时,计算最大值,最小值(冒泡排序)得出差值,跟PD判断:最后重置索引。其代码附上:
频率突变:
void recd_proc(void) { // 定义两个静态浮点数组,用于存储最近30次测量的频率值 // 静态数组意味着它们在函数调用之间保持其值 static float frq1_array[30] = {0}; // 存储第一个频率信号的数组 static float frq2_array[30] = {0}; // 存储第二个频率信号的数组 // 定义一个静态指针,用于追踪当前数组中的位置 static uint8_t array_ptr = 0; // 定义变量以存储最大值和最小值 float max1_num, min1_num, max2_num, min2_num; // 将最新的频率值添加到数组中 frq1_array[array_ptr] = uwFrequency_PA15_Out; // 假设这是从某个引脚或接口获取的频率值 frq2_array[array_ptr] = uwFrequency_PB4_Out; // 假设这是从另一个引脚或接口获取的频率值 // 更新数组指针,准备下一次存储 array_ptr++; // 当数组填满30个值时,进行以下处理 if (array_ptr == 30){ // 在两个数组中分别找到最大值和最小值 max1_num = find_max_num(frq1_array, 30); // 调用函数找到第一个数组中的最大值 min1_num = find_min_num(frq1_array, 30); // 调用函数找到第一个数组中的最小值 max2_num = find_max_num(frq2_array, 30); // 调用函数找到第二个数组中的最大值 min2_num = find_min_num(frq2_array, 30); // 调用函数找到第二个数组中的最小值 // 计算两个频率信号的最大频率偏差,并与预设阈值进行比较 // 如果偏差超过阈值,则相应的计数器增加 if ((max1_num - min1_num) > Frequency_PD) Frequency_NDA++; // 第一个信号的偏差计数器 if ((max2_num - min2_num) > Frequency_PD) Frequency_NDB++; // 第二个信号的偏差计数器 // 重置数组指针和数组内容,为下一轮测量做准备 array_ptr = 0; for (int i = 0; i < 30; i++){ frq1_array[i] = 0; frq2_array[i] = 0; } } }
4.B3按键逻辑处理
要求:B3:定义为“切换/清零”按键。

解题实路:这个按键其实常规,分两种长按与短按,那么怎么处理呢?先分3种情况既3种界面下的逻辑不同,先实现要求1和要求2,可以看出他是在短按的情况下,就很常规了,直接判断是否在当前界面与按键是否是短按就符合要求了,实现要求3,其实只要判断是是否是长按就行。我们其实按键识别分3个步骤:1 先识别是否按下,2 消抖,3判断按下时长(这里可以分辨长短按键的情况)用状态机就能实现,代码如下:
按键识别:
// 读取四个按键的状态,分别连接到GPIOB的PIN0, PIN1, PIN2和GPIOA的PIN0 keys[0].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); // 读取按键1状态 keys[1].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1); // 读取按键2状态 keys[2].key_sta = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2); // 读取按键3状态 keys[3].key_sta = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); // 读取按键4状态 // 判断是哪个定时器触发了这个回调函数 if(htim->Instance == TIM6){ // 对每个按键进行状态检测 for(uint8_t i=0;i<4;i++){ // 根据按键的当前步骤(key_step)进行不同的处理 switch (keys[i].key_step) { case 0: // 初始状态,等待按键按下 // 如果按键被按下(GPIO_PIN_RESET,假设低电平表示按下) if(keys[i].key_sta == GPIO_PIN_RESET) keys[i].key_step = 1; // 进入下一步,等待按键保持按下状态 break; case 1: // 确认按键保持按下状态 // 如果按键仍然保持按下 if(keys[i].key_sta == GPIO_PIN_RESET){ keys[i].key_step = 2; // 进入下一步,开始计时 keys[i].key_time = 1; // 计时器初始化为1(单位根据系统时钟和TIM6配置决定) } else{ // 如果按键被释放,则重置状态 keys[i].key_step = 0; keys[i].key_time = 0; } break; case 2: // 计时阶段,判断是短按还是长按 // 如果按键被释放(GPIO_PIN_SET,假设高电平表示释放) if(keys[i].key_sta == GPIO_PIN_SET){ keys[i].key_step = 0; // 重置到初始状态 // 根据按键保持按下的时间判断是短按还是长按 if(keys[i].key_time < 70) // 假设70个单位时间是短按的阈值 keys[i].single_flag = 1; // 设置短按标志 if(keys[i].key_time > 100) // 假设100个单位时间是长按的阈值 keys[i].long_flag = 1; // 设置长按标志 } else{ // 如果按键仍然保持按下,继续计时 keys[i].key_time++; } break; } } }逻辑实现:
if(keys[2].single_flag==1){ keys[2].single_flag=0; if(lcd_mode==1) { /*在参数界面下,按下 B3 按键,切换当前选择的参数,切换模式如图 7 * 所示。*/ if(++LCD_D_H_X==3) LCD_D_H_X=0; } else if(lcd_mode==0)/*在数据界面下,按下 B3 按键,切换频率或周期显示模式。*/ LCD_F_T^=1; }/*在记录界面下,长按 B3 按键超过 1 秒后松开按键,清零该界面下的所有记录值,短按无效。*/ if(keys[2].long_flag==1) { keys[2].long_flag=0; keys[2].key_time=0; if(lcd_mode==2){ Frequency_NDA=0;//A 通道频率突变次数 NDA清零 Frequency_NDB=0;//B 通道频率突变次数 NDB清零 Frequency_NHA=0;//A 通道频率超限次数 NHA清零 Frequency_NHB=0;//B 通道频率超限次数 NHB清零 }
三、详细步骤
注:我觉得自己要手敲代码才有进步,所以我用截图来展示详细步骤,希望大家能都有进步哈。
1.STM32CubeMX的初始化

引脚分布

输入捕获

定时器配置
2.初始化:(这个很重要喔)

3.LCD界面书写:
数据界面:

参数界面和显示界面:

4.按键逻辑实现:
要求:
- 按键应进行有效的防抖处理,避免出现一次按键动作触发多次功能等情形。 按键动作不应影响数据采集过程和屏幕显示效果,不改变显示字体前景色和背景色。 参数调整应考虑边界值,不出现无效参数。
- 突变参数 PD : 100Hz ~ 1000Hz
- 超限参数 PH : 1000Hz ~ 10KHz
- 校准值参数 PX : -1000Hz ~ 1000Hz
- 当前界面下无功能的按键按下,不触发其它界面的功能。
1.B4逻辑要考虑清屏以及标志位的重置(这个小篇都被坑过)

2.B2,B1逻辑实现(要考虑边界问题喔!)

5.LED功能实现:
要求:(小编不想敲字了,就贴图了哈哈)

值得注意是要电灯的适合要考虑其他灯是否受影响。所以小编我就写了一个led不发生冲突的函数库,其原理是与或(至于为什么请ai或者百度哈哈)led库奉上,希望给我个赞谢谢哈哈
LED点灯函数库:
#include "led.h" uint8_t led[9]={0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; uint8_t led_sta; void LED_Disp(uint8_t dsled,_Bool flag) { if(flag) led_sta=(led_sta&(~led[dsled]))|led[dsled]; else led_sta=(led_sta&(~led[dsled])); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_All, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOC, led_sta<<8, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); }
LED逻辑:

6相关变量:
主要库:
/*----------------lcd模式切换-------------*/ uint8_t lcd_mode=0;//显示功能切换 _Bool LCD_F_T=0;//周期与频率显示切换 uint8_t LCD_D_H_X=0; /*----------------标志位----------------*/ /*----------------按键参数变量-------------*/ extern KeyInputypeDef keys[4]; _Bool led1=1; /*----------------输入捕获脉冲-------------*/ /* Frequency Value */ extern uint32_t uwFrequency_PA15;//PA15频率 extern uint32_t uwFrequency_PB4;//PB4频率 /*----------------数据区----------------*/ /*初始状态说明 请严格按照下列要求设计作品上电后的初始状态: 1) 处于数据显示界面(频率)。 2) PD 默认参数:1000Hz。 3) PH 默认参数:5000Hz。 4) PX 默认参数:0。*/ uint32_t Frequency_PD=1000;//突变参数 PD uint32_t Frequency_PH=5000;//超限参数 PH int Frequency_PX=0;//校准值参数 PX uint8_t Frequency_NDA=0;//A 通道频率突变次数 NDA uint8_t Frequency_NDB=0;//B 通道频率突变次数 NDB uint8_t Frequency_NHA=0;//A 通道频率超限次数 NHA uint8_t Frequency_NHB=0;//B 通道频率超限次数 NHB long int uwFrequency_PA15_Out;//PA15频率校准后 long int uwFrequency_PB4_Out;//PB4频率校准后 long int uwDiffCapture_PA15_Out;//PA15频率校准后 long int uwFrequency_PA15_old;//上次测量的值 long int uwDiffCapture_PB4_Out;//PB4频率校准后 long int uwFrequency_PB4_old;//上次测量的值 /*----------------时间变量----------------*/ uint32_t lcd_tim_100ms;//LCD 显示数据刷新时间 0.1 秒,显示效果清晰、稳定,无噪点。输入库:
/*----------------按键参数变量-------------*/ KeyInputypeDef keys[4]; /*----------------输入捕获脉冲-------------*/ /*pa15相关变量*/ /* Captured ValuesPA15 */ uint32_t uwIC2Value1_PA15 = 0; uint32_t uwIC2Value2_PA15 = 0; uint32_t uwDiffCapture_PA15 = 0; /* Capture index */ uint16_t uhCaptureIndex_PA15 = 0; /* Captured ValuesPB4 */ uint32_t uwIC2Value1_PB4 = 0; uint32_t uwIC2Value2_PB4 = 0; uint32_t uwDiffCapture_PB4 = 0; /* Capture index */ uint16_t uhCaptureIndex_PB4 = 0; /* Frequency Value */ uint32_t uwFrequency_PB4 = 0; /* Frequency Value */ uint32_t uwFrequency_PA15 = 0;按键结构体:
typedef struct { GPIO_PinState key_sta; uint8_t key_step; _Bool single_flag; _Bool long_flag; uint32_t key_time; } KeyInputypeDef;
四、代码执行逻辑:
死循环中的逻辑
void My_pro(void)
{
key_pro();
lcd_pro();//每100ms进行一次
led_pro();
}
中断:
按键扫描和读取频率值,以及频率突破函数
五、结尾
我希望我写的这个文章是对刚开始写省赛的同学有帮助的,回想刚开始接触的时候,也是看其他博主的代码一点一点学出来的,也很艰辛,希望大家能坚持下来,我也希望我也能在我热爱的职业找到我一份属于我自己的工作,作为学生的我,我也希望我能通过自己的能力回报自己的家庭,虽然有点菜,但也想记录自己的学习生涯,希望大家多多支持,我也希望自己能坚持下来。加油,蓝桥备赛人!!如果有地方写的不好,请大家多多指正哈哈!有需要原工程的可以私信我喔。
更多推荐

所有评论(0)