文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 本文目标与读者收获](#1.2 本文目标与读者收获)
- [1.3 技术栈](#1.3 技术栈)
- [1.4 项目代码文件清单](#1.4 项目代码文件清单)
- 二、系统架构与原理
-
- [2.1 系统整体架构](#2.1 系统整体架构)
- [2.2 MEMS 麦克风工作原理](#2.2 MEMS 麦克风工作原理)
- [2.3 FFT 频谱分析原理](#2.3 FFT 频谱分析原理)
- 三、硬件设计与接线
-
- [3.1 硬件清单](#3.1 硬件清单)
- [3.2 接线方案](#3.2 接线方案)
- 四、软件开发环境搭建
-
- [4.1 STM32CubeIDE 项目创建](#4.1 STM32CubeIDE 项目创建)
- [4.2 添加 CMSIS-DSP 库](#4.2 添加 CMSIS-DSP 库)
- 五、核心代码实现
-
- [5.1 ADC + DMA 采样模块](#5.1 ADC + DMA 采样模块)
- [5.2 FFT 频谱分析模块](#5.2 FFT 频谱分析模块)
- [5.3 声压级计算与噪声等级判定](#5.3 声压级计算与噪声等级判定)
- [5.4 OLED 频谱显示模块](#5.4 OLED 频谱显示模块)
- [5.5 主程序](#5.5 主程序)
- 六、测试验证
-
- [6.1 编译与烧录](#6.1 编译与烧录)
- [6.2 串口调试验证](#6.2 串口调试验证)
- [6.3 OLED 显示验证](#6.3 OLED 显示验证)
- [6.4 校准说明](#6.4 校准说明)
- 七、故障排查与问题解决
-
- [7.1 环境配置问题](#7.1 环境配置问题)
-
- [问题 1:编译报错 "arm_math.h: No such file or directory"](#问题 1:编译报错 "arm_math.h: No such file or directory")
- [问题 2:链接报错 "undefined reference to arm_rfft_fast_init_f32"](#问题 2:链接报错 "undefined reference to arm_rfft_fast_init_f32")
- [7.2 硬件问题](#7.2 硬件问题)
-
- [问题 3:ADC 采样值始终为 0 或 4095](#问题 3:ADC 采样值始终为 0 或 4095)
- [问题 4:FFT 结果全为零或出现异常尖峰](#问题 4:FFT 结果全为零或出现异常尖峰)
- [7.3 性能问题](#7.3 性能问题)
-
- [问题 5:OLED 刷新率低,频谱显示卡顿](#问题 5:OLED 刷新率低,频谱显示卡顿)
- [八、I2S 数字 MEMS 麦克风方案补充](#八、I2S 数字 MEMS 麦克风方案补充)
- 九、总结与扩展方向
-
- [9.1 核心知识点回顾](#9.1 核心知识点回顾)
- [9.2 扩展方向](#9.2 扩展方向)
- [9.3 学习资源](#9.3 学习资源)
一、前言
1.1 技术背景
噪声污染已成为现代城市面临的重要环境问题之一。工厂车间、建筑工地、交通干道等场景中,长期暴露在高分贝噪声下会对人体健康造成严重危害。传统的噪声检测设备价格昂贵且功能单一,而基于嵌入式系统的噪声检测方案具有成本低、体积小、可定制化的优势。
MEMS(微机电系统)麦克风凭借其体积小、一致性好、抗干扰能力强的特点,已广泛应用于消费电子和工业检测领域。结合 STM32F4 系列微控制器强大的 DSP 运算能力和硬件浮点单元(FPU),我们可以在嵌入式平台上实现实时的音频采集与 FFT(快速傅里叶变换)频谱分析,从而构建一套完整的噪声检测系统。
1.2 本文目标与读者收获
完成本教程后,你将能够:
- 理解 MEMS 麦克风的工作原理及其与 STM32F4 的接口方式
- 掌握 STM32F4 的 ADC + DMA 高速音频采样配置
- 使用 ARM CMSIS-DSP 库实现 FFT 频谱分析
- 计算声压级(SPL)并实现噪声等级判定
- 在 OLED 屏幕上实时显示频谱图和分贝值
本教程适合有一定 STM32 基础、了解 C 语言编程的嵌入式开发者。
1.3 技术栈
技术栈:
- 主控芯片:STM32F407VET6(ARM Cortex-M4,168MHz,带 FPU)
- 麦克风模块:INMP441(I2S 数字 MEMS 麦克风)/ MAX9814 + 模拟 MEMS 麦克风
- 显示模块:0.96 寸 SSD1306 OLED(I2C 接口)
- 开发环境:STM32CubeIDE 1.13+ / Keil MDK 5.38+
- 固件库:STM32 HAL 库 + CMSIS-DSP 库
- 调试工具:ST-Link V2、串口调试助手、逻辑分析仪
1.4 项目代码文件清单
本教程将创建以下代码文件,建议按此结构组织项目:
noise_detector/
├── Core/
│ ├── Inc/
│ │ ├── main.h
│ │ ├── adc_config.h // ADC 采样配置头文件
│ │ ├── fft_process.h // FFT 处理头文件
│ │ ├── noise_level.h // 噪声等级判定头文件
│ │ └── oled_display.h // OLED 显示头文件
│ └── Src/
│ ├── main.c // 主程序入口
│ ├── adc_config.c // ADC + DMA 采样实现
│ ├── fft_process.c // FFT 频谱分析实现
│ ├── noise_level.c // 声压级计算与噪声判定
│ └── oled_display.c // OLED 频谱显示实现
├── Drivers/
│ ├── CMSIS/ // ARM CMSIS-DSP 库
│ └── STM32F4xx_HAL_Driver/ // STM32 HAL 库
└── noise_detector.ioc // STM32CubeMX 配置文件
二、系统架构与原理
2.1 系统整体架构
本噪声检测系统的数据流如下:MEMS 麦克风将声音信号转换为电信号,经 STM32F4 的 ADC 模块进行模数转换,采样数据通过 DMA 传输至内存缓冲区,CPU 对缓冲区数据执行 FFT 变换得到频谱信息,最后计算声压级并在 OLED 上实时显示。
模拟信号
放大后信号
DMA 传输
数据就绪
频谱数据
分贝值 + 频谱
超标告警
MEMS 麦克风
前置放大电路
STM32F4 ADC
采样缓冲区
FFT 频谱分析
声压级计算
OLED 显示
蜂鸣器/LED
2.2 MEMS 麦克风工作原理
MEMS 麦克风内部包含一个微型振膜和一个固定背板,二者构成一个电容器。当声波到达振膜时,振膜振动导致电容值变化,内置 ASIC 芯片将电容变化转换为电压信号输出。
本教程以模拟输出型 MEMS 麦克风(如 MAX9814 模块)为例。MAX9814 模块集成了自动增益控制(AGC)放大器,输出的模拟信号可直接接入 STM32 的 ADC 引脚,无需额外设计放大电路,非常适合快速原型开发。
💡 提示:如果你使用数字 MEMS 麦克风(如 INMP441),需要通过 I2S 接口采集数据,本文后续章节会简要说明 I2S 方案的差异。
2.3 FFT 频谱分析原理
FFT(快速傅里叶变换)是 DFT(离散傅里叶变换)的高效算法实现,能将时域信号转换为频域信号。对于噪声检测而言,FFT 可以帮助我们分析噪声的频率成分------例如判断噪声是低频轰鸣(如发动机)还是高频尖锐声(如电锯)。
核心公式为 DFT 的定义:
X[k] = Σ(n=0 to N-1) x[n] * e^(-j*2π*k*n/N)
其中 x[n] 是时域采样数据,X[k] 是频域结果,N 是采样点数。STM32F4 的 Cortex-M4 内核支持单周期 MAC(乘累加)指令,配合 CMSIS-DSP 库中高度优化的 FFT 函数,可以高效完成 1024 点甚至 4096 点的 FFT 运算。
关键参数关系:
- 采样率 Fs:本项目使用 44100 Hz(CD 音质),满足奈奎斯特定理对 20kHz 可听频率的采样要求
- FFT 点数 N:1024 点
- 频率分辨率 Δf:Fs / N = 44100 / 1024 ≈ 43.07 Hz
- 可分析最高频率:Fs / 2 = 22050 Hz
三、硬件设计与接线
3.1 硬件清单
| 序号 | 元器件 | 型号/规格 | 数量 | 说明 |
|---|---|---|---|---|
| 1 | 主控板 | STM32F407VET6 开发板 | 1 | 带 ST-Link 调试接口 |
| 2 | 麦克风模块 | MAX9814 MEMS 麦克风模块 | 1 | 模拟输出,自带 AGC |
| 3 | 显示屏 | 0.96 寸 SSD1306 OLED | 1 | I2C 接口,128×64 分辨率 |
| 4 | 蜂鸣器 | 有源蜂鸣器模块 | 1 | 噪声超标告警 |
| 5 | 调试器 | ST-Link V2 | 1 | 程序下载与调试 |
| 6 | 其他 | 杜邦线、面包板 | 若干 | 连接用 |
3.2 接线方案
SSD1306_OLED
MAX9814模块
STM32F407
PA0 - ADC1_CH0
PB6 - I2C1_SCL
PB7 - I2C1_SDA
PB8 - GPIO 蜂鸣器
VCC 3.3V
GND
OUT 模拟输出
GAIN 增益选择
VCC 3.3V
GND
SCL
SDA
接线说明:
| STM32F407 引脚 | 连接目标 | 说明 |
|---|---|---|
| PA0 | MAX9814 OUT | ADC1 通道 0,模拟音频输入 |
| PB6 | SSD1306 SCL | I2C1 时钟线 |
| PB7 | SSD1306 SDA | I2C1 数据线 |
| PB8 | 蜂鸣器 IN | GPIO 推挽输出,告警控制 |
| 3.3V | MAX9814 VCC / OLED VCC | 统一供电 |
| GND | MAX9814 GND / OLED GND | 共地 |
⚠️ 注意:MAX9814 的 GAIN 引脚悬空时默认增益为 60dB。如果环境噪声较大导致信号饱和,可将 GAIN 引脚接 VCC(40dB)或接 GND(50dB)来降低增益。
四、软件开发环境搭建
4.1 STM32CubeIDE 项目创建
打开 STM32CubeIDE,新建 STM32 项目,选择 STM32F407VETx 芯片。在 CubeMX 图形化配置界面中完成以下配置:
时钟配置:
- 外部高速时钟(HSE):8MHz 晶振
- PLL 配置:SYSCLK = 168MHz
- APB1 = 42MHz,APB2 = 84MHz
ADC1 配置:
- ADC1 通道 0(PA0)
- 分辨率:12 位
- 采样时间:15 个 ADC 时钟周期
- 连续转换模式:开启
- DMA 连续请求:开启
DMA 配置:
- DMA2 Stream0,通道 0
- 方向:外设到内存
- 模式:循环模式(Circular)
- 数据宽度:半字(Half Word,16 位)
TIM2 配置(用作 ADC 触发源):
- 时钟源:内部时钟(APB1 = 84MHz,经 2 倍频后 TIM2 时钟 = 84MHz)
- 预分频器(PSC):0
- 自动重装载值(ARR):84000000 / 44100 - 1 = 1904
- 触发输出(TRGO):Update Event
- 这样 ADC 采样率精确为 84MHz / 1905 ≈ 44094 Hz ≈ 44.1kHz
I2C1 配置:
- SCL:PB6,SDA:PB7
- 速率:400kHz(Fast Mode)
4.2 添加 CMSIS-DSP 库
CMSIS-DSP 是 ARM 官方提供的数字信号处理库,包含高度优化的 FFT、滤波器等函数。添加步骤如下:
-
在项目属性中,进入 C/C++ Build → Settings → MCU GCC Compiler → Include paths,添加 CMSIS-DSP 头文件路径:
Drivers/CMSIS/DSP/Include -
在 MCU GCC Compiler → Preprocessor 中添加宏定义:
ARM_MATH_CM4 __FPU_PRESENT=1 -
在 MCU GCC Linker → Libraries 中添加库文件:
- 库名:
arm_cortexM4lf_math - 库路径:
Drivers/CMSIS/Lib/GCC
- 库名:
-
确保项目开启了 FPU 支持:MCU Settings 中 Floating point unit 设为 FPv4-SP-D16 ,Floating point ABI 设为 Hard。
💡 提示:如果使用 Keil MDK,可以在 Options → Target 中勾选 "Use MicroLIB",并在 C/C++ → Define 中添加
ARM_MATH_CM4宏。DSP 库文件位于 Keil 安装目录下的ARM/PACK/ARM/CMSIS/x.x.x/CMSIS/Lib/ARM/中。
五、核心代码实现
5.1 ADC + DMA 采样模块
本模块负责以 44.1kHz 的采样率持续采集麦克风的模拟信号,并通过 DMA 自动搬运到内存缓冲区,实现零 CPU 开销的数据采集。
📄 创建文件:
Core/Inc/adc_config.h
c
/* adc_config.h - ADC 采样配置头文件 */
#ifndef __ADC_CONFIG_H
#define __ADC_CONFIG_H
#include "stm32f4xx_hal.h"
/* 采样参数定义 */
#define SAMPLE_RATE 44100 // 采样率 44.1kHz
#define FFT_SIZE 1024 // FFT 点数
#define ADC_BUF_SIZE (FFT_SIZE * 2) // 双缓冲,共 2048 个采样点
/* ADC 参考电压和分辨率 */
#define ADC_VREF 3.3f // ADC 参考电压 3.3V
#define ADC_RESOLUTION 4096.0f // 12 位 ADC,2^12 = 4096
/* 缓冲区状态标志 */
typedef enum {
BUF_STATE_IDLE = 0, // 空闲
BUF_STATE_HALF, // 前半部分就绪
BUF_STATE_FULL // 后半部分就绪(全部完成)
} ADC_BufState_t;
/* 外部变量声明 */
extern uint16_t adc_raw_buf[ADC_BUF_SIZE]; // ADC 原始数据缓冲区
extern volatile ADC_BufState_t adc_buf_state;
/* 函数声明 */
void ADC_Sampling_Init(void);
void ADC_Sampling_Start(void);
void ADC_Sampling_Stop(void);
float ADC_Raw_To_Voltage(uint16_t raw_value);
#endif /* __ADC_CONFIG_H */
📄 创建文件:
Core/Src/adc_config.c
c
/* adc_config.c - ADC + DMA 采样实现 */
#include "adc_config.h"
/* 全局变量 */
uint16_t adc_raw_buf[ADC_BUF_SIZE]; // ADC 原始数据缓冲区(DMA 目标)
volatile ADC_BufState_t adc_buf_state = BUF_STATE_IDLE; // 缓冲区状态
/* HAL 库句柄(由 CubeMX 生成,在 main.c 中定义) */
extern ADC_HandleTypeDef hadc1;
extern TIM_HandleTypeDef htim2;
/**
* @brief ADC 采样初始化
* @note 配置 ADC1 + DMA2 + TIM2 触发,实现精确 44.1kHz 采样
*/
void ADC_Sampling_Init(void)
{
/* 清零缓冲区 */
memset(adc_raw_buf, 0, sizeof(adc_raw_buf));
adc_buf_state = BUF_STATE_IDLE;
}
/**
* @brief 启动 ADC 采样
* @note 启动 DMA 循环传输和定时器触发
*/
void ADC_Sampling_Start(void)
{
/* 启动 ADC DMA 传输(循环模式) */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_raw_buf, ADC_BUF_SIZE);
/* 启动 TIM2 作为 ADC 外部触发源 */
HAL_TIM_Base_Start(&htim2);
}
/**
* @brief 停止 ADC 采样
*/
void ADC_Sampling_Stop(void)
{
HAL_TIM_Base_Stop(&htim2);
HAL_ADC_Stop_DMA(&hadc1);
adc_buf_state = BUF_STATE_IDLE;
}
/**
* @brief ADC 原始值转换为电压值
* @param raw_value: ADC 原始采样值 (0~4095)
* @return 对应的电压值 (0~3.3V)
*/
float ADC_Raw_To_Voltage(uint16_t raw_value)
{
return ((float)raw_value / ADC_RESOLUTION) * ADC_VREF;
}
/**
* @brief DMA 半传输完成回调(前 1024 个点采集完毕)
* @note 此回调由 HAL 库自动调用,表示缓冲区前半部分数据就绪
*/
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
adc_buf_state = BUF_STATE_HALF;
}
}
/**
* @brief DMA 传输完成回调(后 1024 个点采集完毕)
* @note 此回调由 HAL 库自动调用,表示缓冲区后半部分数据就绪
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1) {
adc_buf_state = BUF_STATE_FULL;
}
}
双缓冲机制说明:
DMA 工作在循环模式下,缓冲区大小为 2048(2 × FFT_SIZE)。当 DMA 填满前 1024 个点时触发半传输中断,CPU 可以对前半部分数据做 FFT 处理;与此同时 DMA 继续采集后 1024 个点。当后半部分也填满时触发全传输中断,CPU 转而处理后半部分。这种"乒乓缓冲"机制确保了数据采集与处理的并行执行,不会丢失任何采样数据。
CPU FFT处理 采样缓冲区 2048 DMA 控制器 CPU FFT处理 采样缓冲区 2048 DMA 控制器 填充前半部分 [0~1023] 半传输中断:前半就绪 对 [0~1023] 执行 FFT 同时填充后半部分 [1024~2047] 全传输中断:后半就绪 对 [1024~2047] 执行 FFT 循环回到前半部分 [0~1023]
5.2 FFT 频谱分析模块
本模块是系统的核心,负责将时域采样数据转换为频域频谱数据。使用 CMSIS-DSP 库中的 arm_rfft_fast_f32 函数实现高效的实数 FFT 运算。
📄 创建文件:
Core/Inc/fft_process.h
c
/* fft_process.h - FFT 处理头文件 */
#ifndef __FFT_PROCESS_H
#define __FFT_PROCESS_H
#include "stm32f4xx_hal.h"
#include "arm_math.h"
#include "adc_config.h"
/* FFT 参数 */
#define FFT_OUTPUT_SIZE (FFT_SIZE / 2) // FFT 输出有效点数(单边频谱)
/* FFT 处理结果结构体 */
typedef struct {
float magnitude[FFT_OUTPUT_SIZE]; // 各频率点的幅值
float frequency[FFT_OUTPUT_SIZE]; // 各频率点对应的频率值 (Hz)
float peak_freq; // 峰值频率 (Hz)
float peak_magnitude; // 峰值幅度
uint32_t peak_index; // 峰值索引
} FFT_Result_t;
/* 函数声明 */
void FFT_Init(void);
void FFT_Process(uint16_t *adc_data, uint32_t offset);
FFT_Result_t* FFT_GetResult(void);
#endif /* __FFT_PROCESS_H */
📄 创建文件:
Core/Src/fft_process.c
c
/* fft_process.c - FFT 频谱分析实现 */
#include "fft_process.h"
#include <string.h>
#include <math.h>
/* CMSIS-DSP FFT 实例 */
static arm_rfft_fast_instance_f32 fft_instance;
/* FFT 工作缓冲区 */
static float fft_input[FFT_SIZE]; // FFT 输入(浮点格式)
static float fft_output[FFT_SIZE]; // FFT 输出(复数交替排列)
/* Hanning 窗函数系数表 */
static float hanning_window[FFT_SIZE];
/* FFT 处理结果 */
static FFT_Result_t fft_result;
/* 频率分辨率 */
static const float freq_resolution = (float)SAMPLE_RATE / (float)FFT_SIZE;
/**
* @brief 初始化 FFT 模块
* @note 初始化 CMSIS-DSP FFT 实例和 Hanning 窗函数
*/
void FFT_Init(void)
{
/* 初始化 RFFT 实例,FFT_SIZE 必须是 2 的幂 */
arm_rfft_fast_init_f32(&fft_instance, FFT_SIZE);
/* 预计算 Hanning 窗函数系数 */
for (uint32_t i = 0; i < FFT_SIZE; i++) {
/*
* Hanning 窗公式:w[n] = 0.5 * (1 - cos(2π*n / (N-1)))
* 窗函数的作用是减少频谱泄漏(spectral leakage),
* 因为实际采样的信号不是严格周期性的,直接做 FFT 会导致
* 频谱"泄漏"到相邻频率 bin 中,窗函数可以有效抑制这一现象。
*/
hanning_window[i] = 0.5f * (1.0f - arm_cos_f32(2.0f * PI * (float)i / (float)(FFT_SIZE - 1)));
}
/* 预计算各频率 bin 对应的频率值 */
for (uint32_t i = 0; i < FFT_OUTPUT_SIZE; i++) {
fft_result.frequency[i] = (float)i * freq_resolution;
}
/* 清零结果 */
memset(fft_result.magnitude, 0, sizeof(fft_result.magnitude));
fft_result.peak_freq = 0.0f;
fft_result.peak_magnitude = 0.0f;
fft_result.peak_index = 0;
}
/**
* @brief 执行 FFT 频谱分析
* @param adc_data: ADC 原始数据缓冲区指针
* @param offset: 数据起始偏移量(用于双缓冲选择)
*
* 处理流程:
* 1. 将 ADC 原始数据(uint16_t)转换为浮点数,并去除直流偏置
* 2. 应用 Hanning 窗函数
* 3. 执行实数 FFT
* 4. 计算各频率 bin 的幅值
* 5. 查找峰值频率
*/
void FFT_Process(uint16_t *adc_data, uint32_t offset)
{
/* ---- 步骤 1:ADC 原始值转浮点 + 去直流偏置 ---- */
/*
* ADC 12 位采样范围 0~4095,对应 0~3.3V
* 麦克风信号以 VCC/2 ≈ 1.65V 为中心摆动
* 因此直流偏置约为 4096/2 = 2048
* 去除直流偏置后,信号在 -2048 ~ +2047 范围内波动
*/
float dc_offset = 0.0f;
/* 先计算实际直流偏置(取平均值,比固定 2048 更准确) */
for (uint32_t i = 0; i < FFT_SIZE; i++) {
dc_offset += (float)adc_data[offset + i];
}
dc_offset /= (float)FFT_SIZE;
/* 转换为浮点并去除直流分量 */
for (uint32_t i = 0; i < FFT_SIZE; i++) {
fft_input[i] = (float)adc_data[offset + i] - dc_offset;
}
/* ---- 步骤 2:应用 Hanning 窗函数 ---- */
/* 逐点乘以窗函数系数,使用 CMSIS-DSP 向量乘法加速 */
arm_mult_f32(fft_input, hanning_window, fft_input, FFT_SIZE);
/* ---- 步骤 3:执行实数 FFT ---- */
/*
* arm_rfft_fast_f32 专为实数输入优化,效率是复数 FFT 的两倍
* 输入:fft_input[FFT_SIZE],实数序列
* 输出:fft_output[FFT_SIZE],复数序列(实部虚部交替排列)
* fft_output[0] = DC 分量的实部
* fft_output[1] = Nyquist 频率分量的实部
* fft_output[2k], fft_output[2k+1] = 第 k 个频率 bin 的实部和虚部
* 最后一个参数 0 表示正变换(FFT),1 表示逆变换(IFFT)
*/
arm_rfft_fast_f32(&fft_instance, fft_input, fft_output, 0);
/* ---- 步骤 4:计算幅值谱 ---- */
/* DC 分量(索引 0)单独处理 */
fft_result.magnitude[0] = fabsf(fft_output[0]) / (float)FFT_SIZE;
/* 计算各频率 bin 的幅值:|X[k]| = sqrt(Re^2 + Im^2) */
for (uint32_t i = 1; i < FFT_OUTPUT_SIZE; i++) {
float real = fft_output[2 * i]; // 实部
float imag = fft_output[2 * i + 1]; // 虚部
/*
* 幅值计算并归一化
* 乘以 2.0 是因为单边频谱需要补偿对称的另一半能量
* 除以 FFT_SIZE 是 FFT 的归一化系数
*/
fft_result.magnitude[i] = 2.0f * sqrtf(real * real + imag * imag) / (float)FFT_SIZE;
}
/* ---- 步骤 5:查找峰值频率(跳过 DC 分量) ---- */
float max_mag = 0.0f;
uint32_t max_index = 1; // 从索引 1 开始,跳过 DC
for (uint32_t i = 1; i < FFT_OUTPUT_SIZE; i++) {
if (fft_result.magnitude[i] > max_mag) {
max_mag = fft_result.magnitude[i];
max_index = i;
}
}
fft_result.peak_index = max_index;
fft_result.peak_magnitude = max_mag;
fft_result.peak_freq = (float)max_index * freq_resolution;
}
/**
* @brief 获取 FFT 处理结果
* @return FFT 结果结构体指针
*/
FFT_Result_t* FFT_GetResult(void)
{
return &fft_result;
}
关键技术点解析:
为什么要加窗函数? 实际采集的音频信号不是严格周期性的,如果直接对有限长度的信号做 FFT,信号的截断边界会引入不连续性,导致频谱能量"泄漏"到相邻频率 bin 中,使频谱变得模糊。Hanning 窗将信号两端平滑过渡到零,有效抑制了频谱泄漏。代价是主瓣宽度略有增加(频率分辨率略有下降),但对于噪声检测这种不需要极高频率分辨率的应用来说完全可以接受。
为什么用 arm_rfft_fast_f32 而不是 arm_cfft_f32? 因为我们的输入是纯实数信号(ADC 采样值),arm_rfft_fast_f32 利用了实数信号的共轭对称性,计算量仅为复数 FFT 的一半,在 STM32F4 上 1024 点实数 FFT 仅需约 0.3ms。
5.3 声压级计算与噪声等级判定
📄 创建文件:
Core/Inc/noise_level.h
c
/* noise_level.h - 噪声等级判定头文件 */
#ifndef __NOISE_LEVEL_H
#define __NOISE_LEVEL_H
#include "stm32f4xx_hal.h"
#include "fft_process.h"
/* 声压级参考值(空气中最小可听声压,20μPa) */
#define SPL_REF_PRESSURE 20e-6f
/* 麦克风灵敏度参数(需根据实际模块校准) */
#define MIC_SENSITIVITY -44.0f // MAX9814 灵敏度 (dBV/Pa @1kHz)
#define MIC_GAIN_DB 60.0f // MAX9814 默认增益 60dB
#define ADC_VOLTAGE_REF 3.3f // ADC 参考电压
/* 噪声等级枚举 */
typedef enum {
NOISE_LEVEL_QUIET = 0, // 安静 (< 40 dB)
NOISE_LEVEL_MODERATE, // 适中 (40~60 dB)
NOISE_LEVEL_LOUD, // 较吵 (60~80 dB)
NOISE_LEVEL_VERY_LOUD, // 很吵 (80~100 dB)
NOISE_LEVEL_DANGEROUS // 危险 (> 100 dB)
} NoiseLevel_t;
/* 噪声检测结果结构体 */
typedef struct {
float spl_db; // 声压级 (dB SPL)
float rms_voltage; // RMS 电压值
NoiseLevel_t level; // 噪声等级
const char *level_str; // 噪声等级描述字符串
uint8_t alarm_flag; // 告警标志(1=超标)
} NoiseResult_t;
/* 函数声明 */
void Noise_Init(void);
void Noise_Calculate(uint16_t *adc_data, uint32_t offset, uint32_t length);
NoiseResult_t* Noise_GetResult(void);
void Noise_SetAlarmThreshold(float threshold_db);
#endif /* __NOISE_LEVEL_H */
📄 创建文件:
Core/Src/noise_level.c
c
/* noise_level.c - 声压级计算与噪声等级判定 */
#include "noise_level.h"
#include <math.h>
#include <string.h>
/* 噪声检测结果 */
static NoiseResult_t noise_result;
/* 告警阈值,默认 85dB(工业安全标准) */
static float alarm_threshold_db = 85.0f;
/* 噪声等级描述字符串 */
static const char *noise_level_strings[] = {
"Quiet", // 安静:图书馆、卧室
"Moderate", // 适中:正常交谈
"Loud", // 较吵:繁忙街道
"Very Loud", // 很吵:工厂车间
"DANGEROUS" // 危险:需要佩戴听力保护
};
/**
* @brief 初始化噪声检测模块
*/
void Noise_Init(void)
{
memset(&noise_result, 0, sizeof(noise_result));
noise_result.level = NOISE_LEVEL_QUIET;
noise_result.level_str = noise_level_strings[0];
noise_result.alarm_flag = 0;
}
/**
* @brief 计算声压级和噪声等级
* @param adc_data: ADC 原始数据缓冲区
* @param offset: 数据起始偏移
* @param length: 数据长度
*
* 计算流程:
* 1. 计算信号的 RMS(均方根)电压
* 2. 根据麦克风灵敏度和增益,将 RMS 电压转换为声压
* 3. 计算声压级 SPL = 20 * log10(P / P_ref)
* 4. 根据 SPL 值判定噪声等级
*/
void Noise_Calculate(uint16_t *adc_data, uint32_t offset, uint32_t length)
{
/* ---- 步骤 1:计算 RMS 电压 ---- */
/* 先计算直流偏置 */
float dc_offset = 0.0f;
for (uint32_t i = 0; i < length; i++) {
dc_offset += (float)adc_data[offset + i];
}
dc_offset /= (float)length;
/* 计算去除直流后的均方值 */
float sum_squares = 0.0f;
for (uint32_t i = 0; i < length; i++) {
float sample = (float)adc_data[offset + i] - dc_offset;
sum_squares += sample * sample;
}
/* RMS = sqrt(均方值),然后转换为电压 */
float rms_adc = sqrtf(sum_squares / (float)length);
noise_result.rms_voltage = (rms_adc / ADC_RESOLUTION) * ADC_VOLTAGE_REF;
/* ---- 步骤 2:RMS 电压转换为声压 ---- */
/*
* 麦克风灵敏度 S = -44 dBV/Pa,即 1Pa 声压产生 10^(-44/20) = 6.31mV 输出
* MAX9814 增益 G = 60dB,即放大 1000 倍
* 因此:1Pa 声压 → 6.31mV × 1000 = 6.31V(会被 ADC 截断)
*
* 反推声压:P = V_rms / (S_linear × G_linear)
* 其中 S_linear = 10^(S_dBV/20), G_linear = 10^(G_dB/20)
*/
float sensitivity_linear = powf(10.0f, MIC_SENSITIVITY / 20.0f); // V/Pa
float gain_linear = powf(10.0f, MIC_GAIN_DB / 20.0f); // 倍数
float pressure_pa = noise_result.rms_voltage / (sensitivity_linear * gain_linear);
/* ---- 步骤 3:计算声压级 SPL ---- */
/*
* SPL = 20 * log10(P / P_ref)
* P_ref = 20μPa(空气中人耳可听阈值)
*
* 防止 log10(0) 的情况,设置最小声压值
*/
if (pressure_pa < 1e-10f) {
pressure_pa = 1e-10f;
}
noise_result.spl_db = 20.0f * log10f(pressure_pa / SPL_REF_PRESSURE);
/* 限制 SPL 范围在合理区间 */
if (noise_result.spl_db < 0.0f) {
noise_result.spl_db = 0.0f;
}
if (noise_result.spl_db > 140.0f) {
noise_result.spl_db = 140.0f;
}
/* ---- 步骤 4:判定噪声等级 ---- */
if (noise_result.spl_db < 40.0f) {
noise_result.level = NOISE_LEVEL_QUIET;
} else if (noise_result.spl_db < 60.0f) {
noise_result.level = NOISE_LEVEL_MODERATE;
} else if (noise_result.spl_db < 80.0f) {
noise_result.level = NOISE_LEVEL_LOUD;
} else if (noise_result.spl_db < 100.0f) {
noise_result.level = NOISE_LEVEL_VERY_LOUD;
} else {
noise_result.level = NOISE_LEVEL_DANGEROUS;
}
noise_result.level_str = noise_level_strings[noise_result.level];
/* 告警判定 */
noise_result.alarm_flag = (noise_result.spl_db >= alarm_threshold_db) ? 1 : 0;
}
/**
* @brief 获取噪声检测结果
* @return 噪声结果结构体指针
*/
NoiseResult_t* Noise_GetResult(void)
{
return &noise_result;
}
/**
* @brief 设置告警阈值
* @param threshold_db: 告警阈值 (dB SPL)
*/
void Noise_SetAlarmThreshold(float threshold_db)
{
alarm_threshold_db = threshold_db;
}
5.4 OLED 频谱显示模块
📄 创建文件:
Core/Inc/oled_display.h
c
/* oled_display.h - OLED 频谱显示头文件 */
#ifndef __OLED_DISPLAY_H
#define __OLED_DISPLAY_H
#include "stm32f4xx_hal.h"
#include "fft_process.h"
#include "noise_level.h"
/* SSD1306 OLED 参数 */
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_I2C_ADDR 0x78 // SSD1306 I2C 地址(7位地址 0x3C 左移1位)
/* 频谱显示参数 */
#define SPECTRUM_BARS 32 // 频谱柱状条数量
#define SPECTRUM_BAR_WIDTH 3 // 每个柱状条宽度(像素)
#define SPECTRUM_BAR_GAP 1 // 柱状条间距(像素)
#define SPECTRUM_MAX_HEIGHT 40 // 柱状条最大高度(像素)
#define SPECTRUM_Y_OFFSET 20 // 频谱区域 Y 起始位置
/* 函数声明 */
void OLED_Init(void);
void OLED_Clear(void);
void OLED_DrawSpectrum(FFT_Result_t *fft_result);
void OLED_ShowNoiseInfo(NoiseResult_t *noise_result);
void OLED_UpdateDisplay(FFT_Result_t *fft_result, NoiseResult_t *noise_result);
#endif /* __OLED_DISPLAY_H */
📄 创建文件:
Core/Src/oled_display.c
c
/* oled_display.c - OLED 频谱显示实现 */
#include "oled_display.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
/* I2C 句柄(由 CubeMX 生成) */
extern I2C_HandleTypeDef hi2c1;
/* OLED 显示缓冲区(128×64 / 8 = 1024 字节) */
static uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8];
/* 简易 6×8 ASCII 字体表(部分字符) */
static const uint8_t Font_6x8[][6] = {
{0x00,0x00,0x00,0x00,0x00,0x00}, // 空格 (32)
{0x00,0x00,0x5F,0x00,0x00,0x00}, // ! (33)
/* ... 省略中间字符,实际项目中需要完整字体表 ... */
{0x3E,0x51,0x49,0x45,0x3E,0x00}, // 0 (48)
{0x00,0x42,0x7F,0x40,0x00,0x00}, // 1 (49)
{0x42,0x61,0x51,0x49,0x46,0x00}, // 2 (50)
{0x21,0x41,0x45,0x4B,0x31,0x00}, // 3 (51)
{0x18,0x14,0x12,0x7F,0x10,0x00}, // 4 (52)
{0x27,0x45,0x45,0x45,0x39,0x00}, // 5 (53)
{0x3C,0x4A,0x49,0x49,0x30,0x00}, // 6 (54)
{0x01,0x71,0x09,0x05,0x03,0x00}, // 7 (55)
{0x36,0x49,0x49,0x49,0x36,0x00}, // 8 (56)
{0x06,0x49,0x49,0x29,0x1E,0x00}, // 9 (57)
/* ... 完整字体表请参考 SSD1306 驱动库 ... */
};
/**
* @brief 向 SSD1306 发送命令
*/
static void OLED_WriteCmd(uint8_t cmd)
{
uint8_t data[2] = {0x00, cmd}; // Co=0, D/C#=0 (命令)
HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, data, 2, 100);
}
/**
* @brief 初始化 SSD1306 OLED
*/
void OLED_Init(void)
{
HAL_Delay(100); // 等待 OLED 上电稳定
/* SSD1306 初始化序列 */
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频
OLED_WriteCmd(0x80); // 默认值
OLED_WriteCmd(0xA8); // 设置多路复用率
OLED_WriteCmd(0x3F); // 64行
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00); // 无偏移
OLED_WriteCmd(0x40); // 设置起始行
OLED_WriteCmd(0x8D); // 电荷泵设置
OLED_WriteCmd(0x14); // 开启电荷泵
OLED_WriteCmd(0x20); // 设置内存寻址模式
OLED_WriteCmd(0x00); // 水平寻址模式
OLED_WriteCmd(0xA1); // 段重映射
OLED_WriteCmd(0xC8); // COM 扫描方向
OLED_WriteCmd(0xDA); // COM 引脚配置
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 设置对比度
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // VCOMH 电压
OLED_WriteCmd(0x40);
OLED_WriteCmd(0xA4); // 全局显示开启(跟随 RAM)
OLED_WriteCmd(0xA6); // 正常显示(非反转)
OLED_WriteCmd(0xAF); // 开启显示
OLED_Clear();
}
/**
* @brief 清屏
*/
void OLED_Clear(void)
{
memset(oled_buffer, 0x00, sizeof(oled_buffer));
}
/**
* @brief 在缓冲区中设置一个像素点
*/
static void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color)
{
if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
if (color) {
oled_buffer[x + (y / 8) * OLED_WIDTH] |= (1 << (y % 8));
} else {
oled_buffer[x + (y / 8) * OLED_WIDTH] &= ~(1 << (y % 8));
}
}
/**
* @brief 绘制一个竖直线段(用于频谱柱状条)
*/
static void OLED_DrawVLine(uint8_t x, uint8_t y_start, uint8_t y_end)
{
for (uint8_t y = y_start; y <= y_end; y++) {
OLED_SetPixel(x, y, 1);
}
}
/**
* @brief 在指定位置显示一个字符
*/
static void OLED_DrawChar(uint8_t x, uint8_t y, char ch)
{
if (ch < 32 || ch > 127) return;
uint8_t idx = ch - 32;
for (uint8_t i = 0; i < 6; i++) {
uint8_t line = Font_6x8[idx][i];
for (uint8_t j = 0; j < 8; j++) {
if (line & (1 << j)) {
OLED_SetPixel(x + i, y + j, 1);
}
}
}
}
/**
* @brief 在指定位置显示字符串
*/
static void OLED_DrawString(uint8_t x, uint8_t y, const char *str)
{
while (*str) {
OLED_DrawChar(x, y, *str);
x += 6;
str++;
}
}
/**
* @brief 将缓冲区内容刷新到 OLED 屏幕
*/
static void OLED_Flush(void)
{
/* 设置列地址范围 */
OLED_WriteCmd(0x21);
OLED_WriteCmd(0x00); // 起始列
OLED_WriteCmd(0x7F); // 结束列 (127)
/* 设置页地址范围 */
OLED_WriteCmd(0x22);
OLED_WriteCmd(0x00); // 起始页
OLED_WriteCmd(0x07); // 结束页 (7)
/* 发送显示数据 */
uint8_t tx_buf[1025];
tx_buf[0] = 0x40; // Co=0, D/C#=1 (数据)
memcpy(&tx_buf[1], oled_buffer, sizeof(oled_buffer));
HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, tx_buf, sizeof(tx_buf), 500);
}
/**
* @brief 绘制频谱柱状图
* @param fft_result: FFT 处理结果
*
* 将 512 个频率 bin 合并为 32 个柱状条显示
* 每个柱状条代表一个频率段的平均幅值
*/
void OLED_DrawSpectrum(FFT_Result_t *fft_result)
{
/* 每个柱状条对应的频率 bin 数量 */
uint32_t bins_per_bar = FFT_OUTPUT_SIZE / SPECTRUM_BARS; // 512/32 = 16
for (uint32_t bar = 0; bar < SPECTRUM_BARS; bar++) {
/* 计算当前柱状条对应频率段的平均幅值 */
float avg_magnitude = 0.0f;
uint32_t start_bin = bar * bins_per_bar;
for (uint32_t i = 0; i < bins_per_bar; i++) {
avg_magnitude += fft_result->magnitude[start_bin + i];
}
avg_magnitude /= (float)bins_per_bar;
/*
* 将幅值映射为柱状条高度
* 使用对数刻度(dB),使小信号也能可见
* 20*log10(mag) 范围约 -60dB ~ 0dB,映射到 0 ~ SPECTRUM_MAX_HEIGHT 像素
*/
float mag_db = 0.0f;
if (avg_magnitude > 1e-6f) {
mag_db = 20.0f * log10f(avg_magnitude);
} else {
mag_db = -60.0f;
}
/* 将 dB 值映射到像素高度 (-60dB→0px, 0dB→40px) */
float bar_height = ((mag_db + 60.0f) / 60.0f) * SPECTRUM_MAX_HEIGHT;
if (bar_height < 0) bar_height = 0;
if (bar_height > SPECTRUM_MAX_HEIGHT) bar_height = SPECTRUM_MAX_HEIGHT;
uint8_t height = (uint8_t)bar_height;
/* 绘制柱状条(从底部向上) */
uint8_t x_start = bar * (SPECTRUM_BAR_WIDTH + SPECTRUM_BAR_GAP);
uint8_t y_bottom = OLED_HEIGHT - 1; // 屏幕底部
uint8_t y_top = y_bottom - height;
if (height > 0) {
for (uint8_t w = 0; w < SPECTRUM_BAR_WIDTH; w++) {
OLED_DrawVLine(x_start + w, y_top, y_bottom);
}
}
}
}
/**
* @brief 显示噪声信息(分贝值和等级)
* @param noise_result: 噪声检测结果
*/
void OLED_ShowNoiseInfo(NoiseResult_t *noise_result)
{
char str_buf[32];
/* 第一行:显示分贝值 */
snprintf(str_buf, sizeof(str_buf), "SPL: %.1f dB", noise_result->spl_db);
OLED_DrawString(0, 0, str_buf);
/* 第二行:显示噪声等级 */
snprintf(str_buf, sizeof(str_buf), "Level: %s", noise_result->level_str);
OLED_DrawString(0, 10, str_buf);
/* 如果超标,显示告警标志 */
if (noise_result->alarm_flag) {
OLED_DrawString(100, 0, "!!");
}
}
/**
* @brief 更新整个 OLED 显示(信息 + 频谱)
* @param fft_result: FFT 处理结果
* @param noise_result: 噪声检测结果
*/
void OLED_UpdateDisplay(FFT_Result_t *fft_result, NoiseResult_t *noise_result)
{
OLED_Clear();
OLED_ShowNoiseInfo(noise_result);
OLED_DrawSpectrum(fft_result);
OLED_Flush();
}
5.5 主程序
📄 创建文件:
Core/Src/main.c
c
/* main.c - 噪声检测系统主程序 */
/* 标准库头文件 */
#include "main.h"
#include "adc_config.h"
#include "fft_process.h"
#include "noise_level.h"
#include "oled_display.h"
/* HAL 库句柄(由 CubeMX 自动生成) */
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
TIM_HandleTypeDef htim2;
I2C_HandleTypeDef hi2c1;
/* 蜂鸣器控制引脚 */
#define BUZZER_PORT GPIOB
#define BUZZER_PIN GPIO_PIN_8
/* 串口调试输出(可选) */
extern UART_HandleTypeDef huart1;
/* 私有函数声明 */
static void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
static void MX_TIM2_Init(void);
static void MX_I2C1_Init(void);
static void Buzzer_Control(uint8_t state);
static void Debug_PrintResult(FFT_Result_t *fft, NoiseResult_t *noise);
/**
* @brief 主函数
*/
int main(void)
{
/* ---- 系统初始化 ---- */
HAL_Init();
SystemClock_Config();
/* 外设初始化(顺序很重要:先 DMA,再 ADC) */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_TIM2_Init();
MX_I2C1_Init();
/* ---- 模块初始化 ---- */
ADC_Sampling_Init();
FFT_Init();
Noise_Init();
OLED_Init();
/* 设置噪声告警阈值为 85dB(工业安全标准) */
Noise_SetAlarmThreshold(85.0f);
/* ---- 启动 ADC 采样 ---- */
ADC_Sampling_Start();
/* ---- 主循环 ---- */
while (1)
{
/*
* 检查 DMA 缓冲区状态
* 当前半或后半缓冲区数据就绪时,执行 FFT 和噪声计算
*/
if (adc_buf_state == BUF_STATE_HALF)
{
/* 前半缓冲区就绪,处理 [0 ~ FFT_SIZE-1] */
adc_buf_state = BUF_STATE_IDLE;
/* 执行 FFT 频谱分析 */
FFT_Process(adc_raw_buf, 0);
/* 计算声压级和噪声等级 */
Noise_Calculate(adc_raw_buf, 0, FFT_SIZE);
/* 获取结果 */
FFT_Result_t *fft_result = FFT_GetResult();
NoiseResult_t *noise_result = Noise_GetResult();
/* 更新 OLED 显示 */
OLED_UpdateDisplay(fft_result, noise_result);
/* 蜂鸣器告警控制 */
Buzzer_Control(noise_result->alarm_flag);
/* 串口调试输出(可选,正式部署时可注释掉) */
Debug_PrintResult(fft_result, noise_result);
}
else if (adc_buf_state == BUF_STATE_FULL)
{
/* 后半缓冲区就绪,处理 [FFT_SIZE ~ 2*FFT_SIZE-1] */
adc_buf_state = BUF_STATE_IDLE;
FFT_Process(adc_raw_buf, FFT_SIZE);
Noise_Calculate(adc_raw_buf, FFT_SIZE, FFT_SIZE);
FFT_Result_t *fft_result = FFT_GetResult();
NoiseResult_t *noise_result = Noise_GetResult();
OLED_UpdateDisplay(fft_result, noise_result);
Buzzer_Control(noise_result->alarm_flag);
Debug_PrintResult(fft_result, noise_result);
}
/*
* 当没有数据需要处理时,CPU 可以进入低功耗模式
* 等待 DMA 中断唤醒
*/
// __WFI(); // 取消注释可启用低功耗等待
}
}
/**
* @brief 蜂鸣器控制
* @param state: 1=开启告警, 0=关闭告警
*/
static void Buzzer_Control(uint8_t state)
{
if (state) {
HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(BUZZER_PORT, BUZZER_PIN, GPIO_PIN_RESET);
}
}
/**
* @brief 通过串口输出调试信息
* @param fft: FFT 结果
* @param noise: 噪声结果
*/
static void Debug_PrintResult(FFT_Result_t *fft, NoiseResult_t *noise)
{
char debug_buf[128];
snprintf(debug_buf, sizeof(debug_buf),
"SPL=%.1f dB | Level=%s | Peak=%.0f Hz (%.4f) | Alarm=%d\r\n",
noise->spl_db,
noise->level_str,
fft->peak_freq,
fft->peak_magnitude,
noise->alarm_flag);
HAL_UART_Transmit(&huart1, (uint8_t *)debug_buf, strlen(debug_buf), 100);
}
/*
* 以下为 CubeMX 自动生成的外设初始化函数框架
* 实际代码由 CubeMX 根据图形化配置自动生成
* 这里仅展示关键配置参数,供手动配置参考
*/
/**
* @brief ADC1 初始化
*/
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // APB2/4 = 21MHz
hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12 位分辨率
hadc1.Init.ScanConvMode = DISABLE; // 单通道
hadc1.Init.ContinuousConvMode = DISABLE; // 非连续(由定时器触发)
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; // TIM2 触发
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = ENABLE; // DMA 连续请求
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
HAL_ADC_Init(&hadc1);
/* 配置 ADC 通道 0 (PA0) */
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; // 15 个采样周期
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
/**
* @brief TIM2 初始化(ADC 采样触发定时器)
*/
static void MX_TIM2_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; // 不分频
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = (84000000 / SAMPLE_RATE) - 1; // ARR = 1904
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim2);
/* 配置 TRGO 输出为 Update Event */
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
}
/* SystemClock_Config, MX_GPIO_Init, MX_DMA_Init, MX_I2C1_Init 等函数
* 由 CubeMX 自动生成,此处省略。请在 CubeMX 中完成配置后自动生成。 */
主程序工作流程:
IDLE
HALF 前半就绪
FULL 后半就绪
是
否
系统上电
HAL 初始化 + 时钟配置
外设初始化: GPIO/DMA/ADC/TIM/I2C
模块初始化: ADC/FFT/Noise/OLED
启动 ADC+DMA 采样
检查缓冲区状态
FFT 处理前半数据
FFT 处理后半数据
计算声压级 SPL
更新 OLED 显示
SPL >= 85dB?
蜂鸣器告警
关闭蜂鸣器
串口输出调试信息
六、测试验证
6.1 编译与烧录
- 在 STM32CubeIDE 中点击 Build Project(Ctrl+B),确保编译无错误无警告
- 连接 ST-Link V2 调试器到开发板
- 点击 Debug (F11)进入调试模式,或点击 Run(Ctrl+F11)直接烧录运行
6.2 串口调试验证
打开串口调试助手,设置波特率 115200,8N1。正常运行后应看到类似如下输出:
SPL=42.3 dB | Level=Moderate | Peak=344 Hz (0.0523) | Alarm=0
SPL=43.1 dB | Level=Moderate | Peak=344 Hz (0.0498) | Alarm=0
SPL=65.7 dB | Level=Loud | Peak=1032 Hz (0.1247) | Alarm=0
SPL=88.2 dB | Level=Very Loud | Peak=516 Hz (0.3891) | Alarm=1
验证要点:
- 安静环境:SPL 应在 30~45 dB 之间,等级为 Quiet 或 Moderate
- 正常说话:SPL 应在 55~70 dB 之间,等级为 Loud
- 大声喊叫/播放音乐:SPL 应在 75~95 dB 之间,等级为 Very Loud,可能触发告警
- 峰值频率:对着麦克风吹口哨,峰值频率应与口哨频率大致吻合(通常 500~2000 Hz)
6.3 OLED 显示验证
OLED 屏幕上方两行显示 SPL 分贝值和噪声等级文字,下方显示 32 条频谱柱状图。在安静环境中柱状条应较低且均匀,当有特定频率的声音输入时(如手机播放单频正弦波),对应频率位置的柱状条应明显升高。
6.4 校准说明
由于不同 MEMS 麦克风模块的灵敏度存在个体差异,系统首次使用时建议进行简单校准:
-
准备一个已校准的分贝计(手机 App 也可作为粗略参考)
-
在同一位置同时测量,记录分贝计读数和系统读数
-
计算差值 ΔdB = 分贝计读数 - 系统读数
-
在
noise_level.c中的Noise_Calculate函数里,给spl_db加上补偿值:cnoise_result.spl_db += CALIBRATION_OFFSET; // 在 noise_level.h 中定义
七、故障排查与问题解决
7.1 环境配置问题
问题 1:编译报错 "arm_math.h: No such file or directory"
错误现象:
fatal error: arm_math.h: No such file or directory
原因分析:
- CMSIS-DSP 头文件路径未正确添加到编译器 Include 路径中
- CMSIS-DSP 库文件未包含在项目中
解决方案:
方案 1:检查 Include 路径
在 STM32CubeIDE 中,右键项目 → Properties → C/C++ Build → Settings → MCU GCC Compiler → Include paths,确认包含以下路径:
../Drivers/CMSIS/DSP/Include
方案 2:手动复制 DSP 库
如果项目中没有 CMSIS-DSP 库文件,从 STM32Cube 固件包中复制:
bash
# 固件包路径示例(根据实际安装路径调整)
cp -r STM32Cube_FW_F4/Drivers/CMSIS/DSP ./Drivers/CMSIS/
验证修复: 重新编译项目,arm_math.h 相关错误应消失。
问题 2:链接报错 "undefined reference to arm_rfft_fast_init_f32"
错误现象:
undefined reference to `arm_rfft_fast_init_f32'
undefined reference to `arm_rfft_fast_f32'
原因分析:
- 未链接 CMSIS-DSP 静态库文件
- 库文件架构不匹配(如使用了非 Cortex-M4 版本)
解决方案:
在 MCU GCC Linker → Libraries 中添加:
- Libraries (-l):
arm_cortexM4lf_math - Library search path (-L):
../Drivers/CMSIS/Lib/GCC
⚠️ 注意:库名中的
lf表示 "little-endian, float",对应带 FPU 的 Cortex-M4。如果使用 Keil,库文件名为arm_cortexM4lf_math.lib。
7.2 硬件问题
问题 3:ADC 采样值始终为 0 或 4095
原因分析:
- ADC 引脚未正确配置为模拟输入模式
- 麦克风模块未供电或接线错误
- ADC 时钟配置不正确
解决方案:
方案 1:检查 GPIO 配置
确保 PA0 在 CubeMX 中配置为 ADC1_IN0(模拟模式),而非 GPIO 输入。
方案 2:检查硬件连接
用万用表测量 MAX9814 模块的 OUT 引脚电压,正常情况下静音时应约为 VCC/2 ≈ 1.65V。如果为 0V 或 3.3V,检查模块供电和焊接。
方案 3:简单 ADC 测试
c
/* 在 main 循环中添加简单测试代码 */
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
uint32_t adc_val = HAL_ADC_GetValue(&hadc1);
printf("ADC Raw: %lu, Voltage: %.3f V\r\n", adc_val, adc_val * 3.3f / 4096.0f);
HAL_Delay(500);
问题 4:FFT 结果全为零或出现异常尖峰
原因分析:
- DMA 传输未正确启动,缓冲区数据全为零
- 未正确去除直流偏置,导致 DC 分量过大
- FFT 点数与实际数据长度不匹配
- 未开启 FPU,浮点运算结果异常
解决方案:
方案 1:验证 DMA 数据
在 FFT 处理前打印原始 ADC 数据,确认数据在合理范围内(约 1500~2500 波动):
c
printf("ADC[0]=%d, ADC[1]=%d, ADC[100]=%d\r\n",
adc_raw_buf[0], adc_raw_buf[1], adc_raw_buf[100]);
方案 2:检查 FPU 配置
在项目属性 → MCU Settings 中确认:
- Floating point unit = FPv4-SP-D16
- Floating point ABI = Hard
如果 FPU 未开启,所有浮点运算将由软件模拟,不仅速度极慢,还可能因为 CMSIS-DSP 库的硬浮点指令导致 HardFault。
7.3 性能问题
问题 5:OLED 刷新率低,频谱显示卡顿
原因分析:
- I2C 传输速率过低
- 每次刷新传输全部 1024 字节显示数据耗时过长
- FFT 处理时间过长,阻塞了显示更新
解决方案:
方案 1:提高 I2C 速率
将 I2C 速率从 100kHz(Standard Mode)提高到 400kHz(Fast Mode)。在 CubeMX 中修改 I2C1 的 Clock Speed 为 400000。
方案 2:使用 DMA 传输 OLED 数据
将 OLED 的 I2C 数据传输改为 DMA 方式,CPU 不必等待传输完成:
c
HAL_I2C_Master_Transmit_DMA(&hi2c1, OLED_I2C_ADDR, tx_buf, sizeof(tx_buf));
方案 3:降低刷新频率
不必每次 FFT 完成都刷新显示,可以每隔 2~3 次 FFT 刷新一次:
c
static uint8_t refresh_counter = 0;
if (++refresh_counter >= 3) {
refresh_counter = 0;
OLED_UpdateDisplay(fft_result, noise_result);
}
八、I2S 数字 MEMS 麦克风方案补充
如果你使用 INMP441 等 I2S 数字 MEMS 麦克风,主要差异在于数据采集部分。I2S 麦克风直接输出数字 PDM/PCM 数据,无需 ADC 转换,信噪比通常优于模拟方案。
关键配置差异:
- 使用 STM32F4 的 I2S2 或 I2S3 外设,而非 ADC
- I2S 时钟频率需要精确配置以匹配采样率
- DMA 接收的数据为 24 位有符号整数(存储在 32 位字中),需要做符号扩展和格式转换
- FFT 处理和噪声计算部分的代码可以复用,只需修改数据预处理部分
c
/* I2S DMA 接收回调中的数据转换示例 */
void I2S_DataConvert(int32_t *i2s_data, float *output, uint32_t length)
{
for (uint32_t i = 0; i < length; i++) {
/* INMP441 输出 24 位数据,左对齐在 32 位字的高位 */
int32_t sample = i2s_data[i] >> 8; // 右移 8 位得到 24 位有效数据
output[i] = (float)sample / 8388608.0f; // 归一化到 -1.0 ~ +1.0
}
}
九、总结与扩展方向
9.1 核心知识点回顾
本教程完整实现了一个基于 STM32F4 的噪声检测系统,涵盖了从硬件接线到软件算法的全流程。核心知识点包括:MEMS 麦克风的工作原理与接口方式、STM32F4 ADC + DMA + 定时器触发的精确采样架构、Hanning 窗函数与 FFT 频谱分析的原理及 CMSIS-DSP 库的使用、声压级(SPL)的计算方法与噪声等级判定逻辑,以及 SSD1306 OLED 的频谱可视化显示。
9.2 扩展方向
在本项目基础上,可以进一步扩展以下功能:
数据存储与回放:添加 SD 卡模块,将采样数据和 SPL 记录以 WAV 或 CSV 格式存储,便于后续分析。可以使用 FATFS 文件系统库实现文件读写。
无线传输与远程监控:集成 ESP8266/ESP32 WiFi 模块或 NB-IoT 模块,将噪声数据上传到云平台(如 ThingsBoard、阿里云 IoT),实现远程实时监控和历史数据查询。
A 计权滤波:当前系统计算的是线性声压级(dB SPL),而人耳对不同频率的敏感度不同。添加 A 计权滤波器可以得到更符合人耳感知的 dB(A) 值,这在环境噪声监测标准中是必需的。
多频段分析:将频谱划分为倍频程或 1/3 倍频程频段,分别计算各频段的能量,可以更精确地分析噪声的频率特征,便于噪声源识别和针对性治理。
9.3 学习资源
官方文档:
官方 GitHub: