1. 项目概述与核心挑战
在嵌入式信号处理领域,快速傅里叶变换(FFT)一直是个让人又爱又恨的工具。爱它,是因为它能将一串看似杂乱无章的时域采样数据,瞬间变成清晰的频谱图,让你一眼看穿信号里藏着哪些频率成分。恨它,尤其是在Arduino这类资源捉襟见肘的微控制器上,标准的FFT库动辄需要几十毫秒甚至上百毫秒的计算时间,内存占用也常常让人望而却步。当你试图用它来做实时音频分析、振动监测或者频谱显示时,这种性能瓶颈就成了项目推进路上最大的绊脚石。
我自己在做一个基于Arduino的简易频谱分析仪时,就深刻体会到了这种痛苦。采样率不敢设高,点数不敢取多,否则屏幕刷新就会卡成幻灯片,完全失去了“实时”的意义。市面上能找到的Arduino FFT库,要么像EasyFFT那样追求精度但慢如蜗牛,要么像QuickFFT那样速度飞快但结果偏差太大,根本没法用。正是在这种两难境地下,我开始琢磨,有没有一种方法,能在速度和精度之间找到一个绝佳的平衡点?这就是ApproxFFT诞生的背景。
ApproxFFT,顾名思义,是一系列近似计算策略的集合。它的核心思想非常直接:在FFT计算中,哪些环节最耗时?我们能否用一些“足够好”的近似方法来替换掉这些瓶颈,从而换来性能的飞跃?答案是肯定的。通过将耗时的浮点三角函数计算替换为基于位操作的快速近似,并优化幅度计算等环节,我最终实现了一个在Arduino Mega上运行速度提升3倍以上,同时保持可用精度的FFT函数。这篇文章,我就来拆解ApproxFFT的每一个优化细节,从设计思路到代码实现,再到实际应用中的避坑指南,希望能给同样在嵌入式信号处理泥潭中挣扎的你,提供一条可行的上岸路径。
2. 设计思路:在速度与精度之间走钢丝
做嵌入式优化,本质上就是在资源约束下做权衡。ApproxFFT的设计哲学,就是系统地识别标准FFT实现中的性能热点,然后用计算代价更低的近似方法去替换它们,同时严格控制由此引入的误差,确保其在实际应用中是可接受的。
2.1 性能瓶颈分析:标准FFT的“阿喀琉斯之踵”
要优化,先得知道哪里慢。在一个典型的基2时间抽取FFT算法中,计算负担主要来自两个部分:
- 复数乘法的蝴蝶运算:这是FFT的核心。每一次蝴蝶运算都涉及与旋转因子(由正弦和余弦函数生成)的复数乘法。在软件中,特别是没有硬件浮点单元(FPU)的Arduino上,计算
sin()和cos()函数以及随后的浮点乘法是极其昂贵的操作。 - 幅度谱计算:FFT输出的是复数数组,为了得到每个频率分量的幅度,需要对每个复数计算其模值,即
sqrt(real^2 + imag^2)。开平方根sqrt()是另一个计算密集型函数。
传统的优化手段,比如使用预先计算好的正弦/余弦查找表,确实能带来一些提升。EasyFFT就采用了这种方法。但查找表本身占用内存,而且查表后依然需要进行浮点乘法。QuickFFT走得更激进,它用方波来近似正弦波,虽然速度极快,但方波富含奇次谐波,会严重污染频谱结果,导致幅度严重失真并出现虚假频率峰值,实用性大打折扣。
ApproxFFT的目标很明确:我们必须找到一种比查表+乘法更快的方法来近似三角函数,同时其结果要比方波近似好得多,误差必须控制在大多数应用可以容忍的范围内。
2.2 核心优化策略:三位一体的加速方案
ApproxFFT的加速主要依赖于三个关键创新,它们共同作用,才实现了质的飞跃。
策略一:基于二分搜索的快速正弦/余弦近似 (fast_sine/fast_cosine)这是整个算法速度提升的最大贡献者。它完全摒弃了计算sin(θ)再乘以幅度A的传统路径(即计算A * sin(θ))。相反,它直接计算A * sin(θ)的近似值。其灵感来源于数值分析中的二分搜索和反正弦函数的特性。
系统内部维护了一个isin_data查找表,这个表存储的不是正弦值,而是角度值。它描述的是arcsin(x)的逆关系(经过特定缩放)。对于给定的幅度A和角度θ,算法将A视为搜索范围(0到A),将θ与查找表中的值进行比较。通过一系列二分迭代,它快速定位到A * sin(θ)的值落在哪个子区间,并将区间的中点作为近似值。每一次迭代仅涉及移位、加法和比较操作,完全避免了浮点乘法和三角函数调用。
策略二:快速平方根近似 (fastRSS)在计算复数模值sqrt(a^2 + b^2)时,ApproxFFT采用了条件近似法。它首先比较a和b的大小。如果其中一个远大于另一个(例如大于3倍),则直接返回较大的那个值作为近似结果,因为此时较小的那个对模值贡献甚微。如果两者大小相近,则根据其比值,从一个预定义的RSSdata表中获取一个迭代次数,通过几次加法和移位操作来逼近真实模值。这比调用标准的sqrt()或甚至计算sqrt(a*a + b*b)要快得多。
策略三:动态整数缩放与溢出保护为了充分利用整数运算的速度并避免浮点数,ApproxFFT在内部将输入数据缩放至-512到+512的整数范围内。在整个FFT的蝴蝶运算过程中,算法会持续监测实部(out_r)和虚部(out_im)数组的值。一旦发现任何数值的绝对值超过安全阈值(如15000),就会立即对整个数组进行右移一位(即除以2)的操作,并记录下缩放因子(scale)。这个“动态定标”机制有效防止了中间值在迭代过程中溢出,同时保留了尽可能多的有效比特。计算完成后,最终的幅度结果会根据总的缩放因子进行补偿。
2.3 精度控制机制:不是无脑快,而是可控的快
速度的提升不能以牺牲全部精度为代价。ApproxFFT的精妙之处在于它引入了可调的精度参数。
在fast_sine函数中,有一个关键的accuracy变量(默认值为5)。这个变量直接控制着二分搜索的迭代次数。accuracy=1迭代次数最少,速度最快,但近似结果最粗糙;accuracy=7迭代次数最多,速度稍慢,但结果最接近真实的正弦值。用户可以根据自己项目的实时性要求和精度容忍度来调整这个参数。在大多数情况下,accuracy=5提供了一个非常好的折衷,这也是默认推荐值。
这种设计赋予了开发者灵活性。在对频率检测要求极高、但对绝对幅度要求不严的场合(比如判断哪个音符在响),可以适当降低精度以换取更快的响应。而在需要相对准确幅度信息的场合(比如粗略的频谱能量显示),则可以使用更高的精度设置。
3. 算法核心:快速正弦函数深度解析
fast_sine函数是ApproxFFT的灵魂,也是理解其如何实现加速的关键。让我们深入其代码,看看这个“魔法”是如何发生的。
3.1 数据结构准备:isin_data表的奥秘
一切始于那个神秘的isin_data数组。它不是一个普通的正弦查找表。它的长度为128,每个值对应一个角度索引,但这个角度是经过特殊映射的。
byte isin_data[128]={0,1,3,4,5,6,8,9,10,11,13,14,15,17,18,19,20,22,23,24,26,27,28,29,31,32,33,35,36,37,39,40,41,42,44,45,46,48,49,50,52,53,54,56,57,59,60,61,63,64,65,67,68,70,71,72,74,75,77,78,80,81,82,84,85,87,88,90,91,93,94,96,97,99,100,102,104,105,107,108,110,112,113,115,117,118,120,122,124,125,127,129,131,133,134,136,138,140,142,144,146,148,150,152,155,157,159,161,164,166,169,171,174,176,179,182,185,188,191,195,198,202,206,210,215,221,227,236};这个表存储的是什么?它实际上是角度值。更具体地说,它定义了arcsin(x)的逆关系在一个离散集合上的近似,其中x被线性映射到0-127的区间。表中的数值代表角度,其单位是“自定义角度单位”,整个圆周(360度)对应1024个单位。因此,值128大约对应45度(1024/8),256对应90度,以此类推。
这个表的设计是非线性的。在接近0度(sin值接近0)时,角度变化缓慢;在接近90度(sin值接近1)时,角度变化加快。这正好匹配了正弦函数的变化率:在0度附近斜率大,角度微小变化会引起正弦值较大变化;在90度附近斜率小。这种非线性分布使得后续的二分搜索能在整个范围内保持相对均匀的精度。
3.2 算法流程:二分搜索实现快速估值
现在,我们来看函数如何工作。假设我们要计算A * sin(θ),其中A=500,θ=50度(在算法内部,50度会被转换为50/360 * 1024 ≈ 142个自定义单位)。
- 角度归一化:首先,函数通过循环加减1024,将输入角度
th规范到0-1023的范围内。这处理了任何周期外的输入。 - 象限处理:根据角度的高两位(
quad = th >> 8),判断其位于哪个象限(0-255: 第一象限;256-511: 第二象限;512-767: 第三象限;768-1023: 第四象限)。利用三角函数的对称性(sin(θ) = sin(π-θ),sin(θ) = -sin(θ-π),sin(θ) = -sin(2π-θ)),将所有计算都映射到第一象限(0-256个单位,即0-90度)来处理。这极大地压缩了所需查找表的大小。 - 二分搜索初始化:
temp1 = 0: 搜索区间下限索引。temp2 = 128: 搜索区间上限索引(对应isin_data表的最大索引)。m1 = 0: 对应的幅度下限。m2 = Amp(即500): 对应的幅度上限。temp3 = (m1 + m2) >> 1 = 250: 当前猜测的幅度中点。Amp = temp3: 当前最佳估计值。
- 迭代搜索: 循环进行
accuracy次(例如5次)。每次迭代: a. 计算角度查找表的中点索引:test = (temp1 + temp2) >> 1。 b. 更新幅度步长:temp3 = temp3 >> 1(即减半,第一次是125,第二次是62,...)。 c. 比较:如果当前角度th(142)大于isin_data[test],说明sin(θ)应该比当前测试点的sin值大,因此真实幅度应落在当前猜测幅度的上半区间。于是更新temp1 = test(提升角度下限),Amp = Amp + temp3(提升幅度估计),m1 = Amp(更新幅度下限)。 d. 反之,如果th小于isin_data[test],则更新temp2和m2,Amp = Amp - temp3。 - 结果修正:根据第二步确定的象限信息,对最终得到的
Amp施加正确的符号(第二、三象限为负)。 - 返回:返回
Amp作为A * sin(θ)的近似值。
通过这个过程,我们仅用了几次整数加法、移位和比较,就得到了一个相当不错的近似值。fast_cosine函数则更加简单,它利用cos(θ) = sin(π/2 - θ)的恒等式,直接调用fast_sine来完成计算。
实操心得:精度与速度的权衡我强烈建议你在项目初期将
accuracy设置为5。在我的大量测试中,这个值在速度和精度之间取得了最佳平衡。除非你的信号非常纯净,且对频率分辨率要求极高,否则你很难察觉accuracy=5和accuracy=7结果之间的区别,但速度却有可观的差异。只有当你在处理极低信噪比的信号,或者需要非常精确的幅度测量时,才考虑将其提高到6或7。反之,对于只是检测频率是否存在(比如敲击检测)的应用,降到3或4也能获得不错的效果,速度会更快。
4. 实现细节:从理论到可运行的代码
理解了核心算法,我们来看看如何将ApproxFFT集成到你的Arduino项目中,并理解其完整的工作流程。
4.1 代码集成步骤
使用ApproxFFT非常简单,只需要三步:
声明查找表:将以下三个全局数组复制到你的Arduino代码文件顶部。它们是所有快速计算的基础,必须作为全局变量存在。
//---------------------------------lookup>// 示例:分析一个包含128个样本的数组,采样率为1000 Hz int sampleData[128]; // ... 这里填充你的采样数据,例如从ADC读取 ... float dominantFreq = Approx_FFT(sampleData, 128, 1000.0); Serial.println(dominantFreq); // 打印主频
4.2 Approx_FFT主函数流程详解
主函数Approx_FFT(int in[], int N, float Frequency)是调度中心,它按以下步骤组织计算:
数据预处理与缩放:
- 首先,函数会找到小于等于N的最大的2的幂(
a = Pow2[o])。这是FFT的要求。如果你的N不是2的幂,多余的数据将被忽略。 - 计算输入数据的平均值(
data_avg)、最大值(data_max)和最小值(data_min)。 - 计算数据的峰峰值(
data_mag)。然后通过循环左移或右移(缩放),将数据的动态范围调整到大约-512到+512之间。这个目标范围是经验值,旨在充分利用整数精度同时避免后续计算溢出。整个过程中记录的缩放因子保存在变量scale中。
- 首先,函数会找到小于等于N的最大的2的幂(
比特反转排序: FFT算法要求输入数据按比特反转的顺序排列。函数使用一个巧妙的迭代算法生成比特反转索引表(存储在
out_im中 initially),然后按照这个顺序将输入数据in[]重排到out_r[]数组中。out_im数组在此之后被清零,准备用于存储虚部。核心FFT蝶形运算: 这是标准的基2时间抽取FFT循环结构,但关键步骤被替换了。
- 外层循环遍历FFT的级(stage)。
- 内层循环计算每个蝶形组。
- 最内层循环计算每个蝶形。这里原本需要计算
cos(θ)和sin(θ),并与复数相乘。在ApproxFFT中,这一步被替换为对fast_cosine和fast_sine的调用。
tr = fast_cosine(out_r[temp4], c) - fast_sine(out_im[temp4], c); ti = fast_sine(out_r[temp4], c) + fast_cosine(out_im[temp4], c);- 动态缩放检查:完成每个蝶形计算后,会检查实部和虚部的值是否接近整数溢出边界(±15000)。如果超过,一个全局标志
check被置位。在每一级(stage)运算结束后,如果check为1,则对整个实部和虚部数组进行右移一位(除以2)的操作,并将总缩放因子scale减1。这是一个至关重要的稳定性保障机制。
计算幅度谱并寻找主频:
- FFT变换完成后,
out_r和out_im数组中存储的是复数结果。 - 对于每个频率点(忽略直流分量和奈奎斯特频率之后的部分),调用
fastRSS(out_r[i], out_im[i])快速计算其幅度,结果存回out_r[i]。频率值(Hz)则存入out_im[i]。 - 遍历幅度数组,找到最大值及其对应的索引
fm。 - 三点重心法插值:为了获得比整数倍频率分辨率更精确的主频估计,函数在最大值点
fm及其前后各一点(fm-1,fm,fm+1)的幅度(fa, fb, fc)上进行加权平均。这是一种简单的谱峰插值方法,可以有效减轻“栅栏效应”带来的误差。
fstep = (fa*(fm-1) + fb*fm + fc*(fm+1)) / (fa+fb+fc); return (fstep * Frequency / N);- 最终返回值是插值后的精确主频(Hz)。
- FFT变换完成后,
结果缩放补偿: 在函数返回前,会根据全局记录的
scale因子,对输出的幅度值进行补偿。由于在计算过程中可能进行了多次除以2的操作,最终的幅度值实际上是真实幅度的1/(2^scale)倍。函数通过注释提示,如果需要使用原始幅度值,需要将out_r[i]左移scale位。对于只关心频率的应用,则可以忽略这一步。
4.3 内存与性能考量
ApproxFFT在内存使用上非常节俭。除了输入数组,它主要需要两个大小为N(2的幂)的整数数组(out_r和out_im)作为工作空间。对于Arduino Uno/Mega(2KB RAM),处理128点FFT(需要21282字节 = 512字节的工作数组)是轻松愉快的。256点(1KB工作数组)也在安全范围内。512点FFT就会比较紧张,需要确保没有其他大的全局变量。
在性能方面,在16MHz的Arduino Uno上,一个128点的ApproxFFT(accuracy=5)大约需要4-6毫秒。相比之下,一个朴素的浮点FFT实现可能需要15-20毫秒。在Arduino Mega或ESP8266/ESP32这类更快的平台上,速度优势会更加明显。
注意事项:输入数据的准备
ApproxFFT期望输入数据是以零点为中心的。虽然函数内部会减去平均值进行直流偏移移除,但最好在外部也确保你的信号没有大的直流偏置。例如,如果你的ADC采样范围是0-1023,信号幅值在100-900之间波动,那么你应该在填充数组时先减去512(或信号的估计平均值),将其转换为大致在-412到+388之间波动的数据。这能让内部缩放机制工作得更好,提高计算精度。直接输入0-1023的原始ADC值可能会导致动态范围判断失误和精度损失。
5. 实战应用与性能调优
理论再漂亮,也要落地到实际项目。下面我们探讨几个典型应用场景,以及如何根据场景调整ApproxFFT的参数以获得最佳效果。
5.1 应用场景一:实时音频频谱分析
这是FFT最经典的应用之一。假设你想用Arduino和一个麦克风模块做一个LED音乐频谱灯。
- 采样率与点数选择:人耳可听范围是20Hz到20kHz。根据奈奎斯特采样定理,采样率至少需要40kHz。但对于视觉显示,我们往往更关注中低频(比如125Hz到4kHz)。你可以将采样率设置为8kHz(奈奎斯特频率4kHz),这足以分析大多数音乐的低频节奏部分。点数选择128点,那么每个频率点的分辨率是
8000/128 = 62.5 Hz。这对于区分低音鼓(~60-120Hz)和贝斯(~80-250Hz)是足够的。 - 数据采集:使用
analogRead()在loop()中连续采样是不可靠的,因为它的时间不精确。必须使用定时器中断来触发ADC转换,确保采样间隔严格相等。将采样值存入一个环形缓冲区。 - 调用FFT:当缓冲区存满128个点时,将其复制到
ApproxFFT的输入数组中。记得减去一个估计的直流偏置(比如ADC参考电压的一半对应的数字量)。 - 处理结果:
ApproxFFT返回主频,但对于频谱灯,你需要的是各个频带的能量。修改Approx_FFT函数末尾的循环,不进行三点插值,而是将计算出的out_r[i](幅度值)输出到一个数组。这个数组就是你的频谱。你可以将其分组(例如,将128个点分成8组,每组16个点,取平均或最大值),然后映射到LED的亮度上。 - 精度设置:对于视觉显示,幅度值的绝对精度要求不高,更看重相对大小和变化趋势。可以将
fast_sine中的accuracy降到4甚至3,以获得更高的刷新率,让灯光变化更跟手。
5.2 应用场景二:旋转机械振动监测
假设你要监测一个电机的转速,通过其振动信号分析。
- 特点:信号频率相对较低且稳定,可能包含一个强烈的基频(转速)及其谐波。对抗噪声和精度要求较高。
- 采样与点数:假设电机转速范围是1000-6000 RPM(约16.7Hz - 100Hz)。采样率设为250Hz就足够了。为了提高频率分辨率,以便精确区分接近的转速,需要使用更多的点数,例如256点。此时频率分辨率为
250/256 ≈ 0.98 Hz,足以进行精确的转速测量。 - 信号调理:振动传感器(如压电式或MEMS加速度计)的输出信号可能需要放大和滤波。在进入ADC之前,最好加一个抗混叠低通滤波器,截止频率设为125Hz(采样率的一半)。
- 精度设置:为了准确捕捉基频的幅度和可能存在的谐波,需要较高的计算精度。建议将
accuracy设置为6或7。虽然计算时间稍长,但对于低速采样(250Hz)来说,完成256点FFT的时间仍然远小于采样一帧数据的时间(256/250 = 1.024秒),实时性没有问题。 - 结果解读:寻找幅度谱中的最高峰,其对应的频率即为估计的转速基频。你还可以检查是否存在2倍、3倍频的谐波,这有时能反映机械不对中、轴承故障等问题。
5.3 性能调优指南
- 样本数 (
N) 的选择:永远是2的幂。越大,频率分辨率越高,但计算时间和内存占用呈O(N log N)增长。从128点开始尝试,平衡分辨率和速度。 - 采样率 (
Frequency) 的设置:必须大于你感兴趣的最高频率的2倍。更高的采样率能捕获更高频的信号,但也会增加数据量。根据你的应用需求,选择刚好够用的最低采样率。 - 精度参数 (
accuracy) 的调整:这是最重要的调优旋钮。- 1-3级:速度极快,精度较低。适用于对频率检测要求不严、需要极高刷新率的场景,如简单的节拍检测、阈值触发。
- 4-5级(默认):最佳平衡点。适用于绝大多数需要同时关注频率和相对幅度的应用,如音频可视化、基础振动分析。
- 6-7级:精度最高,速度最慢。适用于对幅度精度有要求,或信号非常微弱需要精细分析的场合,如传感器标定、低频精确测频。
- 利用缩放因子 (
scale):如果你需要真实的幅度值(例如,计算分贝值),一定要记录并补偿函数内部产生的scale因子。最终的幅度值需要乘以pow(2, scale)。如果只关心频率,则可以忽略。
避坑技巧:避免频谱泄漏与栅栏效应即使算法再快,如果输入信号处理不当,结果也是错误的。FFT假设输入信号是周期性的,且你的数据窗口正好包含整数个信号周期。如果不是,就会发生“频谱泄漏”,导致能量分散到多个频点上。解决方案:在调用
Approx_FFT之前,对输入数据数组应用一个窗函数,如汉宁窗(Hanning Window)。这能有效减少因数据截断带来的频谱泄漏。窗函数很简单,就是一个系数数组,在FFT前将你的采样数据逐个乘以对应的窗系数。虽然ApproxFFT代码中没有内置窗函数,但你可以在数据预处理阶段轻松加入。对于实时性要求高的场景,可以预先计算好窗系数表。
6. 常见问题与深度排查
在实际使用ApproxFFT的过程中,你可能会遇到一些典型问题。下面是我在多个项目中总结出来的排查清单和解决方案。
6.1 问题:输出频率总是漂移或不稳定
- 可能原因1:采样时序不精确。这是最常见的问题。
analogRead()本身执行时间就有微秒级的波动,在loop()中用delayMicroseconds()控制间隔是非常不准确的。- 解决方案:使用硬件定时器中断来触发ADC采样。对于Arduino,可以利用
Timer1或Timer2库。这是实现稳定频谱分析的基础。
- 解决方案:使用硬件定时器中断来触发ADC采样。对于Arduino,可以利用
- 可能原因2:直流偏移未消除。信号有较大的直流分量,会影响FFT结果,特别是低频部分。
- 解决方案:在数据送入
Approx_FFT前,先计算数组的平均值并减去。或者,在硬件上使用交流耦合(串联一个电容)来隔离直流。
- 解决方案:在数据送入
- 可能原因3:噪声过大。环境噪声淹没了你想要的信号。
- 解决方案:硬件上增加滤波电路(如RC低通/带通滤波器)。软件上可以对采集到的数据进行简单的滑动平均滤波,或者多次FFT后对结果进行平均。
- 可能原因4:频谱泄漏。信号频率不是频率分辨率的整数倍。
- 解决方案:如前所述,应用窗函数(汉宁窗、汉明窗等)。或者,尝试微调采样率,使其接近信号频率的整数倍。
6.2 问题:幅度值看起来不合理(太小或太大)
- 可能原因1:忽略了缩放因子(
scale)。Approx_FFT内部为防止溢出进行了动态缩放,输出的out_r[i]是缩放后的幅度。真正的幅度需要magnitude = out_r[i] * pow(2, scale)。- 解决方案:修改
Approx_FFT函数,使其返回缩放因子,或者在函数内部将幅度结果补偿后再输出。查看代码中注释掉的部分,取消对幅度输出行的注释,并确保进行了正确的缩放补偿。
- 解决方案:修改
- 可能原因2:输入信号幅度超出预期范围。虽然算法有内部缩放,但如果输入信号本身幅值过大或过小,初始缩放可能不理想。
- 解决方案:确保你的输入信号在ADC量程内具有合适的幅度。可以通过增益电路进行硬件调整,或者在软件上对采样值进行预缩放。
6.3 问题:在ESP32等32位平台上运行出错(如Guru Meditation Error)
- 可能原因:原始代码是针对8/16位的AVR架构(如Arduino Uno/Mega)编写的,大量使用了
int类型(在AVR上是16位)。在ESP32(32位)上,int是32位,一些位操作和溢出检查的逻辑可能不匹配,导致数组越界或非法内存访问。- 解决方案:
- 仔细检查所有数组索引。确保没有潜在的越界访问。
- 将代码中明确用于位反转、索引的变量声明为
uint16_t,以确保其行为与AVR上的int一致。 - 关注
fast_sine函数中的角度处理循环while(th>1024){th=th-1024;}。在32位平台上,确保th是16位无符号数或进行模运算,防止意外的大数导致无限循环。 - 社区中已有一些针对ESP32的移植版本,可以搜索参考。
- 解决方案:
6.4 问题:如何获取完整的频谱,而不是仅仅一个主频?
- 解决方案:默认的
Approx_FFT函数只返回主频。要获得完整频谱,你需要修改函数的最后一部分。- 将函数返回值类型改为
void,或增加一个输出参数。 - 注释掉寻找主频和三点插值的代码段(
fout=0;fm=0;开始的循环以及其后的插值计算)。 - 在计算完每个点的幅度(
out_r[i]=fastRSS(...))后,将结果(可能需要根据scale补偿)存储到一个全局数组或通过指针传回。 - 同时,频率轴信息已经存在
out_im[i]中(out_im[i]=out_im[i-1]+fstp),它就是每个频点对应的中心频率(Hz)。
- 将函数返回值类型改为
6.5 性能极限与进阶优化思路
即使使用了ApproxFFT,在Arduino Uno上做实时处理仍有其极限。以下是一些进一步的思路:
- 降低样本数:这是最直接的方法。64点FFT比128点快近一倍。
- 降低精度:将
accuracy调到3或4。 - 只计算感兴趣的频段:标准的FFT计算所有频点。如果你的应用只关心某个特定频段(比如50-60Hz的工频干扰),可以研究Zoom FFT或Goertzel算法,后者是计算单个频点能量的高效算法,速度远超FFT。
- 升级硬件:如果项目允许,考虑使用更强大的微控制器,如Teensy 4.0(带有硬件FPU和更高主频)、ESP32或STM32系列。在这些平台上,你甚至可以运行更完整的DSP库。
- 并行处理:对于极其复杂的应用,可以考虑使用两个单片机,一个负责高速采集数据,另一个专司FFT计算,通过SPI或I2C通信。
ApproxFFT的价值在于,它在有限的资源下打开了一扇窗,让你能在Arduino这类简单设备上实现原本不可能的信号处理任务。它不是万能的,但在其设计目标内——快速、低内存消耗、足够精度的频域分析——它表现得非常出色。理解其原理,善用其参数,你就能在下一个嵌入式信号处理项目中游刃有余。