关于adc采样与fft转换之间的关系与应用
最近在要实现一个功能,对外部波形进行采样,然后用官方fft变换对外部波形进行谐波分析。但是本人数学不好,不喜欢研究算法,不喜欢看一堆公式,不想要知道原理,只想知道如何应用。在网上找了很久都没有找到想要的demo,大部分都解释得不够全面,自己研究后,上传一包源码给大家分享。本人能力有限,关于后文对其中的解释,有不正确的地方,还请大家指正。
如果有对傅里叶变换原理感兴趣的朋友,强烈推荐大家看这一位大佬的博客,对理解傅里叶分析的意义很有帮助:https://www.cnblogs.com/h2zZhou/p/8405717.html
附上完整的工程链接,需要的朋友请自行下载(代码中的注释部分,采用ANSI编码格式):https://github.com/WangYooNestik/STM32F10X-ADC-FFT
1.说一下总的框架
2.说一下采样频率及点数的选择(重点)
由于fft变换需要考虑多个变量对最终结果的影响,比较难办;我建议,对采样频率及点数的选择按以下步骤进行分析:
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.最后贴上源码
这个玩意儿好像下载源码要积分,我就直接贴在后面了,讲得不是很清楚大家见谅。
#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);
}
}
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();
}
}
}