1. 赛题解析与开发环境搭建

参加蓝桥杯嵌入式国赛的同学们都知道,拿到赛题后的第一步不是急着写代码,而是要全面理解题目要求。第十二届国赛题目看似常规,但暗藏玄机。题目要求实现的功能模块包括LED控制、LCD显示、按键处理、串口通信、光照传感器采集和定时器输入捕获等。这些模块单独实现都不难,难点在于如何让它们协同工作。

我建议先用CubeMX快速搭建基础工程框架。新建工程时选择STM32G431RBT6芯片型号,这是官方指定的竞赛开发板主控。时钟树配置要特别注意,系统时钟建议设置为170MHz,这个频率既能满足性能需求又不会导致芯片过热。外设初始化时,建议按照以下顺序配置:

  1. 系统核心(SYS)中的调试接口
  2. 时钟配置(RCC)
  3. GPIO引脚分配
  4. 定时器模块
  5. ADC模块
  6. USART串口

在配置LED相关引脚时,很多同学会忽略锁存器74HC573的控制。实际开发中,PC8-PC15用于LED数据输出,PD2作为锁存信号线。每次更新LED状态时,必须严格按照"打开锁存-写入数据-关闭锁存"的时序操作,否则会出现显示异常。

2. 模块驱动开发实战

2.1 LED驱动设计与优化

LED模块看似简单,但在资源受限的竞赛环境中需要特别注意效率。我推荐使用位带操作来提升控制效率。比如对GPIOC的写操作可以这样优化:

#define LED_PORT ((GPIO_TypeDef*)GPIOC_BASE)
#define LED_PINS (GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15)

void updateLEDs(uint8_t states) {
    LED_PORT->ODR = (LED_PORT->ODR & ~LED_PINS) | ((states << 8) & LED_PINS);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

这种写法比HAL库的标准接口执行速度快3-5倍,在需要频繁更新LED状态的场景下优势明显。另外,建议将所有LED状态集中管理,定义一个全局变量存储当前LED状态,避免频繁操作硬件。

2.2 LCD显示优化技巧

LCD显示最容易出现的问题是刷新闪烁和与LED的冲突。实测发现,直接调用官方提供的LCD驱动会导致LED异常,这是因为驱动中修改了共享的GPIO配置。我的解决方案是重写LCD_WriteReg函数:

void LCD_WriteReg(uint8_t LCD_Reg, uint16_t LCD_RegValue) {
    uint32_t currentLEDs = GPIOC->ODR & 0xFF00;
    
    LCD->LCD_REG = LCD_Reg;
    LCD->LCD_RAM = LCD_RegValue;
    
    GPIOC->ODR = (GPIOC->ODR & 0x00FF) | currentLEDs;
}

这个技巧的关键是在LCD操作前后保存和恢复LED状态。另外,频繁刷新整个屏幕会导致明显的闪烁,建议采用局部刷新策略,只更新变化的数据区域。

3. 关键功能实现详解

3.1 定时器输入捕获的精准实现

定时器输入捕获是本届赛题的技术难点,主要用于测量PWM信号的频率和占空比。配置TIM16和TIM3为输入捕获模式时,要注意以下几点:

  1. 分频系数设置为170-1,这样计数器时钟为1MHz,每个计数对应1μs
  2. 自动重装载值设为最大值0xFFFF
  3. 捕获边沿要正确设置,频率测量用上升沿,占空比测量需要双沿触发

实测中发现,标准的两次捕获(上升沿+下降沿)方法在测量占空比时误差较大。经过多次测试,我改进为三次捕获法:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if(htim->Instance == TIM16) {
        uint32_t cnt = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        
        if(captureCount == 0) { // 第一次上升沿
            rise1 = cnt;
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
        } 
        else if(captureCount == 1) { // 下降沿
            fall = cnt;
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
        }
        else { // 第二次上升沿
            rise2 = cnt;
            dutyCycle = (float)(fall - rise1) / (rise2 - rise1) * 100;
            period = rise2 - rise1; // 单位μs
            frequency = 1000000.0f / period;
            
            // 重置状态
            __HAL_TIM_SET_COUNTER(htim, 0);
            captureCount = -1;
        }
        
        captureCount++;
        HAL_TIM_IC_Start_IT(htim, TIM_CHANNEL_1);
    }
}

这种方法测量误差可以控制在1%以内,完全满足赛题要求。

3.2 串口通信的可靠处理

赛题要求实现不定长串口数据接收和协议解析,这里我采用中断+DMA的方案。CubeMX中配置USART1为异步模式,波特率115200,开启全局中断和DMA接收。

关键实现步骤如下:

  1. 初始化时启动DMA接收:
HAL_UART_Receive_DMA(&huart1, uartBuffer, UART_BUF_SIZE);
  1. 实现空闲中断检测:
void USART1_IRQHandler(void) {
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        HAL_UART_DMAStop(&huart1);
        
        uint32_t len = UART_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        processUartData(uartBuffer, len);
        
        HAL_UART_Receive_DMA(&huart1, uartBuffer, UART_BUF_SIZE);
    }
    HAL_UART_IRQHandler(&huart1);
}
  1. 数据解析函数要处理粘包和异常情况:
void processUartData(uint8_t *data, uint32_t len) {
    static uint8_t cmdBuffer[32];
    static uint8_t cmdIndex = 0;
    
    for(uint32_t i=0; i<len; i++) {
        if(data[i] == '\n' || cmdIndex >= sizeof(cmdBuffer)-1) {
            cmdBuffer[cmdIndex] = '\0';
            parseCommand(cmdBuffer);
            cmdIndex = 0;
        } else {
            cmdBuffer[cmdIndex++] = data[i];
        }
    }
}

这种实现方式既节省CPU资源,又能可靠处理各种长度的数据帧。

4. 系统集成与调试技巧

4.1 多任务调度策略

在整合所有功能模块时,合理的任务调度至关重要。我推荐采用时间片轮询架构,将不同任务分配到不同时间周期:

void mainLoop(void) {
    uint32_t tick = HAL_GetTick();
    
    // 1ms任务
    if(tick != lastTick) {
        lastTick = tick;
        
        // 10ms任务
        if(tick % 10 == 0) {
            keyScan();
            ledUpdate();
        }
        
        // 50ms任务
        if(tick % 50 == 0) {
            lcdRefresh();
        }
        
        // 100ms任务
        if(tick % 100 == 0) {
            sensorUpdate();
            dataProcess();
        }
        
        // 1000ms任务
        if(tick % 1000 == 0) {
            systemMonitor();
        }
    }
}

这种架构既保证了实时性,又避免了复杂RTOS带来的资源开销。在实际测试中,系统运行稳定,各任务响应及时。

4.2 调试与性能优化

系统联调阶段最容易遇到的问题是资源冲突和性能瓶颈。我总结了几点实用调试技巧:

  1. 使用GPIO引脚作为调试探头,在关键代码段前后拉高拉低引脚,用逻辑分析仪测量执行时间
  2. 合理利用STM32的硬件调试功能,设置数据观察点(DWT)监测关键变量
  3. 优化printf重定向,使用半主机模式或ITM输出调试信息
  4. 对于耗时操作,可以考虑使用DMA解放CPU资源

内存优化方面,建议:

  • 将频繁访问的变量定义为register类型
  • 使用const修饰不变量,让编译器将其放入Flash
  • 对于大型数组,考虑使用__attribute__((section(".ccmram")))将其放入核心耦合内存

在竞赛最后的调试阶段,一定要预留足够时间进行整体测试。建议按照以下顺序验证:

  1. 单独测试每个功能模块
  2. 两两组合测试可能存在冲突的模块
  3. 全功能集成测试
  4. 压力测试(长时间运行+极端条件)

5. 常见问题解决方案

在开发过程中,我遇到了几个典型问题,这里分享解决方案:

  1. LED与LCD显示冲突:根本原因是LCD驱动修改了GPIOC的配置寄存器。解决方法是在每次LCD操作前后保存和恢复GPIOC->MODER、GPIOC->OTYPER等寄存器值。

  2. 定时器捕获数据不稳定:这个问题通常是由于信号抖动引起的。我在硬件上增加了RC滤波电路,软件上实现了中值滤波算法:

uint32_t medianFilter(uint32_t newVal) {
    static uint32_t filterBuf[5] = {0};
    static uint8_t index = 0;
    
    filterBuf[index++] = newVal;
    if(index >= 5) index = 0;
    
    uint32_t temp[5];
    memcpy(temp, filterBuf, sizeof(temp));
    
    // 简单冒泡排序
    for(int i=0; i<4; i++) {
        for(int j=i+1; j<5; j++) {
            if(temp[i] > temp[j]) {
                uint32_t t = temp[i];
                temp[i] = temp[j];
                temp[j] = t;
            }
        }
    }
    
    return temp[2]; // 返回中值
}
  1. 串口数据丢失:这个问题往往是因为接收缓冲区太小或处理不及时。我的解决方案是使用双缓冲机制:一个缓冲区用于接收数据,另一个用于处理数据,通过标志位切换。

  2. ADC采样噪声大:对于光照传感器这类模拟量采集,软件上可以采用多次采样取平均的方法:

#define ADC_SAMPLE_TIMES 16

uint16_t getAdcValue(ADC_HandleTypeDef* hadc) {
    uint32_t sum = 0;
    for(int i=0; i<ADC_SAMPLE_TIMES; i++) {
        HAL_ADC_Start(hadc);
        HAL_ADC_PollForConversion(hadc, 10);
        sum += HAL_ADC_GetValue(hadc);
    }
    return sum / ADC_SAMPLE_TIMES;
}

硬件上要确保参考电压稳定,必要时可以在ADC输入引脚加0.1μF滤波电容。

6. 竞赛实战经验分享

参加嵌入式竞赛不同于平时的项目开发,有严格的时间限制和评分标准。根据我的参赛经验,总结出以下几点建议:

开发节奏控制非常重要。建议将5小时的比赛时间划分为:

  • 30分钟:题目分析、方案设计、工程搭建
  • 3小时:模块功能实现
  • 1小时:系统集成与调试
  • 30分钟:最终测试与优化

代码版本管理也很关键。每完成一个功能模块,就提交一次代码到本地仓库。当遇到严重bug时,可以快速回退到上一个稳定版本。我习惯使用Git进行管理,基本命令序列如下:

git init
git add .
git commit -m "LED module completed"

在资源使用方面要精打细算。STM32G431的资源虽然比往届比赛用的芯片丰富,但依然有限。特别要注意:

  • 定时器资源分配
  • DMA通道使用
  • 中断优先级设置
  • 内存使用情况

最后强调一点:一定要仔细阅读评分标准。有些功能实现起来复杂但分值不高,而一些基础功能反而分值占比很大。合理分配时间和精力,优先确保基础功能完美实现。

更多推荐