1. 蓝桥杯嵌入式竞赛与STM32G431RBT6开发板

参加蓝桥杯嵌入式竞赛的同学们应该都对STM32G431RBT6这款开发板不陌生。作为竞赛指定使用的开发平台,这款板子集成了丰富的外设资源,包括GPIO、ADC、DAC、I2C、UART、TIM等常用模块,完全能够满足竞赛中各种题目的需求。我在去年指导学生参赛时,就深刻体会到搭建一个完善的工程框架对比赛效率的提升有多重要。

STM32G431RBT6基于Arm Cortex-M4内核,主频高达170MHz,内置128KB Flash和32KB SRAM,性能完全够用。板载的资源中,最需要关注的是LED、按键、LCD显示屏和各类通信接口。这些外设在比赛中出现的频率极高,提前做好驱动封装能节省大量调试时间。

2. 从零搭建工程框架的关键步骤

2.1 CubeMX基础配置

使用CubeMX初始化项目是第一步,这里有几个关键点需要注意:

  1. 时钟树配置要正确,系统时钟建议设置为170MHz以获得最佳性能
  2. GPIO模式要根据实际用途设置,比如LED要配置为输出模式,按键要配置为上拉输入
  3. 外设时钟必须使能,这是很多新手容易忽略的地方

我建议在CubeMX中就把所有可能用到的外设都初始化好,包括:

  • 8个LED对应的GPIO
  • 4个按键对应的GPIO
  • LCD显示屏的接口
  • I2C接口(用于EEPROM和可编程电阻)
  • USART接口(用于串口通信)
  • ADC和DAC通道
  • 定时器(用于PWM、输入捕获等)

2.2 Keil工程设置

CubeMX生成工程后,还需要在Keil中进行一些必要配置:

  1. 编译器版本选择V5,并勾选Use MicroLIB
  2. 添加官方提供的LCD和I2C驱动文件
  3. 设置正确的头文件包含路径
  4. 优化等级建议选择-O1,兼顾性能和调试便利性

这里有个实用技巧:可以创建一个专门的"Drivers"文件夹,把LCD、I2C等官方驱动文件都放在这里,方便管理。我在实际项目中发现,良好的文件组织结构能显著提高开发效率。

2.3 模块化代码组织

竞赛时间有限,模块化的代码结构能让你快速定位和修改功能。我推荐这样的目录结构:

/Project
  /Core         // 核心文件
  /Drivers      // 驱动文件
  /Modules      // 功能模块
    /LED
    /KEY
    /LCD
    /I2C
    ...
  /Applications // 应用层代码

每个模块都应该有对应的.h和.c文件,比如led.h和led.c。在头文件中定义接口函数,在源文件中实现具体功能。这种结构在比赛后期需要添加新功能时会非常方便。

3. 关键外设驱动开发实战

3.1 GPIO应用:LED与按键

LED控制看似简单,但在比赛中经常需要处理锁存器问题。因为LCD和LED共用部分引脚,使用时需要特别注意:

// LED控制示例代码
void LED_Control(uint8_t led_num, uint8_t state)
{
    // 先使能锁存器
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
    
    // 控制指定LED
    if(state) {
        HAL_GPIO_WritePin(GPIOC, 1<<(led_num+7), GPIO_PIN_SET);
    } else {
        HAL_GPIO_WritePin(GPIOC, 1<<(led_num+7), GPIO_PIN_RESET);
    }
    
    // 禁用锁存器
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}

按键处理则需要考虑消抖问题。我推荐使用状态机的方式实现,既能准确检测按键动作,又能处理长按、短按等复杂情况:

// 按键状态机示例
typedef struct {
    uint8_t step;
    uint8_t state;
    uint16_t press_time;
} Key_TypeDef;

void Key_Scan(Key_TypeDef *key, GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    uint8_t current_state = HAL_GPIO_ReadPin(GPIOx, GPIO_Pin);
    
    switch(key->step) {
        case 0: // 等待按下
            if(current_state == 0) {
                key->step = 1;
                key->press_time = 0;
            }
            break;
            
        case 1: // 消抖确认
            if(current_state == 0) {
                key->step = 2;
                key->state = KEY_PRESSED;
            } else {
                key->step = 0;
            }
            break;
            
        case 2: // 等待释放
            if(current_state == 1) {
                key->step = 0;
                key->state = KEY_RELEASED;
            } else {
                key->press_time++;
                if(key->press_time > LONG_PRESS_THRESHOLD) {
                    key->state = KEY_LONG_PRESS;
                }
            }
            break;
    }
}

3.2 ADC与DAC应用

ADC采集需要注意采样时间和滤波处理。比赛中经常需要采集电位器电压,这里分享一个实用的滤波函数:

#define SAMPLE_NUM 10

float ADC_GetFilteredValue(ADC_HandleTypeDef* hadc)
{
    uint32_t sum = 0;
    uint16_t samples[SAMPLE_NUM];
    
    // 采集多个样本
    for(int i=0; i<SAMPLE_NUM; i++) {
        HAL_ADC_Start(hadc);
        HAL_ADC_PollForConversion(hadc, 10);
        samples[i] = HAL_ADC_GetValue(hadc);
    }
    
    // 排序后取中间值
    qsort(samples, SAMPLE_NUM, sizeof(uint16_t), compare);
    
    // 取中间几个样本求平均
    for(int i=2; i<SAMPLE_NUM-2; i++) {
        sum += samples[i];
    }
    
    return (sum * 3.3f) / ((SAMPLE_NUM-4) * 4095.0f);
}

DAC输出则需要注意电压范围,STM32G431的DAC是12位精度,输出电压范围0-3.3V:

void DAC_SetVoltage(DAC_HandleTypeDef* hdac, uint32_t channel, float voltage)
{
    if(voltage > 3.3f) voltage = 3.3f;
    if(voltage < 0.0f) voltage = 0.0f;
    
    uint32_t value = (uint32_t)(voltage * 4095.0f / 3.3f);
    HAL_DAC_SetValue(hdac, channel, DAC_ALIGN_12B_R, value);
    HAL_DAC_Start(hdac, channel);
}

3.3 I2C通信:EEPROM与可编程电阻

EEPROM读写是比赛常见考点,这里给出一个可靠的读写模板:

// EEPROM写一个字节
void EEPROM_WriteByte(uint16_t addr, uint8_t data)
{
    I2CStart();
    I2CSendByte(0xA0); // 器件地址
    I2CWaitAck();
    I2CSendByte(addr >> 8); // 地址高字节
    I2CWaitAck();
    I2CSendByte(addr & 0xFF); // 地址低字节
    I2CWaitAck();
    I2CSendByte(data);
    I2CWaitAck();
    I2CStop();
    HAL_Delay(5); // 必须延时
}

// EEPROM读一个字节
uint8_t EEPROM_ReadByte(uint16_t addr)
{
    uint8_t data;
    
    I2CStart();
    I2CSendByte(0xA0);
    I2CWaitAck();
    I2CSendByte(addr >> 8);
    I2CWaitAck();
    I2CSendByte(addr & 0xFF);
    I2CWaitAck();
    
    I2CStart();
    I2CSendByte(0xA1);
    I2CWaitAck();
    data = I2CReceiveByte();
    I2CSendNotAck();
    I2CStop();
    
    return data;
}

对于MCP4017可编程电阻,其控制方式略有不同:

void MCP4017_SetResistance(uint8_t value)
{
    I2CStart();
    I2CSendByte(0x5E); // 器件地址+写
    I2CWaitAck();
    I2CSendByte(value & 0x7F); // 7位有效数据
    I2CWaitAck();
    I2CStop();
}

4. 定时器高级应用技巧

4.1 PWM输出与输入捕获

PWM输出常用于控制蜂鸣器和LED亮度,配置时需要注意:

  1. 定时器时钟频率
  2. 预分频系数(PSC)
  3. 自动重装载值(ARR)
  4. 脉冲宽度(CCR)
// PWM初始化示例
void PWM_Init(TIM_HandleTypeDef* htim, uint32_t channel, uint32_t freq, float duty)
{
    uint32_t timer_clock = 170000000; // 170MHz
    uint32_t psc = 0;
    uint32_t arr = (timer_clock / freq) - 1;
    
    __HAL_TIM_SET_PRESCALER(htim, psc);
    __HAL_TIM_SET_AUTORELOAD(htim, arr);
    __HAL_TIM_SET_COMPARE(htim, channel, (uint32_t)(arr * duty));
    
    HAL_TIM_PWM_Start(htim, channel);
}

输入捕获则用于测量频率和占空比,这里给出一个测量PWM参数的实现:

typedef struct {
    uint32_t rising_time;
    uint32_t falling_time;
    uint32_t period;
    float duty;
    uint32_t last_capture;
    uint8_t is_rising;
} PWM_Measure_TypeDef;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    static PWM_Measure_TypeDef pwm;
    
    if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        uint32_t capture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        
        if(pwm.is_rising) {
            pwm.rising_time = capture;
            pwm.period = pwm.rising_time - pwm.falling_time;
            pwm.duty = (float)(pwm.falling_time - pwm.last_capture) / pwm.period;
            pwm.last_capture = capture;
        } else {
            pwm.falling_time = capture;
        }
        
        pwm.is_rising = !pwm.is_rising;
    }
}

4.2 定时器中断与软件定时器

硬件定时器资源有限,可以通过一个硬件定时器实现多个软件定时器:

#define MAX_SOFT_TIMERS 4

typedef struct {
    uint32_t count;
    uint32_t reload;
    uint8_t enabled;
    void (*callback)(void);
} SoftTimer_TypeDef;

SoftTimer_TypeDef soft_timers[MAX_SOFT_TIMERS];

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM2) {
        for(int i=0; i<MAX_SOFT_TIMERS; i++) {
            if(soft_timers[i].enabled) {
                if(++soft_timers[i].count >= soft_timers[i].reload) {
                    soft_timers[i].count = 0;
                    if(soft_timers[i].callback) {
                        soft_timers[i].callback();
                    }
                }
            }
        }
    }
}

void SoftTimer_Start(uint8_t id, uint32_t ms, void (*callback)(void))
{
    if(id >= MAX_SOFT_TIMERS) return;
    
    soft_timers[id].reload = ms;
    soft_timers[id].count = 0;
    soft_timers[id].callback = callback;
    soft_timers[id].enabled = 1;
}

5. 串口通信与调试技巧

5.1 三种通信方式对比

在STM32中有三种常见的串口通信方式:

  1. 轮询方式:简单但效率低
  2. 中断方式:响应及时,适合小数据量
  3. DMA方式:不占用CPU,适合大数据量

我整理了一个对比表格:

方式 优点 缺点 适用场景
轮询 实现简单 阻塞CPU效率低 简单调试输出
中断 响应及时 频繁中断影响性能 中小数据量通信
DMA 不占用CPU 配置复杂 大数据量传输

5.2 不定长数据接收

比赛中经常需要接收不定长数据,DMA+空闲中断是最佳方案:

uint8_t rx_buf[128];
uint16_t rx_len = 0;

void USART1_IRQHandler(void)
{
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        
        // 停止DMA
        HAL_UART_DMAStop(&huart1);
        
        // 获取接收到的数据长度
        rx_len = sizeof(rx_buf) - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        
        // 处理数据
        if(rx_len > 0) {
            ProcessData(rx_buf, rx_len);
        }
        
        // 重新启动DMA接收
        HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
    }
    
    HAL_UART_IRQHandler(&huart1);
}

void UART_Init(void)
{
    HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

5.3 调试信息输出

在比赛中,通过串口输出调试信息是非常有用的调试手段。我通常会封装一个调试输出函数:

#include <stdarg.h>

void Debug_Printf(const char *fmt, ...)
{
    char buffer[128];
    va_list args;
    
    va_start(args, fmt);
    int len = vsnprintf(buffer, sizeof(buffer), fmt, args);
    va_end(args);
    
    if(len > 0) {
        HAL_UART_Transmit(&huart1, (uint8_t*)buffer, len, 100);
    }
}

使用时可以像printf一样方便:

Debug_Printf("ADC Value: %.2f, Status: %d\n", adc_value, status);

6. LCD显示优化与性能提升

6.1 基本显示功能

蓝桥杯竞赛板通常配备一块LCD显示屏,官方会提供驱动代码。我们需要封装一些常用功能:

void LCD_ShowString(uint16_t x, uint16_t y, uint8_t *str, uint16_t color)
{
    LCD_SetTextColor(color);
    LCD_DisplayStringLine(y, str);
}

void LCD_ShowFloat(uint16_t x, uint16_t y, float value, uint8_t decimal, uint16_t color)
{
    char buffer[16];
    sprintf(buffer, "%.*f", decimal, value);
    LCD_ShowString(x, y, (uint8_t*)buffer, color);
}

void LCD_ShowInt(uint16_t x, uint16_t y, int32_t value, uint16_t color)
{
    char buffer[16];
    sprintf(buffer, "%ld", value);
    LCD_ShowString(x, y, (uint8_t*)buffer, color);
}

6.2 显示性能优化

直接使用官方驱动刷新整个屏幕会比较慢,在比赛中可以优化以下几点:

  1. 只刷新变化的部分,避免全屏刷新
  2. 使用局部清屏代替全屏清屏
  3. 对频繁更新的区域使用双缓冲技术

这里给出一个局部刷新的例子:

// 定义显示区域结构体
typedef struct {
    uint16_t x;
    uint16_t y;
    uint16_t width;
    uint16_t height;
    uint8_t *buffer;
} DisplayArea_TypeDef;

// 初始化显示区域
void DisplayArea_Init(DisplayArea_TypeDef *area, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
    area->x = x;
    area->y = y;
    area->width = w;
    area->height = h;
    area->buffer = malloc(w * h * 2); // 每个像素2字节
}

// 刷新显示区域
void DisplayArea_Refresh(DisplayArea_TypeDef *area)
{
    LCD_SetWindow(area->x, area->y, area->x + area->width - 1, area->y + area->height - 1);
    LCD_WriteRAM_Prepare();
    
    for(uint32_t i = 0; i < area->width * area->height; i++) {
        LCD_WriteRAM(*(uint16_t*)(area->buffer + i * 2));
    }
}

7. 竞赛实战经验与常见问题

7.1 时间管理策略

蓝桥杯嵌入式比赛通常为5小时,合理的时间分配至关重要:

  1. 前30分钟:仔细阅读题目,规划实现方案
  2. 第1小时:搭建工程框架,完成基础驱动
  3. 第2-4小时:逐个实现题目要求功能
  4. 最后1小时:整体测试,优化显示效果

我建议按照"先易后难"的顺序解题,确保基础分拿全。每个功能完成后立即测试,避免最后集成时问题复杂化。

7.2 常见问题与解决方法

在指导往届比赛时,我总结了一些常见问题:

  1. LCD显示异常

    • 检查锁存器控制是否正确
    • 确认初始化顺序是否正确
    • 检查背光是否开启
  2. 按键响应不灵敏

    • 增加消抖处理
    • 检查上拉/下拉电阻配置
    • 确认中断优先级设置
  3. ADC采样值跳动

    • 增加软件滤波
    • 检查参考电压是否稳定
    • 适当增加采样时间
  4. I2C通信失败

    • 检查从设备地址是否正确
    • 确认上拉电阻已接
    • 降低通信速率测试

7.3 代码版本管理

即使是5小时的比赛,也建议使用简单的版本管理:

  1. 每完成一个功能就备份一次工程
  2. 使用不同的文件夹区分版本
  3. 在代码中添加版本注释

这样当新功能引入问题时,可以快速回退到稳定版本。我在去年比赛中就看到有学生因为一个功能没调通,最后把所有功能都改乱了,非常可惜。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐