亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Manufacturing Technology >> 制造工艺

EasyFFT:面向 Arduino 项目的可靠 FFT 库

组件与物料

Arduino Nano R3 ×1

应用与在线服务

项目简介

在算力有限的 Arduino 上从采集信号中测量频率颇具挑战。零交叉法通过统计信号在给定时间内穿过零点的次数来估算频率,但对多频率混合信号无效。对于非专业背景的开发者,编写此类代码更是困难。本项目旨在提供一个易于在 Arduino 上实现、无需深究数学原理的 FFT 代码,适用于音乐、信号分析等项目。

本文不讲解 FFT 原理,仅说明 FFT 函数的应用。视频中也有同步讲解。若只关心代码应用,可直接跳至步骤 3。

若需 3 倍速度且可接受约 5% 精度损失,请参考我的另一篇文章 ApproxFFT:Arduino 最快 FFT 函数。

步骤 1:快速傅里叶变换 (FFT)

James Cooley 和 John Tukey 开发的 FFT 算法将 DFT 计算加速,被公认为 20 世纪最重要算法之一。它将信号分解为奇偶序列,将复数乘法次数从 DFT 的 N² 降至 NlogN,样本数越大优势越明显。

数学推导参考资料:

  1. FFT 推导
  2. 理解 FFT
  3. FFT 详解
  4. 维基百科 FFT

步骤 2:代码核心优化

快速正弦/余弦查表

FFT 需大量正弦/余弦值。Arduino 内置函数太慢(64 点采样耗时翻倍)。解决方案:预存 0°~90° 正弦值的 255 倍整数到 byte sine_data[91],查表代替浮点运算,内存占用减少 75%。该数组必须置于全局作用域。

同时声明全局数组 float f_peaks[5],每次 FFT 运行后更新,f_peaks[0] 为最强频率,以降序排列。

byte sine_data[91] = { 
  0, 4, 9, 13, 18, 22, 27, 31, 35, 40, 44, 49, 53, 57, 62, 66, 70, 75, 79, 83, 
  87, 91, 96, 100, 104, 108, 112, 116, 120, 124, 127, 131, 135, 139, 143, 146, 
  150, 153, 157, 160, 164, 167, 171, 174, 177, 180, 183, 186, 189, 192, 195, 
  198, 201, 204, 206, 209, 211, 214, 216, 219, 221, 223, 225, 227, 229, 231, 
  233, 235, 236, 238, 240, 241, 243, 244, 245, 246, 247, 248, 249, 250, 251, 
  252, 253, 253, 254, 254, 254, 255, 255, 255, 255 
};
float f_peaks[5];

利用对称性可由 0°~90° 表推算任意角度正弦/余弦,仅需一次浮点除法。此法虽牺牲精度,但 64 点加速 8ms,128 点加速 20ms。

步骤 3:FFT 函数详解

FFT 仅支持 2 的幂次样本数 (2, 4, 8, 16, 32, 64, 128...)。非 2ⁿ 值将向下取整(如 70 取 64)。建议直接使用 2ⁿ 采样数。out_r 和 out_im 两个 float 数组占用大量内存,Arduino Nano 限于 128 点甚至 64 点。


unsigned int data[13] = {1,2,4,8,16,32,64,128,256,512,1024,2048};
int a, c1, f, o, x;
a = N;
for (int i = 0; i < 12; i++) {
  if (data[i] <= a) { o = i; }
}
int in_ps[data[o]] = {};
float out_r[data[o]] = {};
float out_im[data[o]] = {};

处理流程:

  1. 生成给定采样数的位反转序列
  2. 按位反转序重排输入数据
  3. 执行 FFT 运算
  4. 计算复数模值 (振幅)
  5. 检测峰值并按幅度降序排列
  6. 结果存入全局 f_peaks[] (前 5 个主频率)

若需访问除峰值频率外的其他数据,需修改代码将局部变量复制到预定义全局变量。

步骤 4:代码测试

输入采样频率 10Hz 的三角波 (信号频率 1.25Hz)。输出与 Scilab 计算结果一致,因使用低精度正弦表存在微小偏差。输出频率桶为 1.25Hz 和 3.75Hz,实际值可能落在桶范围内。

Arduino Nano 执行耗时:

步骤 5:结论

此 FFT 代码适合实时应用 (~30ms 完成计算)。分辨率受样本数限制,样本数又受 Arduino 内存制约。使用 Arduino Mega 或更高性能板卡可提升精度。

欢迎提出疑问、建议或指正。

完整代码


/* 
 * Example data: 
 * int data[64] = {14, 30, 35, 34, 34, 40, 46, 45, 30, 4, -26, -48, -55, -49, -37, -28, 
 * -24, -22, -13, 6, 32, 55, 65, 57, 38, 17, 1, -6, -11, -19, -34, -51, -61, -56, -35, 
 * -7, 18, 32, 35, 34, 35, 41, 46, 43, 26, -2, -31, -50, -55, -47, -35, -27, -24, -21, 
 * -10, 11, 37, 58, 64, 55, 34, 13, -1, -7}; 
 */
//---------------------------------------------------------------------------
// 将 sine_data[91] 放在程序顶部作为全局变量
byte sine_data[91] = { 
  0, 4, 9, 13, 18, 22, 27, 31, 35, 40, 44, 49, 53, 57, 62, 66, 70, 75, 79, 83, 
  87, 91, 96, 100, 104, 108, 112, 116, 120, 124, 127, 131, 135, 139, 143, 146, 
  150, 153, 157, 160, 164, 167, 171, 174, 177, 180, 183, 186, 189, 192, 195, 
  198, 201, 204, 206, 209, 211, 214, 216, 219, 221, 223, 225, 227, 229, 231, 
  233, 235, 236, 238, 240, 241, 243, 244, 245, 246, 247, 248, 249, 250, 251, 
  252, 253, 253, 254, 254, 254, 255, 255, 255, 255 
};
float f_peaks[5]; // 降序存储前 5 个主频率
//---------------------------------------------------------------------------

void setup() {
  Serial.begin(250000);
}

void loop() {
  /* 
  // 示例用法:
  FFT(data, 64, 100); // 64 点采样, 100Hz 采样率
  Serial.println(f_peaks[0]);
  Serial.println(f_peaks[1]);
  delay(99999);
  */
  /* 运行 FFT() 后,前 5 个主频率可从 f_peaks[0]~f_peaks[4] 获取 */
}

//----------------------------- FFT Function ------------------------------//
float FFT(int in[], int N, float Frequency) {
  /* 
   * Arduino FFT 实现
   * 使用前置条件:
   * 1. 将 sine_data[91] 放在程序顶部 [全局变量]
   * 2. 将本函数放在程序末尾
   * 参数:
   * 1. in[] : 数据数组
   * 2. N : 采样数 (建议 2,4,8,16,32,64,128...)
   * 3. Frequency: 采样频率 (Hz)
   * 非 2 的幂次采样数将向下截取
   * Arduino Nano 受限于内存,建议 64 点,128 点可能失败
   * 更高采样数可能导致内存问题
   * 作者: ABHILASH | 联系: abhilashpatel121@gmail.com
   * 文档: https://www.instructables.com/member/abhilash_patel/instructables/
   * 2021/3/2: 将 N 类型从 float 改为 int 以支持 >=256 采样
   */
  unsigned int data[13] = {1,2,4,8,16,32,64,128,256,512,1024,2048};
  int a, c1, f, o, x;
  a = N;
  for (int i = 0; i < 12; i++) {
    if (data[i] <= a) { o = i; }
  }
  int in_ps[data[o]] = {};
  float out_r[data[o]] = {};
  float out_im[data[o]] = {};

  x = 0;
  for (int b = 0; b < o; b++) {
    c1 = data[b];
    f = data[o] / (c1 + c1);
    for (int j = 0; j < c1; j++) {
      x = x + 1;
      in_ps[x] = in_ps[j] + f;
    }
  }

  for (int i = 0; i < data[o]; i++) {
    if (in_ps[i] < a) out_r[i] = in[in_ps[i]];
    if (in_ps[i] > a) out_r[i] = in[in_ps[i] - a];
  }

  int i10, i11, n1;
  float e, c, s, tr, ti;
  for (int i = 0; i < o; i++) {
    i10 = data[i];
    i11 = data[o] / data[i + 1];
    e = 360 / data[i + 1];
    e = 0 - e;
    n1 = 0;
    for (int j = 0; j < i10; j++) {
      c = cosine(e * j);
      s = sine(e * j);
      n1 = j;
      for (int k = 0; k < i11; k++) {
        tr = c * out_r[i10 + n1] - s * out_im[i10 + n1];
        ti = s * out_r[i10 + n1] + c * out_im[i10 + n1];
        out_r[n1 + i10] = out_r[n1] - tr;
        out_r[n1] = out_r[n1] + tr;
        out_im[n1 + i10] = out_im[n1] - ti;
        out_im[n1] = out_im[n1] + ti;
        n1 = n1 + i10 + i10;
      }
    }
  }

  // 计算振幅
  for (int i = 0; i < data[o - 1]; i++) {
    out_r[i] = sqrt(out_r[i] * out_r[i] + out_im[i] * out_im[i]);
    out_im[i] = i * Frequency / N;
  }

  // 峰值检测
  x = 0;
  for (int i = 1; i < data[o - 1] - 1; i++) {
    if (out_r[i] > out_r[i - 1] && out_r[i] > out_r[i + 1]) {
      in_ps[x] = i;
      x = x + 1;
    }
  }

  // 按幅度重排
  s = 0; c = 0;
  for (int i = 0; i < x; i++) {
    for (int j = c; j < x; j++) {
      if (out_r[in_ps[i]] < out_r[in_ps[j]]) {
        s = in_ps[i];
        in_ps[i] = in_ps[j];
        in_ps[j] = s;
      }
    }
    c = c + 1;
  }

  // 更新全局 f_peaks 数组
  for (int i = 0; i < 5; i++) {
    f_peaks[i] = out_im[in_ps[i]];
  }
}

float sine(int i) {
  int j = i;
  float out;
  while (j < 0) j = j + 360;
  while (j > 360) j = j - 360;
  if (j > -1 && j < 91) out = sine_data[j];
  else if (j > 90 && j < 181) out = sine_data[180 - j];
  else if (j > 180 && j < 271) out = -sine_data[j - 180];
  else if (j > 270 && j < 361) out = -sine_data[360 - j];
  return (out / 255);
}

float cosine(int i) {
  int j = i;
  float out;
  while (j < 0) j = j + 360;
  while (j > 360) j = j - 360;
  if (j > -1 && j < 91) out = sine_data[90 - j];
  else if (j > 90 && j < 181) out = -sine_data[j - 90];
  else if (j > 180 && j < 271) out = -sine_data[270 - j];
  else if (j > 270 && j < 361) out = sine_data[j - 270];
  return (out / 255);
}
//------------------------------------------------------------------------------------//

原理图

EasyFFT:面向 Arduino 项目的可靠 FFT 库

制造工艺

  1. 仅用 Arduino 构建 DTMF 解码器 – 无需额外 IC
  2. SERENA:基于 Arduino Mega 2560 与 TFT 触摸屏的多级密码报警系统
  3. Python 3 控制 Arduino UNO - 串口通信点亮内置 LED 入门教程
  4. 校园智能体温监测系统 - 精准、实时、安全的学生健康守护方案
  5. Arduino 8 位并行 IO 端口库:一行代码实现 8 位数字读写
  6. Arduino 64 键原型键盘矩阵:用移位寄存器打造全自定义键盘
  7. Arduino Nano 专用 2.8 英寸 TFT 扩展板 – 320×240 SPI 显示屏
  8. 为Arduino构建隔离模拟输入电路
  9. 掌握直流电机控制:PID 与 PWM 专家技巧全解析
  10. 适用于 Arduino 项目的智能室内导航机器人套件
  11. 液压泵HS编码分类与应用指南
  12. 傅里叶变换详解:原理、应用与信号分解