蓝桥杯嵌入式竞赛STM32G431项目实战:模块化设计、DMA采集与状态机应用 📅 发布时间:2026/8/28 19:25:25 👁 浏览次数: 1. 项目概述一次嵌入式竞赛的复盘与代码遗产“蓝桥杯”全国软件和信息技术专业人才大赛对于国内电子信息、计算机相关专业的学生而言是一个绕不开的名字。它不仅是检验学习成果的试金石更是通往更高舞台的敲门砖。而其中的“嵌入式设计与开发”赛道以其综合性强、实践要求高的特点成为了众多工科学子心中的“硬核”挑战。今天分享的是我参加第十三届蓝桥杯嵌入式组第二场省赛的完整代码实现。标题里那句“可能无缘国赛了”是我赛后的真实心境——一个带着些许遗憾却又满载收获的阶段性总结。这份代码不仅仅是一份解题答案更是一个完整的项目开发过程记录包含了从赛题分析、外设驱动、算法实现到系统调试的全套思路。无论你是正在备赛的学弟学妹还是对STM32嵌入式开发感兴趣的爱好者希望这份详尽的代码解析和背后的“踩坑”经验能为你提供一条更清晰的路径让你在备赛或自学中少走弯路。2. 赛题核心与硬件平台深度解析2.1 第十三届第二场省赛题目回顾与需求拆解那一届省赛的题目延续了蓝桥杯嵌入式一贯的风格基于指定的竞赛板通常是CT117E-M4核心为STM32G431RBT6实现一个综合性的测控系统。题目通常会包含多个模块例如数据采集与处理通过板载的ADC读取电位器电压、光敏电阻值或者通过扩展接口读取温度传感器如LM75的数据。人机交互通过LCD液晶屏通常是1.3寸IPS屏驱动IC为ST7789显示丰富的界面包括数值、波形、状态指示等通过按键独立按键或矩阵键盘进行模式切换、参数设置。执行器控制控制板载的LED灯、继电器或者通过PWM信号控制舵机、直流电机用L298N模块模拟。通信功能可能需要实现串口UART通信用于接收指令或上传数据。高级算法有时会涉及简单的数据滤波算法如滑动平均、PID控制算法雏形或者状态机逻辑。具体到“第二场”题目可能要求设计一个“环境监测与控制系统”。例如系统需要实时显示当前温度、光照强度通过电位器可以设定一个温度阈值当温度超过阈值时自动开启“风扇”用PWM驱动电机模拟并点亮报警LED同时可以在LCD上绘制温度变化的曲线图。整个系统需在有限的资源CPU主频、内存下稳定运行且界面响应流畅。2.2 硬件平台STM32G431RBT6与CT117E板卡要点工欲善其事必先利其器。深入理解手中的武器至关重要。核心MCUSTM32G431RBT6。这是ST公司基于Arm® Cortex®-M4内核的微控制器主频高达170MHz具备丰富的数字和模拟外设。备赛时必须熟练掌握其常用外设的CubeMX配置与HAL库编程GPIO输入按键检测需注意消抖、输出LED、继电器控制。定时器TIM用于产生精确延时HAL_Delay替代方案、PWM输出控制电机/舵机、输入捕获可能用于测频。ADC用于采集模拟信号。关键点要理解其分辨率12位、采样时间、规则组与注入组的区别以及如何通过DMA进行连续、高效的传输避免因轮询ADC而阻塞主程序。I2C用于连接LM75温度传感器、EEPROM等。关键点通信时序、从机地址、应对总线忙状态的处理。SPI用于驱动LCD屏ST7789通常使用SPI接口。关键点通信速率、数据/命令发送模式、屏幕初始化序列。USART用于调试输出或与上位机通信。关键点波特率设置、中断接收、printf重定向。竞赛板外设布局必须像熟悉自己的手掌一样熟悉板子。哪个按键接在哪个引脚、LED的阳极/阴极连接方式、ADC通道与电位器/光敏的对应关系、电机控制接口的定义这些必须在赛前形成肌肉记忆。比赛时翻看原理图会浪费宝贵时间。注意比赛提供的HAL库版本和板级支持包BSP可能与平时练习用的略有差异。赛前一定要用官方发布的模拟器或最新资料进行适配避免比赛时出现编译不过或驱动不兼容的致命问题。3. 代码架构设计与模块化思想面对一个综合性赛题最忌讳的就是把所有代码都堆在main.c里。清晰的架构是稳定性和可调试性的基础。我的项目采用了典型的“硬件抽象层应用层”模块化设计。3.1 项目目录结构规划Project/ ├── Core/ │ ├── Src/ │ │ ├── main.c │ │ ├── gpio.c │ │ ├── adc.c │ │ └── ... │ ├── Inc/ │ │ ├── main.h │ │ ├── gpio.h │ │ ├── adc.h │ │ └── ... ├── Drivers/ │ ├── STM32G4xx_HAL_Driver/ │ └── BSP/ (板级支持包包含LCD、按键、LED的驱动) ├── Middlewares/ ├── Application/ │ ├── Src/ │ │ ├── data_process.c // 数据处理模块 │ │ ├── ui_controller.c // 界面控制模块 │ │ ├── system_ctrl.c // 系统控制逻辑状态机 │ │ └── ... │ ├── Inc/ │ │ ├── data_process.h │ │ ├── ui_controller.h │ │ └── ... └── README.md这种结构将STM32CubeMX生成的硬件初始化代码在Core/Drivers与自己编写的应用逻辑代码在Application分离使得代码逻辑清晰便于多人协作虽然比赛是单人和后期维护。3.2 关键模块接口定义模块化核心在于定义清晰、简洁的接口。以按键模块为例// Inc/key.h #ifndef __KEY_H #define __KEY_H #include main.h typedef enum { KEY_NONE 0, KEY1_PRESS, KEY2_PRESS, KEY3_PRESS, KEY4_PRESS, KEY1_LONG_PRESS, // 长按支持 KEY2_LONG_PRESS, } Key_Event_t; void KEY_Init(void); Key_Event_t KEY_Scan(uint32_t interval_ms); // 间隔扫描返回事件 uint8_t KEY_IsAnyPressed(void); // 快速检测是否有键按下 #endif在key.c中实现消抖、长按检测等底层逻辑。应用层如ui_controller.c只需调用KEY_Scan()获取事件无需关心具体是哪个GPIO和消抖算法。这种设计极大降低了模块间的耦合度。实操心得在比赛高压环境下清晰的模块划分能让你快速定位问题。比如显示异常就重点检查ui_controller和LCD驱动控制逻辑出错就查看system_ctrl的状态机。千万不要把所有逻辑混在一起。4. 核心外设驱动实现与优化技巧4.1 ADC多通道DMA采集与数据处理题目通常要求同时采集多个模拟量如电位器、光敏。使用轮询或中断方式会占用大量CPU时间而DMA是完美解决方案。// adc.c 片段 ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; uint16_t adc_raw_buf[4]; // 假设4个通道 void ADC_Init_With_DMA(void) { // CubeMX配置ADC1规则组多通道扫描连续转换使能DMA循环模式 // ... HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_raw_buf, 4); } // 获取转换后的电压值 (假设Vref3.3V) float ADC_GetVoltage(uint8_t ch) { if(ch 4) return 0.0f; // 12位ADC最大值4095对应3.3V return (float)adc_raw_buf[ch] / 4095.0f * 3.3f; }优化技巧校准上电后或定期调用HAL_ADCEx_Calibration_Start()进行ADC校准减少零点误差和增益误差。滤波ADC值通常存在噪声。可以在应用层对adc_raw_buf进行软件滤波。最常用的是滑动平均滤波。// data_process.c #define FILTER_DEPTH 10 uint16_t voltage_filter_buf[FILTER_DEPTH]; uint8_t filter_index 0; float Voltage_Filter_Update(uint16_t new_sample) { voltage_filter_buf[filter_index] new_sample; filter_index (filter_index 1) % FILTER_DEPTH; uint32_t sum 0; for(int i0; iFILTER_DEPTH; i) { sum voltage_filter_buf[i]; } return (float)sum / FILTER_DEPTH / 4095.0f * 3.3f; }动态调整采样时间对于高阻抗信号源如电位器可以适当增加ADC的采样时间让采样电容充放电更充分读数更稳定。4.2 LCD屏显与图形界面高效绘制LCD显示是比赛的“门面”也是耗时大户。优化显示效率至关重要。底层驱动优化ST7789驱动核心是LCD_WriteData()和LCD_WriteCommand()函数。确保SPI时钟配置在板卡和屏幕允许的最高频率通常20MHz以上。使用HAL_SPI_Transmit_DMA传输大量像素数据如图片、清屏可以解放CPU。建立图形库不要每次画图都从头计算。实现一些基本图形函数// ui_controller.c 或独立的 gui.c void GUI_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); void GUI_DrawRectangle(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color, uint8_t filled); void GUI_DrawCircle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color, uint8_t filled); void GUI_DrawString(uint16_t x, uint16_t y, const char* str, sFONT* font, uint16_t color, uint16_t bgcolor);界面局部刷新避免全屏刷新。例如仅更新变化的数值区域。可以设计一个Widget结构体管理屏幕上每个元素如标签、数值框、按钮的位置、内容和状态刷新时只更新dirty脏标志为真的部件。双缓冲机制如果内存允许在内存中开辟一块和屏幕一样大的缓冲区frame_buffer所有绘图操作先在缓冲区中进行完成一帧后一次性通过DMA传输到LCD。这能完全消除屏幕撕裂感但STM32G431的RAM128KB在驱动132*162的16位色屏幕约42KB时需谨慎评估可能留给应用的内存就不多了。4.3 定时器精准调度与任务管理一个复杂的系统需要有条不紊地执行多个任务按键扫描、ADC读取、界面刷新、控制算法计算。简单的while(1)加延时已经力不从心需要引入调度器概念。// system_ctrl.c typedef struct { void (*task)(void); // 任务函数指针 uint32_t interval_ms; // 执行间隔 uint32_t last_run_tick; // 上次运行的时间戳 } Task_t; Task_t task_list[] { {KEY_Scan_Task, 20, 0}, // 20ms扫描一次按键 {ADC_Process_Task, 50, 0}, // 50ms处理一次ADC数据 {UI_Refresh_Task, 100, 0}, // 100ms刷新一次界面非全局刷新 {Control_Algorithm_Task, 200, 0}, // 200ms运行一次控制算法 // ... 更多任务 }; #define TASK_COUNT (sizeof(task_list)/sizeof(Task_t)) void Scheduler_Run(void) { uint32_t current_tick HAL_GetTick(); // 获取系统毫秒滴答 for(int i0; iTASK_COUNT; i) { if(current_tick - task_list[i].last_run_tick task_list[i].interval_ms) { task_list[i].task(); // 执行任务 task_list[i].last_run_tick current_tick; } } } // 在主循环中不断调用 Scheduler_Run()这是一个非常简洁的时间片轮询调度器。它保证了关键任务如按键响应能按时执行同时又不会因为某个任务阻塞而影响其他任务。比直接用HAL_Delay高级和实用得多。5. 系统集成、调试与比赛现场实录5.1 状态机实现核心业务逻辑对于“环境监测与控制”这类题目系统状态如手动模式、自动模式、设置模式、报警状态清晰非常适合用有限状态机来实现。// system_ctrl.h typedef enum { SYS_MODE_AUTO 0, SYS_MODE_MANUAL, SYS_MODE_SETTING_TEMP, SYS_MODE_SETTING_LIGHT, SYS_MODE_ALARM } System_Mode_t; typedef struct { System_Mode_t current_mode; float current_temp; float current_light; float setpoint_temp; uint8_t fan_speed; // 0-100% uint8_t is_alarm; } System_State_t;在Control_Algorithm_Task()中根据current_mode执行不同的逻辑。例如在SYS_MODE_AUTO下比较current_temp和setpoint_temp决定fan_speed。状态切换由按键事件触发。这种设计使逻辑条理清晰易于调试和扩展。5.2 调试技巧与现场问题排查比赛现场环境陌生、时间紧迫高效的调试能力是救命稻草。串口调试助手是第二双眼睛一定要重写好printf通过串口实时打印系统状态、变量值、错误标志。这是定位问题最快的方式。// 在usart.c中 #ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif PUTCHAR_PROTOTYPE { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 1000); return ch; }利用LED作为状态指示灯在代码关键分支如进入某个模式、发生错误点亮不同的LED。当程序“跑飞”或卡住时观察LED的闪烁模式能提供线索。分段测试法不要试图一次性写完所有功能然后调试。应该写一个模块测试一个模块。例如先让ADC读数稳定显示在串口再移植到LCD上显示然后加入按键修改阈值最后整合控制逻辑。现场常见问题速查表现象可能原因排查方向LCD白屏/花屏1. 电源或背光未开启2. SPI时序或速率不对3. 初始化序列错误4. 数据/命令引脚混淆1. 检查硬件连接2. 降低SPI速率测试3. 核对ST7789数据手册初始化代码4. 用逻辑分析仪抓取SPI波形ADC读数跳动大1. 未滤波2. 采样时间太短3. 电源噪声4. 信号线干扰1. 增加软件滤波深度2. 在CubeMX中增加ADC采样周期3. 检查板卡供电模拟部分可尝试加滤波电容4. 远离电机等大电流线路按键不灵敏或连击1. 消抖算法不佳2. 扫描间隔太长/太短3. 上拉电阻问题1. 优化消抖逻辑如采用状态机消抖2. 调整KEY_Scan的调用频率20-50ms为宜3. 检查硬件原理图确认内部/外部上拉已启用PWM控制电机不转或异常1. PWM通道未正确映射2. 占空比计算错误3. 电机驱动模块L298N使能端未拉高4. 电源功率不足1. 用示波器测量PWM输出引脚波形2. 确认__HAL_TIM_SET_COMPARE()参数正确3. 检查驱动模块所有控制线4. 单独给驱动模块供电测试程序偶尔死机1. 数组越界2. 栈溢出3. 中断冲突4. 硬件看门狗未喂狗1. 检查所有数组访问索引2. 在CubeMX中适当调大栈Stack大小3. 检查中断优先级避免在中断中处理耗时任务4. 若启用看门狗确保定时喂狗5.3 代码分享核心片段与整体思路由于完整代码过长这里分享几个最具代表性的片段并附上Github仓库链接模拟实际请自行托管。片段1基于状态机的按键处理与模式切换// ui_controller.c 中处理按键事件 void UI_Handle_KeyEvent(Key_Event_t event) { static System_Mode_t last_mode SYS_MODE_AUTO; switch(sys_state.current_mode) { case SYS_MODE_AUTO: if(event KEY1_PRESS) { last_mode sys_state.current_mode; sys_state.current_mode SYS_MODE_SETTING_TEMP; GUI_ShowSettingPage(); // 切换到设置页面 } else if(event KEY2_PRESS) { // 手动/自动切换 sys_state.current_mode SYS_MODE_MANUAL; } break; case SYS_MODE_SETTING_TEMP: if(event KEY1_PRESS) { // 确认设置返回原模式 sys_state.current_mode last_mode; GUI_ShowMainPage(); } else if(event KEY2_PRESS) { // 数值加 sys_state.setpoint_temp 0.5; if(sys_state.setpoint_temp 50.0) sys_state.setpoint_temp 50.0; GUI_UpdateSettingValue(); } else if(event KEY3_PRESS) { // 数值减 sys_state.setpoint_temp - 0.5; if(sys_state.setpoint_temp 10.0) sys_state.setpoint_temp 10.0; GUI_UpdateSettingValue(); } break; // ... 其他模式处理 } }片段2自动控制逻辑实现// system_ctrl.c 中的控制任务 void Control_Algorithm_Task(void) { if(sys_state.current_mode SYS_MODE_AUTO) { // 简单的迟滞比较控制防止继电器在临界点频繁动作 if(sys_state.current_temp sys_state.setpoint_temp 0.5) { // 温度过高开启风扇 sys_state.fan_speed 80; // 设定一个固定速度或根据温差计算 sys_state.is_alarm (sys_state.current_temp sys_state.setpoint_temp 2.0); // 温差过大报警 } else if (sys_state.current_temp sys_state.setpoint_temp - 0.5) { // 温度过低关闭风扇 sys_state.fan_speed 0; sys_state.is_alarm 0; } // 更新PWM输出 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, (htim2.Init.Period * sys_state.fan_speed / 100)); // 控制报警LED HAL_GPIO_WritePin(ALARM_LED_GPIO_Port, ALARM_LED_Pin, sys_state.is_alarm ? GPIO_PIN_SET : GPIO_PIN_RESET); } else if (sys_state.current_mode SYS_MODE_MANUAL) { // 手动模式风扇速度可能由另一个电位器控制 // ... } }整体代码仓库结构说明完整的工程会在Github上包含所有模块源文件、CubeMX的.ioc配置文件、详细的README.md说明工程结构、如何编译、硬件连接图以及可能遇到的问题解决方案。这份代码的价值在于其完整性和可复现性从CubeMX配置到应用逻辑形成了一个闭环读者可以完整地导入、编译、下载到开发板运行并在此基础上进行修改和学习。6. 备赛策略与个人经验反思回顾整个备赛和参赛过程有几点深刻的体会或许比代码本身更有价值。6.1 高效备赛路线图基础夯实期1-2个月死磕STM32G431的HAL库。不是背API而是理解外设的工作原理。把GPIO、定时器PWM、输入捕获、ADC单通道、多通道、DMA、I2C、SPI、USART这六大外设的CubeMX配置和基本读写操作练到形成条件反射。配套练习就是驱动板载的所有器件。模块强化期1个月针对比赛高频考点进行专项训练。LCD图形界面练习绘制各种界面元素数字、汉字、曲线、进度条并实现一个简单的多级菜单系统。综合控制找一个往届真题实现一个完整的“采集-显示-控制”闭环。例如用ADC读电位器控制PWM占空比进而控制LED亮度或电机转速并在LCD上实时显示波形和数值。通信与存储练习I2C读写EEPROM保存参数练习USART与PC通信发送数据包。真题模拟期1个月严格按照比赛时间通常是4-5小时在无网络、无外部资料的环境下完成近2-3届的省赛真题。这是最关键的阶段它能暴露出你知识体系的漏洞、时间管理的短板和临场调试的弱点。每次模拟后必须花双倍的时间复盘总结哪里卡壳了、为什么、如何避免。查漏补缺与心态调整期赛前1周不再学习新东西而是反复回顾自己的笔记、整理的常见错误、各个模块的“肌肉记忆”代码。调整作息准备好比赛所需的软件环境CubeIDE、Keil、CH340驱动等、硬件开发板、USB线、杜邦线等。6.2 比赛现场时间分配与应急策略比赛时间极其宝贵必须合理分配。前30分钟不急于写代码仔细阅读题目至少两遍用笔在纸上画出系统功能框图、状态转换图明确每个按键的功能、每个显示页面的内容。这个规划阶段能避免后期大规模返工。第1-2小时搭建工程框架。用CubeMX配置所有必要的外设时钟、引脚。生成代码后立即将你平时积累的模块化驱动LCD、按键、ADC DMA等移植进去并编写简单的测试代码验证每个外设基本功能是否正常。确保基础工程是好的。第2-4小时实现核心业务逻辑。按照之前规划的状态机逐个模式、逐个功能去实现。遵循“实现一点测试一点”的原则。优先保证基本功能数据显示、按键切换畅通。最后1小时集成测试与优化。将所有功能整合进行全流程测试。处理边界情况如数值超限。优化界面显示效果。如果还有时间可以尝试实现一些“加分项”比如数据曲线绘制更平滑、增加系统自检功能等。应急策略如果某个模块比如I2C温度传感器死活调不通不要死磕超过40分钟。果断放弃该模块的完美实现用模拟数据或固定值代替先保证系统其他部分能跑起来拿到基础分。比赛是积分制不是二进制0或1。6.3 关于“无缘国赛”的思考与收获标题里的遗憾是真实的。或许是因为某个低级失误或许是因为临场策略失当又或许是高手如云。但这个过程给予我的远超一纸证书。工程能力的飞跃从只会点灯到能架构一个完整的、可维护的嵌入式系统这种能力是课堂上很难学到的。调试思维的建立学会了如何像侦探一样通过有限的现象LED、串口数据去推理程序内部的运行状态系统地定位问题。抗压能力的提升在倒计时和未知问题面前保持冷静快速决策这种心理素质对未来的职业生涯至关重要。代码的遗产这份为比赛写的代码成为了我后续课程设计、毕业设计甚至工作中原型开发的宝贵模板。里面模块化的思想、状态机的应用、调试的技巧持续产生着价值。所以无论比赛结果如何全力以赴准备和参与的过程本身就是最大的奖赏。这份代码分享出来是希望将这份收获传递下去。如果你正在备赛希望它能助你一臂之力如果你只是对嵌入式感兴趣希望它能为你打开一扇窗展示一个真实的小型嵌入式项目是如何从零构建的。嵌入式开发的世界很大蓝桥杯只是一个起点重要的是保持热爱持续动手在代码与硬件的交响中创造属于自己的价值。