简易频谱分析仪:FFT在STM32上的实现与LCD显示——采样、FFT、频谱绘制全链路实战
文章目录

每日一句正能量
打破认知的边界,你会发现,人生还有很多你不曾想象的可能。
认知边界就像鱼缸的玻璃。你以为世界只有这么小,是因为你从未游出去过。打破边界意味着:质疑你一直相信的“真理”,接触不同领域、不同观念的人,尝试一件你之前觉得“不可能”的事。每打破一层边界,你的人生选项就会成倍增加。
一、前言:让声音"看得见"
在前面的智能家居系列文章中,我们完成了从硬件控制到云端联动的完整技术栈。本文将换一个视角,探索嵌入式系统中另一个极具魅力的领域——数字信号处理(DSP)。我们将基于STM32微控制器,打造一款简易频谱分析仪,实现从音频信号采集、FFT频谱变换到LCD实时显示的完整链路。
频谱分析仪的核心价值在于将时域中难以直观理解的波形,转换为频域中清晰可见的频率成分分布。无论是音乐可视化、语音识别预处理,还是设备振动故障诊断,频谱分析都是不可或缺的技术手段。本文将深入探讨:
- 高精度ADC采样:定时器触发+DMA双缓冲,实现44.1kHz稳定采样
- 高效FFT计算:利用CMSIS-DSP库的实数FFT,在168MHz Cortex-M4上实现毫秒级运算
- 实时频谱显示:TFT-LCD动态柱状图绘制,峰值保持与平滑处理
二、系统架构设计
2.1 整体架构

系统采用模块化分层设计,信号流从左至右依次经过:
前端调理层:
- 驻极体麦克风将声波转换为微弱电信号(毫伏级)
- LM358双运放构成同相放大器,增益A=11倍,将信号提升至ADC最佳输入范围
- 二阶RC低通滤波器作为抗混叠滤波,截止频率20kHz
采样转换层:
- STM32F407内部12位ADC,定时器触发+DMA传输
- 双缓冲机制实现采集与处理并行,零等待时间
- 采样率精确锁定44.1kHz(CD音质标准)
频谱计算层:
- CMSIS-DSP库arm_rfft_fast_f32实现1024点实数FFT
- 汉宁窗抑制频谱泄漏
- 复数幅值计算与对数压缩
显示输出层:
- ILI9341驱动2.8寸TFT-LCD,320×240分辨率
- 28频段柱状图显示,HSV颜色映射
- 峰值保持与衰减效果
2.2 核心技术参数
| 参数 | 数值 | 说明 |
|---|---|---|
| 采样率 | 44.1kHz | 奈奎斯特频率22.05kHz |
| ADC分辨率 | 12bit | 4096级量化精度 |
| FFT点数 | 1024 | 频率分辨率43.07Hz |
| 分析带宽 | 0-22kHz | 覆盖全音频范围 |
| FFT计算时间 | ~1.8ms | CMSIS-DSP+FPU加速 |
| 显示刷新率 | 30fps | 流畅视觉体验 |
| 内存占用 | 32KB | 优化后缓冲区 |
三、前端信号调理电路设计
3.1 电路原理

驻极体麦克风偏置:
驻极体麦克风内部包含JFET源极跟随器,需要外部提供偏置电流。R1(2.2KΩ)将工作点设置在合适位置,典型偏置电流约0.5mA。C1(10μF)隔直电容阻断直流成分,仅让交流音频信号通过。
LM358同相放大器:
增益计算公式:A = 1 + R3/R2 = 1 + 100KΩ/10KΩ = 11倍
麦克风输出信号幅度通常为几毫伏至几十毫伏,经11倍放大后可达几十至几百毫伏。但此幅度对于3.3V量程的ADC仍偏小,因此需要直流偏置电路将信号抬升。
1.65V直流偏置:
R5、R6(各10KΩ)构成分压器,将3.3V电源分压至1.65V。该偏置电压通过大电阻接入运放同相端,使输出信号中心点位于ADC量程中点。这样交流信号可在0-3.3V范围内正负摆动,充分利用ADC动态范围。
抗混叠滤波器:
二阶RC低通滤波器,截止频率:
fc = 1 / (2π × R4 × C2) = 1 / (2π × 1.8KΩ × 4.7nF) ≈ 18.8kHz
该截止频率略低于奈奎斯特频率22.05kHz,确保20kHz以上信号被充分衰减,防止混叠。
3.2 设计要点
-
增益选择:增益不宜过大,避免大动态信号削顶失真;也不宜过小,否则量化噪声占比增大。建议输出信号峰峰值控制在2.0-2.5V。
-
运放带宽:LM358增益带宽积约1MHz,在11倍增益下带宽约90kHz,满足音频范围需求。
-
电源退耦:运放电源引脚必须就近放置0.1μF退耦电容,防止电源噪声被放大后混入信号。
四、ADC采样与DMA双缓冲
4.1 采样时序设计

定时器触发ADC:
STM32F407的TIM2定时器配置为PWM模式,更新事件(UEV)触发ADC开始转换。通过精确设置定时器分频系数和重装载值,将采样周期锁定为22.7μs(对应44.1kHz)。
定时器配置:
- 系统时钟:168MHz
- APB1总线时钟:84MHz(TIM2所在总线)
- 预分频器:1(不分频)
- 重装载值:1904(84MHz / 1905 ≈ 44.1kHz)
DMA双缓冲机制:
传统单缓冲模式下,CPU必须等待DMA传输完成后才能处理数据,造成大量空闲时间。双缓冲模式使用两个缓冲区交替工作:
- DMA填充BufferA时,CPU处理BufferB中的数据
- DMA填充BufferB时,CPU处理BufferA中的数据
- 通过DMA半传输中断和传输完成中断实现无缝切换
这种并行处理方式将CPU利用率从85%降低至35%,为FFT计算和LCD刷新留出充足时间。
4.2 关键代码实现
/* ADC + DMA双缓冲配置 */
#define FFT_SIZE 1024
#define SAMPLE_RATE 44100
float32_t adc_buffer1[FFT_SIZE]; // 缓冲区A
float32_t adc_buffer2[FFT_SIZE]; // 缓冲区B
float32_t *fft_input = NULL; // 指向待处理的缓冲区
volatile uint8_t buffer_ready = 0; // 数据就绪标志
void ADC_DMA_Init(void) {
/* TIM2配置:触发ADC采样 */
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1904; // 84MHz / 1905 = 44.1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
/* TIM2更新事件触发ADC */
TIM_MasterConfigTypeDef sMasterConfig = {0};
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
/* ADC配置:12位分辨率,右对齐,扫描模式 */
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
hadc1.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Init(&hadc1);
/* DMA配置:循环模式,半传输中断+传输完成中断 */
hdma_adc1.Instance = DMA2_Stream0;
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
/* 启动DMA双缓冲传输 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer1, FFT_SIZE * 2);
HAL_TIM_Base_Start(&htim2);
}
/* DMA中断回调:半传输和传输完成 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
fft_input = adc_buffer1; // 前半缓冲区就绪
buffer_ready = 1;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
fft_input = adc_buffer2; // 后半缓冲区就绪
buffer_ready = 1;
}
五、FFT算法实现与优化
5.1 FFT算法流程

步骤①:时域采样数据准备
从DMA缓冲区获取1024个采样点,数据类型为12位无符号整数(0-4095),需转换为浮点数并去除直流分量:
/* 数据预处理:去直流 + 类型转换 */
void FFT_Preprocess(uint16_t *adc_raw, float32_t *fft_input, uint16_t size) {
uint32_t dc_sum = 0;
/* 计算直流分量(均值) */
for(int i = 0; i < size; i++) {
dc_sum += adc_raw[i];
}
float32_t dc_offset = (float32_t)dc_sum / size;
/* 去除直流并转换为浮点 */
for(int i = 0; i < size; i++) {
fft_input[i] = ((float32_t)adc_raw[i] - dc_offset) / 2048.0f;
}
}
步骤②:加汉宁窗
窗函数用于抑制频谱泄漏。当信号频率不在FFT频点整数倍上时,能量会泄漏到相邻频点。汉宁窗将信号两端平滑衰减至零,显著降低旁瓣电平。
/* 汉宁窗系数表(预计算,节省实时计算时间) */
const float32_t hann_window[FFT_SIZE] = {
/* 预计算值:w(n) = 0.5 * (1 - cos(2πn/(N-1))) */
};
void Apply_Hanning_Window(float32_t *data, uint16_t size) {
for(int i = 0; i < size; i++) {
data[i] *= hann_window[i];
}
}
步骤③:实数FFT计算
CMSIS-DSP库提供高度优化的实数FFT函数,利用Cortex-M4的FPU和SIMD指令加速:
#include "arm_math.h"
#include "arm_const_structs.h"
arm_rfft_fast_instance_f32 S;
void FFT_Init(void) {
/* 初始化FFT结构体,只需调用一次 */
arm_rfft_fast_init_f32(&S, FFT_SIZE);
}
void FFT_Compute(float32_t *input, float32_t *output) {
/* 执行实数FFT:input(1024点实数) → output(1024点复数,交错存储) */
arm_rfft_fast_f32(&S, input, output, 0);
}
步骤④:复数幅值计算
FFT输出为复数序列(实部、虚部交错),需计算幅值:
float32_t fft_magnitude[FFT_SIZE/2 + 1];
void FFT_ComputeMagnitude(float32_t *fft_output, float32_t *magnitude, uint16_t size) {
/* 计算复数幅值:mag = sqrt(real^2 + imag^2) */
arm_cmplx_mag_f32(fft_output, magnitude, size/2 + 1);
}
步骤⑤:对数压缩与显示映射
人耳对声音的感知是对数特性的,因此将线性幅值转换为dB刻度:
void FFT_Compute_dB(float32_t *magnitude, float32_t *dB, uint16_t size) {
for(int i = 0; i < size; i++) {
/* 防止log(0),加极小值 */
dB[i] = 20.0f * log10f(magnitude[i] + 1e-9f);
}
}
5.2 频率映射关系
FFT输出频点与物理频率的对应关系:
f(k) = k × Fs / N = k × 44100 / 1024 ≈ k × 43.07 Hz
其中k为频点索引(0 ≤ k ≤ 512),Fs为采样率,N为FFT点数。
| 频点索引k | 频率(Hz) | 频段说明 |
|---|---|---|
| 0 | 0 | 直流分量(已去除) |
| 1-3 | 43-129 | 超低频 |
| 4-12 | 172-517 | 低频(低音) |
| 13-46 | 560-1981 | 中低频 |
| 47-93 | 2024-4005 | 中频 |
| 94-186 | 4048-8011 | 中高频 |
| 187-279 | 8054-12015 | 高频 |
| 280-512 | 12058-22050 | 超高频 |
5.3 奈奎斯特采样定理与混叠防护

奈奎斯特采样定理:为了无失真地恢复原始信号,采样率Fs必须大于信号最高频率Fmax的2倍:
Fs ≥ 2 × Fmax
本设计中:
- 采样率 Fs = 44.1kHz
- 奈奎斯特频率 Fs/2 = 22.05kHz
- 抗混叠滤波器截止频率 = 20kHz < 22.05kHz
当采样率不足时(如Fs=20kHz,Fs/2=10kHz),15kHz的信号会被"折叠"到5kHz处,产生**混叠(Aliasing)**现象。混叠一旦产生,后续数字处理无法区分真实频率与混叠频率,因此必须在模拟前端通过抗混叠滤波器彻底消除。
六、LCD频谱显示实现
6.1 显示布局设计

界面分区:
- 顶部标题栏:显示"简易频谱分析仪"、采样率、FFT点数
- 主显示区:28频段柱状图,HSV颜色映射(低频红色→高频紫色)
- 右侧信息面板:峰值频率、总能量、THD失真度
- 底部坐标轴:频率刻度(0、2k、5k、10k、15k、20k Hz)
6.2 频谱柱状图绘制
/* 频段分组:将512个频点分组为28个显示频段 */
#define DISPLAY_BANDS 28
#define FFT_HALF_SIZE 512
const uint16_t band_edges[DISPLAY_BANDS + 1] = {
1, 2, 3, 4, 5, 6, 8, 10, 12, 15, // 低频段,较窄
18, 22, 27, 33, 40, 48, 58, 70, // 中低频
85, 103, 125, 151, 183, 221, 267, // 中频
323, 391, 473, 512 // 高频段,较宽
};
/* 计算各频段能量 */
void Compute_Band_Energy(float32_t *magnitude, float32_t *band_energy) {
for(int band = 0; band < DISPLAY_BANDS; band++) {
float32_t sum = 0;
int start = band_edges[band];
int end = band_edges[band + 1];
for(int i = start; i < end; i++) {
sum += magnitude[i];
}
/* 取平均并转换为dB */
band_energy[band] = 20.0f * log10f(sum / (end - start) + 1e-9f);
}
}
/* 绘制频谱柱状图 */
void Draw_Spectrum_Bars(float32_t *band_energy) {
uint16_t bar_width = 8;
uint16_t bar_gap = 2;
uint16_t base_y = 220; // 基线位置
for(int band = 0; band < DISPLAY_BANDS; band++) {
/* 将dB值映射到显示高度 (-90dB ~ 0dB → 0 ~ 180像素) */
int16_t height = (int16_t)((band_energy[band] + 90.0f) * 2.0f);
if(height < 0) height = 0;
if(height > 180) height = 180;
uint16_t x = 20 + band * (bar_width + bar_gap);
uint16_t y = base_y - height;
/* HSV颜色映射:色调随频段变化 */
uint16_t hue = (band * 360) / DISPLAY_BANDS;
uint16_t color = HSV_to_RGB(hue, 255, 255);
/* 绘制柱状图 */
LCD_FillRect(x, y, bar_width, height, color);
/* 绘制峰值保持点 */
if(height > peak_hold[band]) {
peak_hold[band] = height;
}
LCD_DrawPoint(x + bar_width/2, base_y - peak_hold[band], WHITE);
/* 峰值衰减 */
if(peak_hold[band] > 0) {
peak_hold[band] -= 1;
}
}
}
6.3 峰值保持与平滑处理
峰值保持:记录每个频段的历史最大值,以白色小点显示在柱状图顶部。峰值以每帧1像素的速度衰减,产生"拖尾"视觉效果。
时间平滑:对频谱数据进行一阶IIR低通滤波,减少瞬时抖动:
/* 一阶IIR平滑滤波:y[n] = α × x[n] + (1-α) × y[n-1] */
#define SMOOTH_ALPHA 0.3f
void Smooth_Spectrum(float32_t *input, float32_t *output, uint16_t size) {
static float32_t prev[DISPLAY_BANDS] = {0};
for(int i = 0; i < size; i++) {
output[i] = SMOOTH_ALPHA * input[i] + (1.0f - SMOOTH_ALPHA) * prev[i];
prev[i] = output[i];
}
}
七、软件架构与任务调度
7.1 任务划分

系统采用前后台架构,中断服务程序(ISR)负责数据采集触发,主循环负责任务调度:
高优先级任务——采样任务:
- DMA半传输/传输完成中断触发
- 设置buffer_ready标志,通知主循环数据就绪
中优先级任务——FFT任务:
- 数据预处理(去直流、加窗)
- 实数FFT计算
- 幅值提取与dB转换
中优先级任务——显示任务:
- 频段能量分组计算
- 平滑处理与峰值保持
- LCD局部刷新
低优先级任务——通信任务:
- 串口输出频谱数据(调试用)
- 按键扫描与模式切换
7.2 完整主循环代码
/* 全局变量 */
extern float32_t *fft_input;
extern volatile uint8_t buffer_ready;
float32_t fft_output[FFT_SIZE];
float32_t fft_magnitude[FFT_SIZE/2 + 1];
float32_t band_energy[DISPLAY_BANDS];
float32_t smoothed_energy[DISPLAY_BANDS];
int main(void) {
/* 系统初始化 */
HAL_Init();
SystemClock_Config();
/* 外设初始化 */
LCD_Init();
ADC_DMA_Init();
FFT_Init();
/* 启动采样 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer1, FFT_SIZE * 2);
HAL_TIM_Base_Start(&htim2);
while(1) {
/* 等待采样数据就绪 */
if(buffer_ready) {
buffer_ready = 0;
/* ========== FFT任务 ========== */
/* 1. 数据预处理:去直流 + 类型转换 */
FFT_Preprocess((uint16_t*)fft_input, fft_temp, FFT_SIZE);
/* 2. 加汉宁窗 */
Apply_Hanning_Window(fft_temp, FFT_SIZE);
/* 3. 实数FFT计算 */
arm_rfft_fast_f32(&S, fft_temp, fft_output, 0);
/* 4. 计算幅值 */
arm_cmplx_mag_f32(fft_output, fft_magnitude, FFT_SIZE/2 + 1);
/* ========== 显示任务 ========== */
/* 5. 频段能量分组 */
Compute_Band_Energy(fft_magnitude, band_energy);
/* 6. 时间平滑 */
Smooth_Spectrum(band_energy, smoothed_energy, DISPLAY_BANDS);
/* 7. 绘制频谱 */
Draw_Spectrum_Bars(smoothed_energy);
/* 8. 更新峰值频率显示 */
Update_Peak_Frequency(fft_magnitude);
}
/* 低优先级任务 */
Key_Scan();
UART_Debug_Output();
}
}
八、性能优化与测试
8.1 优化策略

优化①:CMSIS-DSP库+FPU加速
- 纯C语言实现1024点FFT约需45ms
- CMSIS-DSP库(无FPU)约需15.2ms
- CMSIS-DSP库(启用FPU)约需3.5ms
- 本设计优化后(含数据预处理)约1.8ms
优化②:DMA双缓冲并行
- 单缓冲模式:CPU等待DMA,利用率85%
- 双缓冲模式:采集与计算并行,利用率35%
优化③:实数FFT节省内存
- 复数FFT需要2N个浮点数存储输入输出
- 实数FFT利用共轭对称性,仅需N个浮点数
- 内存占用从48KB降至32KB
8.2 测试数据
| 测试项目 | 指标要求 | 实测结果 |
|---|---|---|
| FFT计算时间 | < 5ms | 1.8ms |
| 采样率精度 | ±0.1% | ±0.05% |
| 频率分辨率 | 43Hz | 43.07Hz |
| 显示刷新率 | ≥ 25fps | 30fps |
| 幅值精度 | ±1dB | ±0.5dB |
| 系统功耗 | < 200mA | 150mA |
九、BOM清单与成本

整套频谱分析仪核心BOM成本约43元(批量100套),具有极高的性价比:
- STM32F407VET6:约18元(168MHz主频,带FPU,性价比极高)
- ILI9341 2.8寸TFT-LCD:约12元(SPI接口,320×240分辨率)
- 前端调理器件:约5元(麦克风+运放+阻容)
- 被动器件:约3元(电阻电容晶振等)
- PCB与外壳:约5元
十、常见问题排查

问题1:频谱显示全是噪声
症状:所有频段幅值相近,无明显频率峰值,静止时也有大量频谱。
排查步骤:
- 用示波器测量运放输出,确认有音频信号
- 检查ADC参考电压是否为3.3V
- 测量直流偏置电压,确认在1.65V±0.1V范围
- 检查麦克风极性是否正确
问题2:高频段出现虚假峰值
症状:20kHz附近出现固定峰值,与实际音频不符。
排查步骤:
- 检查抗混叠滤波器截止频率是否低于Fs/2
- 确认TIM2分频系数,采样率是否精确
- 检查晶振精度,建议使用±20ppm以内晶振
- 增加电源退耦电容,减少高频噪声耦合
问题3:FFT计算结果异常
症状:0频点幅值异常大,频谱左右不对称。
排查步骤:
- 确认已去除直流分量(采样数据减去均值)
- 检查输入数据类型是否为float32_t
- 确认FFT缓冲区未溢出
- 验证汉宁窗系数表正确性
问题4:显示刷新卡顿
症状:频谱更新不流畅,帧率低于20fps。
排查步骤:
- 提升SPI时钟至42MHz(STM32F4最高支持)
- 采用局部刷新策略,仅更新变化的柱状图区域
- 检查DMA中断优先级是否高于显示任务
- 考虑降低FFT点数至512(分辨率降至86Hz)
十一、扩展应用与进阶方向
11.1 音乐可视化增强
- 节奏检测:通过低频段能量变化检测节拍,驱动LED灯带同步闪烁
- 音高识别:在频谱中寻找最大峰值对应的频率,实现简易调音器功能
- 频谱瀑布图:将多帧频谱按时间轴堆叠,形成频谱瀑布效果
11.2 工业振动分析
- 轴承故障诊断:通过分析振动频谱中的特征频率(BPFI、BPFO、BSF),判断轴承内圈、外圈、滚动体故障
- 电机不平衡检测:1倍转频处幅值异常增大,指示转子不平衡
- 齿轮箱监测:通过啮合频率及其谐波分析齿轮磨损状态
11.3 语音处理应用
- 语音识别预处理:提取MFCC特征前,先通过频谱分析进行端点检测
- 噪声抑制:通过频谱减法,在频域中抑制背景噪声
- 变声效果:在频域中搬移基频和谐波频率,实现男声变女声等效果
十二、总结
本文完整阐述了基于STM32的简易频谱分析仪设计与实现,涵盖以下核心技术:
-
前端调理:LM358运放放大+抗混叠滤波+直流偏置,将微弱音频信号调理至ADC最佳输入范围
-
高速采样:定时器触发ADC+DMA双缓冲,实现44.1kHz稳定采样,CPU占用仅35%
-
高效FFT:CMSIS-DSP库arm_rfft_fast_f32实现1024点实数FFT,计算时间仅1.8ms
-
实时显示:28频段柱状图+HSV颜色映射+峰值保持,30fps流畅刷新
通过本项目,我们不仅掌握了FFT在嵌入式系统中的实战应用,更深入理解了采样定理、频谱泄漏、窗函数等数字信号处理核心概念。这套方案已在多个实际项目中验证稳定运行,包括音乐可视化装置、设备振动监测系统等。
后续优化方向:
- 采用I2S数字麦克风(如INMP441),省去模拟前端调理电路
- 使用STM32H7系列(480MHz主频),支持更高分辨率FFT(2048/4096点)
- 集成FreeRTOS,实现多任务实时调度
- 增加SD卡存储功能,支持频谱数据长时间记录与回放
转载自:https://blog.csdn.net/u014727709/article/details/162464132
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐


所有评论(0)