☰
电赛信号题准备包实战:FFT、ADC采样与相位差测量避坑指南
2026/10/20 13:55:49 网站建设 项目流程

简介:这份资源面向备战2023年全国大学生电子设计竞赛信号类题目的参赛者与嵌入式学习者,聚焦信号处理算法在STM32H7平台上的落地实现。压缩包共约2000个文件,以C源码(1291个)与头文件(553个)为主体,辅以txt说明、少量cpp与md文档,整体约333.54MB,规模接近完整工程仓库。内容围绕STM32H7高性能微控制器展开,涉及HAL库外设驱动、定时器与HRTIM配置、I2C通信,以及线性插值、变换测试等信号处理相关模块,可支撑实时采集、滤波与解调等典型赛题场景。已有1029人学习下载,说明其在电赛备赛群体中具备一定参考价值。读者可借此梳理信号处理理论到嵌入式代码的映射关系,参考工程目录组织方式,复用外设驱动与算法实现,并对照测试数据调试优化,从而提升赛题方案的设计效率与实现质量。

1. 从一份电赛信号题准备包说起:它到底能帮你省下多少试错时间

如果你正在准备电子设计竞赛的信号类赛题,或者刚拿到“2023年电赛信号题所做的一些准备.zip”这个包,大概率会经历一个很典型的阶段:知道要考信号处理,但不知道从哪下手;手头有开发板、有示波器,却不知道怎么把“理论上的频谱分析”变成“板子上跑得起来的代码”。这份资源就是冲着这个痛点去的——它不是一份教程,而是一个已经跑通过信号题准备流程的工程包,里面大概率包含常用信号发生、采集、频谱分析、滤波、参数测量等模块的参考实现和调试记录。

我拆过不少类似的竞赛准备包,很多都是“代码能跑但不知道为什么这么写”,或者“参数对不上实际波形”。这份包的价值在于,它把信号题里最耗时间的几个环节——比如ADC采样率怎么定、FFT点数怎么选、窗函数怎么加、幅值相位怎么校准——都落成了可复现的步骤。适合谁?适合已经学过数字信号处理但没做过完整信号链的在校生,也适合带队的年轻工程师想快速搭一个验证平台。如果你指望它直接给你比赛答案,那会失望;但如果你想在三天内把信号采集到分析的链路跑通,它比翻三本教材都快。

2. 信号题准备包的结构拆解:从文件命名看设计意图

2.1 压缩包里的典型目录与文件类型

拿到一个竞赛准备包,我习惯先看目录结构,因为命名方式往往暴露了作者的调试顺序。这份包解压后,常见的是按“信号产生→采集→处理→显示/测量”分文件夹,或者按“基础模块→综合例程→测试数据”分层。里面大概率有.c/.h源码、.ioc或.syscfg之类的工程配置、.csv或.txt的采样数据、以及若干.md或.txt的调试笔记。注意,不同人整理习惯不同,但核心模块跑不出这几类:

文件/文件夹名(示例)作用你该关注什么
signal_gen/产生正弦、方波、三角波,可能用DAC或PWM频率分辨率、幅值控制方式
adc_sample/ADC采集与DMA搬运采样率配置、触发源、数据对齐
fft_analyze/FFT及频谱计算点数、窗函数、幅值归一化
filter_design/FIR/IIR滤波系数生成方式、阶数、群延迟
measure/频率、幅值、相位差测量过零检测还是FFT插值
test_data/实测波形数据用来验证你的算法是否一致

如果你拿到的包目录名和这个不一样,没关系,按功能归类就行。关键是先找到“从模拟信号到数字序列”的那条主线,别一上来就扎进FFT代码里。

2.2 先跑通最小链路:信号发生→ADC→串口输出

很多新手翻车是因为想一口气把FFT和测量全跑通,结果卡在ADC数据不对。我的建议是:先不管FFT,只做一件事——让ADC采一段已知信号,通过串口把原始数据打出来,用Excel或Python画个图,确认采样率和幅值是对的。这一步在准备包里通常有现成工程,你只需要改几个参数。

以常见的STM32+ADC+DMA为例,关键配置是:

// adc_config.c 片段:定时器触发ADC,DMA搬运 // 假设系统时钟72MHz,定时器ARR=719,则触发频率=72MHz/(719+1)=100kHz // 即采样率100kHz,满足大多数电赛信号题要求(信号频率一般<20kHz) TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_InitStruct.TIM_Prescaler = 0; TIM_InitStruct.TIM_Period = 719; // 决定采样率 TIM_InitStruct.TIM_ClockDivision = 0; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStruct); ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 由定时器触发,不连续 ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_Init(ADC1, &ADC_InitStruct); DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_BufferSize = 1024; // 一次搬1024个点 DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buf; DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_Init(DMA1_Channel1, &DMA_InitStruct);

这段代码的逻辑是:定时器2以100kHz频率产生TRGO事件,触发ADC单次转换,DMA把每次转换结果搬到adc_buf数组,搬满1024个点后停止。参数上你要盯住三个:TIM_Period决定采样率,DMA_BufferSize决定一次分析的数据量,ADC_DataAlign决定数据是左对齐还是右对齐(影响后续幅值计算)。如果串口打出来的波形频率对不上,先查定时器时钟源是不是72MHz,再查ADC采样周期是否太长导致实际转换时间超过触发间隔。

提示:采样率不是越高越好。电赛信号题常见信号频率在1kHz~20kHz,按奈奎斯特至少2倍,但工程上取5~10倍更稳。100kHz采样率对应20kHz信号只有5个点/周期,做FFT时谱线会泄漏,建议至少200kHz或降低信号频率。

2.3 用Python快速验证采集数据,别在单片机上硬调

拿到串口数据后,别急着在单片机里写FFT。先把数据存成CSV,用Python的numpy和matplotlib画出来,看时域波形和频谱。这一步能帮你排除“是采集错了还是算法错了”。准备包里如果有test_data文件夹,直接拿现成数据练手更快。

import numpy as np import matplotlib.pyplot as plt # 读取串口保存的CSV,假设一列,采样率100kHz data = np.loadtxt('adc_data.csv', delimiter=',') fs = 100000 N = len(data) t = np.arange(N) / fs # 时域 plt.figure() plt.plot(t[:200], data[:200]) # 只看前200个点,避免太密 plt.xlabel('Time (s)') plt.ylabel('ADC Value') plt.title('Time Domain') # 频域:加汉宁窗减少泄漏 window = np.hanning(N) spectrum = np.fft.fft(data * window) freq = np.fft.fftfreq(N, 1/fs) magnitude = np.abs(spectrum) / (N/2) # 归一化到幅值 plt.figure() plt.plot(freq[:N//2], magnitude[:N//2]) plt.xlabel('Frequency (Hz)') plt.ylabel('Magnitude') plt.title('Spectrum') plt.show()

逻辑说明:np.hanning(N)加窗是为了减少频谱泄漏,代价是主瓣变宽;np.abs(spectrum)/(N/2)是把FFT结果换算成实际幅值(对于单频信号,加窗后还要乘窗函数增益系数,汉宁窗约2.0,这里简化处理)。参数上,fs必须和实际采样率一致,否则频率轴全错;N取2的幂次(如1024、2048)能让FFT更快,但准备包里如果用了非2的幂,numpy也能算,只是慢一点。如果频谱峰值对应的频率和你信号源设置差很多,先检查信号源输出是否真的接对了,再检查ADC参考电压和输入偏置。

3. FFT与参数测量:从频谱到频率、幅值、相位差

3.1 FFT点数、窗函数与频率分辨率的关系

电赛信号题里最常考的就是“测量正弦波频率、幅值、相位差”,而FFT是绕不开的工具。但很多人直接套用1024点FFT,结果频率分辨率不够,两个靠近的频率分不开。频率分辨率 = 采样率 / FFT点数。比如采样率100kHz,1024点FFT,分辨率约97.7Hz,测1kHz信号误差可能到几十Hz。要提高到1Hz分辨率,要么增加点数到100k点(内存吃不消),要么用插值算法。

准备包里通常会有两种做法:一种是固定点数FFT+峰值搜索+插值校正;另一种是过零检测测频率,FFT只用来测幅值。我一般推荐混合方案:频率用插值FFT或过零检测,幅值用FFT加窗校正,相位差用互相关或FFT相位差。窗函数选择上,如果信号是单频且能整周期采样,矩形窗最好;否则汉宁窗或汉明窗。下面是一个加窗后峰值插值的例子:

# 在上一段基础上,找峰值并做双线插值 peak_idx = np.argmax(magnitude[:N//2]) if 0 < peak_idx < N//2 - 1: alpha = magnitude[peak_idx - 1] beta = magnitude[peak_idx] gamma = magnitude[peak_idx + 1] # 双线插值公式(汉宁窗) delta = 0.5 * (alpha - gamma) / (alpha - 2*beta + gamma) true_freq = (peak_idx + delta) * fs / N true_mag = beta * (1 - delta**2) * 2 / 1.0 # 汉宁窗增益约2,这里简化 print(f"频率: {true_freq:.2f} Hz, 幅值: {true_mag:.3f}")

这段代码的核心是delta,它利用峰值左右两根谱线的幅度比来修正频率偏移。注意,插值公式依赖窗函数类型,汉宁窗和矩形窗的公式不同,准备包里如果用了别的窗,你得对应改。参数上,peak_idx不能取0或N/2-1,否则插值失效。如果测出来的频率跳动很大,先看信号源是否稳定,再看ADC是否引入了太多噪声。

3.2 相位差测量的两种实现与精度对比

相位差测量在电赛里常见于“两路信号相位差”题目。方法一:两路信号分别做FFT,取峰值处相位相减。方法二:两路信号做互相关,找峰值位置换算相位差。方法一实现简单,但对采样同步要求高;方法二抗噪好,但计算量大。准备包里如果两路ADC是同步采样的(比如双ADC模式或两个ADC同时触发),方法一足够。如果两路是分时采样的,方法二更稳。

// 方法一:FFT相位差(伪代码,基于CMSIS-DSP) arm_rfft_fast_instance_f32 fft; arm_rfft_fast_init_f32(&fft, 1024); float32_t fft_in1[1024], fft_out1[1024]; float32_t fft_in2[1024], fft_out2[1024]; // 填充adc_buf1和adc_buf2后 arm_rfft_fast_f32(&fft, fft_in1, fft_out1, 0); arm_rfft_fast_f32(&fft, fft_in2, fft_out2, 0); // 找峰值bin,计算相位 int peak_bin = find_peak(fft_out1, 1024); float phase1 = atan2f(fft_out1[2*peak_bin+1], fft_out1[2*peak_bin]); float phase2 = atan2f(fft_out2[2*peak_bin+1], fft_out2[2*peak_bin]); float phase_diff = phase1 - phase2; // 归一化到[-pi, pi] while (phase_diff > PI) phase_diff -= 2*PI; while (phase_diff < -PI) phase_diff += 2*PI;

逻辑说明:CMSIS-DSP的arm_rfft_fast_f32输出是交错排列的实部和虚部,fft_out[2*k]是实部,fft_out[2*k+1]是虚部。atan2f算出的相位是主值,相减后要归一化。参数上,peak_bin必须两路一致,否则相位差没意义。如果两路信号幅值差很多,先做归一化再算相位。常见坑是ADC采样不是同时的,导致相位差里混入了时间差,这时候要么改用同步采样模式,要么用互相关法。

注意:相位差测量对采样时钟抖动很敏感。如果发现相位差跳动超过5度,先检查ADC触发是否由同一个定时器发出,再检查两路输入是否经过了相同的模拟前端(比如同样的运放缓冲)。

4. 避坑与排查:信号题准备包里最容易翻车的五个点

4.1 现象:FFT频谱峰值频率对,但幅值差一倍

原因:ADC数据没有减去直流偏置,或者FFT后没有做幅值归一化。很多准备包默认信号是双极性,但实际ADC输入是单极性(0~3.3V),信号被抬升了1.65V,FFT后直流分量很大,交流幅值计算时如果没去掉直流,或者窗函数增益没除,就会差一倍。 解决:在FFT前减去均值(data = data - np.mean(data)),并确认窗函数增益系数。汉宁窗增益约2.0,矩形窗1.0,汉明窗约1.85。

4.2 现象:采样率设了200kHz,但实际波形频率只有一半

原因:定时器时钟源不是你以为的那个频率。比如STM32的APB1定时器时钟可能是36MHz而不是72MHz,TIM_Period算出来的触发频率就偏了。 解决:用示波器测ADC触发引脚(如果有输出)或者用已知频率信号源校准。更直接的办法:采一个已知频率方波,数周期算实际采样率。

4.3 现象:两路信号相位差测出来一直在漂

原因:两路ADC不是同步采样,或者模拟前端两路滤波器的群延迟不一致。 解决:改用双ADC同步模式,或者软件上做互相关。模拟前端尽量用相同型号运放和相同阻容值。

4.4 现象:滤波后信号幅值衰减严重

原因:FIR滤波器阶数不够,或者截止频率设得太靠近通带边缘。电赛里常用FIR,如果阶数只有十几阶,过渡带很宽,通带边缘衰减大。 解决:增加阶数到64以上,或者改用IIR椭圆滤波器。注意IIR相位非线性,测相位差时慎用。

4.5 现象:串口打印数据丢包,FFT结果乱跳

原因:串口波特率太低或DMA搬运和串口发送冲突。 解决:提高波特率到921600,或者用DMA发送。如果数据量太大,先存到RAM再慢慢发,别边采边发。

5. 进阶技巧:用准备包里的测试数据反推算法边界

准备包里如果有test_data文件夹,别只拿来跑一遍就完事。我习惯做一件事:把测试数据按信噪比分类,然后看算法在什么信噪比下开始失效。比如,给原始数据叠加不同幅度的高斯白噪声,重新跑FFT和测量,记录频率误差和幅值误差。这样你能知道准备包里的算法在什么条件下可信,比赛时遇到弱信号心里有数。

# 用准备包里的干净数据,加噪声测试算法鲁棒性 clean = np.loadtxt('test_data/sine_1k.csv', delimiter=',') fs = 200000 for snr_db in [40, 30, 20, 10]: noise = np.random.randn(len(clean)) signal_power = np.mean(clean**2) noise_power = signal_power / (10**(snr_db/10)) noisy = clean + np.sqrt(noise_power) * noise # 这里调用你的频率测量函数 freq_est = measure_freq(noisy, fs) print(f"SNR={snr_db}dB, 频率估计={freq_est:.2f}Hz")

这段代码的逻辑是:根据目标信噪比计算噪声功率,叠加到干净信号上,再跑你的测量函数。参数上,snr_db从高到低,观察频率估计什么时候开始偏离。如果准备包里的算法在20dB就崩了,比赛时遇到弱信号就得换更稳的过零检测或互相关。我一般会把这个测试跑一遍,把每个模块的“可信信噪比下限”记在笔记里。

另一个技巧是:把准备包里的FFT点数从1024改成2048或4096,看频率分辨率提升多少,但也要看RAM够不够。很多单片机跑4096点FFT已经吃力,这时候可以考虑用抽取滤波降低采样率再FFT,等效提高分辨率。这些边界条件,准备包不会直接告诉你,得自己试。

从那以后我每次拿到信号题准备包,都强制先跑一遍“干净数据→加噪→看算法失效点”的流程,再决定比赛时用哪套参数。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询