目录

前言

一、思路讲解

二、难点分析

1.双通道采集:

2.频率超限:

3.频率突变:

4.B3按键逻辑处理

三、详细步骤

1.STM32CubeMX的初始化

2.初始化:(这个很重要喔)

3.LCD界面书写:

4.按键逻辑实现:

5.LED功能实现:

6相关变量:

四、代码执行逻辑:

五、结尾

前言

这是我写的第二篇文章。第一篇收到了很多反馈,也认识到了很多朋友。所以,我经过反思之后,决定改正一些问题。希望通过第十五届省赛题目解析,让大家都能明白解题思路。废话不多说,开搞!!


第十五届省赛在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界面书写:

数据界面:

要求:
1.频率单位为 Hz 或 KHz,当某个通道测量到的频率大于 1000Hz 时,其显 示单位自动从 Hz 切换到 KHz。单位为 Hz 时取整数,为 KHz 时保留小数点后 2 位有效数字。K/H 大写,z 小写,
2.周期显示单位为 mS 或 uS,当某个通道的周期大于 1000uS 时,其显示单 位自动从 uS 切换为 mS。单位为 uS 时取整数,为 mS 时保留小数点后 2 位有效数字。u/m 小写,S 大写。(一定要注意大小写喔!)
3.
  

参数界面和显示界面:

4.按键逻辑实现:

要求:

  1. 按键应进行有效的防抖处理,避免出现一次按键动作触发多次功能等情形。 按键动作不应影响数据采集过程和屏幕显示效果,不改变显示字体前景色和背景色。 参数调整应考虑边界值,不出现无效参数。
  2. 突变参数 PD : 100Hz ~ 1000Hz
  3. 超限参数 PH : 1000Hz ~ 10KHz
  4. 校准值参数 PX : -1000Hz ~ 1000Hz
  5. 当前界面下无功能的按键按下,不触发其它界面的功能。

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

中断:

按键扫描和读取频率值,以及频率突破函数

五、结尾

我希望我写的这个文章是对刚开始写省赛的同学有帮助的,回想刚开始接触的时候,也是看其他博主的代码一点一点学出来的,也很艰辛,希望大家能坚持下来,我也希望我也能在我热爱的职业找到我一份属于我自己的工作,作为学生的我,我也希望我能通过自己的能力回报自己的家庭,虽然有点菜,但也想记录自己的学习生涯,希望大家多多支持,我也希望自己能坚持下来。加油,蓝桥备赛人!!如果有地方写的不好,请大家多多指正哈哈!有需要原工程的可以私信我喔。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐