简介:2024 年全国大学生电子设计竞赛 H 题配套代码方案,面向正在备战电赛、承担嵌入式软件部分的参赛队伍和个人开发者。方案以 STM32 微控制器为处理核心,通过 C 语言实现信号采集、滤波、频谱分析以及对外围设备的控制等关键流程,能够帮助读者在有限备赛周期内搭建起从硬件初始化到核心算法部署的完整工程框架。压缩包为 zip 格式,整体约 114.28MB,内容围绕竞赛功能展开,便于对照赛题要求快速理解程序结构,并在自己的板卡上进行移植与调优。已有 5720 人学习下载,说明该方案具有较高的实践参考价值。读者可以获得一套清晰的工程组织思路、底层驱动调用结构以及主要功能模块的代码参考,减少重复开发工作,把更多时间投入到 H 题相关的信号处理算法和系统联调上;建议具备 C 语言基础、熟悉 STM32 外设开发流程的读者使用。
1. 2024年电赛H题代码包:不只是跑通,是要能改得动
电赛H题历年都绕不开“信号采集+闭环控制”这套组合拳,代码的难度从来不在算法有多深,而在外设配置和参数标定是否干净利落。这份以2024年电赛H题为蓝本的代码包,把多路模拟信号采集、数字滤波、PID输出控制、OLED显示和串口调试完整串成了一条可运行的主链路,核心不是给你一个能出DEMO的固件,而是给你一个拆得开、改得动的框架。备赛学生拿它省掉造轮子的时间,做快速原型验证的嵌入式开发者也能直接借它的驱动层和算法层,适合所有被外设初始化折腾过的从业者。
2. 硬件选型与代码架构:H题场景下的器件搭配逻辑
2.1 主控与外设选型:为什么选这款MCU而不是盲目堆料
电赛H题留给你的开发窗口通常只有几天,硬件选型不能追新,要追“资料多、库函数稳、手头有货”。常见的做法是用Cortex-M3内核的MCU,比如某经典型号F103系列,理由有三条:第一,HAL库和外设例程覆盖得最全,ADC、定时器、I2C、UART都有现成模板可抄;第二,网上能搜到的排错经验最多,真翻车了有后悔药可吃;第三,芯片单价低,备赛时多买几片烧坏了也不心疼。
外设选型要分清楚模拟输出和数字输出两种传感器。模拟输出的器件,比如电位器分压、光敏电阻电路,直接进ADC引脚,优点是响应快,缺点是抗干扰差,硬件上要加RC低通,软件上必须跟数字滤波;数字输出的器件,比如I2C接口的温湿度模块,协议是现成的,但时序问题得对着数据手册慢慢调。H题通常两种都会碰到,所以代码包里ADC驱动和I2C驱动都给了完整实现,你只需要按实际引脚改宏定义。
| 模块 | 选型建议 | 关键参数 |
|---|---|---|
| 主控 | Cortex-M3内核MCU | 72MHz主频,12位ADC |
| 显示 | 0.96寸OLED,I2C接口 | 128x64分辨率,4pin引出 |
| 调试 | UART转USB模块 | 115200-8-N-1,带TX/RX指示灯 |
| 供电 | 5V转3.3V LDO | 压差<0.5V,纹波<50mV |
选型上最容易被忽略的是供电纹波。模拟采集对电源噪声极其敏感,开关电源直接给传感器供电会出现0.1V级别的随机跳动,这个跳动在ADC读数是几十个LSB的误差。代码包里默认配置的是LDO线性稳压方案,如果你手头只有DCDC模块,务必在输出端并联一个100uF电解电容和一个100nF瓷片电容。
2.2 代码目录结构与模块划分:驱动、算法、应用三层拆开
这份代码包的目录结构按照“驱动层—算法层—应用层”三层划分,我拿到手第一反应是清爽。很多电赛代码习惯把所有功能堆在main.c里,一个文件两千行,出了问题只能逐行排查。这套结构把每个外设单独拆成.c/.h对,算法模块独立出来,应用层只负责调度,这样你在赛场上改某个功能不会牵连到其他模块。
project/ ├── Core/ │ ├── main.c │ └── stm32f1xx_it.c ├── Drivers/ │ ├── adc.c/h // 多通道ADC采样,DMA搬运 │ ├── oled.c/h // OLED显示驱动,含字库 │ ├── uart.c/h // 串口打印与协议解析 │ └── key.c/h // 按键扫描,带消抖 ├── Algorithm/ │ ├── filter.c/h // 滑动平均滤波 + 一阶低通 │ └── pid.c/h // 增量式PID,参数可在线整定 └── App/ ├── control.c/h // 主控制逻辑,状态机 └── data_process.c/h // 数据预处理与单位换算驱动层往下封装硬件寄存器,算法层不依赖具体芯片,应用层只调接口。如果你要换主控,比如从F103换到G031,只需要改驱动层的.c文件,算法层完全不用动。这种解耦在电赛现场特别实用,因为临时换芯片是家常便饭,代码可分模块迁移就赢了别人一半。
2.3 核心数据流:采集、滤波、控制、输出怎么串起来
整个系统的数据流我用一句话概括:ADC以DMA方式持续采样多通道信号,每个采样周期触发一次回调,在回调里把原始值丢进滑动平均滤波器,滤波结果一方面送PID计算输出占空比或模拟量,另一方面经过单位换算显示到OLED,同时通过串口按固定帧格式上抛给上位机。看起来链路长,但每一环都是解耦的。
// main.c 主循环里的调度逻辑 while (1) { key_scan(); // 按键扫描,检测短按/长按 if (g_sample_flag) // 1ms定时器置位一次 { g_sample_flag = 0; adc_get_raw(&raw0, &raw1); // 取DMA搬运的原始值 filt_val[0] = filter_run(&filt0, raw0); // 滑动平均滤波 filt_val[1] = filter_run(&filt1, raw1); pid_out = pid_calc(&pid0, setpoint, filt_val[0]); control_apply(pid_out); // 更新PWM或模拟输出 oled_update(filt_val[0], filt_val[1], pid_out); uart_send_frame(filt_val[0], filt_val[1], pid_out); // 上抛调试 } }这里的g_sample_flag是定时器中断里置位的全局标志,主循环轮询它来实现1ms采样周期,比在中断里直接做滤波更安全。filter_run返回的是滤波后的浮点值,pid_calc内部包含了限幅和积分分离,输出直接给执行机构。这段逻辑说明了一个关键设计:中断只负责标记时间片,所有计算都放在主循环里,避免中断嵌套导致采样抖动。参数上,采样周期1ms对应滤波窗口建议10到20点,PID控制周期也是1ms,如果执行机构响应慢,可以把控制周期拉长到5ms,减少微分噪声。
3. 工程配置与参数调试:从烧录到看到正确波形
3.1 工程导入与编译配置:别让IDE配置吃掉一上午
拿到代码包后第一步不是急着打开,而是先核对工程对应的芯片型号。以常见IDE为例,打开工程后第一件事是进入Options for Target,把Device改成你手头实际用的型号,如果型号不匹配,编译能过但烧录后跑飞,这是很多人浪费半天时间的地方。我一般会先确认三件事:芯片型号、Flash大小、调试器协议。
编译前检查清单: 1. Device型号与实际MCU一致 2. Debug页选择SWD或JTAG,并设置对应下载器 3. 烧录算法(Flash Download)选择对应容量 4. C/C++ Compiler里Define的芯片宏(如 STM32F103xB)移植到其他型号时,最容易出问题的不是引脚定义,而是启动文件和芯片宏定义。比如从F103C8换到F103RB,Flash从64KB变到128KB,你得改启动文件startup_stm32f103xb.s、宏定义STM32F103xB以及烧录算法里的Flash大小三处,少一处程序就起不来或烧不进去。代码包沿用了一套经过验证的组合,你在自己工程里对照着改就行。
3.2 关键参数标定:ADC基准、滤波窗口、PID参数
跑通代码只是开始,出数据才是电赛H题的核心。参数标定我按三条线来走:ADC通道校准、滤波系数选择、PID整定。每一条都有对应的代码位置和调整方法,直接说结论。
ADC部分,代码里默认用的是内部基准,精度一般但胜在稳定。如果你对绝对精度有要求,改成外部基准芯片,并且每次上电做一次两点校准。逻辑很简单:采集两个已知电压,算出斜率和偏移,把所有采样值做线性修正。代码包里adc_adjust()函数就是干这个的,默认系数是1.0和0.0,你可以按实验数据替换。
// adc.c 中的线性校准函数 float adc_adjust(uint16_t raw) { // 默认两点校准:raw=0时输出0V,raw=4095时输出3.3V // 实际使用时用两个标准电压源标定,替换 slope 和 offset float slope = 3.3f / 4095.0f; float offset = 0.0f; return (float)raw * slope + offset; }滤波窗口的大小是调和矛盾:窗口越平滑度越好,但延迟越大。H题如果控制对象是电机,滤波延迟5ms就可能引起震荡;如果是测温度,延迟100ms也没关系。代码里默认窗口为10,适用于大多数快速反馈场景。你要是发现波形毛刺大,优先增大窗口到20;发现响应迟钝,反而要把窗口缩小到5。
PID参数整定,我给一个不碰运气的方法:先设Ki=Kd=0,只留Kp,从0.1开始一步步加,直到输出出现等幅振荡,记下此时的Kp和振荡周期Tu。然后按Ziegler-Nichols经验公式算其他参数:Kp=0.6Ku,Ki=1.2Ku/Tu,Kd=0.075Ku*Tu。这套方法在电赛现场比瞎调高效得多,代码里PID初始化参数的注释位置已经给你留好了填参数的格子。
3.3 功能验证:用串口把系统内部状态拉出来看
代码包默认在串口1上输出一个固定格式的调试帧,帧头是两个字节0xAA 0x55,后面跟数据负载和校验位。我用串口助手收到的数据类似这样:AA 55 1F 40 02 1C。但裸看十六进制很难直观判断滤波效果,我一般会写一个十几行的Python脚本,把串口数据解析成浮点数,再用matplotlib画波形。
# uart_plot.py 串口数据解析与实时绘图 import serial, struct import matplotlib.pyplot as plt ser = serial.Serial('COM3', 115200, timeout=1) buf = b'' plt.ion() fig, ax = plt.subplots() ydata = [] while True: buf += ser.read(64) idx = buf.find(b'\xaa\x55') if idx >= 0 and len(buf) >= idx + 7: frame = buf[idx:idx+7] # 帧格式: AA 55 + float32(滤波值) + uint8(校验) val = struct.unpack('<f', frame[2:6])[0] ydata.append(val) if len(ydata) > 200: ydata.pop(0) ax.clear() ax.plot(ydata) plt.pause(0.05) buf = buf[idx+7:]脚本里的帧格式必须与固件保持一致,重点确认 float 是4字节小端,校验字节是前5字节异或的结果。这段代码的作用是把黑匣子里的滤波、控制状态可视化成曲线,你调整参数时能直接看到波形变化。我习惯同时开两个通道:一路显示原始ADC值,一路显示滤波后值,两条曲线叠在一起,滤波效果一目了然,也方便判断是传感器噪声还是真实信号变化。
4. 电赛H题代码避坑:五条血泪经验
4.1 ADC采集值跳动量超出正常范围
现象:滤波前的ADC读数在±30 LSB之间随机跳,信号源明明很稳定,但采集曲线像毛刺一样。
原因:最常见的是参考地没处理好,传感器信号地和MCU地之间存在地环路;其次是ADC采样时间太短,代码里默认采样周期是1.5个ADC时钟周期,对于高内阻的传感器源阻抗,采样保持电容没充满电。
解决:先检查硬件,把传感器地和MCU地单点连接,避免大电流走线穿过模拟区;然后在adc_init()里把采样周期改为239.5个时钟周期,这是大多数MCU支持的最大值,代价是转换速度略降,但对H题这种低频采样场景完全够用。改完之后,跳动量通常能压到±5 LSB以内。
4.2 OLED屏幕显示花屏残留
现象:OLED刚通电正常,运行几分钟后出现残影闪烁,有时候局部区域刷新不过来。
原因:I2C总线上没有上拉电阻,或者上拉电阻太小导致信号边沿过缓;另一个高频原因是OLED电源纹波太大,尤其是和电机共用一个电源时。
解决:I2C的SCL和SDA各加一个4.7kΩ上拉到3.3V,如果MCU内部已经带上拉,外部再加会变慢,那就去掉外部的只保留内部的。供电上把OLED电源单独用一颗LDO供电,和电机驱动完全隔离。改完后花屏概率直接归零。
4.3 PID输出震荡收不住
现象:Kp调到稍大一点,输出就来回摆,执行机构嗡嗡响,系统像抽风一样。
原因:增量式PID在积分项没做限幅,输出限幅也没做好,误差累积导致执行机构反复过冲;另一个常见原因是控制周期太短,代码里1ms控制周期配合微分项放大了高频噪声。
解决:在PID结构体里增加积分限幅和输出限幅字段,积分项钳位到±50,输出钳位到PWM的10%到90%。微分项改成“微分先行”,只对反馈量求导,不对误差求导,这样设定值突变不会造成微分爆炸。我调整完这两个限幅之后,震荡基本消除。
4.4 程序烧录失败提示连接不上
现象:点击下载按钮,IDE报错“Cannot connect to target”,重复上电也没用。
原因:芯片内部读保护被意外打开,或者SWD引脚被复用成了GPIO功能,导致调试器无法访问。
解决:如果是读保护,用下载器的解除保护功能一键擦除整片之后再烧录;如果是SWD引脚复用,按住复位引脚的同时点击下载,在芯片启动初期强占调试端口,或者干脆把代码里SWD引脚初始化部分注释掉,重新烧录一个空程序恢复调试功能。这个坑在电赛现场最容易让人血压升高,其实两步就解决。
4.5 串口输出乱码且帧头不稳定
现象:串口助手收到的数据偶尔出现整帧乱码,或者帧头错位,数据和预期完全对不上。
原因:串口波特率虽然配置成115200,但MCU的时钟源用的内部RC振荡器,精度只有1%到2%,长时间通信累积误差就会导致采样点偏移;另一种情况是上位机解析代码里帧结构定义和固件不一致。
解决:优先用外部8MHz晶振作为系统时钟源,代码包里时钟配置默认打开外部晶振,如果你移植到没有外部晶振的板子上,在SystemClock_Config()里修改时钟源并检查实际波特率误差。上位机那边,帧解析不能用简单的“找帧头”,要加状态机处理粘包和半包,防止数据错位一错到底。我一般会先在串口助手里手动发一帧已知数据,确认收发的字节完全一致,再跑完整协议。
5. 最后一步:把代码改造成自己的赛题方案
5.1 模块化裁剪与参数固化
代码包里有些功能你在赛题里用不到,比如按键扫描或者OLED显示,可以直接在工程里移除对应文件,同时把main.c里的调用注释掉。裁剪的好处不只是减少Flash占用,更能降低干扰——每个外设中断都是潜在的Bug源,少一个外设就少一条排查路径。我在实际移植时会保留ADC、滤波、控制三个核心模块,把显示和按键改成条件编译,通过宏开关控制是否生效。
5.2 加一个自定义工作模式
赛题往往要求两三种工作模式切换,比如“手动模式”和“自动模式”。我习惯在control.c里加一个枚举类型的状态变量,用按键或串口命令切换。每切换一次模式,PID参数重新加载,输出先归零再启动,防止模式切换瞬间产生冲击。这套代码的PID模块已经留了参数数组接口,你可以预存三组PID参数,按模式索引加载。
5.3 赛前验证清单
交代码前,我从血的教训里总结了一个十二项检查清单,走完一遍再封箱。重点看这几条:所有全局变量确认初始化再使用;ADC采样周期和滤波窗口是否匹配;PID输出限幅是否小于等于执行机构能承受的上限;串口帧格式是否与上位机完全一致;供电电压在满载时是否跌出芯片工作范围。如果你用的是无线模块或功率驱动,还要额外检查地线是否够粗。
让我把话说得更直白一点:这套代码包最宝贵的不是那一堆能编译的.c文件,而是它把“采集—滤波—控制—显示—通信”这条链路以可拆解的方式摆在你面前。你抄它的驱动,改它的算法,删掉不需要的功能,最后长出自己的代码。从那以后,我每次移植这套框架到新项目,都会强制走一遍串口波形验证流程,哪怕是第一次烧录也先确认数据链路再调控制环。希望这份代码能帮你把赛前焦虑换成实打实的进度,也希望你在调PID震荡时能想起本文第四节那些坑——少走弯路,一次跑通。
本文还有配套的精品资源,点击获取