07 FFT实验
Fast Fourier Transform 快速傅里叶变换
up的视频讲解: https://space.bilibili.com/1455872040
up视频里的实验代码: 👣FFT实验代码
FFT驱动代码: 👣FFT驱动代码
1 🐣 FFT 简介
1 ⭐️采到声音之后,怎么看频率
前面我们使用ADC采集麦克风电压,再通过DMA搬进数组。把这些值按时间画出来,就是时域波形。这节课我们再走一步,看看声音里有哪些频率。
FFT是快速计算离散傅里叶变换的方法。输入是一帧采样数据,输出是不同频率对应的分量。它不负责采样,频率刻度必须由实际采样率算出来。
本课按照作者FFT_TEST工程,使用 Q15定点复数FFT:arm_cfft_q15。浮点快速实数FFT接口可查阅Arm文档,两者的数据格式和缩放规则不能混用。
2 🔌实验连接与CubeMX配置
麦克风OUT接PA1,即ADC1_IN1;PA9接CH340的RXD,模块与主控板共地。麦克风输出保持在ADC允许的模拟输入范围内,本课按0~3.3 V使用。
以下取值来自工程的.ioc与外设初始化代码:
| 项目 | 工程配置 |
|---|---|
| 芯片 | STM32F103C8T6 / Cortex-M3 |
| 时钟 | HSE 8 MHz,PLL×9,HCLK 72 MHz;PCLK1 36 MHz,TIM3 72 MHz |
| TIM3 | PSC=72−1,ARR=200−1,TRGO=Update Event |
| 实际采样率 | 72000000 / 72 / 200 = 5000 Hz |
| ADC1 | IN1、右对齐、单通道、关闭连续转换、TIM3 TRGO触发 |
| ADC时钟/采样时间 | 12 MHz,7.5 cycles,总转换约1.67 μs |
| DMA | DMA1 Channel1,Normal,外设到内存,两端Half Word |
| 地址 | 外设不自增、内存自增 |
| NVIC | 开启DMA1 Channel1中断 |
| USART1 | 115200、8N1,PA9/TX、PA10/RX |
7.5 cycles是工程现值,不是所有模拟源都适用;源阻抗较大时要延长采样时间并验证精度。生成代码后先初始化DMA,再初始化ADC。
2.1 频点怎么换成Hz
N = 256,Fs = 5000 Hz
频点间隔 = Fs/N = 19.53125 Hz
一帧采集时间 = N/Fs = 51.2 ms
第k点的频率 = k × 19.53125 Hz
工程显示k=0~127,共128点;第128点对应2500 Hz奈奎斯特频率,本例不输出它。最后显示的第127点对应2480.46875 Hz。
1000 Hz不恰好落在这个频率栅格上,会分散到邻近点。若要验证“正好落在第32点”,测试频率应为 625 Hz。
3 📁把驱动加入Keil工程
解压驱动包,将FFT目录放在工程根目录,与Core、Drivers、MDK-ARM并列。
FFT/
├─ arm_math.h
├─ arm_common_tables.h
├─ arm_const_structs.h
└─ arm_cortexM3l_math.lib
- Keil新建FFT分组,把arm_cortexM3l_math.lib加入工程。头文件可加入分组方便阅读,但仅添加头文件不会链接算法实现。
- C/C++的Include Paths添加
../FFT,保留CubeMX原有CMSIS、HAL和Core路径。路径相对于MDK-ARM中的工程文件。 - Define保留
USE_HAL_DRIVER,STM32F103xB,添加ARM_MATH_CM3。 - 源码包含arm_math.h和arm_const_structs.h。
原工程选择 ARM Compiler 5.06 update 6(build 750)。先按原配置复现;更换编译器时重新核对库兼容性。配套arm_math.h版本标记V1.5.3,不能随意替换成另一版本的头文件。合集中的其他架构库不用添加到本工程。
4 ➡️从ADC数组到频谱数组
4.1 三个数组
#define FFT_LEN 256
uint16_t adc_dma_buf[FFT_LEN]; // 256个ADC原始值
q15_t fft_buf[FFT_LEN * 2]; // 256组实部、虚部
q15_t mag_out[FFT_LEN / 2]; // 前128个频点的模值
三个数组合计1792字节,不包括其他变量与栈。q15_t是有符号16位存储类型;在Q1.15约定下,32768对应实数1的尺度,最大正存储值为32767。
这里调用复数FFT,每个输入位置需要实部与虚部。ADC采到的是实数,所以实部放数据,虚部填0:[实0,虚0,实1,虚1,…]。
4.2 去偏置与缩放
工程使用 (ADC值−2048)×8。减2048按理想中点去掉麦克风直流偏置;乘8更充分地利用定点范围。12位ADC的0~4095变成−16384~16376,仍在int16范围内。
先转int32再减中点,避免无符号运算造成误解。实际静音偏置未必是2048,更准确的处理可改用实测均值。
4.3 视频工程原代码
下面直接摘自作者FFT_TEST工程的USER CODE区域,保留原写法、变量、调用顺序与输出内容。
#include <stdio.h>
#include "arm_math.h"
#include "arm_const_structs.h"
#define FFT_LEN 256
uint16_t adc_dma_buf[FFT_LEN];
q15_t fft_buf[FFT_LEN*2];
q15_t mag_out[FFT_LEN/2];
void FFT()
{
for(int i=0; i<FFT_LEN; i++)
{
fft_buf[2*i] = (q15_t)(((int32_t)adc_dma_buf[i]-2048)*8);//实部
fft_buf[2*i+1] = 0; //虚部
}
const arm_cfft_instance_q15 *fft_init = &arm_cfft_sR_q15_len256;
arm_cfft_q15(fft_init, fft_buf, 0, 1);
arm_cmplx_mag_q15(fft_buf, mag_out, FFT_LEN/2);
mag_out[0] = 0;
// for(uint16_t i=1; i < FFT_LEN/2; i++)
// {
// mag_out[i] = mag_out[i];
// }
}
FFT结果写回fft_buf,求模结果保存在mag_out。mag_out[0]置零用于隐藏直流频点。定点运算内部有缩放,本课先观察相对频谱强弱。
5 💻视频工程的采集与输出
5.1 串口重定向
// printf输出重定向
int fputc(int ch, FILE *f)
{
uint8_t temp = ch;
HAL_UART_Transmit(&huart1, &temp, 1, HAL_MAX_DELAY);
return ch;
}
5.2 全局与回调
uint8_t flag = 0;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
flag = 1;
}
5.3 main初始化
HAL_TIM_Base_Start(&htim3);
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_dma_buf,FFT_LEN);
5.4 while循环
if(flag)
{
FFT();
for(int i=0; i<FFT_LEN/2; i++)
{
printf("%d\r\n",adc_dma_buf[i]);
}
flag = 0;
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)adc_dma_buf,FFT_LEN);
}
这是Normal DMA分帧采集示例,采集和发送之间存在空档。连续采样与带宽预算见补充目录。
6 ✅验证与排错
先在启动采样前生成一帧已知数据,再调用FFT:
#include <math.h>
for (uint32_t n = 0; n < FFT_LEN; ++n) {
float x = 2048.0f + 1000.0f * sinf(2.0f * 3.14159265358979323846f
* 32.0f * n / FFT_LEN);
adc_dma_buf[n] = (uint16_t)(x + 0.5f);
}
FFT();