在单片机上玩转数字信号处理:FFT 实战

Evek Golden Lv4

FFT (快速傅里叶变换) 是许多嵌入式工程师心中的痛。复杂的蝶形运算、倒位序算法,看着就头大。
但好消息是,现在的 MCU(特别是 Cortex-M4/M7)自带了 DSP 扩展指令,配合官方的 CMSIS-DSP 库,跑 FFT 跟呼吸一样简单。

1. 为什么需要 FFT?

时域信号(示波器看到的波形)往往看不出什么规律,特别是叠加了噪声后。
频域信号(频谱图)能一眼看穿信号的本质。

  • 应用场景:音频可视化(音乐律动灯)、电机振动分析(故障诊断)、心率提取。

2. 采样定理与分辨率

在开始写代码前,必须搞懂两个数学公式:

  1. 奈奎斯特频率:$ F_{max} = F_s / 2 $。如果你采样率是 10kHz,你最多能测到 5kHz 的信号。
  2. 频率分辨率:$ \Delta F = F_s / N $。N 是 FFT 的点数(如 1024)。分辨率越小,频谱越精细。
    • 例如:Fs=10kHz, N=1024。分辨率 ≈ 10Hz。

3. CMSIS-DSP 实战

不要自己手写 FFT 算法(除非为了学习)。CMSIS-DSP 库针对 ARM 汇编指令做了极致优化。

3.1 初始化

我们在 M4 上通常使用定点 FFT (q15q31 格式) 以获得最高性能。FPU 虽然能算浮点,但定点通常更快且更省内存。

1
2
3
4
5
6
7
#include "arm_math.h"

#define FFT_LENGTH 1024
q15_t fft_input[FFT_LENGTH * 2]; // 实部+虚部
q15_t fft_output[FFT_LENGTH]; // 幅值

arm_rfft_instance_q15 S; // 结构体句柄

3.2 运算流程

  1. ADC 采样:通过 DMA 将数据填入 buffer。
  2. **RFFT (实数 FFT)**:因为我们的输入信号是实数(电压值),使用 arm_rfft_q15 比通用的 CFFT 快一倍。
  3. **计算模值 (Magnitude)**:$ Mag = \sqrt{Real^2 + Imag^2} $。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void DSP_Process(void) {
// 1. 初始化
arm_rfft_init_q15(&S, FFT_LENGTH, 0, 1);

// 2. 执行 FFT
// 注意:输入数据会被修改,需要 scaling 以防溢出
arm_rfft_q15(&S, adc_buffer, fft_input);

// 3. 计算幅值 (复数取模)
arm_cmplx_mag_q15(fft_input, fft_output, FFT_LENGTH);

// 4. 寻找最大峰值频率
q15_t max_value;
uint32_t max_index;
arm_max_q15(fft_output, FFT_LENGTH/2, &max_value, &max_index);

printf("Main Freq = %d Hz\n", max_index * SAMPLING_RATE / FFT_LENGTH);
}

4. 性能与陷阱

  • 溢出风险:定点运算最怕溢出。q15 的范围只有 -32768 到 32767。如果 FFT 过程中数据不断累加,很容易溢出。CMSIS-DSP 内部会自动进行位移缩放(downscaling)来防止溢出,但这会导致小信号精度丢失。
  • 窗函数:直接做 FFT 会有频谱泄露。建议在 FFT 前对输入数据加窗(如汉宁窗 Hanning Window)。

总结

不要被数学公式吓倒。对于嵌入式工程师,FFT 就是一个函数调用。把数据喂进去,它告诉你哪个频率最强,就这么简单。

  • Title: 在单片机上玩转数字信号处理:FFT 实战
  • Author: Evek Golden
  • Created at : 2024-08-05 16:00:00
  • Updated at : 2026-06-12 08:57:02
  • Link: https://blog.cocodemo.uno/posts/fft7k3m/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments