简介:这份获奖代码包来自2019年全国大学生电子设计竞赛D题,由获得国家二等奖的团队整理,适合备赛学生、嵌入式开发者学习参考。资源内含完整的Keil MDK工程,基于STM32F10x系列实现,覆盖ADC采集、定时器控制、LCD显示、I2C/CAN通信等外设驱动,并包含编译脚本与烧录配置,便于直接编译验证;压缩包共156个文件,以c/h源码、o/crf/d等编译中间文件为主,另含uvprojx工程、hex/axf固件及bat批处理脚本,整体仅2.98MB,结构紧凑。目前已有5347人学习下载,代码版本标注为v3,说明经过多轮迭代优化,逻辑清晰且经测试无bug,可作为D题软硬件协同设计的完整参考。通过研读工程架构、驱动封装和数据处理流程,能够有效提升STM32项目实战能力,为竞赛或课程设计提供可复用的实现思路。
1. 2019年电赛D题:为什么一个“二等奖代码包”值得反复看
2019年全国大学生电子设计竞赛的D题「线路负载及故障检测装置」,是当年B、C、D三题里最“硬”的一道:它不考花哨的算法,考的是从信号发生、采样调理到阻抗计算的一整条测量链路。网上流传的“2019年全国电子设计大赛D题国家二等奖代码.rar”,真正值钱的部分不在主函数写成什么样,而在里面藏着的一整套测量方案——如何让单片机产生稳定的激励信号、如何测准毫伏级的线路压降、用什么策略在几毫秒内区分空载、阻性、容性、感性和短路五种状态。这篇文章就沿着这条链路,把国二方案的原理、代码结构和必须调的参数拆开讲。新手能照着重现一遍,熟手能拿它当基线去优化测量精度。
2. 赛题拆解与系统架构:先搞清楚测量对象,再谈代码
2.1 赛题要求:不是做电源,是做“阻抗分析仪”
D题表面上是“检测线路负载和故障”,本质上要求你做一台小型的阻抗分析仪。装置需要往待测线路里注入一个已知频率的交流激励信号,再测量线路上的电压和电流,从而推算出负载的阻抗模值和相位角。负载类型通过相位角判断:相位接近0°是纯阻性,电流滞后电压是感性,电流超前电压是容性,阻抗模值接近0是短路,电流几乎为零是空载或断路。
这个题目的坑在于:它不是直接用万用表测电阻,而是要在动态激励下完成测量。因此整台装置的信号链必须闭环——激励信号要稳定,采样要同步,计算要实时。国二方案之所以能拿奖,不是因为单片机的运算能力多强,而是把这条链路上的每一环都做得足够扎实:DDS信号源保证频率可调且稳定,电流采样用精密电阻或互感器保证线性度,ADC同步采样保证电压电流相位差不漂移。
2.2 系统框图与信号流动方向
代码只能在硬件已经正确搭建的前提下工作。常见做法是分成四个功能块:信号源、采样前端、数字处理、人机交互。
第一个功能块是信号源,用DDS芯片或者单片机内部定时器翻转生成SPWM,再经低通滤波和功率放大后输出到待测线路。第二个功能块是采样前端,线路电流经过采样电阻变成电压信号,线路端电压经分压网络衰减到ADC可接受的范围。第三个功能块是数字处理,单片机对两路信号做同步采样、计算幅值和相位差。第四个功能块是人机交互,按键切换负载类型显示、数码管或液晶屏显示结果。
这四块里最容易在代码阶段翻车的是采样前端。很多方案采用普通的电阻分压和运放跟随,但高频下的分布电容会让相位测量偏移几度,这几度之差足以把感性负载误判成容性。国二方案通常会在运放反馈网络里加一个几皮法的补偿电容来抵消运放输入电容的相移,或者在软件里做相位零点校准来解决。
2.3 关键器件选型逻辑:从代码需求倒推硬件
选型直接从代码需求倒推,不要先订硬件再写程序。ADC采样率决定了你能用的激励频率上限。比如采用12位ADC、采样率设定为1MHz,激励频率开到10kHz至20kHz是比较稳的,超过这个范围每周期采样点太少,幅度和相位计算的误差会急剧恶化。
激励频率的选择也反过来影响代码逻辑。如果只测量纯阻性负载,低频就够用;但测量感性负载需要足够的感抗才能产生可分辨的相位差,所以频率不能太低。国二方案里常见的选择是固定1kHz或者2kHz的激励频率,容性和感性负载在这个频率下都能产生30°以上的相位偏转,足以可靠区分。如果采用多频点扫描的进阶方案,代码里就需要一个频率切换的状态机,每个频点完成一次完整测量后再切下一频点。
DDS芯片选型上常见的是AD9833这类串行接口芯片,输出正弦波后接一级二阶低通滤波器再加缓冲放大。如果不用DDS,用单片机定时器在IO口翻转生成方波,再经过有源滤波器整形,也能得到可用的正弦激励源——但数字滤波器的转折频率必须回避激励频率的谐波分量,否则测出来的信号会带明显的毛刺,这一点在代码里要通过滤波参数预先规划好,也是一种常见的做法。
3. 核心测量算法:频率、相位差与负载识别的实现
3.1 测频与测相位差:过零检测该怎么写才稳
负载识别的核心是相位差,而相位差测量的第一步是先确定激励信号的频率。国二方案里最常用的是过零检测法:用比较器把正弦波整形成方波,定时器输入捕获通道捕获上升沿,记录相邻两次上升沿的时基计数值,从而算出频率。
过零检测看似简单,实际有两个容易出错的地方。第一是噪声引起的过零抖动,信号在零点附近微小的电压抖动就会产生毛刺计数;第二是相位差计算必须用同一个时基,否则两边各自采样会导致误差。
以下代码是过零检测加相位差测量的常用写法,基于定时器输入捕获实现,能直接嵌入主流MCU工程:
// 定时器输入捕获中断服务程序 volatile uint32_t cap_edge_time = 0; // 上一次捕获时刻 volatile uint32_t period_ticks = 0; // 一个完整周期的定时器计数个数 volatile uint8_t edge_count = 0; // 捕获边沿计数 void TIM_IRQHandler(void) { uint32_t curr_time = TIM_GetCapture(); // 读捕获寄存器(单位:定时器计数) uint32_t delta; if (edge_count % 2 == 0) { period_ticks = curr_time - cap_edge_time; // 半个周期的计数值 } cap_edge_time = curr_time; edge_count++; }这段代码的逻辑是:在输入捕获中断里连续记录相邻同极性边沿的计数之差,间接获得半周期或者整周期的计数值。之所以在偶数次进中断时更新周期值,是为了只在上升沿(或下降沿)到来时才刷新,避免高低电平宽度不一致导致频率计算跳动。定时器时钟频率和信号频率的关系是:信号频率 = 定时器时钟频率 / period_ticks。注意这段代码里读两次捕获寄存器之间要有足够短的空窗期,否则会丢失边沿。
参数说明:输入捕获的触发边沿要设成上升沿加下降沿交替触发(即边沿计数),这样能同时获得半周期信息和占空比信息。延时滤波可以配合一个简单的防抖计数——连续捕获到多次间隔相近的边沿后才更新周期值,防止随机噪声干扰。
3.2 相位差测量:两个通道的过零点必须“找齐”
负载阻抗的相位角等于电压过零点和电流过零点的时间差除以周期再乘以360°。常见做法是构造两个过零检测比较器,分别接在线路电压和电流采样信号后,用单片机的两路输入捕获一起记录过零时刻。
相位差计算的C代码逻辑如下:
// 计算电压与电流的相位差,单位:度 float calc_phase_diff(uint32_t vol_zero_ticks, uint32_t cur_zero_ticks, uint32_t period_ticks) { int32_t delta; float phase_deg; delta = (int32_t)cur_zero_ticks - (int32_t)vol_zero_ticks; // 允许多个周期差纠正,保证差值落在一个周期范围内 while (delta < 0) delta += (int32_t)period_ticks; while (delta >= (int32_t)period_ticks) delta -= (int32_t)period_ticks; phase_deg = 360.0f * (float)delta / (float)period_ticks; return phase_deg; }这段代码是相位差测量的主干:两路捕获中断各维护一个过零时刻,主循环在读到的时刻差上做周期模运算。因为电压和电流的捕获中断可能在同一个信号周期内发生,也可能跨周期发生,所以delta需要将差值修正到0到周期值的范围内。参数说明:period_ticks最好取最近多次测量的平均值,不要用单次测量值,否则相位差计算会被单周期频率抖动放大。
工程上要注意的一个细节是:电压过零和电流过零的捕获不能一个用查询一个用中断,两个通道必须对称。常见错误是用轮询查电流通道的电平,导致相位差误差达几十微秒,在1kHz信号下换算成角度就是十几度,直接导致容性和感性的误判。
3.3 负载识别:阻抗模值与相位角的联合判断
有了相位差和有效值,就可以计算负载阻抗。计算阻抗模值通常用电压有效值除以电流有效值。这里有一个工程上的重点:电流采样信号经过运放放大后会有增益误差和直流偏置,必须在校准阶段测量并保存增益系数。
负载识别状态机的常见逻辑是:
// 负载类型判定函数 typedef enum { LOAD_OPEN = 0, // 空载或断路 LOAD_SHORT, // 短路 LOAD_RES, // 阻性 LOAD_IND, // 感性 LOAD_CAP, // 容性 } load_type_t; load_type_t classify_load(float abs_z, float phase_deg) { // 阈值需要根据实际线路和校准结果调整 if (abs_z > Z_OPEN_THRESHOLD) { return LOAD_OPEN; // 电流很小,视为空载 } if (abs_z < Z_SHORT_THRESHOLD) { return LOAD_SHORT; // 阻抗极小,视为短路 } // 相位角阈值通常取 ±10 度,落在区间内视为阻性 if (phase_deg > -10.0f && phase_deg < 10.0f) { return LOAD_RES; } if (phase_deg >= 10.0f) { return LOAD_IND; // 电流滞后电压:感性 } return LOAD_CAP; // 电流超前电压:容性 }这段状态机代码的逻辑顺序是:先判断极端情况(空载和短路),再判断阻性区间,最后根据相位差符号区分感性和容性。顺序不能颠倒——如果短路时相位差接近0°,先判阻性就会出错。参数说明:Z_OPEN_THRESHOLD和Z_SHORT_THRESHOLD需要结合线路本身阻抗来设定,线路较长时线阻本身也会贡献几十毫欧甚至几百毫欧,短路阈值必须高于最大线阻。
国二方案在这个状态机上通常还加了一级确认机制:连续两次测量得到相同分类才输出结果,否则维持上一次状态。这个机制可以防止继电器触点抖动或者瞬时干扰造成的误判,实现成本只有一段简单的状态计数代码。
4. 软件结构与调参路径:拿到代码包后从哪里看起
4.1 代码模块划分:不是所有函数都值得改
解压这样一个竞赛代码包,常见文件结构里会包含硬件初始化、信号生成、ADC采样、计算处理、显示和按键这几个模块。不同模块的“可修改程度”差异很大,调试时通常把注意力集中在计算和判定模块,而不是底层驱动。
一个典型的锦标赛代码包,各模块的职责划分如下:
| 模块 | 职责 | 典型文件/函数 | 调试重点 |
|---|---|---|---|
| 初始化 | 时钟、GPIO、DDS配置 | init_hw.c | 上电时序是否正确 |
| 信号生成 | SPDWM/DDS波表输出 | signal_gen.c | 输出频率和幅度稳定度 |
| 数据采集与计算 | ADC读取、均方根值、阻抗相位 | measure.c | 采样点数、滤波参数 |
| 负载识别 | 状态机与阈值判定 | classify.c | 阈值与延时确认 |
| 人机交互 | 按键扫描、显示刷新 | ui.c | 刷新频率是否拖慢主循环 |
拿到代码包先看measure.c和classify.c。这两个文件决定了测量链路的上限,也最常被改动。初始化文件和显示文件基本不需要动——DDS的初始化参数可能要根据实际晶振频率调整,但那是板级差异,一般只改一个常量即可。
4.2 主循环状态机:测量和刷新不能抢时间
很多电赛程序是裸机“大循环”结构,主循环里依次做采样、处理、显示。这个结构在D题里有个隐患:测量过程对实时性要求很高,而显示刷新和按键扫描会时不时打断采样的连续性,导致ADC采到的波形不完整。
国二方案通常把主循环设计成简单的状态机,测量只在“测量使能”状态下进行,其他时刻只刷新界面代码:
// 主循环状态机:MEASURE / DISPLAY / WAIT void main_loop(void) { uint8_t state = STATE_IDLE; uint32_t last_measure_time = 0; while (1) { switch (state) { case STATE_IDLE: if (button_pressed()) { start_measure_timer(); state = STATE_MEASURE; } display_refresh(); // 空闲时刷新显示 break; case STATE_MEASURE: if (measure_timer_expired()) { sample_and_calc(); // 同步采样并计算阻抗 state = STATE_SHOW; } break; case STATE_SHOW: output_result(); state = STATE_IDLE; break; } } }这段主循环的逻辑是:按键触发后开始测量,测量完成后显示一次结果,然后回到空闲状态。状态机的优势在于测量过程中不会被显示刷新打断——display_refresh()只出现在空闲状态,测量状态里完全没有耗时操作。注意measure_timer的触发周期必须大于一次完整采样计算的时间,否则状态机永远停留在测量态,表现为主界面卡死。
4.3 三个必调参数:采样长度、滤波系数与判定阈值
代码包能跑通不代表能在你手头硬件上跑得准。三个参数几乎必须根据实物重新调。
第一个参数是单次测量的采样长度。国二方案一般每个通道采256点或512点,对应1kHz激励信号、10kHz采样率就是每周期10个点。如果采样长度太短,均方根值计算受波形截断效应影响;如果太长,测量周期变大,按键响应变慢。常用做法是先按期望的刷新率反推采样长度:主循环每次刷新不超过50毫秒的前提下,采样点数尽量多。
第二个参数是滤波系数。多数电路板存在轻微的50Hz工频干扰和运放失调噪声。常见做法是采样数据先做一阶低通,但滤波系数过大会带来几十毫秒的群延迟,导致相位差计算出现系统误差。一个稳妥的折中方案是解耦滤波:对有效值做较强滤波用于幅度计算,对捕获时刻的原始值只做轻微滤波或不滤波用于相位计算。
第三个参数是负载分类阈值。这个参数在不同线路长度、不同激励幅度下都不同。常见做法是先接一个已知阻值的精密电阻做基准测量,记录实测阻抗模值和相位,再根据基准值计算该线路下的阈值。没有这一步,代码里写的默认阈值基本就是摆设。
5. 踩坑记录:从现象、原因到排查方法
5.1 频率显示乱跳:过零检测被噪声干扰
现象是不接负载时频率显示在980Hz到1050Hz之间来回跳,接上负载后反而稳定。原因是空载时电流很小,过零比较器的输入信号接近噪声电平,信号在零点附近反复抖动,产生大量虚假过零沿。
排查方法是先用示波器看比较器输入端的波形,确定空载时噪声幅度。解决思路有两个:一是通过减小电流采样电阻阻值来恶化信噪比,这方向不对;正确的方法是提升比较器滞回电压,让信号必须越过一个死区才触发翻转,或者在代码里加一个边沿间隔有效性检查——只有在间隔接近上次周期的80%到120%范围内的边沿才被接受。
5.2 容性负载被识别成短路
现象是接入几微法电容后,程序报“短路”而不是“容性”。原因是电容刚接入的瞬间产生充电浪涌电流,阻抗模值的瞬时值远低于稳态值,状态机在稳态前就完成了判定。
原因是代码里的采样窗口太短,电容充电过程尚未结束就计算了均方根值。解决思路:检测到负载切换后,延长200到300毫秒延时再开始采样计算;如果电容容量很大,还要在代码里增加一个等待电流趋稳的判断——连续几次计算有效值的波动小于预设百分比后才输出结果。
5.3 感应负载的相位差总是偏小
现象是接入一个标称电感后,实测相位差只有10°左右,与理论值30°以上相差甚远。原因是电感在1kHz激励下感抗有限,同时线路存在明显的寄生电阻,阻抗角被拉低。
排查时先用LCR表测量该电感在1kHz下的真实感抗和等效串联电阻,再计算理论相位角。解决思路集中在软件校准:采样前端和信号调理电路自身也贡献了额外的相移,所以在测量之前需要做一次系统零点校准——把纯阻性负载接入线路,记录当前的相位差作为系统偏置,之后的每次测量都减去这个偏置。
5.4 采样点数加大后相位差反而漂移
现象是程序每次采样点数从128提升到512后,计算结果反而不稳定,在不同时刻测量同一负载的相位差波动达几度。原因是采样点数增加后,单次采集时间变长,而激励源存在微小频率漂移,导致电压通道和电流通道的过零时刻不再落在同一周期的对应位置。
排查方向是检查激励源频率是否稳定——如果DDS的参考时钟来自片上RC振荡器,温漂可能达到百分之一甚至更高。解决思路是将激励源时钟切换为外部晶振,或者在代码中增加频率跟踪:每轮测量先测频率,用实测频率而非设定频率去换算相位差。
5.5 继电器切换负载后出现随机误判
现象是每次用继电器切换接入的负载类型,第一次测量结果偶尔错误,第二次起恢复正常。原因是继电器触点在吸合瞬间存在抖动和接触电阻变化,ADC在这段时间采到的是不完整的电压电流波形。
解决思路是在软件里对继电器切换进行“异步确认”:切换动作发出后,先等待并执行一段独立的延时任务,再开启采样。更可靠的做法是双重确认:先等待固定延时,然后连续采集两轮,两轮结果一致才更新显示,否则丢弃重测。这样可以避免在触点抖动余波未散时开始测量。
6. 进阶技巧:把方案从“比赛能用”磨成“测量工具”
如果把国二代码看作基线,最值得投入的进阶改造是把固定单频点测量改成双频点复测。增加一个5kHz的测量频点,可以大幅提升感性负载的区分度——同一个电感在1kHz和5kHz下的感抗差5倍,而纯阻性负载在两种频率下阻抗几乎不变。两者交叉比对,既能判断负载类型又能估算电感量的大致范围。
第二个值得做的进阶改造是自动校准流程。很多参赛者的方案是手动校准,在代码里固定一个零点相位偏置。更好的做法是在装置上电时自动进入校准状态:断开线路输出,内接一个精密电阻作为负载,自动测量并保存当前温度、当前供电电压下的系统增益和相位偏置。这样环境温度导致的运放漂移可以在每次测量前被吸收掉。
第三个技巧是关于查看实时中间量的调试习惯。我一般会在代码里保留一个调试模式,按键切换到调试界面后,把原始的阻抗模值和相位角显示出来,而不是只显示负载类型。比赛现场排查问题时,这个模式能让你一眼看出是测量误差还是判定逻辑问题。如果只看到最终类型判断,出了错根本没法定位。
第四个技巧是保存一组“黄金测试用例”。我习惯把自己手头的精密电阻、电感和电容的实际特性用LCR表测一遍,记录下来作为回归测试标准。改动任何参数后,先把这组标准负载逐一接入,确保判定结果与之前完全一致。这个习惯帮我在很多次参数调优后避免了“改好一个,弄挂三个”的尴尬境地。
以上这些方法,都是从那份国二代码包的实际使用经验里逐步沉淀出来的。如果能有一套精度可靠的负载样本,配合自动校准和双频交叉验证,这套装置就不止是一个竞赛作品,而是一台能应对日常线路排查的小工具了。希望帮到你。
电赛D题顺手的解法,往往不是把算法写得多深——测量链路稳得住,代码结构清爽,阈值留出余量,才是真正决定成绩的细节。
本文还有配套的精品资源,点击获取