Arduino 上的快速傅里叶变换 (FFT)
有多个库可帮助您在 Arduino 上计算快速傅里叶变换 (FFT)。我们将介绍 arduinoFFT 库。此库可通过库管理器安装(搜索 arduinoFFT)。

安装完成后,前往:File→Examples→arduinoFFT 并打开 FFT_01 示例。
示例
此示例首先创建一个频率为 1000Hz(采样率为 5000Hz)的正弦波。然后使用 Hamming 函数 对其进行加窗。之后,计算 FFT,确定幅度最高的频率,并将其作为基频返回。如果该值接近 1000 Hz,则此代码有效。
让我们开始代码演练。首先,我们引入库并创建 arduinoFFT() 对象。
#include "arduinoFFT.h" arduinoFFT FFT = arduinoFFT(); // Create FFT object We then define the variables specific to the signal. const uint16_t samples = 64; //This value MUST ALWAYS be a power of 2 const double signalFrequency = 1000; const double samplingFrequency = 5000; const uint8_t amplitude = 100;
我们将使用 64 个信号样本来生成时域数组。此外,样本数量应始终为 2 的幂。
之后,我们定义两个数组,分别用于存储 FFT 输出的实部和虚部,以及初始的原始数据。
double vReal[samples]; double vImag[samples];
最后,定义 4 个常量。这些常量将作为参数传递给稍后定义的 printVector() 函数,并帮助确定缩放因子。我们将在讲解 printVector() 函数时了解它们的用法。
#define SCL_INDEX 0x00 #define SCL_TIME 0x01 #define SCL_FREQUENCY 0x02 #define SCL_PLOT 0x03
在设置过程中,我们只需初始化 Serial 即可。
void setup()
{
Serial.begin(115200);
while(!Serial);
Serial.println("Ready");
}
在循环中,我们首先构建时域信号数组。
void loop()
{
/* 构建原始数据 */
// 采样将读取的信号周期数
double cycles = (((samples-1) * signalFrequency) / samplingFrequency);
for (uint16_t i = 0; i < samples; i++)
{
/* 构建包含正值和负值的数据*/
vReal[i] = int8_t((amplitude * (sin((i * (twoPi * cycles)) / samples))) / 2.0);
// vReal[i] = uint8_t((amplitude * (sin((i * (twoPi * cycles)) / samples) + 1.0)) / 2.0);
/* 构建 Y 轴上位移的数据,使其仅包含正值*/
/* 循环时虚部必须清零,以避免计算错误和溢出 */
vImag[i] = 0.0;
}
然后我们打印信号,对信号应用汉明窗并再次打印。然后我们使用 FFT.Compute() 计算 FFT,并打印实部和虚部矢量。随后,我们使用 FFT.ComplexToMagnitude() 函数,根据实部和虚部向量计算幅度,并打印该幅度。
最后,我们计算幅度最高的频率(使用 FFT.majorPeak() 函数)并打印。
运行此函数后,幅度最高的频率为 1004.225670,非常接近 1000 Hz。
/* 根据时间打印模拟采样的结果 */
Serial.println("Data:");
PrintVector(vReal, samples, SCL_TIME);
/* Weigh data */
FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
Serial.println("Weighed data:");
PrintVector(vReal, samples, SCL_TIME);
FFT.Compute(vReal, vImag, samples, FFT_FORWARD); //计算 FFT
Serial.println("计算的实数值:");
PrintVector(vReal, samples, SCL_INDEX);
Serial.println("计算的虚数值:");
PrintVector(vImag, samples, SCL_INDEX);
FFT.ComplexToMagnitude(vReal, vImag, samples); //计算幅度
Serial.println("计算的幅度:");
PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);
double x = FFT.MajorPeak(vReal, samples, samplingFrequency);
Serial.println(x, 6);
while(1); / * 运行一次 */
// delay(2000); /* 延迟后重复 */
}
最后,定义 printVector 函数。
此函数接收要打印的向量、要打印的条目数以及缩放因子。
如果缩放因子为 SCL_INDEX,则打印向量中每个条目的索引号。
如果缩放因子为 SCL_TIME,则从 0 开始打印向量中每个条目的时间(使用采样频率)。如果采样频率为 100,则每次读数需要 1/100 或 0.01 秒。因此,可以计算每次读数的时间。
如果缩放因子为 SCL_FREQUENCY,则打印每个条目对应的频率。请注意,这仅在计算完幅度后使用。
PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);
请注意,我们将样本右移了 1 位。由于样本始终是 2 的幂,因此右移意味着将样本数除以 2。因此,对于 64 个样本,右移 1 位后的值变为 32。
FFT 给出的频率值从 0 到 采样频率/2(奈奎斯特准则)。因此,每个索引处的频率值为 index*sampling_frequency/n_samples。这就是我们获取频率的方法。
void PrintVector(double *vData, uint16_t bufferSize, uint8_t scaleType)
{
for (uint16_t i = 0; i < bufferSize; i++)
{
double abscissa;
/* Print abscissa value */
switch (scaleType)
{
case SCL_INDEX:
abscissa = (i * 1.0);
break;
case SCL_TIME:
abscissa = ((i * 1.0) / samplingFrequency);
break;
case SCL_FREQUENCY:
abscissa = ((i * 1.0 * samplingFrequency) / samples);
break;
}
Serial.print(abscissa, 6);
if(scaleType==SCL_FREQUENCY)
Serial.print("Hz");
Serial.print(" ");
Serial.println(vData[i], 4);
}
Serial.println();
}
请注意,计算出的幅度与原始信号的振幅之间的关系尚未建立。建议您阅读此库附带的其他示例。

