Arduino 上的快速傅里叶变换 (FFT)

arduinohardwaresoftware & coding更新于 2026/1/12 6:52:17

有多个库可帮助您在 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();
}

请注意,计算出的幅度与原始信号的振幅之间的关系尚未建立。建议您阅读此库附带的其他示例。


相关文章