STM32F4 Timer+ADC+DMA+FFT的理解与应用

STM32F407 利用Timer+ADC+DMA在FFT上的理解与运用

最近遇到有关信号的总谐波失真THD值的测量,根据THD的计算公式可知我们需要知道该信号各次谐波分量的幅值,所以需要将ADC采样到的信号进行FFT,将时域上的信号转换到频域上,获得其幅值。

这里我们假设要采集的信号为1Khz



方案

利用STM32F407上的定时器Timer来触发ADC采样,并利用DMA搬运采样到的AD值,最后用dsp库里的有关FFT运算的函数进行各次谐波幅值的获取。
dsp



原理

设定数据

被采信号频率: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.

以上内容均为本人个人理解和实践做出的总结。文笔较差,还望包涵 。如有错误或表达不当之处,欢迎指正! 感谢。



  • 32
    点赞
  • 84
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
### 回答1: 《stm32f4库函数开发指南 pdf》是一本关于使用STM32F4系列微控制器的库函数进行开发的指南手册。这本指南针对初学者,详细介绍了如何使用STM32F4库函数进行开发。 这本指南介绍了STM32F4系列微控制器的库函数以及如何使用这些函数进行硬件的驱动、中断处理和数据处理等。从函数定义、函数调用和参数说明等方面逐一进行讲解,让读者能够深入了解函数的底层实现原理,并掌握函数的使用。 此外,这本指南还介绍了基于STM32F4系列微控制器的嵌入式开发流程。包括搭建开发环境、创建工程、编写代码、编译程序、烧录程序等步骤。通过实例代码和详细的注释,让读者能够快速理解和掌握嵌入式开发的基本流程。 总的来说,《stm32f4库函数开发指南pdf》是一本非常实用的嵌入式开发指南,可以帮助初学者快速入门STM32F4系列微控制器的开发,并且精通库函数的使用。如果你是一位初学者,或者想要深入了解STM32F4系列微控制器的开发,这本指南是非常值得一读的。 ### 回答2: 《STM32F4库函数开发指南》是一本关于STM32F4单片机库函数使用的指南,适合从事STM32F4单片机开发的软硬件工程师阅读。该书详细介绍了STM32F4库函数的使用方法,包括GPIO、SPI、USART、ADC等常用模块的操作,同时还介绍了如何利用库函数进行中断处理、DMA数据传输等高级应用。该书使用通俗易懂的语言,采用实践教学的方式,对每个模块的常见操作进行了具体的实例讲解,对初学者非常友好。 该书主要内容包括:STM32F4开发环境搭建、STM32F4库函数使用方法、STM32F4常用外设操作、中断处理、DMA数据传输等。 其中,STM32F4库函数使用方法部分详细介绍了库函数的各种使用方法,如库函数初始化、寄存器配置、数据读取等等。常用外设操作部分介绍了GPIO、SPI、USART、ADC等模块的常用操作方法,同时还包括了SD卡、NAND Flash、WIFI模块等特殊外设的使用方法。中断处理部分详细介绍了如何使用中断操作来提高STM32F4的效率和实现高级应用DMA数据传输部分详细介绍了如何利用DMA实现数据传输和网络通信等高级应用。 总的来说,《STM32F4库函数开发指南》是一本非常实用的STM32F4开发指南,适合初学者和有一定STM32基础的开发者参考学习,并能在实际开发中得到应用。 ### 回答3: "stm32f4库函数开发指南 pdf" 是一本关于STM32F4芯片的开发指南。这本指南主要介绍了STM32F4库函数开发的重点,包括如何配置寄存器、使用标准外设库和HAL库等。对于初学者来说,这是一本非常好的学习资料。 这本指南共分为16章,介绍了STM32F4芯片的各种基础知识以及如何使用库函数进行开发。首先是对芯片的介绍,包括芯片的架构、特性、资源和开发环境等。接着,指南详细讲解了如何使用库函数进行GPIO、UART、SPI、I2C等外设的编程,其中讲解了相关寄存器的配置和库函数的使用方法。最后,指南还介绍了如何使用中断处理和DMA传输等高级特性,提高程序的效率和可靠性。 总的来说,这本指南对于学习STM32F4库函数开发非常有用。它适合初学者学习和实践,通过实验和练习能够让人快速地掌握STM32F4库函数开发的核心技能。同时,对于有一定基础的开发者来说,也能够提供一些新的思路和技巧,让他们更加深入地了解STM32F4芯片的特性和工作原理。总之,这本指南的价值非常高,是入门学习STM32F4库函数开发的必备资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值