简介:本资源为2019年全国大学生电子设计竞赛D题的国家二等奖完整代码工程,面向备战电赛的高校学生、嵌入式开发者及希望提升STM32实战能力的工程师。压缩包共156个文件,约2.98MB,以C语言源文件与头文件为主体,包含大量STM32标准外设库驱动、LCD显示、ADC采样、定时器与I2C通信等模块,另附Keil工程配置、编译中间文件与烧录脚本,可直接导入开发环境复现与调试。目前已有5345人学习下载,热度较高。读者可借此研究获奖方案的模块划分与系统架构,理解信号采集、数据处理与控制策略的实现思路,学习硬件接口调用与异常处理机制,并参考其命名规范、注释风格与调试方法,为自身项目开发与赛前训练提供可落地的参考范本。
1. 从一份电赛D题国二代码说起:简易电路特性测试仪到底在测什么
2019年全国电子设计大赛D题,题目名字叫“简易电路特性测试仪”。如果你手头正好有一份当年国家二等奖的代码包,打开之后大概率会看到几个文件夹:MCU主控、AD采样、DDS信号源、LCD显示、串口上位机。很多人第一反应是“这不就是个扫频仪吗”,但真正做过这道题的人知道,它比扫频仪多了一层——要在未知电路网络面前,自动判断它是什么类型的放大器、增益多少、输入输出阻抗大概在什么量级。
这道题的核心诉求其实很朴素:给你一个黑匣子电路,只露出输入输出两个端口,你要在尽量少的接线条件下,测出它的幅频特性、输入阻抗、输出阻抗,并且判断它属于低通、高通、带通还是谐振类型。2019年那批国二方案里,主流做法是DDS扫频加峰值检波,再用MCU做幅值采集和曲线拟合。代码包里最值钱的部分不是显示界面,而是扫频策略、阻抗计算和类型判定的那几百行逻辑。
适合读这篇的人有三类:正在准备电赛信号类题目的学生、想复现这套测量思路的嵌入式工程师、以及手头有类似代码包但跑不起来想搞懂原理的人。下面我按“信号怎么发、数据怎么收、阻抗怎么算、类型怎么判、坑在哪”这条线,把这份代码背后的方案拆开讲。
2. DDS扫频与峰值检波:信号源和采集链路怎么搭
2.1 为什么选DDS而不是PWM滤波
当年很多队伍第一反应是用MCU的PWM加RC滤波产生正弦波,成本低、电路简单。但D题要求扫频范围覆盖1kHz到1MHz甚至更宽,PWM方案在高频段谐波分量压不下去,检波出来的幅值会严重偏高,导致幅频曲线在高频段“翘尾巴”。国二方案普遍改用专用DDS芯片,常见的是AD9833或AD9850这一类,MCU通过SPI写频率控制字,输出干净的正弦波。
以AD9833为例,输出频率公式是:
f_out = (FREQREG × f_MCLK) / 2^28其中f_MCLK一般是25MHz晶振。要输出10kHz,FREQREG = 10000 × 2^28 / 25000000 ≈ 107374。代码里通常封装成一个函数:
// AD9833设置输出频率,freq单位Hz,mclk单位Hz void AD9833_SetFreq(uint32_t freq, uint32_t mclk) { uint32_t reg = (uint32_t)(((uint64_t)freq << 28) / mclk); uint16_t lsb = reg & 0x3FFF; uint16_t msb = (reg >> 14) & 0x3FFF; AD9833_Write(0x2000 | lsb); // 写频率低14位 AD9833_Write(0x2000 | msb); // 写频率高14位 }这里有个容易翻车的点:freq << 28在32位机上会溢出,必须先转成64位再移位。我见过不止一个队伍在这个地方得到乱七八糟的频率,查了半天以为是DDS坏了。
2.2 峰值检波电路的关键参数
DDS出来的是正弦波,经过被测网络之后幅值会随频率变化。要测这个幅值,最常用的方案是二极管峰值检波。电路看起来简单——一个二极管、一个电容、一个电阻——但参数选错,整个扫频曲线就是废的。
检波二极管要选高频肖特基,比如常见的1N60或BAT85,普通1N4148在高频段结电容太大,检波效率掉得厉害。检波电容和负载电阻构成放电时间常数τ=RC,这个τ必须远大于信号周期但远小于扫频步进间隔。假设最低频率1kHz,周期1ms,τ至少取10ms;扫频每步停留5ms的话,τ又不能超过2ms左右,否则电容还没放完电就读数,幅值会虚高。
实际调试时我一般这样定:C取10nF,R取100kΩ,τ=1ms。然后在1kHz和1MHz两个端点分别用示波器看检波输出纹波,如果纹波超过直流分量的10%,就把C加大一档。这个经验值不是绝对的,跟二极管正向压降和后级ADC输入阻抗都有关系。
2.3 ADC采样与软件滤波
检波出来的直流电压送到MCU的ADC引脚。STM32的12位ADC在3.3V参考下分辨率是0.8mV,看起来够用,但DDS输出幅度本身可能有±5%波动,电源纹波也会串进来。代码里通常做两件事:一是每次频率点连续采16次取平均,二是去掉第一次采样(因为切换频率后检波电容需要建立时间)。
// 采集一个频率点的幅值,丢弃前2次,剩余14次取平均 uint16_t Measure_Amplitude(void) { uint32_t sum = 0; for (int i = 0; i < 16; i++) { uint16_t val = ADC_Read(); if (i >= 2) sum += val; Delay_us(200); // 等检波电容稳定 } return (uint16_t)(sum / 14); }这里的200us延时不是随便写的。检波时间常数1ms的话,建立到99%需要约5τ=5ms,但那是从零开始的情况。扫频时相邻频率点幅值变化不会太剧烈,200us通常够用。如果发现曲线在谐振峰附近跳动很大,就把延时加到500us甚至1ms。
3. 输入输出阻抗计算:从分压法到三点法
3.1 输入阻抗为什么不能直接测
被测网络输入端等效为一个电阻并联一个电容,直接拿万用表测电阻档是不行的,因为万用表输出的是直流,电容开路,测到的只是电阻分量,而且万用表的测试电压可能让有源电路进入非线性区。正确做法是在输入端串联一个已知电阻R_s,分别测R_s两端对地的交流幅值,用分压比反推输入阻抗。
假设DDS输出幅值为V_s,串联电阻R_s后接到被测网络输入端,测得输入端对地幅值为V_in,则:
Z_in = R_s × V_in / (V_s - V_in)这个公式只在输入阻抗为纯电阻时严格成立。如果输入端有电容,V_in会随频率变化,算出来的Z_in也是频率的函数。代码里一般选一个中间频率(比如10kHz)来算输入阻抗的模值,同时记录多个频率点的值来看趋势。
3.2 输出阻抗的两步测量法
输出阻抗的测量思路类似,但操作上多一步。先测被测网络空载时的输出幅值V_o1,然后接上已知负载R_L,再测输出幅值V_o2,则:
Z_out = R_L × (V_o1 - V_o2) / V_o2R_L的选取很关键。太大,V_o2和V_o1差别太小,ADC分辨率不够;太小,输出可能被拉垮甚至进入非线性。经验做法是先估一个大概范围,比如假设输出阻抗在几百欧到几kΩ,就取R_L=1kΩ试一次,如果V_o2不到V_o1的一半,说明Z_out比R_L大,换10kΩ再测;如果V_o2几乎等于V_o1,说明Z_out远小于R_L,换100Ω。
代码里通常把这两个步骤做成自动切换:
// 自动选择负载电阻测输出阻抗 float Measure_Zout(void) { float Vo1 = Measure_Amplitude(); // 空载 float Zout = 0; uint16_t loads[] = {100, 1000, 10000}; // 三档负载 for (int i = 0; i < 3; i++) { Set_Load(loads[i]); Delay_ms(10); float Vo2 = Measure_Amplitude(); if (Vo2 > Vo1 * 0.2 && Vo2 < Vo1 * 0.8) { Zout = loads[i] * (Vo1 - Vo2) / Vo2; break; } } Set_Load(0); // 恢复空载 return Zout; }这段逻辑里0.2和0.8这两个阈值是血泪经验。低于0.2说明负载太重,测出来误差大;高于0.8说明负载太轻,信噪比不够。只有落在中间区间,分压比才既有足够变化量又不至于失真。
3.3 幅频特性曲线的扫频策略
扫频不是从1kHz到1MHz均匀取100个点就完事。如果均匀取点,在谐振峰附近可能只采到两三个点,峰值的形状完全看不出来。国二方案里常见的是分段变步长:低频段步进大一些(比如100Hz),接近预估谐振频率时步进缩小到10Hz甚至1Hz。
但问题是你事先不知道谐振点在哪。所以代码里通常先做一次粗扫,比如每100Hz一个点,找到幅值最大值对应的频率f_peak,然后在f_peak附近±500Hz范围内做细扫,步进10Hz。这样既保证速度又保证峰值分辨率。
// 粗扫找峰值,再细扫 uint32_t Coarse_Scan(float *curve, uint32_t start, uint32_t end, uint32_t step) { uint32_t peak_freq = start; float peak_val = 0; for (uint32_t f = start; f <= end; f += step) { AD9833_SetFreq(f, 25000000); Delay_ms(5); float amp = Measure_Amplitude(); curve[(f - start) / step] = amp; if (amp > peak_val) { peak_val = amp; peak_freq = f; } } return peak_freq; }粗扫步进100Hz、范围1k到1M的话,一共约10000个点,每点5ms就是50秒,太慢了。实际代码里粗扫步进会取1kHz甚至10kHz,把总时间压到5秒以内,然后再在峰值附近细扫。这个时间分配需要根据题目要求的测量速度来调,没有固定值。
4. 类型判定与避坑:那些让曲线变成玄学的细节
4.1 四种滤波器类型的判定逻辑
测出幅频曲线之后,要判断被测网络是低通、高通、带通还是谐振。判定逻辑不复杂,但阈值设不好就会把带通判成谐振。常见做法是看曲线形状:
- 低频段幅值低、高频段幅值低、中间有单峰:带通或谐振
- 低频段幅值高、高频段幅值低:低通
- 低频段幅值低、高频段幅值高:高通
- 峰值频率处幅值远高于其他频段(比如高20dB以上):谐振
带通和谐振的区别在于品质因数Q。Q = f_peak / 带宽,带宽是幅值降到峰值0.707倍的两个频率之差。Q大于10一般认为是谐振,小于10认为是带通。这个阈值不是死的,题目如果明确要求区分,代码里会把Q值也显示出来让用户判断。
// 根据扫频曲线判定类型 FilterType Judge_Type(float *curve, uint32_t n, uint32_t start, uint32_t step) { float max_val = 0, min_val = 1e9; uint32_t max_idx = 0; for (uint32_t i = 0; i < n; i++) { if (curve[i] > max_val) { max_val = curve[i]; max_idx = i; } if (curve[i] < min_val) min_val = curve[i]; } float low_avg = (curve[0] + curve[1] + curve[2]) / 3; float high_avg = (curve[n-1] + curve[n-2] + curve[n-3]) / 3; float ratio = max_val / min_val; if (ratio < 2) return TYPE_UNKNOWN; // 曲线太平,可能没接好 if (low_avg < max_val * 0.3 && high_avg < max_val * 0.3) { // 中间高两边低,看Q值 float bw = 0; for (uint32_t i = 0; i < n; i++) { if (curve[i] > max_val * 0.707) bw += step; } float Q = (float)(start + max_idx * step) / bw; return (Q > 10) ? TYPE_RESONANT : TYPE_BANDPASS; } if (low_avg > high_avg * 3) return TYPE_LOWPASS; if (high_avg > low_avg * 3) return TYPE_HIGHPASS; return TYPE_UNKNOWN; }这段代码里ratio < 2那个判断是防止误判。如果曲线最大最小值差别不到2倍,很可能是接线没接好或者信号源幅度太小,这时候强行判类型没有意义,直接报未知更诚实。
4.2 避坑:五个让国二方案也翻车的细节
现象一:扫频曲线在低频段整体偏高,高频段正常。原因是检波电容放电太慢,低频时周期长,电容上的电压还没放完下一个周期就来了,检波输出偏高。解决方法是减小检波电容或者增大负载电阻,把τ降下来。我一般会把τ控制在最低信号周期的1/5左右。
现象二:谐振峰附近曲线跳动超过±10%。原因是扫频步进太粗,峰值点没采到;或者DDS切换频率后检波电容建立时间不够。解决方法是峰值附近细扫,步进降到10Hz,同时把每点延时从200us加到1ms。
现象三:输入阻抗测出来是负数。原因是V_in > V_s,说明被测网络有源或者发生了自激。这时候要检查被测电路的供电是否正常,输入端是否有隔直电容。如果被测网络本身有增益,分压法就不适用了,得换用注入电流法。
现象四:输出阻抗三档负载测出来差很多。原因是输出端有电容,不同负载下电容的充放电时间不同,导致检波读数不一致。解决方法是固定测量频率,选一个远低于输出端截止频率的点来测,比如1kHz。
现象五:类型判定在带通和谐振之间反复横跳。原因是Q值刚好在10附近,阈值太敏感。解决方法是把判定结果和Q值一起显示,让用户自己判断,或者把阈值改成8和12两个档,中间区域报“带通/谐振”。
5. 从能跑到好用:扫频速度与精度的平衡技巧
代码能跑通只是第一步,真正拉开差距的是扫频速度和精度的平衡。国二方案里有一个细节很值得学:他们不是全程用同一个步进,而是根据曲线斜率动态调整。斜率大的地方步进小,斜率小的地方步进大。
具体做法是每采一个点,和上一个点比较幅值变化率。如果变化超过5%,下一步步进减半;如果变化小于1%,步进加倍。这样在平坦区域快速通过,在峰值附近自动放慢。实测下来比固定步进省30%到50%的时间,而且峰值分辨率不降。
// 动态步进扫频 void Adaptive_Scan(float *curve, uint32_t *freqs, uint32_t *n) { uint32_t f = 1000; uint32_t step = 1000; float last_amp = 0; *n = 0; while (f <= 1000000) { AD9833_SetFreq(f, 25000000); Delay_ms(3); float amp = Measure_Amplitude(); curve[*n] = amp; freqs[*n] = f; (*n)++; if (*n > 1) { float change = fabs(amp - last_amp) / (last_amp + 1); if (change > 0.05) step = step / 2; else if (change < 0.01) step = step * 2; if (step < 10) step = 10; if (step > 10000) step = 10000; } last_amp = amp; f += step; } }这段代码里last_amp + 1是为了防止除零。步进上下限10Hz和10kHz是根据DDS切换速度和ADC采样时间定的,太小了总时间爆炸,太大了会漏掉峰值。
还有一个技巧是上位机显示。代码包里通常带一个串口上位机,把扫频数据实时画成曲线。但串口传输1000个点如果每个点都发,波特率115200下也要好几秒。实际做法是MCU端先做峰值检测和类型判定,只把关键点(起点、峰值点、-3dB点、终点)和判定结果发上去,曲线用插值画。这样传输时间压到200ms以内,用户体验好很多。
最后说一个我自己的习惯:每次改完扫频参数,先用一个已知的RC低通网络验证一遍。RC低通的截止频率是1/(2πRC),算出来和测出来对得上,才说明整个链路是准的。这个验证步骤花不了五分钟,但能省掉后面几小时的排查。希望帮到你。
本文还有配套的精品资源,点击获取