STM32F407 利用Timer+ADC+DMA在FFT上的理解与运用
最近遇到有关信号的总谐波失真THD值的测量,根据THD的计算公式可知我们需要知道该信号各次谐波分量的幅值,所以需要将ADC采样到的信号进行FFT,将时域上的信号转换到频域上,获得其幅值。
这里我们假设要采集的信号为1Khz。
方案
利用STM32F407上的定时器Timer来触发ADC采样,并利用DMA搬运采样到的AD值,最后用dsp库里的有关FFT运算的函数进行各次谐波幅值的获取。
原理
设定数据
被采信号频率:f
被采信号周期:T = 1/f
采样频率:fs
采样周期:Ts = 1/fs
采样总点数:NPT
采样总时间:t = NPT * Ts
频谱图频率分辨率:f0 = fs/NPT
采到的被采信号周期数:NT = t/T
一个被采信号周期的采样点数:fs/f
带入数据
我们需要采的信号频率为1KHz,此次准备以32KHz的采样频率进行采集,并且一共只采集256个点进行FFT运算。
故:
被采信号频率:f = 1000Hz
被采信号周期:T = 1/f = 0.001s
采样频率:fs = 32 000Hz
采样周期:Ts = 1/fs =0.000 031 25s
采样总点数:NPT = 256
采样总时间:t = NPT * Ts = 0.008s
频谱图频率分辨率:f0 = fs/NPT = 32 000Hz/256 = 125Hz
采到的被采信号周期数:NT = t/T = 0.008s/0.001s = 8
一个被采信号周期的被采到的点数:fs/f = 32 000Hz/1000Hz = 32
分析数据
FFT后的幅度谱的横坐标是频率,并且是离散的。是以频率f0的n倍展开的。故横坐标为
… , -nf0, …-2f0, -f0, 0, f0, 2f0, … ,nf0, … 而纵坐标为对应频点的幅值信息。
本次是设定的f0 = 125Hz, 理想情况下FFT后的幅度谱看起来应该会是下面这个样子:
其中横坐标0刻度的地方即频率为0的点,为直流分量 。如果被采信号没有偏置的话这一点幅度应该为0。而横坐标其它点皆为125Hz的倍数。
由于被采信号的频率为1000Hz,故横坐标的正半轴和负半轴的1000Hz处都会有“擎天柱”。但在运用中我们只会取正半轴部分,毕竟正半轴知道了也就知道了负半轴,所以负半轴可以说是没什么用的。
值得注意的是:对于直流分量来说,纵坐标对应的值就是直流分量的幅度,而其他频率对应的纵坐标值只是其实际幅值的1/2
关于这一点有一个简单的理解方式,就是它的幅度平均分到了正负半轴,导致只有一半。
如果从公式的角度,可以这样理解:
可以看到,正负两边均只有信号峰值A的一半,对于幅度而言也是一样的也是一半。这是本人个人的理解方式,可能会不严谨。
代码
ADC+DMA部分:
ADC注意配置成外部时钟触发,不连续转换,单通道不扫描。
DMA需要外设不自增,内存自增的方式来存储采到的连续的256个点,非循环模式。
#include "ADCDMA.h"
//PF3 ADC3:IN9
uint16_t AD3_Value[AD3_Value_Length];
void AD3_Init(){
//结构体
GPIO_InitTypeDef GPIO_InitStructure; //GPIO结构体
DMA_InitTypeDef DMA_InitStructure; //DMA结构体
ADC_CommonInitTypeDef ADC_CommonInitStructure; //ADCcommon结构体
ADC_InitTypeDef ADC_InitStructure; //ADC结构体
//时钟开启
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能GPIOF时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE); //使能DMA2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3, ENABLE); //使能ADC3时钟
//GPIO配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; //模拟模式
// GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //浮空
GPIO_Init(GPIOF, &GPIO_InitStructure); //GPIO初始化
//ADCcommon配置
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; //非多重模式,多重模式下才开启此配置的DMA
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; //采样频率4分频 84MHz/4 = 21MHz
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
ADC_CommonInit(&ADC_CommonInitStructure); //ADCcommon结构体初始化
//ADC配置
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //是否连续转换
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐:右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO;//
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_RisingFalling ; //
ADC_InitStructure.ADC_NbrOfConversion = AD3_Value_Length; //转换数量:一波采集采集的AD值个数,多通道时一般为通道数量
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; //12bit
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //是否扫描:多通道需扫描
ADC_Init(ADC3, &ADC_InitStructure); //ADC3初始化
//ADC序列 转换顺序
ADC_RegularChannelConfig(ADC3,ADC_Channel_9, 1,ADC_SampleTime_84Cycles);
//DMA配置
DMA_InitStructure.DMA_Channel = DMA_Channel_2; //DMA_CH2
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //非循环
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; //外设到内存
DMA_InitStructure.DMA_BufferSize = AD3_Value_Length; //传输次数,数组长度
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级
//DMA_FIFO
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)AD3_Value; //内存地址:收集AD值得数组
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //16 bit
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存自增
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC3->DR); //外设地址,ADC3地址,多通道但仅有一个寄存器
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //16 bit
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设不自增,始终为ADC3地址
DMA_Init(DMA2_Stream0,&DMA_InitStructure); //DMA2_Stream0 初始化
DMA_Cmd(DMA2_Stream0,ENABLE); //DMA2_Stream0 使能
ADC_DMARequestAfterLastTransferCmd(ADC3, ENABLE); //源数据变化时开启DMA传输
ADC_DMACmd(ADC3, ENABLE); //ADC3_DMA使能
ADC_Cmd(ADC3, ENABLE); //ADC3 使能
// ADC_SoftwareStartConv(ADC3); //软件触发ADC转换
}
//******清空ADC与DMA的中断标志位,起到再次触发ADC与DMA的作用,否则ADC只会采集一波数据然后DMA搬运,若需多次采集必须使用次函数,可在数据处理完成后再次使用***//
void ADC_DMA_Trigger(){
DMA_Cmd(DMA2_Stream0,DISABLE);//若用循环模式就可不用disable再enable,关掉再重启主要起重装NDTR和保护数据的作用
// DMA_SetCurrDataCounter(DMA2_Stream0,AD3_Value_Length);
// DMA_ClearITPendingBit( DMA2_Stream0 ,DMA_IT_TCIF0|DMA_IT_DMEIF0|DMA_IT_TEIF0|DMA_IT_HTIF0|DMA_IT_TCIF0 );
DMA_ClearITPendingBit( DMA2_Stream0 ,DMA_IT_TCIF0);
ADC_ClearITPendingBit(ADC3,ADC_IT_OVR);//ADC3->SR = 0;
DMA_Cmd(DMA2_Stream0,ENABLE);
}
#ifndef __ADCDMA_H__
#define __ADCDMA_H__
#include "stm32f4xx.h"
#define AD3_Value_Length 256 //因为采样点数是256,故需要长度为256的数组
extern uint16_t AD3_Value[AD3_Value_Length];
void AD3_Init();
void ADC_DMA_Trigger();
#endif
Timer部分:
由于是定时器触发ADC所以采样率由定时器控制,即:
fs = 84MHz/(arr*psc)
#include "Timer.h"
//主频84M
//TIM2 32bit
void TIM2_Init(uint16_t arr, uint16_t psc){
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //TIM2 时钟使能
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //时基结构体
TIM_TimeBaseInitStructure.TIM_Period = arr; //设置自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler =psc; //设置预分频值
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式
TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update); //更新溢出向外触发
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //时基初始化
TIM_Cmd(TIM2, ENABLE); //定时器使能
}
#ifndef __TIMER_H__
#define __TIMER_H__
#include "stm32f4xx.h" // Device header
void TIM2_Init(uint16_t arr, uint16_t psc);
#endif
main部分:
#include "stm32f4xx.h" // Device header
#include "delay.h"
#include "usart.h"
#include "fft_calculate.h"
#include "math.h"
#include "Timer.h"
#include "ADCDMA.h"
/********************************************工程介绍***********************************************************
此FFT工程,利用Timer定时触发ADC3_IN9触发AD采集并利用DMA2_Stream0搬运至AD3_Value[256] 即采样256个点
/***********************************FFT相关理论计算介绍********************************************************
设:
被采目标信号频率:f
被采目标信号周期:T = 1/f
采样频率:fs
采样周期:Ts = 1/fs
采样点数:NPT
采样总时间:t = NPT*Ts
频谱图频率分辨率:f0 = fs/NPT
采到的被采信号周期数:NT = t/T
一个被采信号周期的采样点数:fs/f
*************************************************************************************************************/
u16 i;
int main(){
delay_init(168);
uart_init(9600);
printf("Start !\r\n");
TIM2_Init(5-1, 525-1);//fs=32KHz fs=84MHz/(arr*psc)
AD3_Init();
delay_ms(10);
while(1){
ADC_DMA_Trigger(); //每次都要重新触发,否则只采样一波数据(NPT个AD值)便停止了
for(i=0; i<NPT; i++){
InBufArray[i] = ((signed short)(AD3_Value[i])) << 16; //将AD值移至实部
printf("%d\r\n",AD3_Value[i]); //打印AD值(片内12位AD:0~4095)
}
cr4_fft_256_stm32(OutBufArray, InBufArray, NPT); //FFT运算
GetPowerMag(); //获取信号各次谐波分量的幅值
for(i=0; i<NPT/2; i++){
printf("%d:%d\r\n",i,MagBufArray[i]); //打印幅值
}
delay_ms(5000);
}
}
在用函数cr4_fft_256_stm32(OutBufArray, InBufArray, NPT)
时,注意将AD值送入InBufArray[]
的实部,它的高16位表示复数的实部,低16位表示虚部。
而函数GetPowerMag()
是对FFT后输出的复数OutBufArray[]
求模值。
关于函数ADC_DMA_Trigger()
的作用,这里本人才疏学浅也很迷惑,但没有这一句有关中断标志位清除,确实只会运作一次,即只会采一次256个点运算,然后就没有然后了(还望高手指点迷津[抱拳])。我通过debug模式观察寄存器,发现ADC并没有在采样,DMA也没有在搬运数据。但这反而更方便了,让每一次采集都变得可控。需要采集时,使用此函数触发即可。
实践
做了这么多工作现在来实操验证一下。此次实验输入的被采的信号均为正弦波。
输入1KHz 0V到3V(即幅度:3V,偏移:1.5V)
用示波器先浅浅观察一下:
将信号接入信号采集的引脚,这里是PF3脚 ADC3_IN9。运行程序观察串口打印的数据。这里我将AD值和幅值都打印了出来。我们先来看看AD值下的图形:
很明显,通过数Excel画出来的这个正弦波周期个数可知正好是8个,满足公式:
采到的被采信号周期数:NT = t/T = 0.008s/0.001s = 8
打印的AD值:
一个周期采到32个点,满足公式:
一个被采信号周期的被采到的点数:fs/f = 32 000Hz/1000Hz = 32
顺便再看一下峰值大概在3750左右,根据公式:
Voltage = AD_Value*3.3V/4096
和我们输入的峰值3V的信号差不太多。
接下来看一下幅度谱的情况:
前文已经提及过,幅度谱的负半轴是没有什么作用的,所以这里只有原点(直流分量)和正半轴的数据。现在我们来分析一下红框框住的数据:
0:
处就是所说的直流分量。因为这里我们是拿AD值计算的FFT,所以输出的值也是类似于AD值,所以我们又可以用:Voltage = AD_Value*3.3V/4096 计算可得1.504,满足我们所设置的偏移量也就是直流分量。
8:
处就是我们的1KHz的幅度信息了。为什么会是8呢?我们又根据前文的分析:
频谱图频率分辨率:f0 = fs/NPT = 32 000Hz/256 = 125Hz
在这里的意思就是横坐标一格代表125Hz,而我们是1000Hz的信号,自然在第8个点的位置了(1000Hz/125Hz=8)。
再来看幅值信息,通过公式Voltage = AD_Value*3.3V/4096发现差不多也是1.5V左右。但这只是一半,所以在1KHz处的幅度是1.5*2=3V。
它的幅度谱长这样:
输入2KHz 0V到3V(即幅度:3V,偏移:1.5V)
这一次我们换成2KHz的试试看。
对于2KHz的信号,采到信号的16个周期,也是满足此公式的:
采到的被采信号周期数:NT = t/T
对于2KHz的信号,一个周期采到16个点,也是满足此公式的:
一个被采信号周期的被采到的点数:fs/f = 32 000Hz/2000Hz = 16
这里为什么会在第16个点,想必大家也能够理解了。(2000Hz/125Hz=16)
这里提一嘴:对于1KHz的信号,基波频率是1KHz,二次谐波是2KHz,三次谐波是3KHz。也就是第8,第16,第24个点。
输入125Hz 0V到3V(即幅度:3V,偏移:1.5V)
这里我们尝试直接输入f0=125Hz。
如愿以偿的出现在了第一个点的位置。
输入200Hz 0V到3V(即幅度:3V,偏移:1.5V)
这里或许就有人好奇了,输入的信号如果不是125Hz的整数倍会怎样,我们来试试。输入一个200Hz看看。
可以看到,幅值散在了理论上200Hz附近的各个位置,但实际上这个横坐标上是没有200Hz这个点的,所以造成了这样的结果。这样的幅值信息也是相当不准确的,所以我们在应用的的过程中要尽量避免这种情况。所以控制好采样率fs是关键! 毕竟用dsp库的话采样点数NPT是没法控制的嘛。根据公式:
频谱图频率分辨率:f0 = fs/NPT
让横坐标的点尽量踩在被采信号的频率上!
输入1KHz 1V到3V(即幅度:2V,偏移:2V)
这里我们再试一下改变一下偏移和整体幅度看看。
再次利用公式:
Voltage = AD_Value*3.3V/4096
Vdc = 2491 * 3.3V / 4096 = 2.007 V
V(1KHz) = (1255 * 3.3V / 4096) * 2 =2.022 V
与输入的信号差不多相符。
结语
在做FFT时,对其概念的理解尤为重要。从时域到频域,被采信号的频率、采样率、采样点数、频率分辨率的概念和物理意义以及它们之间的关系等等,都是需要去理解的。
而FFT的作用远不止这些,除了幅值,它还可以进行相位相关的运算。本文利用的是256个点,如果有需求也可以尝试使用cr4_fft_1024_stm32(OutBufArray, InBufArray, NPT)
取1024个点。根据需求控制好采样率。利用Timer+ADC+DMA的模式充分利用了外设资源,在使用的过程中注意它们之间联系的相关配置。
ps.
以上内容均为本人个人理解和实践做出的总结。文笔较差,还望包涵 。如有错误或表达不当之处,欢迎指正! 感谢。