EasyFFT:面向 Arduino 项目的可靠 FFT 库
组件与物料
| Arduino Nano R3 | ×1 |
应用与在线服务
- Arduino IDE
项目简介
在算力有限的 Arduino 上从采集信号中测量频率颇具挑战。零交叉法通过统计信号在给定时间内穿过零点的次数来估算频率,但对多频率混合信号无效。对于非专业背景的开发者,编写此类代码更是困难。本项目旨在提供一个易于在 Arduino 上实现、无需深究数学原理的 FFT 代码,适用于音乐、信号分析等项目。
本文不讲解 FFT 原理,仅说明 FFT 函数的应用。视频中也有同步讲解。若只关心代码应用,可直接跳至步骤 3。
若需 3 倍速度且可接受约 5% 精度损失,请参考我的另一篇文章 ApproxFFT:Arduino 最快 FFT 函数。
步骤 1:快速傅里叶变换 (FFT)
James Cooley 和 John Tukey 开发的 FFT 算法将 DFT 计算加速,被公认为 20 世纪最重要算法之一。它将信号分解为奇偶序列,将复数乘法次数从 DFT 的 N² 降至 NlogN,样本数越大优势越明显。
数学推导参考资料:
步骤 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]] = {};
处理流程:
- 生成给定采样数的位反转序列
- 按位反转序重排输入数据
- 执行 FFT 运算
- 计算复数模值 (振幅)
- 检测峰值并按幅度降序排列
- 结果存入全局
f_peaks[](前 5 个主频率)
若需访问除峰值频率外的其他数据,需修改代码将局部变量复制到预定义全局变量。
步骤 4:代码测试
输入采样频率 10Hz 的三角波 (信号频率 1.25Hz)。输出与 Scilab 计算结果一致,因使用低精度正弦表存在微小偏差。输出频率桶为 1.25Hz 和 3.75Hz,实际值可能落在桶范围内。
Arduino Nano 执行耗时:
- 16 点:4ms
- 32 点:10ms
- 64 点:26ms
- 128 点:53ms
步骤 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);
}
//------------------------------------------------------------------------------------//
原理图

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