【蓝桥杯嵌入式】【复盘】第13届国赛真题
1. 前言
最近在准备16届的蓝桥杯嵌入式赛道的国赛,打算出一个系列的博客,记录STM32G431RBT6这块比赛用板上所有模块可能涉及到的所有考点,如果有错误或者遗漏欢迎各位大佬斧正。
本系列博客会分为以下两大类:
1.1. 单独模块的讲解
在这部分,我会分享自己总结的各个模块的相关配置、代码书写模板,涉及到的大致框架如下:
这个框架后续可能会不断更新,欢迎各位给出建议。
这一大类相关的文章链接如下(持续补充中):
【蓝桥杯嵌入式】【模块】一、系统初始化-CSDN博客
【蓝桥杯嵌入式】【模块】二、LED相关配置及代码模板-CSDN博客
【蓝桥杯嵌入式】【模块】三、LCD相关配置及代码模板-CSDN博客
1.2. 蓝桥杯各届的真题、模拟题复盘及个人答案
在这一部分,我会分享个人练过的所有题的复盘思路及代码,每篇文章结构如下:
这一大类相关的文章链接如下(持续补充中):
【蓝桥杯嵌入式】【复盘】第13届国赛真题-CSDN博客
以下是本篇博客正文内容:
2. 4t评测结果
4t平台网址:学单片机,上4T - 4T评测网
虽然评分结果不高,但是据很多人反馈,这套题的评测系统有真题,很多人的程序题评测点全过也只能到76.5。
至于为什么博主还有三分没拿到,后面会提到。
3. 个人解答代码
仓库地址:lanqiao/13_true at main · Dukiyaaa/lanqiao
如果不嫌麻烦的话,可以点个star~
4. 重点和易错点
这一套题,根据我个人的做题经历,我认为应该关注的有以下几点:
- pwm的捕获与输出
- 单个adc的多通道采集
- 按键长按
- lcd翻转
4.1. pwm的捕获与输出
4.1.1. pwm的捕获
我个人的核心代码如下:
float pa1_fre;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM2){uint32_t tmp = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_2);if(tmp == 0) return;pa1_fre = 1000000.0 / (tmp + 1);__HAL_TIM_SET_COUNTER(&htim2, 0);HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); // TIM2, ch2, pa1}
}
其实关于pwm捕获频率,网上有很多种解决办法,有比较严谨的求两次捕获差值,有加入了滤波算法的;就我这个代码而言,没有那么复杂,就单纯捕获计数器,捕获之后清0,并且也没加滤波算法。好处是比较方便,不用计算两次捕获的计数器差值,缺点是最开始几次的捕获值可能会不够精准。
不过,这套题的pwm捕获感觉没有什么需要重点关照的,大家按自己平时的风格实现就行。
4.1.2. pwm的输出
我个人的核心代码如下:
void pwm_out_set_fre_duty(uint32_t fre, uint32_t duty)
{uint32_t clock = 80000000;uint32_t div = 10;uint32_t period = clock / div / fre;uint32_t pulse = period * duty / 100;if(pulse > period){pulse = period - 1;}TIM3->ARR = period;TIM3->CCR2 = pulse;HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
}
可以看到,我封装了一个可以输出指定频率的占空比的pwm函数,其内有一些地方需要注意:
1. 在cubemx初始化pwm输出时,我选择了将分频系数设置为10,这是因为在测量时,pwm捕获的频率最低大概在700,而题目有一个分频的要求,分频值最大可以是4,也就是说,pwm输出的最小频率是700 / 4 = 175,所以我们最好要能满足可以输出175hz的pwm。
而pwm输出选用的是TIM3,最大重装载值是65535,要能输出175hz的pwm,预分频系数应该为80000000 / 65535 / 175 = 6.97,选一个比这个数大一点(越小越好,越小的分频系数可以让pwm有更大的调节分辨率)的作为预分频系数初始化就行。
2. 可以看到,我在进行pwm调整时,采用了寄存器直接赋值的办法,这只是我个人的风格,因为在之前的练习中,我发现使用hal库的__HAL_TIM_SET_AUTORELOAD和__HAL_TIM_SET_COMPARE函数效果没有直接寄存器赋值好,会存在pwm输出不正确的情况。
3. 要保证pulse的值小于arr,在之前的练习中(14届省赛),发现如果没有这个限制,常常会出现pwm输出崩溃的情况。
4.1.3. 关于评测结果中的说明
在我的评测结果中,有部分失分是因为pwm的输出不正确:
但是根据题目要求的5%精度,我的输出并没有超过限制,并且其余的pwm输出测试点都是对的,所以目前我不准备改自己的代码,如果各位有发现我代码中导致评测错误的原因,非常欢迎提出!
4.2. 单个adc的多通道采集
在很多练习中,对于adc的要求都是单个adc单通道的,而这套题要求的是单个adc的多通道,即需要同时采集adc2的13和17通道值,于是在cubemx配置以及代码书写上就有了一些小的区分。
4.2.1. cubemx配置adc多通道采集
参考:蓝桥杯嵌入式ADC单通道+多通道(扫描 + 不连续_哔哩哔哩_bilibili
核心,将adc配置成扫描模式+不连续转换
如上图所示,要开启扫描模式和不连续转换模式,并且每次只转换一个值。
在这里配置每个通道的采样周期,越大越好,因为会比较准确。
4.2.2. adc多通道采集代码书写
我的个人核心代码如下:
HAL_ADCEx_Calibration_Start(&hadc2, ADC_SINGLE_ENDED);for(uint8_t i = 0;i < 2;i++){HAL_ADC_Start(&hadc2);tmp_data[i] = (double)HAL_ADC_GetValue(&hadc2) * 3.3 / 4095.0;HAL_Delay(1);}pa4 = tmp_data[1];pa5 = tmp_data[0];
有以下几点需要注意:
1. 首先开启adc的校准,否则可能会出现测不到3.3v的情况。
2. 使用一个循环采集两次,存在数组。需要特别注意的是,第一次采集到的,反而是配置时作为rank2的通道值(这个我也没弄清楚原因,只是从现象上发现是如此,欢迎各位指正)。
如此,pa4和pa5,便为adc采集两个通道的采集值。
4.3. 按键长按
实际这套题案件长按还是比较常规的,我的核心代码如下:
void key_scan(void)
{key_buffer[0].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);key_buffer[1].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);key_buffer[2].key_pin_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);key_buffer[3].key_pin_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);for(uint8_t i = 0; i < 4; i++){switch(key_buffer[i].key_step){case 0:if(key_buffer[i].key_pin_state == 0){key_buffer[i].key_step = 1;} else{key_buffer[i].key_step = 0;}break;case 1:if(key_buffer[i].key_pin_state == 0){key_buffer[i].key_step = 2;key_buffer[i].key_is_down = 1;} else{key_buffer[i].key_step = 0;key_buffer[i].key_is_down = 0;}break;case 2:if(key_buffer[i].key_pin_state == 0){key_buffer[i].key_time++;if(key_buffer[i].key_time >= 100){key_buffer[i].key_is_long = 1;}} else{key_buffer[i].key_step = 0;key_buffer[i].key_is_down = 0;}break;}}
}
在key_scan函数中,我增加了一个按键计数器,当其长按下计数器会不断增加,达到100时,表明按下时间达到了1s(因为我配置的按键计数器中断是10ms触发一次)。
按键业务函数如下:
void key_task(void)
{for(uint8_t i = 0;i < 4;i++){if(key_buffer[i].key_is_down == 1){switch(i){case 0:key_buffer[i].key_is_down = 0;show_num++;if(show_num > 2){show_num = 0;}if(show_num == 2){pa_num = 4;}break;case 1:key_buffer[i].key_is_down = 0;if(show_num == 1){// 频率参数XX++;if(X > 4){X = 1;}eeprom_write(1, X);HAL_Delay(10);} break;case 2:key_buffer[i].key_is_down = 0;if(show_num == 1){// 电压参数YY++;if(Y > 4){Y = 1;}eeprom_write(0, Y);HAL_Delay(10);} break;case 3:if(show_num == 0){key4_flag = 0;key_buffer[i].key_is_down = 0;// 启动丿次电压测量功能,lcd显示的电压数据更新一欿pa5_lock = 0;pa4_lock = 0;}else if(show_num == 1){key4_flag = 0;key_buffer[i].key_is_down = 0;// 切换pwm输出模式为分颿/倍频pwm_output_mode ^= 1;}else if(show_num == 2){key4_flag = 1;}break;}}if(key_buffer[3].key_is_down == 0){if(key4_flag == 1){key4_flag = 0;if(key_buffer[3].key_is_long == 1){// 清空 if(pa_num == 4){first4 = 1;N_4 = 0;A_4 = 0;a4 = 0;T_4 = 0;t4 = 0;H_4 = 0;h4 = 0;pa4_sum = 0;}else{first5 = 1;N_5 = 0;A_5 = 0;a5 = 0;T_5 = 0;t5 = 0;H_5 = 0;h5 = 0; pa5_sum = 0;}}else{if(pa_num == 4){pa_num = 5;}else{pa_num = 4;}}}// 只要key4没按,都需要清0key_buffer[3].key_is_long = 0;key_buffer[3].key_time = 0;}}
}
这里的核心有以下几点:
1.使用了key4_flag标志位来标记按下。由于key4要区分长按和短按,所以在按下的逻辑里仅作标志位处理,将对应的按键时间处理放在松开的逻辑里,如果检测到松开,先判断标志位状态,如果为1,说明是按下后松开,需要进一步判断是长按还是短按;否则,直接跳过。
2. 长按计数清零逻辑,这一步是非常关键的,我在这个bug上卡了很久。一定要注意,只要检测到key4松开,都需要清零其长按计数器以及标志位。
4.4. lcd翻转
lcd的翻转实际不算太难,可以参考我的另一篇博客:【蓝桥杯嵌入式】【模块】三、LCD相关配置及代码模板-CSDN博客
我的核心代码如下:
if(lcd_mode == 1){LCD_WriteReg(R1, 0x0100); // set SS and SM bit //0x0100LCD_WriteReg(R96, 0xA700); // Gate Scan Line 0xA700}else{LCD_WriteReg(R1, 0x0000); // set SS and SM bit //0x0100LCD_WriteReg(R96, 0x2700); // Gate Scan Line 0xA700}
关键就是对R1和R96的赋值处理,如果保持lcd.c文件里原本的赋值,便是正常显示顺序;如果采用注释里后面那个赋值,便可做到上下翻转、左右翻转,联合起来便是屏幕整体翻转。
总结
本文总结了个人在练习13届国赛过程中的复盘及易错点、重难点分析,主要内容在pwm、adc、按键长按、lcd翻转。