STM32F1X+ADC+FFT应用

关于adc采样与fft转换之间的关系与应用

2023.8.4更新
————————————————————————————————————————————————————————
最近可能又到了课设、毕设季,有很多同学问过一个问题,我之前没有注意到大家的疑惑,关于adc管脚的输入波形,有一个地方需要请大家注意。
描述:adc输入管脚上的波形,要注意波形的电位问题。由于单片机只能采集0~+3.3v的电压,而我们做这个fft变换,采集的是周期波形,那我们要保证输入的波形最大值和最小值都要处在0-+3.3v电压范围内。比如有的同学使用波形发生器,输出50Hz正弦波,理论上要保证波形峰峰值不能大于3.3v(建议不大于3v),并且要将波形抬升到1.6v电位上,也就是要在基波上加上1.6v直流偏置电压,来保证adc能采集到完整的正弦波形(而不是只能采集到正半波)。
————————————————————————————————————————————————————————

写在前面

最近在要实现一个功能,对外部波形进行采样,然后用官方fft变换对外部波形进行谐波分析。但是本人数学不好,不喜欢研究算法,不喜欢看一堆公式,不想要知道原理,只想知道如何应用。在网上找了很久都没有找到想要的demo,大部分都解释得不够全面,自己研究后,上传一包源码给大家分享。本人能力有限,关于后文对其中的解释,有不正确的地方,还请大家指正。

如果有对傅里叶变换原理感兴趣的朋友,强烈推荐大家看这一位大佬的博客,对理解傅里叶分析的意义很有帮助:https://zhuanlan.zhihu.com/p/19763358

附上完整的工程链接,需要的朋友请自行下载(文本采用utf-8编码格式):https://github.com/GoldenWong-yo/STM32F10X-ADC-FFT

直接进入主题

1.说一下总的框架

  • 1.目 的:用ADC采样值进行fft运算,求取采样后波形相关属性(本例主要分析50Hz正弦波,及高/低次谐波)
  • 2.方 案:计划ADC采样频率为512Hz,采样1024个点(也就是采样2秒钟)进行一次fft运算,使频域分辨率达到0.5Hz
  • 3.配 置:由于我用的系统时钟是72MHz,所以ADC本身不能配置到512Hz,采用tim3定时触发采样(当系统时钟为72MHz时,ADC时钟最慢是9MHz,采样周期最大约为256个时钟周期,所以ADC本身采样频率最慢约为35156.25Hz(大于我需要的512Hz),所以选择TIM定时采样)
  • 4.其 他:
    • 为什么要选512Hz:为了满足采样定理(fs > 100Hz);又因为我想要分析高次谐波;又因为我选了1024的fft变换,为了使频域分辨率识别方便(0.5Hz)。这句话有点绕,后文有详细解释。
    • 本来计划采样5120个点(使频域分辨率达到0.1Hz),但是单片机RAM空间不够,有条件的可以尝试
  • 5.相关知识(很重要,本文用到的知识点都在这)
    • 傅里叶变换的目的:求取被测波形的幅频特性/相频特性
    • 采样定理:采样频率需大于被采样波形频率的两倍
    • 频域分辨率 = fs/N,(fs:采样频率,N:采样点数)
    • 幅频特性分析范围(单位:Hz):0 ~ fs/2,(fs:采样频率)
    • 幅频特性序列:FFT_Mag[i] 代表的意思,频率分量为 (i*(fs/N)) 的幅值为 FFT_Mag[i]

2.说一下采样频率及点数的选择(重点)

由于fft变换需要考虑多个变量对最终结果的影响,比较难办;我建议,对采样频率及点数的选择按以下步骤进行分析:

  • 1.采样点数分析:
    • 不考虑耗时,采样点数越多越好。
    • 看你的平台的资源情况,分析整个项目的RAM大小及占用情况,先选择你准备用多少点进行fft变。当你的fs确定下来,采样点数越多,你的频域分辨率越小,你最终的幅频特性序列就越精确。当然我们一般认为越精确越好。(举个例子:我分析了1024个点,则fft变换输入数组大小 = 1024 * 4Byte = 4K,输出数组大小 = 1024 * 4Byte = 4K,这样就已经占用8KRAM)
  • 2.采样频率分析:
    • 满足采样定理:采样频率需大于被采样波形频率的两倍(假设你需要采样的波形频率为50Hz,则采样频率必须在100Hz以上)。
    • 如果你只想分析一个特定频率的波形(比如50Hz),不考虑它的高(>50Hz)/低(<50Hz)次谐波,只需要满足采样定理即可;
    • 如果你需要分析特定频率及低次谐波,只需要满足采样定理即可;
    • 如果你需要分析特定频率,及它的高/低次谐波。那你需要优先考虑你的采样频率对你需要分析的最高次谐波,要满足采样定理;也就是fs要 > 2 * 最高次谐波频率。
      反过来说,就是要考虑你的幅频特性分析范围。
  • 3.采样精度确认:
    • 确定了以上两点后,计算频率分辨率(频域分辨率 = fs/N),看是否满足你的精度要求。

3.官方给出了fft库函数啥意思,咋用?(官方库在哪里下载?我也不知道)

/* 64 points*/
void cr4_fft_64_stm32(void *pssOUT, void *pssIN, uint16_t Nbin);
/* 256 points */
void cr4_fft_256_stm32(void *pssOUT, void *pssIN, uint16_t Nbin);
/* 1024 points */
void cr4_fft_1024_stm32(void *pssOUT, void *pssIN, uint16_t Nbin);

官方给出了3个fft函数,分别是64、256、1024个点的fft变换(就是采样的样本是64、256、1024个样本)。
pssIN:是adc采样的样本
pssOUT:调用官方函数后,输出的傅里叶序列
Nbin:样本的数量(采了多少个点)

为什么是64、256、1024这个3个值?因为官方使用的是基4蝶形算法(啥意思?),简单理解要使用官方fft库函数,必须遵循采样点数Nbin,是4的n次方。

4.傅里叶变换后,输出了个啥,咋用?

我们需要有个基础知识,傅里叶变换的目的:求取幅频特性/相频特性

fft变换后,输出的是一个傅里叶序列(怎么用?)。傅里叶序列本身,不是我们能够肉眼分析的东西,我们还需要对傅里叶序列进行计算,求取幅频特性/相频特性序列。(下面这段代码就是求取幅频特性)

#define SAMPLS_NUM 1024
u32 FFT_OutData[SAMPLS_NUM] = {0};		//fft输出序列
u32 FFT_Mag[SAMPLS_NUM/2] = {0};		//幅频特性序列(序号代表频率分量,值代表幅值大小。由于FFT的频谱结果是关于奈奎斯特频率对称的,所以只计算一半的点即可)
/********************************************************************************************************
函数名称:GetPowerMag()
函数功能:计算各次谐波幅值
参数说明:
备  注:先将ADC_FFT_OutData分解成实部(X)和虚部(Y),然后计算幅频特性序列FFT_Mag
         本函数参考网页:https://wenku.baidu.com/view/08ccee0984868762cbaed532.html,关于幅频特性计算部分
**********************************************************************************************************/
void GetPowerMag(void)
{
	signed short lX,lY;
	float X,Y,Mag;
	unsigned short i;

	for(i=0; i<SAMPLS_NUM/2; i++)
	{
		lX = (FFT_OutData[i] << 16) >> 16;
		lY = (FFT_OutData[i] >> 16);
		
		X = SAMPLS_NUM * ((float)lX) / 32768;
		Y = SAMPLS_NUM * ((float)lY) / 32768;
		
		Mag = sqrt(X * X + Y * Y) / SAMPLS_NUM;
		
		if(i == 0)
			FFT_Mag[i] = (unsigned long)(Mag * 32768);
		else
			FFT_Mag[i] = (unsigned long)(Mag * 65536);
		//printf("%ld\r\n",lBufMagArray[i]);
	}
	//printf("\r\n\r\n");
}

FFT_Mag[SAMPLS_NUM/2],就是幅频特性序列,到此傅里叶变换的目的就达到了。

我输入的是50Hz正弦波,可以看到FFT_Mag[100]这个值,远大于其他值。证明频率分量为50Hz(频域分辨率(0.5Hz) * 100)的谐波,幅值最大,所以可以知道fft后是正确的。

5.最后贴上源码

这个玩意儿好像下载源码要积分,我就直接贴在后面了,讲得不是很清楚大家见谅。

  • ADC配置相关
#define ADC_CHANNEL_NUMS 	3
#define SAMPLS_NUM        	1024

#define TIM2_PERIOD 1953
#define TIM3_PERIOD 1953


u16 ADC_SourceData[SAMPLS_NUM][ADC_CHANNEL_NUMS] = {0};



void ADC_GPIO_Configuration(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);				//使能GPIOA时钟

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_4|GPIO_Pin_5;		//管脚1/4/5
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;						//模拟输入模式
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);								//GPIO组
}

void ADC_TIM2_GPIO_Configuration(void)        //ADC配置函数
{
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //使能GPIOA时钟

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;				//TIM2_CH3
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void ADC_TIM2_Configuration(void)
{ 
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_OCInitTypeDef TIM_OCInitStructure;

	ADC_TIM2_GPIO_Configuration();
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);

	TIM_DeInit(TIM2);

	//配置tim2触发频率为512Hz
	TIM_TimeBaseStructure.TIM_Period = TIM2_PERIOD - 1;				//设置2ms一次TIM2比较的周期
	TIM_TimeBaseStructure.TIM_Prescaler = 71;						//系统主频72M,这里分频71,相当于1000K的定时器2时钟
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM2, & TIM_TimeBaseStructure);

	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;	//TIM_OutputState_Disable;
	TIM_OCInitStructure.TIM_Pulse = (TIM2_PERIOD - 1) / 2;
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;		//如果是PWM1要为Low,PWM2则为High
	TIM_OC3Init(TIM2, & TIM_OCInitStructure);
	
	//TIM_InternalClockConfig(TIM2);
	
	//TIM_OC3PreloadConfig(TIM2,TIM_OCPreload_Enable);
	
	//TIM_UpdateDisableConfig(TIM2,DISABLE);

	//TIM_Cmd(TIM2, ENABLE);//最后面打开定时器使能

	//TIM_CtrlPWMOutputs(TIM2,ENABLE);
}

void ADC_TIM3_Configuration(void)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; 
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);

	TIM_TimeBaseInitStructure.TIM_Period = TIM3_PERIOD - 1;		//设置ADC_CHANNEL_NUMS * 512Hz采样频率
	TIM_TimeBaseInitStructure.TIM_Prescaler = 71;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;			//时钟分频,TIM3是通用定时器,基本定时器不用设置
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;		//向上扫描
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
	
	TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);				//选择TRGO作为触发源为定时器更新时间
	
	TIM_Cmd(TIM3,ENABLE);												//开启定时器3
}

void ADC_DMA_NVIC_Configuration(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;

	NVIC_InitStructure.NVIC_IRQChannel					 = DMA1_Channel1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority		 = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	DMA_ClearITPendingBit(DMA1_IT_TC1);

	DMA_ITConfig(DMA1_Channel1,DMA1_IT_TC1,ENABLE);
}

void ADC_DMA_Configuration(void)
{
	DMA_InitTypeDef  DMA_InitStructure;

	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

	DMA_DeInit(DMA1_Channel1);
	DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR;
	DMA_InitStructure.DMA_MemoryBaseAddr     = (u32)ADC_SourceData;
	DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_BufferSize         = ADC_CHANNEL_NUMS*SAMPLS_NUM;
	DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;
	DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
	DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);

	DMA_Cmd(DMA1_Channel1, ENABLE);//使能DMA	

	ADC_DMA_NVIC_Configuration();
}

void ADC_Init_Configuration(void)//ADC配置函数
{
	ADC_InitTypeDef  ADC_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);

	ADC_DeInit(ADC1);
	ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
	ADC_InitStructure.ADC_ScanConvMode       = ENABLE;								//如果是单通道,关闭扫描模式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_T3_TRGO;
	ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfChannel       = ADC_CHANNEL_NUMS;
	ADC_Init(ADC1, &ADC_InitStructure);

	//==========================================================================   
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1,  1, ADC_SampleTime_239Cycles5);	//AI_VS_A1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_4,  2, ADC_SampleTime_239Cycles5);	//AI_VS_B1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_5,  3, ADC_SampleTime_239Cycles5);	//AI_VS_C1

	ADC_DMACmd(ADC1, ENABLE);
	
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1));
	ADC_StartCalibration(ADC1);
	while(ADC_GetCalibrationStatus(ADC1));

	ADC_ExternalTrigConvCmd(ADC1, ENABLE);
	//ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}



/***********************************************************************************************************************************************
*adc初始化配置
*1.目     的:用ADC采样值进行fft运算,求取采样后波形相关属性(本例主要分析50Hz正选波,及高/低次谐波)
*2.方     案:计划ADC采样频率为512Hz,采样1024个点进行fft运算,使频域分辨率达到0.5Hz
*3.配     置:由于ADC本身不能配置到512Hz,采用tim3定时触发采样
*4.其     他:当系统时钟为72MHz时,ADC时钟最慢是9MHz,采样周期最大约为256个时钟周期,所以ADC本身采样频率最慢约为35156.25Hz(大于我需要的512Hz)
*            为什么要选512Hz:由于采用标准fft基4算法(采样点数必须是4的倍数),为了使频域分辨率为识别方便(0.5Hz),又为了满足采样定理
*            本来计划采样5120个点(使频域分辨率达到0.1Hz),但是单片机RAM空间不够,有条件的可以尝试
*5.相关知识:采样定理:采样频率需大于被采样波形频率的两倍
*            频域分辨率 = fs/N,(fs:采样频率,N:采样点数)
*            幅频特性序列:FFT_Mag[i]代表的意思,频率分量为i*(fs/N)的幅值为FFT_Mag[i]
************************************************************************************************************************************************/
void Adc_Init(void)
{
	ADC_GPIO_Configuration();
	
	//ADC_TIM2_Configuration();

	ADC_TIM3_Configuration();

	ADC_DMA_Configuration();

	ADC_Init_Configuration();
}

//ADC_DMA中断服务程序
void DMA1_Channel1_IRQHandler(void)
{
	if(DMA_GetITStatus(DMA1_IT_TC1) != RESET)
	{
		global.adc_finish_fg = true;
		DMA_ClearITPendingBit(DMA1_IT_TC1);
	}
}
  • fft函数
u32 FFT_SourceData[SAMPLS_NUM] = {0};	//fft输入序列
u32 FFT_OutData[SAMPLS_NUM] = {0};		//fft输出序列
u32 FFT_Mag[SAMPLS_NUM/2] = {0};		//幅频特性序列(序号代表频率分量,值代表幅值大小。由于FFT的频谱结果是关于奈奎斯特频率对称的,所以只计算一半的点即可)


#if 0
/****************************************************************************************************
函数名称:InitBufInArray()
函数功能:模拟采样数据,采样数据中包含3种频率正弦波(350Hz,8400Hz,18725Hz)
参数说明:
备     注:在lBufInArray数组中,每个数据的高16位存储采样数据的实部,低16位存储采样数据的虚部(总是为0)
****************************************************************************************************/

u16 lBufInArray[SAMPLS_NUM];

void InitBufInArray(void)
{
	unsigned short i;
	float fx;
	for(i=0; i<SAMPLS_NUM; i++)
	{
		fx = 1500 * sin(PI2 * i * 55.6 / Fs);
		lBufInArray[i] = ((signed short)fx)<<16;
	}
}
#endif

/********************************************************************************************************
函数名称:GetPowerMag()
函数功能:计算各次谐波幅值
参数说明:
备  注:先将ADC_FFT_OutData分解成实部(X)和虚部(Y),然后计算幅频特性序列FFT_Mag
         本函数参考网页:https://wenku.baidu.com/view/08ccee0984868762cbaed532.html,关于幅频特性计算部分
**********************************************************************************************************/
void GetPowerMag(void)
{
	signed short lX,lY;
	float X,Y,Mag;
	unsigned short i;

	for(i=0; i<SAMPLS_NUM/2; i++)
	{
		lX = (FFT_OutData[i] << 16) >> 16;
		lY = (FFT_OutData[i] >> 16);
		
		X = SAMPLS_NUM * ((float)lX) / 32768;
		Y = SAMPLS_NUM * ((float)lY) / 32768;
		
		Mag = sqrt(X * X + Y * Y) / SAMPLS_NUM;
		
		if(i == 0)
			FFT_Mag[i] = (unsigned long)(Mag * 32768);
		else
			FFT_Mag[i] = (unsigned long)(Mag * 65536);
		//printf("%ld\r\n",lBufMagArray[i]);
	}
	//printf("\r\n\r\n");
}

/************************************************
*由于在fft运算过程中不允许源数据更新
*将源数据拷贝到另一块内存
*此步骤可以被替换为:进行fft运算时,关闭adc转换
*************************************************/
void Get_FFT_Source_Data(EN_FFT_CHANNEL channel_idx)
{
	u16 i,j;

	for(i=0; i<SAMPLS_NUM; i++)
	{
		FFT_SourceData[i] = (u32)ADC_SourceData[i][channel_idx];
	}
}

void FFT_test(void)
{
	//InitBufInArray();
	Get_FFT_Source_Data(FFT_CHANNEL_1);
	cr4_fft_1024_stm32(FFT_OutData, FFT_SourceData, SAMPLS_NUM);
	GetPowerMag();
}
  • 主函数
int main(void)
{
	Adc_Init();

	while(1)
	{
		if(global.adc_finish_fg)
		{
			global.adc_finish_fg = false;
			FFT_test();				//512Hz采样频率,采样1024点,可以看到2s钟周期执行FFT_test();
		}
	}
}
STM32F1上实现ADCFFT的代码可以参考以下步骤: 1. 首先,需要初始化ADC模块。可以使用ADC_Init函数来配置ADC的参数,如采样模式、转换模式、外部触发源等。具体的初始化代码可以参考引用\[2\]中的Adc_Init函数。 2. 接下来,需要配置DMA传输。使用DMA可以实现ADC数据的自动传输,减轻CPU的负担。可以使用DMA_Init函数来配置DMA的参数,如数据传输方向、传输大小等。 3. 在获取到ADC数据后,可以使用FFT算法对数据进行处理。可以使用官方提供的FFT库函数,如cr4_fft_64_stm32、cr4_fft_256_stm32、cr4_fft_1024_stm32等。这些函数可以对指定长度的数据进行FFT计算,并将结果保存在输出数组中。 4. 最后,可以将FFT结果显示在屏幕上。可以使用相应的显示库函数来实现,如在正点原子屏幕上显示频谱图。 需要注意的是,以上代码是基于STM32F1的,如果你使用的是其他型号的STM32芯片,可能需要进行相应的修改。 希望以上信息对你有帮助! #### 引用[.reference_title] - *1* *2* [stm32f1频谱分析LCD显示(adc+tim+dma+fft)](https://blog.csdn.net/qq_42712104/article/details/104147475)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [STM32F1X+ADC+FFT应用](https://blog.csdn.net/qq_38495254/article/details/98883913)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 121
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值