简介:基于STM32F1系列单片机的FFT实现工程,面向嵌入式开发者与数字信号处理学习者,展示如何在Cortex-M3内核上通过蝶形运算与位反转完成快速傅里叶变换,并结合CMSIS-DSP库、中断定时器和内存布局提升频域分析效率。压缩包共225个文件,约4.12MB,以C源码、头文件为主,同时包含MDK工程配置(uvprojx、uvoptx)、编译链接产物(axf、hex、map)及中间过程文件(o、crf、lst),便于直接打开工程、查看完整结构并对照学习。目前已有10172人学习下载。资源保留了标准外设库及TFTLCD、ADC、TIM等驱动代码,可作为STM32信号采集与FFT分析的参考框架;通过分析工程中的FFT实现,可掌握浮点运算优化、内存分配和定时器触发处理等关键细节,适合正在做音频、振动或电力谐波分析项目的开发者快速上手。
1. 需求与选型:为什么这个频谱分析功能要跑在STM32上
很多人听到“STM32实现FFT”,第一反应是频率分析这种数学活应该丢给上位机,单片机负责采集数据就够了。但在实际项目中,尤其是在现场设备、电池供电的便携仪器或产线在线检测场景里,没有上位机可用,或者不允许把原始采样数据原封不动传出去。这时候就需要让STM32自行完成FFT计算,把频域结果直接输出给显示、报警或通信模块。这个需求在课程设计、毕业设计和工程预研里都很常见,也是最容易让评审认可“有完整工程思维”的项目。
这套方案适合两类人:一类是正在做嵌入式课程设计或毕设,需要把一个“STM32单片机FFT频谱分析”项目从零搭起来的学生;另一类是已经在用STM32做工业采集,想在不更换硬件的前提下把频域分析能力加进现有固件的开发者。方案的核心思路是用STM32的ADC采集时域信号,经DMA送到内存,再调用CMSIS-DSP库完成FFT运算,最后把频谱通过串口、LCD或DAC输出。整个设计不需要昂贵的开发板,一块常规的STM32F1系列芯片加一个USB转串口模块就能跑通,只是性能上会有些差别。
我会把总体架构、采样配置、FFT实现、结果输出和常见坑依次拆开讲,最后给出验收标准。这套内容不是零散的知识点,而是一份可以直接照着做的完整方案。
2. 总体架构设计:从ADC采样到频谱输出的三个核心模块
FFT本身只是一个数学函数,但在嵌入式项目里它不是一个孤立算法,而是整条数据链路中的一环。一个能出成果的FFT工程,至少要同时考虑采样、运算和输出三件事。因为FFT的结果质量上限,取决于ADC送给它的数据质量;结果是否能被外部使用,则取决于输出协议和格式。很多翻车案例都是FFT代码写对了,但前面采样配置不对,后面输出格式又让人看不懂。
我把系统拆成三个模块:ADC采样模块、FFT运算模块、结果输出模块。三者之间的数据流是单向的,可以用一条很清晰的线描述:定时器触发ADC采样,DMA把采样值搬进环形缓冲区,主程序从缓冲区取出一整块数据做加窗处理,然后调用FFT函数,计算幅值和频率映射,最后打包发送或刷新显示。模块之间通过缓冲区解耦,避免中断里做Fourier导致实时性失控。
2.1 ADC采样模块:确定采样率与缓冲区大小的匹配关系
采样率是整个FFT系统的基石。按照奈奎斯特采样定理,采样率必须大于信号最高频率的两倍。但工程上不会卡着极限,一般取信号最高频率的4到8倍。例如需要分析2kHz以内的音频信号,采样率通常设为8kHz到16kHz。在实际项目中,一个常见的配置是用8kHz采样率分析1kHz以内的信号,这样既不会丢频,又不会因为高采样率导致数据量过大。
缓冲区大小关系到做一次FFT需要多少样本。STM32F1系列的RAM有限,所以256点、512点、1024点是常用选择。采样率和缓冲区大小的关系直接影响扫描时间:扫描时间等于缓冲区大小除以采样率。例如采样率8kHz、缓冲区1024点,那么完成一次FFT需要128毫秒,也就是刷新率约7.8帧每秒,基本能实时显示。如果用512点,刷新率会提升到15.6帧每秒,但频率分辨率会下降一半。
这里有一个必须强调的匹配关系:采样率太高而缓冲区太小,会导致只能分析很宽的频率范围但分辨率很差;采样率太低而缓冲区太大,虽然分辨率好了,但刷新率又跟不上。所以第一步不是写代码,而是根据目标信号先定这两个参数。通常的做法是先确定信号最高频率和目标分辨率的优先级,再用公式倒推采样率。分辨率等于采样率除以FFT点数,这个公式在第三章会仔细展开。
2.2 FFT运算模块:基于DSP库还是移植自制算法
STM32上做FFT有两条路线。第一条是直接使用ARM的CMSIS-DSP软件库,里面有完备的FFT函数,支持实数、复数、浮点和定点。第二条是从网上找一份C语言实现的FFT源码,或者根据蝶形运算自己写。我的建议是除非课程明确要求“自行实现FFT算法”,否则优先使用CMSIS-DSP库。原因是自写FFT看起来有技术含量,但在实际项目中很容易因为输入格式、旋转因子精度、位反转顺序等问题产生诡异结果,调试代价远高于想象。
使用库函数并不意味着“没有技术含量”,因为关键不在地上算法本身,而在于正确配置它。STM32F1系列主频72MHz,没有硬件浮点单元,所以很多人以为浮点FFT会很慢。但实际上1024点复数FFT的计算时间大约在一个毫秒级,对于128毫秒的采样周期来说完全够用。如果是Cortex-M4或M7系列,浮点性能更强,可以做更高阶的FFT。如果项目对实时性要求特别高,可以改用定点Q15格式的FFT函数,性能会更快,但需要注意数据格式转换的损失。
2.3 结果输出模块:串口、LCD与数据打包协议
FFT算出来的频谱数据是一组复数,不能直接当普通整数输出。为了让结果有用,需要在芯片内部先完成幅值计算、dB转换和最大值搜索,再打包成结构化帧发送。串口是最常见的输出方式,帧格式一般用起始字节、数据长度、频率数组、幅值数组和校验位组成。这样才能在上位机的串口助手里方便地解析。如果是带LCD屏的小板,则直接绘制柱状图即可,这需要把FFT结果映射到屏幕坐标上,通常只取前几十个频点显示,因为屏幕宽度有限。
输出协议是很多新手容易忽略的点。有人直接把printf的带格式字符串发出去,这样做调试时看着方便,但上位机后期解析会很痛苦。我会在项目里定义好一个二进制协议,例如帧头0xAA、0x55,然后是频点数和有效通道,接下来是16位整型频率值和幅度值,最后是累加和校验。这样无论是连上位机软件还是写个小工具都能稳定解析。输出模块的接口要预留好,因为同一份FFT结果可以给串口、LCD或CAN总线复用,代码结构上建议把“FFT处理”和“结果输出”分开写。
在实现顺序上,我的习惯是先跑通一个最小的静态闭环:在内存里放一组已知波形,运行FFT,把结果通过串口打印出来,确认公式无误之后,再接上ADC采样。如果一开始就把ADC和FFT全部接上,出问题时根本分不清是采样坏了还是运算坏了。这个顺序在后面的实现细节里也会体现。
3. 核心实现详解:CMSIS-DSP库、幅值计算和频率轴映射
这一章是整套方案的技术核心。我会从工程应用的角度,用实际可运行的代码把FFT的调用过程、参数含义和期望输出讲清楚。你需要准备的软件环境包括某个STM32集成开发环境、标准外设库或HAL库,以及CMSIS-DSP软件包。如果使用的是最新的Keil或STM32CubeIDE,DSP库已经在里面,只需要在工程设置里启用即可。
3.1 使用CMSIS-DSP库完成复数FFT与实数FFT的选择
CMSIS-DSP库提供了一个非常方便的复数FFT函数,接口是arm_cfft_f32。使用前需要先定义并初始化一个结构体,指定FFT点数、是否做逆变换、是否位反转。然后传入一个长度为“2乘以点数”的浮点数组,因为数组的每两个元素分别代表一个复数的实部和虚部。对于ADC采集的实数信号,通常把虚部填充为0即可,但这样会浪费一半存储和算力,所以更适合用arm_rfft_f32这类实数FFT函数,它的内部会优化,只输出N/2个有效频点。
下面是一段基于实数FFT的调用代码,适用于输入数据已经是实数波形的情况。我在代码里加了详细的注释,方便直接套用:
#include "arm_math.h" #define FFT_SIZE 1024 // FFT点数 #define Fs 12800.0f // 采样率,需要与ADC实际触发频率一致 arm_rfft_fast_instance_f32 fft_inst; // 实数FFT实例 float32_t input_data[FFT_SIZE]; // 采集到的时域数据,长度等于FFT_SIZE float32_t fft_output[FFT_SIZE]; // FFT输出数据,包含频域实部和虚部 float32_t mag_output[FFT_SIZE / 2]; // 幅值输出,实数FFT只产生一半频点 void fft_init(void) { // 初始化实数FFT,点数必须是2的幂。参数1表示只做正变换 arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); } void fft_process(void) { // 执行正变换:输入为时域实数序列,输出为N个复数的实虚交替排列 arm_rfft_fast_f32(&fft_inst, input_data, fft_output, 0); // 计算每个频点的幅度,输出数组长度只有N/2,对应单边频谱 arm_cmplx_mag_f32(fft_output, mag_output, FFT_SIZE / 2); }这段代码中,arm_rfft_fast_f32的第三个参数是ifftFlag,传0表示正变换。第四个参数原本是位反转,但在实数FFT快速版本里不需要管它。fft_output数组的实际有效复数是FFT_SIZE/2个,但物理排列需要用arm_cmplx_mag_f32函数来提取幅度,它会把成对的实部虚部数据当成复数处理。注意mag_output数组长度是FFT_SIZE/2,也就是512个,对应0到6400Hz的频点。这是因为采样率12800Hz时,奈奎斯特频率是6400Hz。
3.2 幅值计算与dB转换:为什么不能直接拿实部虚部做显示
FFT运算直接得到的是复数,包含幅度和相位信息。在频谱分析中,我们通常只关心幅度,也就是“这个频率的能量有多大”。计算幅度的方法是对每个复数求模,即实部平方加虚部平方再开方。CMSIS-DSP的arm_cmplx_mag_f32正好完成这件事,但还需要注意幅度校正系数。FFT函数自带的元素缩放与变换方式有关,通常需要除以FFT点数N,才能得到与该频率分量的原始幅值一致的数值。这在工程上非常关键,否则你输入一个1V的直流信号,输出却显示几十甚至几百,会误以为信号异常。
另外,为了能同时看到大信号和小信号,许多仪器会把幅度转换为dB表示。dB的计算公式是20倍以10为底的对数。我会在代码里写一个小函数,把线性幅度映射到dB,并设置下限,避免对零取对数出现无穷值。如果只是显示柱状图,线性值或归一化百分比也可以,但答辩和验收时用dB更容易体现出专业度。
#define MAG_LOWER_LIMIT 0.0001f float32_t linear_to_db(float32_t linear_value) { if (linear_value < MAG_LOWER_LIMIT) { return -80.0f; // 低于下限视为-80dB,避免log0 } // 换算成dB,20是功率与幅度之间的系数 return 20.0f * log10f(linear_value / FFT_SIZE); }这个函数里的下限值可以根据实际噪声水平调整。如果ADC噪声很低,可以设置为更小的值;如果噪声比较大,设成-80dB能让频谱图看起来不全是毛刺。有人会忘记除以FFT_SIZE,导致所有幅度都偏大。这里我一般在调好后,会在代码上方用注释写下“已考虑缩放因子”的提醒,防止之后又改错。
3.3 频谱分辨率和频率轴映射:让横坐标真正对应到Hz
FFT结果只是一个长度512的数组,每个下标对应一个“频点索引”。要把索引转换成物理频率,公式是:频率值 = 索引 * 采样率 / FFT点数。因此在这个例子里,频率分辨率为12800 / 1024 = 12.5Hz。下标1代表12.5Hz,下标100代表1250Hz,频率轴的最大值就是512个点里的第511个频点,约6387.5Hz,再加上直流分量覆盖0Hz,基本覆盖整个奈奎斯特带宽。
频率分辨率是衡量FFT能力的重要指标。如果需要的分辨率为1Hz,那么FFT点数必须达到12800,这对于STM32F1来说RAM压力很大。通常的替代方案是降低采样率并同时降低分析带宽,或者用“细化和扫描”的思路,只关注感兴趣的窄频带,而非全谱覆盖。验收时如果对方问“为什么分辨率是12.5Hz”,你要能解释清楚它与采样率和FFT点数之间的关系,并说明在当前硬件下是合理的折中。
频率映射常见的问题是把下标直接当成频率输出。我看到很多人上传的代码里,串口打出来的是“1, 2, 3, 4”而不是“12.5, 25.0, 37.5”,这就是因为缺少乘系数这一步。如果输出的是索引,会让上位机人员困惑,也让答辩效果打折扣。建议把频率映射写成一个独立的函数,比如计算当前索引对应的频率,这样无论是显示还是发送,代码可读性都更好。
还有一点要注意:对于实数FFT,输出数组是单边频谱,通常不需要关注高于采样率一半的频率;但对于复数FFT,输出是对称的,如果用了arm_cfft_f32而没做单边提取,拿到的结果会有一半的重复镜像,这也是许多“FFT结果看起来像对称波浪”的原因。
4. 数据采集与预处理:让FFT结果真正能反映真实信号
有不少学生实现了第三章的代码之后,把预先拷贝的固定数组替换成ADC采样数据,结果发现频谱变得乱七八糟。原因通常不在FFT,而在ADC采集和预处理环节。ADC的触发方式、DMA的缓冲区管理、信号是否加窗、直流分量是否去除,都会直接影响FFT输入序列的质量。这一章讲的是这些事情。
4.1 ADC多通道采样与DMA双缓冲:避免中断导致采样周期抖动
在STM32上采集模拟信号,最简单的做法是开启ADC中断,在中断回调里读取数据。但这种模式有两个问题:一是每次转换完成都要进中断,72MHz主频下频繁进出会消耗不少CPU;二是如果在中断里做简单存储,很难保证采样间隔严格一致。FFT对采样间隔一致性比较敏感,如果定时器触发本身是均匀的,但代码处理不及时或中断嵌套导致丢失转换结果,就会引入频谱泄露和杂散。
更可靠的做法是使用定时器触发ADC,然后让DMA自动把结果搬运到内存。定时器输出频率就是采样率,DMA每完成一轮传输就触发一次回调,在回调里切换缓冲区。这就是所谓的DMA半传输和全传输双缓冲机制。简单来说,让DMA不断往两个交替使用的缓冲区里写数据,一个缓冲区被写满时,CPU可以处理另一个缓冲区。这样采样过程完全由硬件驱动,CPU只负责批量处理。
#define ADC_BUF_LEN FFT_SIZE volatile uint16_t adc_buf[2][ADC_BUF_LEN]; // 两个缓冲区交替使用 volatile uint8_t active_buf = 0; // 当前正在被DMA写入的缓冲区 void adc_dma_init(void) { ADC_InitTypeDef adc_init; DMA_InitTypeDef dma_init; TIM_TimeBaseInitTypeDef tim_init; // 配置ADC连续扫描模式关闭,使用外部触发 adc_init.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2; // 配置DMA循环模式,外设地址为ADC数据寄存器,内存地址为adc_buf[0] dma_init.DMA_Mode = DMA_Mode_Circular; dma_init.DMA_BufferSize = ADC_BUF_LEN * 2; // 启动定时器,定时周期决定采样率 }DMA配置成循环模式后,传输完整个数组会重新从头开始。如果要实现双缓冲,需要开启DMA的中断模式,在半传输和全传输中断里切换活动缓冲区的状态。很多教程会把DMA_BufferSize直接设为ADC_BUF_LEN,然后只处理一个固定缓冲区,这种做法能跑,但会在缓冲区满时停止采集,丢失连续数据。我一般会在主循环里维护一个标志位,表示“当前缓冲区已采集完成”,处理完FFT后再允许DMA继续写。这样虽然会丢失一小段数据,但至少不会出现写入和读取同一个缓冲区的竞争问题。
4.2 窗函数的选择与实现:不加密直接FFT会出现明显泄漏
对一段有限长度的信号做FFT,相当于在时间域上给信号乘了一个矩形窗。矩形窗的频谱是sinc函数,会产生大量的旁瓣,导致一个单频信号的能量“泄漏”到相邻频点上。典型表现是:输入一个1000Hz标准正弦波,却发现FFT输出里1000Hz附近上下好几个频点都有明显幅度,看起来像一条“胖谱线”。这并非FFT算法错了,而是矩形窗的固有特性。
解决方法是加窗函数。常用的窗有汉宁窗、海明窗、布莱克曼窗。汉宁窗在工程中应用最广,它的旁瓣衰减比较大,主瓣宽度稍微宽一些,适合绝大多数信号分析。加窗的做法很简单:在FFT之前,把输入时域数据的每一个样本乘以对应窗函数的系数。窗函数系数可以提前计算好存成数组,也可以在实时处理时计算。我习惯用查表法,因为STM32F1的浮点计算能力有限,预先生成表格能省时间。
下面是汉宁窗的生成和加窗过程:
float32_t window_table[FFT_SIZE]; void generate_hann_window(void) { for (int i = 0; i < FFT_SIZE; i++) { // 汉宁窗公式:0.5 - 0.5 * cos(2*pi*i/(N-1)) window_table[i] = 0.5f - 0.5f * cosf(2.0f * PI * i / (FFT_SIZE - 1)); } } void apply_window(float32_t *data) { for (int i = 0; i < FFT_SIZE; i++) { data[i] = data[i] * window_table[i]; } }加窗之后,幅值会发生变化,因为窗函数会降低信号总能量。因此需要进行幅度校正,通常用“恢复系数”乘以幅度结果。汉宁窗的幅度恢复系数约为2。如果不能整,就用实际测得的标准信号标定。我在项目里是先输入一个已知幅值的1kHz标准正弦波,调节系数让FFT输出幅值等于示波器看到的幅值,这样最简单也最准确。
4.3 直流分量与工频干扰的滤除策略
FFT结果里,0Hz频点对应的是信号中的直流分量。如果ADC采集到的信号是2.5V偏置加小幅度交流,直流分量会占据极大能量,影响显示效果,也会让频谱图在其他频点上“看不出小信号”。处理办法有两种:一种是在采集层用差分输入方式消除直流偏置;另一种是在软件里先计算采样序列的平均值,然后把每个样本减去平均值,再去加窗和FFT。第二种办法更通用,几乎适用于所有压电传感器和普通音频信号。
如果你的采样信号是电网环境下的低压信号,50Hz和100Hz工频干扰会非常明显。这是真实项目中常见的烦恼。硬件上用带通滤波器可以抑制,软件上则可以在FFT之后只显示目标频段,比如把输出数组中10Hz以下的频点全部置零,或者显示时忽略相应区间。不过软件措施不能替代前端的抗混叠滤波器,如果信号中存在超过奈奎斯特频率的高频成分,它们会折叠到低频区域,这个坑在书面上很容易被忽略,实际电路里却很致命。所以ADC模拟输入端要加一级RC低通,截止频率要低于采样率的一半。
预处理顺序很关键:先取均值去直流,再加窗,再做FFT,最后做幅值恢复。如果你先加窗再减去均值,算出来的均值包含了窗函数形状,直流分量就移除不干净。这个问题我在调试中反复遇到过。所以建议把预处理代码封装成独立函数,顺序固定,避免后续维护时调整错乱。
5. 常见问题排查:FFT结果乱跳、频率偏移和DMA卡死
这一章汇总几个最容易在STM32 FFT项目里翻车的情况,每条都按现象、原因、解决的思路展开。如果你正在调试,建议对照着排查。很多问题其实不是代码逻辑难,而是没有经验,不知道往哪个方向查。
5.1 现象:频谱出现镜像,左右对称
调试时把FFT输出打印出来,发现数组前半段和后半段几乎一样,呈左右对称形状。如果你用的是复数FFT和长度为2N的数组,这个现象代表输出的完整频谱包含了正负频率,后半部分是前半部分的镜像。原因就是没用实数FFT做单边提取,而读取结果时又要求显示全部频点。
解决方法是改用arm_rfft_fast_f32,或者使用复数FFT时只取前N/2个频点。要注意的是,复数FFT的虚部为了承载ADC实数数据,会在N/2以上产生冗余,并不是真实物理频率,把数组输出到绘图工具时一定要截断。如果你不需要相位信息,实数FFT更方便,也更快。
5.2 现象:ADC采样值正常但FFT结果全为零
用调试器查看adc_buf,数值确实在正常范围内波动,但FFT输出所有幅值都是0。第一种可能是输入数组的格式不对。arm_rfft_fast_f32要求的是浮点数组,如果你直接把unsigned short型ADC数据传进去,编译器会做类型转换,但数据内存布局可能完全不是期望的。第二种可能是FFT函数要求的缓冲区长度和实际数组长度不匹配,导致它只读了一部分数据。
解决方法是先把ADC原始值转换成float32_t,并做归一化到-1到1或0到3.3的工程值,再送入FFT。这里有个常见误区:直接把ADC原始值当浮点用,结果幅度会特别大,但不是零。所以如果输出全为零,优先检查数组类型和长度。另一种可能是采样率太低,信号本身被低通滤除或采样点根本不是目标信号,这条需要观察时域波形放心。
5.3 现象:程序运行一段时间后DMA停止采集
一开始频谱显示正常,几分钟后数据不再更新,ADC缓冲区也不再变化。原因通常是DMA中断没有正确清除标志,或者缓冲区切换逻辑里,主循环处理FFT的时间超过了定时器触发两次DMA半传输的间隔,导致中断标志重叠丢失。另一种常见原因是DMA配置为单次模式而不是循环模式,传输完成一次后就停止了。
解决方法是先检查DMA中断服务例程有没有清标志,然后将DMA模式改成循环模式。对于半传输中断和全传输中断,要区分两个不同的回调。如果处理时间太长,最简单的缓解办法是在处理FFT时暂时关闭DMA中断,等处理完再开启,但这会导致部分数据丢失,也可以接受,只要刷新率足够稳定即可。我更推荐的做法是缩小FFT点数,比如从1024改为512,处理时间减半,丢失数据的概率也会降低。
5.4 现象:频率轴有偏移,测1kHz却看到1.2kHz
输入标准1kHz正弦波,FFT输出的峰却在1.2kHz附近,偏移严重。这往往是采样率的实际值和配置值不一致导致的。比如定时器被设置为12800Hz,但一条带浮点除法或打印的函数占用了大量CPU,导致定时器触发的有效率下降,实际采样率低于设定值,频率轴自然整体偏移。
解决方法是先用示波器或逻辑分析仪测量定时器触发的波形,确认实际频率是否等于设定值;或者用已知信号做全频带标定,反推出真实采样率。如果定时器配置依赖系统时钟,还要检查时钟源是内部还是外部,是否倍频正确。频率轴计算公式里的Fs一定要用实测值,不能用理论值,这是一个典型的“差一个数”的项目坑。
5.5 现象:刷新率太慢,画面明显卡顿
1024点FFT在STM32F1上理论计算只需几毫秒,但整体刷新率却不到一帧每秒。原因往往不是FFT本身,而是串口发送方式太笨。比如使用printf加浮点数格式化发送,每个浮点要转字符串,512个频点要发送很久。如果每次刷新都发全量数据,对串口来说是一个巨大负担。
解决方法是只发送频段内的峰值频点,一般发送前20或50个感兴趣频点即可。如果必须全量发送,就把串口波特率提升到115200或更高,并在发送时使用DMA发送,避免CPU阻塞。还可以缩小FFT点数,或者把结果先保存到缓冲数组,在后台慢慢发送,不要一次性在FFT完成后的回调里同步发送。这个优化做完,刷新率往往可以提升数倍。
6. 项目验收与答辩:用三组可测指标证明FFT实现合格
到了验收阶段,只演示“屏幕上有波形在跳”是不够的。评审会追问准确性和实时性。这章给出三个可以量化的验收维度,以及对应的测试方式,直接用于课程设计答辩或项目技术审查。
6.1 验收指标:频率精度、幅值误差、刷新率
频率精度的验收方法是输入一个已知频率的正弦波,记录FFT输出峰值对应的频点,计算与真实频率的偏差。合格标准通常要求误差在一个频率分辨率以内,也就是12.5Hz左右。幅值误差则是输入已知峰值的正弦波,比较FFT幅度与真实幅值,合格范围在5%以内。刷新率通过统计每秒完成的FFT次数或者串口输出帧数来得到,一般不低于5帧就算实时显示合格。
| 验收指标 | 测试方法 | 合格标准 |
|---|---|---|
| 频率精度 | 用信号源输入1kHz正弦波,记录峰值频点 | 与1kHz偏差≤一个分辨率 |
| 幅值误差 | 输入已知幅值波形,比较FFT幅度与标定值 | 误差≤5% |
| 刷新率 | 统计串口帧数或LED闪烁频率 | ≥5帧每秒 |
6.2 验收演示流程:从信号源到最终波形一次走通
建议准备一台简易信号发生器,如果没有,也可以用手机播放音频通过电位器衰减后输入到开发板的PA口。测试开始前先把信号源归零,确认ADC采样波形在屏上是一条干净的水平线;然后输出1kHz正弦波,记录FFT峰值位置和幅度;再分别切到2kHz和500Hz,观察峰是否跟着移动。最后直接用手碰一下输入端子,屏幕会立刻出现较宽的底噪,这个动作虽然简单,却能让评审直观看到FFT在“处理真实信号”时的行为。
答辩时最容易被问的是“为什么用这个点数”“为什么分辨率是这个数字”。回答的关键是把公式写出来:分辨率=采样率/FFT点数。然后解释自己如何根据目标信号频率范围做出折中。如果你能说清“加窗是为了抑制频谱泄漏,选汉宁窗是为了兼顾主瓣宽度和旁瓣衰减”,基本就能证明这不是纯抄代码,而是理解了算法工程的取舍。
6.3 答辩中的三位一体表述:采样、运算、输出闭环
在展示时,我建议先快速过一遍整体框图,然后直接进入“输入一个1kHz正弦波”的实际演示,边看现象边解释数据流。注意不要先讲数学定义,而是先讲得到的结果,再反推为什么能得到这个结果。这样听众不会陷在细节里。关于“使用官方库是不是工作量太小”的问题,最好提前准备一个补强回答:重点说明参数配置、内存规划、数据格式转换和输出协议设计都是自己完成,算法库只是工具。
我自己的经验是,验收最好提前半天完整演习一次,因为FFT项目最容易在“温度变化导致ADC漂移”或“接触不良产生噪声”这种小事上翻车。从那以后,我每次交项目前都强制走一遍“静音测试—单频测试—扫频测试”三步流程,确认噪声、精度和刷新率都达标才敢给导师看。“静音”对应底噪,“单频”对应准度,“扫频”对应全带一致性。这套流程不用花很多时间,但能帮你在验收前发现大部分隐藏问题。希望你能在这套方案基础上做出自己的完整系统,并在答辩时拿出让评审信服的实测数据。
本文还有配套的精品资源,点击获取