ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蓝桥杯嵌入式历年真题源码解析:STM32 CT117E工程与LCD/ADC/PWM实战

蓝桥杯嵌入式历年真题源码解析:STM32 CT117E工程与LCD/ADC/PWM实战 简介这套蓝桥杯嵌入式竞赛历年真题资源包以历年省赛/国赛完整工程源码与配套说明为核心面向正在备赛的高校学生以及电子、计算机、自动化等专业需要课程设计或毕业设计参考的开发者。压缩包共816个文件、约6.62MB其中包含274个H头文件、238个C源文件、231个S启动/汇编文件另有Keil工程配置、编译输出和doc说明文档整体目录按工程构建便于逐届对照和模块复用。目前已有525人学习浏览适合具备一定单片机与C语言基础、希望通过完整工程反推赛题设计的读者。阅读源码可看到LCD显示、按键扫描、ADC采样、PWM输出等蓝桥杯常见外设驱动写法结合项目说明能快速掌握工程创建、外设初始化与调试流程对备战蓝桥杯嵌入式赛项和准备嵌入式课程设计都有直接的参考价值。1. 蓝桥杯嵌入式历年真题源码CT117E 工程直接复现的备赛底子蓝桥杯嵌入式赛项的备赛资料里最容易被低估的就是这类“历年真题源码 项目说明”打包资源。压缩包里的工程按 CT117E 评估板组织工程名 CT117E-LCD 已经把核心演示内容说明白了基于 STM32F103RBT6 的 LCD 显示工程含标准外设库完整源码、链接产物 .axf、多份 .uvgui 界面缓存以及一个删除垃圾文件.bat 清理脚本。对正按蓝桥杯嵌入式学习路线备赛的学生或临时接手裸机 STM32 工程要快速上手的工程师来说这个包的价值不在“能跑通”而在把真题要求怎么拆模块、每个模块用哪组外设、初始化顺序怎么处理完整演示了一遍。下文按工程文件结构、LCD 与按键、ADC 与 PWM、模板化改造四层拆解每一层都给可直接搬走的代码和参数。2. 工程文件辨识.uvgui 缓存、.axf 镜像与 Keil 标准库骨架2.1 压缩包里每个文件是什么压缩包里出现频率最高的是 CT117E-LCD.uvgui.Administrator这是 Keil MDK 的界面布局缓存。只要 Windows 用户目录名带 Administrator每次打开或关闭工程Keil 都会把窗口停靠位置、断点列表写进同名 .uvgui 文件换一台机器或换用户打开工程又会生成一份所以压缩包里能看到好几份同名文件。它跟 .uvopt 一样不参与编译删掉不影响任何功能真正决定工程行为的是 .uvprojx 工程文件。另一个高频文件 CT117E-LCD.axf 是 ARM 链接器输出的可执行镜像调试器烧录用的就是它里面带完整符号表可以直接被 Keil 的 Debug 模式加载手工删除后下次按 F7 编译会自动重新生成。打包分享源码时建议顺手清掉 .axf它从几百 KB 到几 MB 不等留着只是给别人提供可烧录镜像对阅读源码没有帮助。文件角色对照如下文件/后缀来源能否删除对工程的意义.uvgui.AdministratorKeil 界面布局缓存可删只影响窗口排布.axf链接器输出镜像可删F7 之后自动重建.uvprojx / .uvprojKeil 工程主文件不可删双击打开工程靠它.uvopt工程选项配置保留记录仿真器与断点.c / .h用户源码不可删真题逻辑本体.sct分散加载描述视情况单区镜像不用动有两点容易混淆。这里的 .uvgui 是 Keil 的窗口布局缓存和嵌入式 UI 圈常说的 uguiµGUI 图形库完全是两回事后者是开源图形库检索源码解析资料时不要把这两个词混在一起找。另外如果拿到手的工程是按 MDK5 的 Pack 体系组织的打开 Manage Run-Time Environment 面板会看到 CMSIS 的 Device:Startup 和 Device:StdPeriph Drivers 组件对应的就是标准外设库的运行环境绑定。2.2 删除垃圾文件.bat 到底清理了什么附带脚本通常以echo off开头核心是几条递归删除命令。历年赛题工程里常见的完整内容基本是这个套路echo off rem 删除 Keil 界面布局缓存 del /s /q *.uvgui.* 2nul rem 删除链接输出与目标文件 del /s /q *.axf *.o *.d 2nul rem 删除交叉引用与列表文件 del /s /q *.crf *.lst *.htm 2nul rem 删除依赖关系文件 del /s /q *.dep 2nul echo Clean finish. pause说明几条参数del /s递归处理子目录/q安静模式不逐条确认2nul把“找不到文件”的错误输出丢到空设备避免在干净目录里跑出一屏红色提示。*.o是编译器目标文件*.crf是交叉引用文件供 Keil 的静态浏览窗口使用*.d是依赖关系文件*.lst是汇编列表。这些中间产物全部可以安全删除源码和工程文件不受影响。实际使用中有个细节很多人不知道默认脚本不删 .bak 备份如果 Keil 里开过 Create Backup 选项目录会积压大量 .bak 文件建议手动补一条del /s /q *.bak 2nul。反过来.sct 分散加载文件不要随手加进删除列表一旦工程做 Boot/App 分区sct 里放着内存布局的手工配置删掉后链接地址会全部乱掉。历年真题基本是单区镜像这条主要是给扩展改造提个醒。2.3 标准外设库与工程初始化骨架CT117E 工程跑的是 STM32 标准外设库StdPeriph_Driver不是 HAL 库。竞赛环境选标准库而不是 HAL原因很务实真题客观题的外设框图、寄存器位描述都按标准库口径出题标准库编译速度也比 HAL 快比赛限时场景下每次干净编译都是省出来的调试时间。工程入口结构非常固定#include stm32f10x.h #include bsp_led.h #include bsp_key.h #include bsp_lcd.h int main(void) { SystemInit(); /* HSE 8MHz 倍频到 72MHz */ LED_Init(); /* 板载 LED先点亮做心跳 */ Key_Init(); /* 按键 GPIO 输入 */ ADC_Init_Config(); /* 电位器电压采集 */ LCD_Init(); /* FSMC 接口屏幕最后初始化 */ LCD_Clear(BLACK); while (1) { Task_Process(); /* 真题业务循环 */ } }初始化顺序有三个要点。第一SystemInit 在启动文件里已被调用过main 里再写属于冗余但历年源码常见重复调用保留也不影响结果。第二外设之间无依赖时顺序随意但 LCD 初始化涉及 FSMC 时序和背光延时建议放在最后避免首帧花屏。第三Task_Process 是真题逻辑差异最集中的地方不同年份题目的改动基本都封在它内部初始化骨架可以整段复用。打开工程后先检查 Options for Target 里的 Device 是否为 STM32F103RBT6晶振填 8MHz调试器选 ST-Link这三处不对工程会直接报 device not found 或者下载后串口乱码。3. LCD 界面状态与按键扫描真题交互的第一道门槛3.1 界面状态用一个结构体收敛CT117E 的 LCD 是 FSMC 接口 TFT 屏硬件自动处理读写信时序CPU 只要向地址线写数据就行。真题对显示的考察集中在“界面状态变化”开机显示主界面按键进入设置界面数值变化时局部刷新。历年源码里比较一致的写法是把界面数据收进一个全局结构体typedef struct { uint8_t mode; /* 0 主界面 1 设置界面 */ int16_t temperature; /* 温度值可正可负 */ uint16_t duty; /* PWM 占空比 0~1000 */ uint8_t alarm; /* 报警标志 */ } View_t; View_t g_view {0, 25, 500, 0};通过这个结构体LCD 重绘逻辑与业务逻辑隔离。按键、ADC、传感器回调只修改 g_view 字段显示刷新函数只管读结构体。真题功能再多最后都是往这个结构体里填数画界面刷屏的部分基本不动。如果题目要求同时显示六七个参数结构体加字段比到处传参要稳得多也方便在调试窗口里一次性监视全部界面变量。3.2 按键扫描用状态机去抖CT117E 的四个独立按键低电平有效通常接在 PA0、PB0、PB1、PB2 上。真题需求集中在“B1 切换界面、B2 加、B3 减、B4 确认”。用 delay 去抖最直观但会把主循环卡住几十毫秒按键连续快速切换时明显掉帧。竞赛工程里更稳的是状态机轮询uint8_t Key_Scan(void) { static uint8_t key_state 0; static uint8_t key_hold 0; uint8_t level; uint8_t press 0; /* 低电平有效按位拼成 4 位掩码引脚以板子原理图为准 */ level (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0); level | (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) 0) 1; level | (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) 0) 2; level | (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2) 0) 3; switch (key_state) { case 0: /* 空闲态 */ if (level ! 0) { key_hold level; /* 记下首次电平 */ key_state 1; } break; case 1: /* 确认态 */ if (level key_hold) /* 两次采样一致才上报 */ { press key_hold; } key_state 0; break; default: key_state 0; break; } return press; }逻辑说明state 0 检测到电平变化进入 state 1state 1 发现电平与上次一致才上报按键等效于约 10ms 去抖但不占用 CPU 时间。press 是位掩码第 0 位 B1、第 1 位 B2、第 2 位 B3、第 3 位 B4主循环里直接用if (press 0x02)判断 B2。如果题目要求长按连加把 state 1 改成累计计数连续 N 次扫描仍按住就每轮上报一次代码结构不用大动。真题里按键行为随界面变化打草稿时先画这张分配表再动手写代码能少改一半逻辑界面模式B1B2B3B4主界面进入设置温度 1温度 -1清除报警设置界面返回主界面占空比 50占空比 -50保存数据3.3 局部重绘避免整屏闪烁FSMC 屏幕整屏刷新一次约几十毫秒主循环每轮全刷人眼能看到明显闪烁接摄像头自动评分时更吃亏。真题源码里常见做法是“数值变化才重绘单行区域”void LCD_Refresh_View(void) { char buf[12]; if (g_view.mode 0) { sprintf(buf, %3d, g_view.temperature); LCD_ShowString(0, 24, buf); /* 温度行 */ if ((g_view.temperature 10) || (g_view.temperature 40)) { LCD_ShowString(80, 24, ALARM!); /* 阈值提示 */ } } else { if (g_view.duty ! g_last_duty) { LCD_Fill(0, 48, 127, 54, BLACK); /* 清掉单行 */ sprintf(buf, %4d, g_view.duty); LCD_ShowString(0, 48, buf); g_last_duty g_view.duty; } } }实现细节g_last_duty 记录上一次显示值只有变化才执行一次 Fill 加重写其余时间主循环只跑业务逻辑。LCD_ShowString 与 LCD_ShowNum 是驱动给出的基础接口前者显示字符串后者显示数字注意字符点阵是 8x16 还是 16x24行坐标按像素算字高不足会出现上下行叠字。Fill 的颜色参数用 BLACK 是配合屏幕默认黑底如果界面换了底色这里要同步换掉。4. ADC 电压采集与 PWM 输出真题闭环控制的参数组合4.1 单通道 ADC 初始化与校准蓝桥杯嵌入式历年真题里“读取电位器电压并在 LCD 显示”出现频率极高。电位器输出 0~3.3V 模拟量接在 ADC1 的某个输入通道上。CT117E 不同批次电位器引脚不完全一致有的接 PB0有的接 PA5动手前先看板子原理图确认通道号不要照抄别人工程的引脚这一条能省下大量查错时间。初始化需要两部分GPIO 配成模拟输入ADC1 配成连续转换模式void ADC_Init_Config(void) { ADC_InitTypeDef adc; GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOB, ENABLE); GPIO_StructInit(gpio); gpio.GPIO_Pin GPIO_Pin_0; /* 以 PB0/ADC1_IN8 为例 */ gpio.GPIO_Mode GPIO_Mode_AIN; /* 模拟输入不要开上下拉 */ GPIO_Init(GPIOB, gpio); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; /* 单通道扫描 */ adc.ADC_ContinuousConvMode ENABLE; /* 连续转换 */ adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); /* 校准完才能稳定读值 */ }参数要点ADC_SampleTime_55Cycles5 是采样时间输入阻抗高或信号有毛刺时加大到 239Cycles5能明显压低读数跳动代价是转换速度变慢对电位器这类缓变信号没有影响。校准部分不要裁剪不校准也能出数但 ADC 内部比较器没有补偿转换值的线性度和零偏都会变差电压换算后误差可能到 0.2V 级别。读取转换结果用下面这个函数uint16_t ADC_ReadRaw(void) { uint16_t raw; ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); raw ADC_GetConversionValue(ADC1); return raw; }转换值是 12 位电压换算公式是v (float)raw * 3.3f / 4095.0f。这里的 3.3V 基准建议用万用表实测板载 VREF 再代入直接写 3.3 往往让显示值和表差 0.05V 到 0.1V而真题验收常见“LCD 显示电压与万用表读数一致”的评分点这 0.1V 差距通常就出在基准上。4.2 PWM 输出频率与占空比的计算关系电位器电压的另一半用途是控制输出典型要求是“输出 1kHz 方波占空比随电压从 0 到 100% 连续可调”。PWM 用的定时器通道各年题目不同配置逻辑一致。三个关键参数的换算关系必须先算对参数计算关系1kHz 实例常见坑Prescaler72MHz / (PSC1) 计数频率71 → 1MHz忘加 1频率偏一倍Period计数频率 / (ARR1) 输出频率999 → 1kHz频率量级写错Pulse单周期高电平时间500 → 50%Pulse 大于 ARR 输出恒高配置代码void PWM_Init_Config(TIM_TypeDef *tim, uint8_t ch) { TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; tb.TIM_Prescaler 71; /* 1MHz 计数时钟 */ tb.TIM_Period 999; /* 输出频率 1kHz */ tb.TIM_ClockDivision 0; tb.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(tim, tb); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_Pulse 500; /* 初始占空比 50% */ oc.TIM_OutputState TIM_OutputState_Enable; TIM_OC1Init(tim, oc); /* 对应通道 CH1 */ TIM_Cmd(tim, ENABLE); }逻辑说明TIM_OC1Init 只配置通道 1 的比较输出用 CH2 要换成 TIM_OC2Init。运行时改占空比调TIM_SetCompare1(tim, value)不要重新执行 OCxInit因为整体初始化会把计数器清零输出瞬间跳一下接电机或蜂鸣器时就是一次明显的啁啾声。PWM1 模式在高电平段输出有效电平换成 PWM2 模式占空比含义反相注意对照题意选择。4.3 联动逻辑与滑动平均滤波真题把 ADC 和 PWM 联动起来的常见组合是电位器电压 0~3.3V 映射到 PWM 占空比 0~100%LCD 同时显示电压和占空比。主循环里每 100ms 读一次 ADC滤波后换算电压再换算成 Pulse 值写进比较寄存器。电位器是缓变信号滑动平均就够用uint16_t ADC_FilterMean(uint8_t n) { uint32_t sum 0; uint8_t i; for (i 0; i n; i) { sum ADC_ReadRaw(); } return (uint16_t)(sum / n); }n 取 16 时连读 16 次约耗时 1ms能滤掉大部分工频干扰又不影响响应。换算占空比时先转 float 再乘除、最后取整duty (uint16_t)(voltage * 100.0f / 3.3f);。如果题目要求双向调速即电压中间值对应占空比 50%公式改成先减偏置再缩放。注意比较寄存器上限是 999映射后要加一行限幅if (duty 999) duty 999;电位器拧到满量程且基准偏高时最容易触发超限忽略这行会让占空比瞬间丢失线性。5. 用真题源码攒一套竞赛模板任务宏、模块分层与三板斧验收5.1 用预编译宏在一套工程里切换真题真题源码最大的复用价值在外设初始化部分。不同年份题目的差别基本集中在 Task_Process 内部可以用预编译宏把历年任务保留在同一套工程里#define TASK_2022_A 1 #define TASK_2022_B 0 #define TASK_2023_A 0Task_Process 里按宏展开分支改题只改宏开关LCD、按键、ADC 初始化一份代码全局复用比赛现场切题能省下的时间全部投进功能调试。任务循环骨架可以这样组织void Task_Process(void) { static uint32_t tick 0; uint8_t key Key_Scan(); if (key 0x01) g_view.mode ^ 1; /* B1 切界面 */ if (key 0x02) g_view.temperature 1; if (key 0x04) g_view.temperature - 1; if (key 0x08) g_view.alarm 0; if (tick 100) /* 10ms 周期 */ { tick 0; g_view.duty (uint16_t)(ADC_FilterMean(8) * 1000UL / 4095); PWM_UpdateDuty(g_view.duty); LCD_Refresh_View(); } }这个骨架把读 ADC、更新 PWM、刷新 LCD 放在同一个 10ms tick 里按键扫描在 tick 之外每轮执行保证按键响应不会因为显示刷新被拖慢。tick 计数用无符号自增换周期时改成与调度周期匹配的倍数即可。TPAD 这类需要长按的功能如果题目考到在 Key_Scan 的确认态里加一个按住计时分支就行不用动 tick 结构。5.2 模块命名与依赖方向目录组织按 bsp 与 app 两层bsp_led.c、bsp_key.c、bsp_lcd.c、bsp_adc.c 只做外设初始化和底层读写app_task.c、app_logic.c 放业务。检查依赖方向用一条规则bsp 层不允许调用 app 层函数。改 app 时不会牵连外设代码重新编译硬件问题排查也能直接圈定在 bsp 范围内。真题源码里的 bsp_xxx.c 命名习惯直接沿用评审和现场改题都看得顺。5.3 验收三板斧竞赛验收看现象不看代码。第一板斧是编译输出 0 Error 0 Warning带警告的工程在评审阶段容易被重点盯防。第二板斧是烧录后 3 秒内 LCD 出初始界面且数字变化时看不到闪烁。第三板斧是电位器从零拧到满程电压连续变化、占空比单调变化中间不能有跳变和回跳。用 ST-Link 连上后先在 Debug 窗口里看 ADC_ReadRaw 的实时返回值确认硬件链路在进任务循环前已经正常再断开调试器做一次纯复位验证避免出现“插着仿真器正常、拔掉就异常”的供电依赖问题。本文还有配套的精品资源点击获取
返回列表