1. 赛题核心要点与开发环境搭建

刚刚参加完第十五届蓝桥杯嵌入式省赛,不得不说今年的题目设计确实比去年友好了不少。作为过来人,我最大的感受就是:掌握好几个关键技术点,省赛真的不难拿分。整个赛题围绕频率测量、参数设置和状态统计三个核心功能展开,用的还是我们熟悉的STM32F103RBT6平台。

先说说开发环境吧。我平时习惯用STM32CubeMX + Keil MDK的组合,CubeMX版本是6.9.1,Keil是5.38。硬件平台是官方指定的CT117E开发板,这块板子的外设资源我们都挺熟悉的:4个LED、4个独立按键、LCD显示屏,还有那些常用的通信接口。

在CubeMX配置时,有几个关键点需要注意。首先是时钟树配置,主频要设置为72MHz,这是比赛规定的标准频率。然后是定时器的配置,TIM6用作10ms的基础定时器,负责按键扫描和时间窗口处理;TIM2和TIM3配置为输入捕获模式,分别用于两个通道的频率测量。引脚分配方面,PB4用作TIM3_CH1,PA15用作TIM2_CH1,这些都是题目明确要求的。

记得一定要开启定时器中断,特别是输入捕获定时器的高级功能。在NVIC设置中,要给定时器中断分配合适的优先级。我一般把TIM6的中断优先级设得低一些,因为它的处理任务相对简单;而输入捕获定时器的中断优先级要高一些,确保频率测量的准确性。

2. 输入捕获频率测量实战解析

频率测量是整个赛题最核心的部分,也是很多同学容易踩坑的地方。输入捕获的原理其实很简单:通过测量两个上升沿(或下降沿)之间的时间间隔来计算频率。但在实际编程中,有几个细节需要特别注意。

首先来看定时器的配置。TIM2和TIM3都设置为输入捕获模式,时钟源选择内部时钟,分频系数设置为80-1,这样得到的计数频率是1MHz(72MHz/72=1MHz)。自动重装载值设为0xFFFF,这样可以获得最大的测量范围。触发源选择直接触发,滤波时间根据实际信号质量来设置,一般设为0就可以了。

在代码实现上,关键是要写好中断回调函数。当捕获到信号边沿时,硬件会自动记录当前的计数值,我们需要在中断服务函数中读取这个值并计算频率。具体实现如下:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2)
    {
        if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
        {
            tim2_val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            tim2_val2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
            __HAL_TIM_SetCounter(htim, 0);
            frq_A = ((80000000/80)/tim2_val1) + PX;
            if(frq_A > 20000) frq_A = 20000;
            HAL_TIM_IC_Start(htim, TIM_CHANNEL_1);
            HAL_TIM_IC_Start(htim, TIM_CHANNEL_2);
        }
    }
    // TIM3的处理逻辑类似
}

这里有个很重要的细节:每次捕获完成后要重新启动输入捕获通道,否则下次就捕获不到信号了。另外,记得在计算频率后要重置计数器,这样才能保证每次测量都是准确的。

在实际调试中,我发现信号质量对测量结果影响很大。如果输入信号有毛刺或者噪声,测量结果会跳动得很厉害。这时候可以适当增加滤波时间,或者在软件层面做一些平滑处理。比如连续测量多次取平均值,或者设置一个合理的阈值过滤异常值。

3. 多界面设计与状态管理技巧

界面设计看似简单,但实际上很考验编程功底。题目要求实现三个界面:数据展示界面、参数设置界面和统计界面。每个界面都有不同的显示内容和交互逻辑。

我先定义了一个界面状态变量face,0表示数据界面,1表示参数界面,2表示统计界面。通过按键4来切换界面,每次切换时都要重置一些状态变量,确保界面显示的正确性。

在数据界面中,又要支持频率和周期两种显示模式。我用了display_val变量来控制显示内容,0显示频率,1显示周期。这里要注意单位转换:当频率小于1000Hz时用Hz单位,大于1000Hz时转换为KHz;周期小于1000us时用us单位,大于1000us时转换为ms。

参数界面需要实现三个参数的设置:PD、PH和PX。通过PNUM变量来选择当前要设置的参数,按键1和按键2分别实现参数增减。这里有个细节:参数增减要有上下限限制,比如PD只能在100-1000之间变化,每次步进100。

统计界面相对简单,主要显示四个统计值:NDA、NDB、NHA、NHB。但这些统计值背后的逻辑比较复杂,需要实时更新。

在界面切换时,LED的状态也要相应变化。比如在数据界面时LED1亮,其他界面LED1灭。这种细节虽然小,但却是评分的关键点。

4. 时间窗口处理与频率突变检测

时间窗口处理是本题的一个难点,要求在每个3秒的时间窗口内统计频率的最大最小值,并计算差值。如果差值超过设定的PD值,相应的统计计数器就要加1。

我的实现思路是这样的:用TIM6产生10ms的中断,在中断中累加时间窗口计数器Timewindow。当计数器小于300时(3秒),持续更新频率的最大最小值;当计数器达到300时,计算频率差值并与PD比较,然后重置相关变量。

具体代码实现如下:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM6)
    {
        // 按键处理等其他逻辑
        
        // 3秒时间窗口内获取最大最小值
        if(Timewindow < 300)
        {
            Timewindow++;
            // 获取最大值
            if(frqA_MAX < frq_A) frqA_MAX = frq_A;
            if(frqB_MAX < frq_B) frqB_MAX = frq_B;
            // 获取最小值
            if(frqA_MIN > frq_A || frqA_MIN == 0) frqA_MIN = frq_A;
            if(frqB_MIN > frq_B || frqB_MIN == 0) frqB_MIN = frq_B;
        }
        // 比较参数设置界面的参数
        else if(Timewindow >= 300)
        {
            frq_NDA = frqA_MAX - frqA_MIN;
            frq_NDB = frqB_MAX - frqB_MIN;
            if(frq_NDA > PD) NDA++;
            if(frq_NDB > PD) NDB++;
            
            // 重置变量
            frqA_MAX = 0; frqA_MIN = 0;
            frqB_MAX = 0; frqB_MIN = 0;
            Timewindow = 0;
        }
    }
}

这里有几个需要注意的地方:一是最小值的初始值处理,我用了|| frqA_MIN == 0来确保第一次能正确获取最小值;二是时间窗口结束后一定要重置所有相关变量,否则会影响下一个周期的统计。

在实际测试中,我发现频率突变检测对信号稳定性要求很高。如果输入信号本身就有很大噪声,那么统计结果就会不准确。这时候可能需要在算法层面加入一些滤波处理,比如忽略突变的异常值,或者采用滑动平均等方法来平滑数据。

5. 频率超限检测的状态机实现

频率超限检测要求频率值超过PH值时计数,但需要频率回落到PH以下后才能再次计数。这个需求最适合用状态机来实现,我定义了两个状态标志位PH_A和PH_B。

具体逻辑是这样的:当状态标志为1且频率大于PH时,说明是第一次超限,这时计数并清零状态标志;当状态标志为0且频率小于PH时,重置状态标志,为下一次超限检测做准备。

代码实现如下:

// 通道A的超限检测
if(PH_A == 1)
{
    if(frq_A > PH)
    {
        LED_Set(2, 1);  // 超限指示灯亮
        NHA++;
        PH_A = 0;
    }
}
else if(frq_A < PH)
{
    PH_A = 1;
    LED_Set(2, 0);  // 超限指示灯灭
}

// 通道B的逻辑类似

这种状态机的实现方式既简洁又高效,完全满足了题目的要求。在实际应用中,状态机是嵌入式开发中非常常用的编程模式,特别适合处理这种有状态转换的逻辑。

我还添加了LED指示功能,超限时相应的LED亮起,恢复正常后LED熄灭。这样不仅满足了题目要求,也为调试提供了直观的视觉反馈。

6. 按键处理与参数调节逻辑

按键处理虽然基础,但实现起来也有不少讲究。题目要求支持短按和长按两种操作,不同界面下按键功能也不同。

我定义了一个按键结构体数组,用来记录每个按键的状态:

struct KEY {
    u8 short_flag;  // 短按标志
    u8 long_flag;   // 长按标志
    // 其他状态变量...
} keys[5];

在TIM6的中断中调用按键扫描函数,每10ms检测一次按键状态。检测到按键按下时,根据按下的时间长短设置相应的标志位。

按键功能分配如下:按键1和按键2在参数界面用于增减参数值,按键3短按切换显示模式或参数选择,长按清零统计值,按键4用于切换主界面。

参数调节时有几个边界处理需要注意:一是参数值不能超出设定范围,二是参数变化步长要符合题目要求。比如PD值只能在100-1000之间变化,每次变化100;PX值范围是-1000到1000,也是每次变化100。

在参数调节时,还要实时更新LCD显示,让用户能看到参数的变化。我是在主循环中检测到按键标志位变化后,立即更新显示和参数值。

7. 调试技巧与常见问题排查

在实际开发过程中,调试是最花时间的部分。我总结了一些实用的调试技巧,希望能帮大家少走弯路。

首先是LCD显示调试法,这是最直接的方法。在关键位置添加调试信息输出,可以实时查看变量值和程序状态。比如在频率测量部分,可以把原始计数值和计算后的频率值都显示出来,这样就能快速定位问题是出在测量还是计算环节。

其次是LED指示灯法,用不同的LED闪烁来表示程序运行到哪个阶段。比如在初始化完成后让LED1常亮,进入主循环后让LED2闪烁,发生错误时让LED3快速闪烁等等。

如果遇到频率测量不准的问题,首先要检查定时器配置是否正确。分频系数、计数模式、触发方式这些参数都要仔细核对。其次要检查信号源是否稳定,可以用示波器看看实际波形怎么样。

还有一个常见问题是按键响应不灵敏,这往往是去抖处理没做好。硬件去抖和软件去抖都要做,软件去抖一般设置10-20ms的延时比较合适。

如果程序出现死机或者跑飞,很可能是堆栈溢出或者中断处理不当。可以适当增大堆栈大小,检查中断优先级设置是否合理,避免中断嵌套导致的问题。

最后提醒大家,一定要预留足够的调试时间。编程可能只花一半时间,调试往往要花另一半时间。提前规划好时间分配,不要到最后来不及调试。

更多推荐