基于FFT与数字滤波的信号分离装置设计与实现——电赛H题完整方案
2026/9/9 4:30:42 网站建设 项目流程

简介:2023年电赛H题的代码与解题思路已整理为完整工程包,面向参赛队伍和嵌入式开发者,重点解决两路同频信号叠加后再分离输出的稳定性难题。题目要求对A、B信号求和得到C,由单片机对C作FFT分析,识别各分量频率,再用DDS分别输出A'、B'。资源围绕这一流程给出可直接编译的完整工程,包含信号采集、FFT频谱处理、AD9833驱动和DDS输出,并将晶振由25M换为1M以提高频率分辨率,同时分享了对温漂的补偿思路和实际调测中的注意事项,方便读者快速复现与排查漂移问题。压缩包共136个文件,以C、H源码为主,包含STM32F4外设驱动、LCD显示、中断定时等模块,另有Keil工程配置、hex固件和说明文档,整体约11MB。已有685人学习使用,适合有STM32基础、正在备赛或做信号处理类项目的读者参考。 2023年电赛H题“信号分离装置”,赛后就一直挂在很多实验室群的置顶消息里。直到第二年,还经常看到有人翻找它的代码。这道题之所以让人念念不忘,是因为它把“测量”和“处理”两件事焊在了一起——你不仅要读出混合信号里两路正弦的频率、幅度,还要基于这些参数实时重建两路干净波形。本文就把我从赛题拆解、方案选型、算法设计到代码骨架和踩坑记录的完整思路写出来,适合正在备赛的队伍,也适合想搞懂FFT、数字滤波和波形重构如何协同工作的人。

1. 赛题回顾:这道“信号分离装置”到底卡在哪

1.1 题目任务拆解

以我印象中的H题任务框架为例:给定两路频率、幅度在一定范围内不同的正弦信号,先经过加法器合成为一路混合信号,再由装置接收。装置需要完成两件事——第一,把混合信号中两路信号的频率、幅度测量出来并显示;第二,将分离后的两路信号送出去,让双通道示波器可以稳定地看到两路正弦波形。

注意题面上“经加法器合成”这个表述,其实是出题方在明示:输入是纯线性叠加信号,没有交调产物、没有谐波干扰。这对选手是有利的,意味着常规线性系统分析手段都适用,不需要处理复杂的非线性耦合。真正的难点不在这里,而是在“频率未知、幅度未知”的前提下,系统能不能快速锁定参数并完成分离。

赛题任务对应技术难点
测量频率频率分辨率、频谱泄漏、抗强信号淹没弱信号
测量幅度栅栏效应造成谱线误差、系统增益标定
分离输出滤波器设计、滤波器系数自适应更新、输出波形质量
双通道显示DAC刷新率、两路输出相位连续性、触发稳定性

1.2 “分离”二字的真相:先要知道对方在哪,才能动手滤

很多队伍看到这道题,第一反应是做两个模拟带通滤波器,中心频率分别对准两路信号,后面再加一级放大。这种做法在频率相差比较大的时候确实可行,一旦两路信号频率靠近,问题立刻暴露:两个带通的通带会重叠,互相串扰,调高Q值能让选择性变好,但高Q运放电路很容易自激,而且元件误差会让实际中心频率偏移。

更关键的是,赛题里的信号频率是未知的,需要用仪器先测出来再处理。模拟滤波器的中心频率是固定的,没法自动对准目标。所以数字域的“先测量、后滤波”才是正路:先用FFT之类的算法把两路信号频率、幅度估计出来,再根据这些参数动态计算滤波器系数,相当于系统自己长了眼睛,看到目标在哪再动手。

2. 方案选型:主控、ADC与模拟前级的搭配逻辑

2.1 算力需求估算:为什么STM32F407就够用

关于主控,电赛圈子里最常见的争论就是选单片机还是选FPGA。我的结论很直接:这道题用STM32F407完全够用,FPGA在这个任务里属于堆料,树莓派则压根不合适。

做个简单估算。假设信号最高频率50kHz,采样率取200kHz,FFT点数选1024,频率分辨率大约是195Hz。一次1024点实序列FFT,在168MHz主频的F407上用CMSIS-DSP库跑,耗时在几百微秒级别。就算完成两轮FFT、加上滤波和波形生成,整个处理周期也就一毫秒上下,远低于显示刷新的需求节奏。

树莓派的问题在于,官方ADC采样率和精度都不够看,外挂高精度ADC要折腾驱动,再加上非实时操作系统做混合信号处理,任务调度抖动会直接体现在输出波形上。FPGA的优势是高吞吐并行计算,但这个赛题采样率只要几百kSPS,计算量对F407来说压力不大,没必要承担FPGA的开发周期和调试成本。

2.2 硬件链路:从混合信号输入到双通道输出

我的整体信号链设计如下:

混合信号输入 → 同相比例缩放电路 → STM32F407片上ADC采样 → DMA乒乓缓冲 → FFT频率测量 → 数字分离滤波 → 片内DAC重构波形 → 运放缓冲输出 → 示波器显示

前级用一片低噪声运放,比如OPA227或者MCP6022,做电压跟随和幅度缩放,把信号幅度调整到ADC量程的70%到90%。STM32F407片上12位ADC配合DMA,多通道扫描采样,采样率足够应付这个场景。输出端用片内DAC加一级运放缓冲,DAC后面再加一个简单的RC低通,滤掉采样时钟引入的高频毛刺。

这里有个选型细节很多人忽略:F407的DAC输出默认带缓冲,输出阻抗比较低,直接接示波器没问题,但如果要驱动长同轴电缆,最好再加一级运放做电流增强,否则高频段波形边沿会被电缆分布电容拉垮。

2.3 软硬件分工:模拟只做调理,分离全部交给数字

这道题里模拟电路的角色要非常克制。前级运放只干三件事:阻抗变换、幅度缩放、抗混叠滤波。混合信号里两路正弦的分离,不要试图用模拟带通实现,全部放到数字域处理。

原因很简单:比赛现场最缺的是时间。数字域改滤波器系数就是改两个浮点变量,重新烧录固件也不过几秒钟;模拟电路调滤波特性要换电阻电容,焊接、测量、再焊接,一个参数改下来小十分钟。而且数字滤波器没有温漂,不会因为板子发热导致中心频率偏移。实测下来,所有状态参数尽量做成可调变量,再配一个串口打印通道,现场调参效率会高很多。

3. 核心算法:FFT测量、相关法测幅相与陷波分离的完整链路

3.1 三种信号分离方案对比

开始编码之前,我在纸上列了三种可行方案,反复权衡后选定了方案B。

方案实现思路优点缺点
A两个固定IIR带通滤波器,各自分离一路信号实现最简单,延迟低滤波器系数固定,频率接近时互相干扰严重
BFFT先测量频率幅度,再实时计算陷波器系数分离自适应能力强,两路频率靠得近也能处理需要合理安排测量与滤波的计算时序
C对整块数据做FFT,在频域把不需要的谱线置零,再做IFFT还原思路直观,无滤波器设计过程块延迟大,频谱泄漏严重,信号边界有明显失真

方案A最容易写,但也是最容易在发挥部分翻车的。因为发挥部分通常会设置两路信号频率很接近的工作条件,固定带通在这种工况下基本无能为力。方案C看起来很美,实际操作中,两路信号如果频率差不是FFT分辨率的整数倍,矩形窗泄漏会让两个谱峰拖尾重叠,置零操作会连目标信号的一部分一起切掉,还原出的波形有严重的吉布斯振铃。

方案B的本质是“测量引导滤波”:先通过FFT得到两路频率的粗略值,再用更精确的方法拿到幅度和相位,最后动态构造陷波器,把通道A中的B分量压下去,反过来也一样。这套流程看起来绕,但每一步都有明确目的,稳定性最高。

3.2 频率精估计:峰搜索加抛物线插值

FFT直接给出的峰值谱线位置,只能落在频率分辨率的整数倍上,真实频率往往在两条谱线之间。如果直接用峰值索引反算频率,误差最大可以达到半个频率分辨率。以200kHz采样率、1024点FFT为例,半个分辨率大约是97Hz,这个误差对后级陷波器来说不能接受,因为中心频率偏了100Hz,分离效果会明显恶化。

解决办法是抛物线插值。取峰值谱线k以及相邻两条谱线k-1、k+1的幅度,拟合一条抛物线,找到抛物线顶点位置作为真实峰值的偏移量:

delta = 0.5 * (mag[k-1] - mag[k+1]) / (mag[k-1] - 2*mag[k] + mag[k+1])

真实频率估计为:

f_est = (k + delta) * fs / N

这个公式在加汉宁窗后精度很高,实测可以把频率误差压到几赫兹以内。记得在搜索峰值时避开直流分量和镜像频段,k范围限制在1到N/2-1之间,不然插值数组很容易越界。

3.3 幅度和相位:为什么我不直接读FFT谱线

FFT单根谱线的幅度同样受栅栏效应影响,直接读出来的幅度和真实值有偏差。更麻烦的是,相位信息经过FFT之后受窗函数影响严重,汉宁窗会让相位产生偏移,修正起来很麻烦。

我在代码里用的是I/Q相关法。假设已经通过插值知道了某路信号的频率f0,就生成本地参考信号,对原始采样序列做互相关:

I = 2/N * sum(x[n] * cos(2PIf0n/fs)) Q = 2/N * sum(x[n] * sin(2PIf0n/fs))

幅度A = sqrt(II + QQ),相位φ = atan2(Q, I)。这套计算其实就是一次精简的Goertzel算法,计算量比完整FFT小得多,而且因为没有窗函数影响,幅度和相位都更准确。实测下来,只要频率估计偏差在几赫兹以内,幅度误差可以控制在2%以内。

3.4 分离滤波器:用陷波器而不是带通

确定了每路信号的频率和幅度之后,分离滤波我用的是IIR陷波器。对通道A而言,另一路信号B就是干扰,我只需要在B的频率点挖一个凹坑,A信号自身几乎不受影响。

二阶IIR陷波器的传递函数需要实时计算系数,给定陷波频率f0、采样率fs和品质因数Q,先算归一化角频率w0 = 2PIf0/fs,再计算系数。为了工程实现方便,我直接用了带通Q值固定为30到50的经验值,比赛信号频率差最小也有几百赫兹,这个Q值足够在保证陷波深度的同时不伤到目标信号。

实际调的时候有个心得:陷波器带宽不要太宽。很多队伍喜欢把陷波带宽设得很宽,以为这样能把对方信号“罩住”,结果把自己的目标信号边缘也切掉了一部分,输出波形幅度明显偏小。正确做法是先尽量把频率测准,然后把陷波带宽压到刚好覆盖对方信号的频率偏差范围,这样对目标信号的损伤最小。

4. 代码落地:状态机、双缓冲采样与DDS重构的实现骨架

4.1 软件框架:一个状态机管住所有阶段

整个程序我没有用复杂操作系统,就是裸机状态机加中断。状态分为:SAMPLE(采集)、MEASURE(测量)、RECONSTRUCT(重构输出)、DISPLAY(显示刷新)。主循环不断轮询状态标志,优先保证采样不被阻塞,FFT这些耗时计算放在主循环里做。

一个关键原则:ADC的DMA中断里绝对不要做FFT或者滤波计算,只用标志位通知主循环“数据块准备好了”。否则中断处理时间过长,会直接导致采样丢失,波形出现周期性断裂。

4.2 ADC加DMA双缓冲采样

双缓冲是保证连续采样的标准做法。我开了两个1024点的缓冲区,DMA半传输中断和全传输中断交替触发。下面是简化后的回调逻辑:

#define BLOCK_SIZE 1024 uint16_t adc_data[2][BLOCK_SIZE]; volatile uint8_t active_buf = 0; volatile uint8_t block_ready = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { active_buf ^= 1; block_ready = 1; } }

主循环检测到block_ready后,处理active_buf ^ 1对应的缓冲区,也就是刚刚采满的那个块。DMA开启循环模式,确保硬件一直在采样,软件处理速度跟不上的时候,最新的数据块会自动覆盖最旧的数据,不影响后续处理。

4.3 频率估计与陷波系数更新

频率估计函数我封装成独立模块,输入是FFT幅度谱,输出是精估计频率:

float estimate_freq(float *mag, int n, float fs) { int k = 1; for (int i = 2; i < n / 2; i++) { if (mag[i] > mag[k]) k = i; } float a = mag[k - 1], b = mag[k], c = mag[k + 1]; float delta = 0.5f * (a - c) / (a - 2.0f * b + c); if (delta > 0.5f) delta = 0.5f; if (delta < -0.5f) delta = -0.5f; return (float)(k + delta) * fs / n; }

插值公式里的分母如果接近零,说明峰值附近数据异常,必须加一个delta限幅保护,否则算出来的频率会飞掉。这套保护在我实测过程中救过好几次,信号幅度特别小的时候,频谱噪声会让插值结果跳变。

算完频率后,两路信号频率就确定了,分别标记为f1和f2。对通道A,需要让f2的陷波器生效;对通道B,需要让f1的陷波器生效。陷波器系数在每次测量完成后更新一次,不需要每个采样点都重算,这样可以省掉大量浮点运算。

4.4 双路波形重构:用DDS思路代替每帧重算

分离滤波完成后,最稳妥的输出方式其实是直接用测量得到的频率、幅度参数,用DDS思路重新合成两路正弦波,而不是把滤波后的波形直接送给DAC。这个设计决策很重要。

滤波后的波形会保留原始信号的噪声和采样毛刺,直接DAC输出后示波器波形不够干净。而用参数重构,输出的是理想正弦波,示波器上看几乎没有杂散。代价是相位可能和原始信号有偏差,但赛题只要求看到两路分离后的正弦波形,不要求严格锁定输入相位,所以这个代价可以接受。

DDS的核心是一个32位相位累加器:

uint32_t phase_acc = 0; uint32_t phase_step_a, phase_step_b; float amp_a, amp_b; // 根据频率计算步进 phase_step_a = (uint32_t)(freq_a * 4294967296.0f / DAC_UPDATE_RATE); phase_step_b = (uint32_t)(freq_b * 4294967296.0f / DAC_UPDATE_RATE); // 定时器中断里每周期调用一次 void dac_output_callback(void) { phase_acc += phase_step_a; float va = sinf(2.0f * PI * (float)phase_acc / 4294967296.0f); // 通道B同理,使用独立的相位累加器 uint16_t dac_a = (uint16_t)(2048 + amp_a * 2000.0f * va); HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_a); }

每路输出用独立的相位累加器,好处是相位自然连续,不会出现每帧从零开始带来的跳变。sinf函数在F407上虽然能跑,但中断频率高的时候会比较吃CPU,实测可以换成查表加线性插值,速度能提升好几倍。

5. 实测翻车点与现场调优:从频谱泄漏到DAC噪声的排查记录

5.1 频谱泄漏把小信号淹没了

第一次联调的时候,我设置了两路信号,一路幅度3V,另一路幅度0.3V,频率差只有几百赫兹。FFT频谱上小信号那个峰几乎看不到,被大信号的泄漏拖尾压得只剩一个鼓包。

排查后发现两个问题:第一,FFT前没有加窗,矩形窗的旁瓣衰减只有13dB,强信号旁瓣直接盖过了弱信号;第二,采样数据里混入了直流偏置,零频泄漏进一步污染低频段。解决办法是FFT前先做去直流,再乘汉宁窗。汉宁窗旁瓣衰减可以到31dB左右,虽然频率分辨率会稍微变差,但对这个赛题来说完全够用。

如果两路信号幅度差超过20dB,一轮测量不够,我会做两步测量:第一轮用FFT找到强信号的精确频率,先把强信号陷波掉,第二轮再对残余信号做一次FFT,这时候弱信号就没有强信号压制了,能很干净地测出来。

5.2 示波器波形不停滚动的真正原因

分离后的波形在示波器上一直跳,触发很不稳定,看起来像频率在漂。用示波器的频率测量功能看,数值确实在变,但变化很规律。一开始怀疑是DAC时钟精度问题,排查了定时器配置,确认时钟没问题。

最后定位到问题出在重构波形的相位上。我早期版本是每处理完一块数据,就从相位零开始重新生成波形,两块数据之间会出现相位跳变,示波器触发点每次都不一样,波形自然滚动。这个排查过程花了我一个多小时,教训很深刻。改成双累加器DDS后,相位连续性解决了,波形在示波器上稳如老狗。

5.3 DAC开关噪声串进ADC的经典地线问题

输出波形上出现了一些不随输入变化的固定杂散峰值,频率大约是几百kHz。用频谱仪看,杂散频率和DAC的更新率有整数倍关系,怀疑是DAC切换瞬间的电流冲击通过地线耦合到了ADC输入端。

检查电路发现,模拟地、数字地、DAC输出地全部汇到一根长面包板地线上,地线阻抗把DAC的开关噪声变成了共模干扰。处理方式是重新布线,模拟地单点接入主地,模拟电源和数字电源分开走,运放供电加RC去耦。改动之后杂散峰直接降了20dB以上。这种问题在比赛中很容易被忽视,因为你用万用表量地线是通的,但高频噪声根本不管直流导通,走的是阻抗路径。

5.4 现场调参的优先级和几个实用习惯

比赛现场时间紧张,我最后总结出一套调参顺序:先保证频率测量准,再优化幅度测量,最后才调分离效果。频率测不准的情况下,后级滤波器系数全是错的,分离效果不可能好。

所有关键参数,包括采样率、FFT点数、陷波器Q值、窗函数选择、DAC输出幅度,全部做成全局变量,用一个数组统一管理。配合串口发送命令修改参数,现场调参不用反复断电烧录。这个习惯帮了我大忙——性能测试前一分钟发现陷波带宽太宽,直接串口发一条命令就改完了,不用重新编译工程。

再提一个很多人不看重的点:第一版程序一定要加看门狗。比赛现场的程序会跑很久,如果某个极端输入让程序陷入死循环,有看门狗能在几百毫秒内自动复位恢复正常。没有看门狗,设备死机一次就要人工断电重启,在展示环节非常被动。

关于“代码”这件事,我的最大体会是,H题的核心代码其实不算长,真正难的是状态调度和各模块之间的衔接。采样、测量、滤波、重构这四个环节单独拿出来都不复杂,但把它们串成一个实时流水线,需要考虑缓冲管理、计算耗时、参数更新时机这些工程问题。调试时一定按“采样→测量→分离→重构”的顺序逐级验证,每级确认没问题再进下一级,翻车的概率会小很多。这套“先测量、再陷波、后重构”的方法论,后来我做音频去混叠和传感器信号分离时也一直在用,算是这一届电赛留给我的最大收获。

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

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

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

立即咨询