资讯动态

电赛D题测量链路拆解:扫频激励、双通道同步采样与DFT相位解算

发布时间:2026/10/19 7:59:51 来源:尧图企业网站定制
简介2019年全国电子设计大赛D题国家二等奖代码工程完整呈现获奖团队的软硬件实现适合电子设计竞赛参赛者和嵌入式开发者研读。压缩包共156个文件约2.98MB以.c/.h源文件为核心搭配Keil工程配置、.hex/.axf烧录文件、.map/.lst调试文件及链接脚本等基本覆盖从编写代码到下载调试的完整流程。已有5347人学习浏览作品参考价值得到广泛验证。代码基于STM32F10x可深入分析其定时器、ADC、I2C、CAN等外设驱动以及系统架构与算法设计同时工程内包含多个版本的迭代痕迹便于了解优化过程。研读时建议从模块划分、主流程、中断处理、数据结构等角度入手再结合硬件原理图理解接口配合对备赛和工程实践都有直接帮助。1. 2019年电赛D题到底在考什么稳定测量链路比算法复杂度更值钱2019年电赛D题表面上是测几个电阻和一组频响曲线实际比的是对复阻抗和频率响应的理解深度。真正拉开差距的不是算法复杂度而是测量链路的工程细节信号源稳不稳、两路采样是否严格同步、相位解算有没有处理卷绕。很多队伍三天内能搭通原理却因为一根地线、一次相位跳变在评审现场丢分。这篇笔记按国二方案的常见骨架拆解扫频激励、双通道采集、DFT计算与校准整条链路适合在备赛电赛、做单片机频响测量和阻抗测试的工程师直接照着搭。上面这段话里的“国二方案”指的就是类似标题这种拿奖代码包里最常见的工程架构。后文涉及的具体频段按D题常见指标约1kHz到100kHz讨论方法本身对几百赫兹到几百千赫兹的音频与工控频段同样适用。如果你正卡在“测不准”的问题上按文中的顺序排查会比在代码里乱试系数快得多。提示全文讲的是方案链路和代码背后的取舍逻辑不是复刻某一支队伍的源码。拿到代码包以后对照模块划分去读比从头顺读源码高效得多。2. 测量方案骨架频率扫描要同时解决“发波、同步采样、算相位”三件事D题不是测静态电阻而是在测复阻抗随频率的变化。输入端注入一个已知正弦信号测量点取输入节点和输出节点两个位置的电压波形幅度比就是增益相位差就是相移再换算成分压比例就能推导输入和输出电阻。整台设备拆成三段扫频信号源、双通道采样前端、数字解算单元。每一段都有独立的坑但先要把方案骨架立住后面调试才不会东一榔头西一棒子。2.1 把题目翻译成测量需求三个技术指标要在同一套频域数据里解决输入电阻、输出电阻、增益、相频特性看起来是四件事本质是一组频响数据。用电阻分压法单独测量输入电阻当然可以但更省事的做法是把整个测量过程统一成“对每个频点求幅度比和相位差”再用已知网络关系反推电阻值。前提是每个频点上的幅度测量足够可信相位测量不跳变。题目指标测量链路上对应的量主要误差来源输入电阻输入节点相对激励源的幅度比信号源内阻、分布电容输出电阻输出节点带载与空载的幅度比负载效应、采集探头输入阻抗电压增益输出与输入的幅度比dB检波非线性、ADC失调相频特性输出与输入的同频相位差采样时间差、滤波器群延迟幅频特性不同频点的增益曲线扫频切换暂态、校准漂移把指标翻译成误差预算以后方案优先级立刻清楚。比如增益要求误差在1dB以内幅度测量的系统误差就要控制在约5%以内频率1kHz时周期是1ms相位1°对应约2.8us对ADC触发来说很轻松但到100kHz时1°只有27ns两路采样的同步精度就成为主要矛盾。这也是后面选择采样方案时最重要的判断依据先保证两路信号是同一时刻被抓到的再去优化算法。2.2 信号源选型DDS芯片和单片机DAC查表为什么多数获奖代码选后者很多队伍第一反应是外挂一片DDS芯片SPI写频率字就能输出正弦。这种做法省事但有两个问题一是输出波形带时钟馈通和谐波要额外加滤波器二是DDS的相位和单片机ADC采样时刻没有硬件同步关系测幅度时影响不大测相位时会引入随频率变化的系统偏差。更麻烦的是这类芯片的时钟源往往来自外部晶振与MCU定时器的时钟不是同一个时基两路同步只能靠软件反复校正。我见过的获奖代码里更多是直接用单片机内置DAC加定时器DMA循环输出正弦查表。核心优势是同一个定时器在搬运正弦点的同时可以触发ADC同步采样激励和响应两路在硬件时序上天然对齐。查表法CPU开销极小一个256点的正弦表就能覆盖D题频段且波形纯度和相位可预测性都比外挂方案好控制。下面是D题常用平台上的DAC查表初始化以12位DAC为例#define LUT_SIZE 256 // 表长点数越多波形台阶越密谐波越小 #define PI2 (3.1415926f * 2.0f) // 中点取DAC满量程的一半12位DAC满量程4095中点即2048 static uint16_t sin_lut[LUT_SIZE]; void dds_lut_init(uint16_t vref_code) { for (uint16_t i 0; i LUT_SIZE; i) { float v sinf(PI2 * (float)i / (float)LUT_SIZE); // vref_code 为DAC满量程对应码值幅度预留约30个LSB避免运放饱和削波 sin_lut[i] (uint16_t)(vref_code v * (float)(vref_code - 32)); } } // 目标频率 - 定时器更新频率的换算 uint32_t timer_freq_for_tone(float tone_freq) { // 一个完整正弦周期由 LUT_SIZE 个点组成定时器更新频率 tone_freq * LUT_SIZE return (uint32_t)(tone_freq * (float)LUT_SIZE); }逻辑说明初始化函数把正弦波一个周期的幅值量化到DAC码值里DMA循环搬运即可持续输出正弦。timer_freq_for_tone返回的值用来设置定时器触发频率定时器每更新一次就搬一个点所以最终输出频率是定时器更新频率除以LUT_SIZE。关键参数是DAC中点和幅度预留vref_code要与参考电压匹配幅度留少量LSB是为了防止后级运放输出饱和。参数说明LUT_SIZE取256对D题足够如果被测频率上限继续抬高可以降到128以减少定时器压力代价是高次谐波变大需要后端低通滤波吸收。vref_code就是12位DAC的中值2048。扫频时只需要循环调用timer_freq_for_tone来改定时器重装值频率切换由DMA自动完成CPU只负责改参数。2.3 幅度相位提取为什么不做检波器而是ADC直采加单频DFT测幅度常见三种思路用宽带对数检波器直接读dB、用真有效值芯片转直流再测、用ADC直接采样波形在MCU里算。D题这种多频点、还要同时看相位的场景前两种都不划算。对数检波输出虽然动态范围大但相位信息丢失要另做过零比较电路才能测相真有效值芯片低频精度尚可高频段带宽和建立时间又受限还需要额外的量程切换。ADC直采最大的优势是一次采样同时拿到幅度和相位。对每个频点取一个整数周期的采样序列做一次单频DFT本质上就是和标准正弦、余弦做相关运算幅度和相位一次性解出来。下面是DFT求幅度相位的核心实现#define SAMPLE_N 128 // 每个频点采样点数必须是整数周期 #define PI 3.1415926f // 对一段已知频率的采样序列做单频DFT得到幅度LSB和相位度 void calc_amp_phase(int16_t *buf, uint32_t n, uint32_t f_signal, uint32_t f_sample, float *amp, float *phase_deg) { float re 0.0f, im 0.0f; float w 2.0f * PI * (float)f_signal / (float)f_sample; for (uint32_t i 0; i n; i) { re buf[i] * cosf(w * (float)i); // 与参考余弦做相关 im buf[i] * sinf(w * (float)i); // 与参考正弦做相关 } re * 2.0f / (float)n; im * 2.0f / (float)n; *amp sqrtf(re * re im * im); *phase_deg atan2f(im, re) * 180.0f / PI; // 相位落在[-180, 180] }逻辑说明DFT的实部和虚部分别是采样序列与参考余弦、正弦的相关积累除以n乘以2之后得到该频点正弦分量的幅值相位用atan2算出。由于对激励和响应两路信号做相同的变换两路结果的相位直接相减就得到被测网络的相移。注意相位结果对采样起始时刻敏感因此两路采样窗口必须严格对齐。参数说明SAMPLE_N要尽量满足“采样频率/信号频率是整数”这个条件DFT才没有频谱泄漏。实际操作中我先根据信号频率算采样频率再反推采样点数保证两者成整数比。MCU上直接调cosf和sinf会比较慢如果扫频点数多可以预先建一个正余弦查找表或者把采样频率固定为信号频率的整数倍把w*i的索引简化为查表下标。2.4 一次完整测量的任务序列设置频率、延时稳定、采样、折算有了信号源、同步采样、解算函数还差把流程串起来的调度逻辑。一次测量通常分两个阶段准备阶段初始化DAC表、配置定时器和ADC再做一次直通校准测量阶段对每个频点执行“设置频率-延时稳定-同步采样-DFT-折算增益相位-存储”的循环。整个流程是一台状态机。void scan_frequency_run(void) { // 频点表按题目指标生成例如 1kHz 起步按对数或线性步进到 100kHz const uint32_t freqs_n 64; float freq_tab[64]; for (uint32_t i 0; i freqs_n; i) { freq_tab[i] 1000.0f * powf(100.0f, (float)i / (float)(freqs_n - 1)); } for (uint32_t i 0; i freqs_n; i) { set_tone_frequency(freq_tab[i]); // 只改定时器重装值DMA继续循环 delay_cycles(freq_tab[i], 5); // 等5个完整周期让滤波暂态收敛 dual_adc_capture(SAMPLE_N); // 定时器触发双通道同步采集 float a_in, p_in, a_out, p_out; calc_amp_phase(in_buf, SAMPLE_N, freq_tab[i], SAMPLE_RATE, a_in, p_in); calc_amp_phase(out_buf, SAMPLE_N, freq_tab[i], SAMPLE_RATE, a_out, p_out); float gain_db 20.0f * log10f(a_out / a_in); // 未校准增益 float phase_deg p_out - p_in; // 未解卷绕相位 save_result(i, gain_db, phase_deg); } }逻辑说明set_tone_frequency只改定时器参数不干预DMA过渡过程由后级滤波和延时共同吸收。delay_cycles的作用很关键它让滤波器建立时间被跳过避免采到暂态。dual_adc_capture由内部定时器触发、DMA搬运完成时置标志位。后面的两次calc_amp_phase对激励和响应两路做相同的DFT再在结果域里求差。参数说明延时稳定这里用“周期数”而不是固定毫秒是因为滤波建立时间主要受周期影响高频时5个周期可能只有50us低频时自动拉长到5ms自适应更合理。save_result里建议同时保存原始幅度和相位中间量只保存最终结果遇到bug会很难定位。3. 国二代码的工程组织硬件抽象、测量引擎、校准表三层分离这章拆解代码包里最常见的组织方式。能拿奖的代码不一定算法最精巧但通常在文件划分、数据流和模块边界上特别清晰。三天比赛里清晰的结构意味着任何一个模块出问题都能快速定位也意味着评审现场改参数时不会把别处改坏。3.1 工程结构先想清楚三个边界硬件、算法、显示分离代码分成四层硬件抽象、信号生成与采集、测量计算、界面交互。硬件抽象只负责寄存器和启动配置界面交互只负责显示结果中间两层承担全部核心功能。这样分的好处是换单片机引脚、换显示屏时测量代码不用动调算法时也不会因为操作寄存器失误把硬件搞崩。一个能直接对照的模块划分如下文件/模块职责对外接口bsp_timer_dac.c定时器、DAC、DMA配置set_tone_frequency()bsp_adc.cADC触发、DMA双缓冲dual_adc_capture()measure_engine.c扫频状态机、稳定延时meas_step()dsp_fft_utils.c单频DFT、幅度相位求解calc_amp_phase()calib_table.c校准表存取与查找calib_apply()ui_task.c按键、屏幕输出ui_refresh()这个划分不是唯一答案但有一个共同点测量引擎不直接操作寄存器而是通过bsp层接口完成动作。遇到芯片型号差异时只需要替换bsp文件上面三层完全不动。3.2 测量引擎非阻塞状态机如何把“延稳、采样、解算”串起来测量过程中最容易被忽略的是“不阻塞”。如果在延时稳定阶段用固定的阻塞延时整个CPU在采样期间没法响应按键或显示刷新现场改频点范围会非常痛苦。常规做法是把测量流程拆成状态机每个状态下只做一件事延时用计数器判断。typedef enum { MEAS_IDLE, // 空闲等待启动命令 MEAS_HOLD, // 等待信号稳定非阻塞计时 MEAS_CAPTURE, // 等待ADC/DMA完成 MEAS_COMPUTE, // 执行DFT与结果折算 MEAS_NEXT // 进入下一个频点或结束 } meas_state_t; void meas_step(void) { static meas_state_t st MEAS_IDLE; static uint32_t hold_start 0; switch (st) { case MEAS_HOLD: if (tick_ms() - hold_start hold_ms_for_freq()) { dual_adc_capture(SAMPLE_N); // 触发同步采样 st MEAS_CAPTURE; } break; case MEAS_CAPTURE: if (adc_dma_done()) { st MEAS_COMPUTE; } break; case MEAS_COMPUTE: calc_amp_phase(/* 两路采样缓冲 */); save_next_result(); st MEAS_NEXT; break; // MEAS_NEXT 负责推进频点表并回到 MEAS_HOLD 或 MEAS_IDLE } }逻辑说明每个状态只做一件事回到主循环之后按键扫描和显示刷新可以插在任意两次meas_step调用之间不会因为一次测量卡死整机。这和上一章顺序循环的代码等效但非阻塞版本更适合现场交互。参数说明hold_ms_for_freq建议直接用“5个周期”换算成毫秒存进频点表这样低频段和高频段不会出现一刀切的延时。hold_start用毫秒差值判断完全避开阻塞延时函数。3.3 双路同步采样定时器触发ADC而不是软件顺序启动双路同步采样是相位测量的地基。同一个定时器更新事件触发两路ADC的注入组转换是最常见的硬件同步手段。下面是一个与具体MCU库解耦的写法static volatile uint8_t capture_done 0; // 定时器更新事件同时触发ADC1/ADC2注入组两组DMA并行搬运 void dual_adc_capture(uint32_t n) { capture_done 0; dma1_enable(adc1_buf, n); // 搬激励通道存到 adc1_buf dma2_enable(adc2_buf, n); // 搬响应通道存到 adc2_buf start_injected_conversion(); // 触发源为已配置好的定时器两路起点对齐 } void dma_done_isr(void) { capture_done 1; // 两路DMA都完成时进入中断 }逻辑说明这里最关键的是“同一个定时器事件触发两路转换”而不是在软件里连续调用两次启动。软件连续启动两个ADC会有几十条指令的时间差低频时无所谓高频时直接表现为相位偏差。硬件触发保证两路的第一个采样点打在同一个时钟边沿后面的序列自然等长对齐。参数说明DMA双缓冲是必须的。测量引擎在读上一批数据做DFT时DMA已经在往另一个缓冲填新数据否则下一个频点开始时会覆盖还没算完的旧数据。capture_done标志在DMA传输完成中断里翻转主循环查询该标志即可不需要在中断里做DFT避免中断处理时间过长。3.4 校准表“能交差代码”和“能获奖代码”的分水岭很多队伍搭完硬件、写完DFT发现测量结果总有固定偏差于是给每个误差在代码里加一个魔数修正最后改成一团乱麻。正规做法是做一张校准表在正式测量前先测一条“直通”链路逐个频点记录系统增益误差和相位偏移测量未知网络时逐点扣除。#define CALIB_POINTS 64 typedef struct { uint8_t valid; // 该频点是否已校准 float freq; // 频点值单位Hz float gain_off; // 系统增益偏差单位dB float phase_off; // 系统相位偏差单位度 } calib_point_t; // 校准核心把直通状态下读到的幅度比/相位差存下来 void calib_measure_direct(calib_point_t *pt, float f, float a_in, float a_out, float p_in, float p_out) { pt-freq f; // 直通时幅度比理论为1读到的dB就是系统增益误差 pt-gain_off 20.0f * log10f(a_out / a_in); // 直通时相位差理论为0读到的差值就是系统相位偏移 pt-phase_off p_out - p_in; pt-valid 1; } // 测量未知网络时逐点扣除系统误差 void calib_apply(const calib_point_t *pt, float gain_db, float phase_deg, float *gain_real, float *phase_real) { *gain_real gain_db - pt-gain_off; *phase_real phase_deg - pt-phase_off; }逻辑说明校准的物理含义是消除信号源内阻、滤波器群延迟、ADC增益差异等所有串联在测量链路里的系统误差。把直通状态下的增益和相位作为基准之后测任何未知网络都减去这个基准得到的就是被测网络自身的频响。calib_apply里的两个减法看起来简单实际上是整套代码里最值钱的几行。参数说明校准频点必须和扫频频点一一对应。如果扫频是1kHz起步校准表也要从1kHz开始每个频点存一条。频点较多时可以在两个校准频点之间做线性插值但对D题来说逐点校准已经足够。相位基准要先解卷绕否则校准表本身是错的后面所有测量结果都会跟着错。4. 调试期最容易翻车的五个瞬间从信号源到相位解算的避坑记录下面几条坑都是在调试类似测量系统时反复遇到的。每条按“现象、原因、解决”展开排查时可以对照自己的现象快速定位。4.1 现象同一块板子上午测增益误差0.5dB下午变成4dB幅度读数飘得厉害原因DAC输出带有直流分量运放跟随器输入端存在失调模拟参考地受数字噪声干扰后随环境漂移。上午和下午温度不同数字电路负载不同漂移方向和幅度都随机。解决把DAC输出用电容交流耦合进被测网络耦合电容之后加直流偏置恢复网络模拟地和数字地单点汇合采样前端尽量短接地。校准也要改成“测量前先跑一遍直通”不要用固定校准值。直通校准和正式测量间隔越短抵消效果越好。4.2 现象频率高于50kHz时幅度结果周期性跳动误差曲线像面条一样起伏原因DAC查表输出更新率不够高波形上叠加明显的阶梯状台阶后端低通滤波截止频率又贴近信号频段滤波不彻底时ADC采样点刚好落在台阶的不同位置导致读数随信号相位抖动。解决提高查表点数或者提高定时器更新率。D题频段内我会把DMA更新率设计成目标频率的16倍以上让输出台阶足够密。低通滤波的-3dB点放到目标上限频率的3到5倍以上减少对基波的附加衰减。同时把稳定延时从5个周期加到8到10个周期让滤波器暂态彻底收敛。4.3 现象某一段频率内相位突然从170°跳成-170°增益曲线正常相位曲线像被刀切过原因atan2的返回值范围是[-180°, 180°]。当真实相移跨过±180°边界时结果会“卷绕”回另一侧形成跳变。这是数据后处理的典型翻车点硬件本身没有问题。解决在频点间做相位解卷绕相邻两个频点的相位差如果超过180°就给后一个点补上或减去360°让曲线保持连续。// 相位解卷绕逐点检查相邻差值超过180度则补一个360度偏移 void phase_unwrap(float *phase, uint32_t n) { float acc 0.0f; for (uint32_t i 1; i n; i) { float delta phase[i] - phase[i - 1]; if (delta 180.0f) acc - 360.0f; else if (delta -180.0f) acc 360.0f; phase[i] acc; } }逻辑说明这段代码维护一个累计偏移acc当相邻频点相位差跳变超过180度时判断为卷绕修正后续所有点。它不会改变每个频点的真实测量值只是消除180度的歧义画出来的相位曲线是连续的。参数说明使用前提是频点间距足够小。如果扫频步进太大真实相位变化本身就可能超过180度解卷绕会把正常变化误判成卷绕。建议相邻频点的真实相位变化控制在90度以内步进设置不要贪快。4.4 现象扫频曲线在开头几个频点总是有毛刺后面一切正常原因第一个频点开始采样时DAC刚刚从空闲状态切换到新频率字滤波器还没建立稳定ADC已经开工。前面的稳定延时只覆盖了扫频循环内部的切换没覆盖“从空闲到第一个频点”这个过程。解决进入扫频循环前先把第一个频点的频率写进去延时至少10个周期之后再开始循环。或者在调频点表前先“空跑”一遍首频点把输出链路完全激活。不要依赖循环内的5个周期延时因为从DAC空载到带载的建立过程比频率切换更长。4.5 现象换一根短测试线高频段增益就变化好几个dB手靠近线更明显原因这是分布参数问题。信号经过较长的杜邦线时线间电容和引线电感参与了被测网络。100kHz虽然不算极高频但对高阻节点已经非常敏感手靠近线缆还会改变寄生电容。解决被测网络连线尽量短、尽量固定激励源输出加一级低阻抗缓冲减少线缆电容带来的衰减测量点位用屏蔽线屏蔽层靠近信号地单端接地。实在无法缩短连线时把测试夹具的寄生电容标定进校准表校准一次再测。这些坑都有一个共同特点不是算法复杂度问题而是模拟链路和时序处理问题。把“先校准、再延稳、后解卷绕”这套顺序固化进代码流程能省下大量盲调时间。5. 让测量结果再可信一档端到端校准与现场交叉验证两个习惯到了这一步系统已经能稳定测量。想把分数从“能跑”抬到“评委当场挑不出毛病”靠两个习惯每次开机都重新校准以及测完用已知器件交叉验证。两件事耗时不超过两分钟但能避免评审现场出现冷场。先说校准。不要沿用上一次的校准值每次上电强制跑一遍直通。把直通状态下的增益差和相位差存成一张频点表频率步进与测量用频点一致。典型记录长这样频点kHz增益偏差dB相位偏差deg1.0-0.182.45.0-0.151.920.0-0.101.160.0-0.21-2.6100.0-0.35-8.2这张表不一定平滑但必须保证每个扫频频点都有记录。现场更换信号线或测量夹具之后重新校准比在代码里追误差来源快得多。我通常把校准动作绑定到“上电自动执行”或“按键确认后执行”绝不用上一次的校准值因为评审现场很可能更换了设备或线缆。然后说交叉验证。找一个精度1%的金属膜电阻网络阻值已知、频率特性平坦在全部扫频频点上测一遍。实测增益与理论增益误差在0.5dB以内、相位误差在3度以内说明测量链路正常。这个做法成本极低却能快速定位问题因为答案事先确定偏差一旦超限能立刻判断是校准表问题还是硬件问题。相位验证有个细节纯电阻网络的理论相移应为0度但不要看到非零值就认为是故障。A同学当年在这个环节上翻过车测出-178度而不是0度他第一反应是校准表错了实际是衰减器引入微小相移加上相位在边界卷绕最终显示成-178度。正确处理是先解卷绕再对比理论值。如果这两步都通过基本可以放心进入正式测量。我习惯在测完一组数据后把原始采样点一并保存下来不只存界面上那两条曲线。评审老师问“这个数据怎么来的”时能立刻调出中间量比在代码里现找更有说服力。备赛最后一天与其反复怀疑算法不如把这些确定性测试完整跑一遍把校准、交叉验证、原始数据保留固化成一个固定流程。希望这些调试记录能帮你在真正重要的评测现场少走几个弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑