基于STM32-AD7606的FFT交流采样

基于STM32-AD7606的FFT交流采样

前言

​ 之前写AD7606驱动移植的时候挖下的坑,关于添加FFT计算的部分,同时本文会对AD7606的驱动程序工作方式做简要的分析。鉴于本人的水平所限,如有错误希望大家能够在评论区指正。(这次写在前面了)

​ 本文不会对FFT的具体原理进行讨论,有许多大佬写的非常生动形象,我会放几个我觉得不错的链接在下面。

知乎3.2万赞 傅里叶分析

B站 李永乐老师 傅里叶变换如何理解

系统分析

AD7606运行机制分析

​ 本文使用的AD7606的运行机制主要分为以下几个阶段:

​ 初始化阶段 开始采样阶段 结束采样阶段

​ 具体的各个阶段的过程由下图展现:

初始化阶段

初始化阶段主要用来设置过采样模式

开始采样阶段

采样时采样频率主要由TIM4定时器来控制,定时器对时间的控制比用函数控制要精确的多,而且占用的CPU资源要少,所以一般对时间控制要求高的情况都会用定时器来控制。

具体对TIM4频率设置的代码如下

void MX_TIM4_Init(uint32_t _ulFreq)
{
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

	uint16_t usPrescaler;
  uint16_t usPreiod;

  if (_ulFreq == 0)
  {
      return;                                                  //采样率为0,停止采样
  }
  else if (_ulFreq <= 100)                                     //采样频率小于100hz
  {
      usPrescaler = 42000-1;
      usPreiod = 2000 / _ulFreq;
  }
  else if(_ulFreq <= 200000)                                    //采样频率100hz - 200khz
  {
      usPrescaler = 42-1;
      usPreiod = 2000000 / _ulFreq;
  }
  else
  {
      return;
  }
	
  htim4.Instance = TIM4;
  htim4.Init.Prescaler = usPrescaler;
  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim4.Init.Period = usPreiod -1;
  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }

}

前半部分是加的,后半部分使用cubemx生成的。

这里有一个大坑需要提醒一下,单片机的IO口的速度有限,虽然AD7606只有200K的采样率,但是8通道同时采样的数据量还是比较大的,而且我用的是软件spi的模式,所以在这种应用场景下,如果以200K 8通道同时采集的模式运行的话,32会因为IO口速度的问题跳入 ERROR函数(hal库的)

然后在定时器的中断中我们开启采集

/*
************************************************************
*函数名 ad7606_IRQSrc
*功能 定时调用函数,用于读取ADC转换数据
*形参 无
*返回值 无
************************************************************
*/
void ad7606_IRQSrc(void)
{
    uint8_t i;
    uint16_t usReadValue;
		static int j;

    /* 读取数据
    示波器监视,CS低电平持续时间 35us
    */
    AD_CS_LOW();
    for (i = 0; i < CH_NUM; i++)
    {
        usReadValue = ad7606_ReadBytes();
        if (g_tAD.usWrite < FIFO_SIZE)
        {
            g_tAD.usBuf[g_tAD.usWrite] = usReadValue;
            ++g_tAD.usWrite;
        }
    }

    AD_CS_HIGH();
//	g_tAD.usWrite = 0;
    ad7606_StartConv();

中断代码的前半部分如上,每次中断进入后会读取一个值。

开始FFT

数据存放

首先我在开头定义了这些变量,用于后面计算缓存与输出,具体功能都有注释在上面了:

/*采样率*/
static uint32_t sample_freq;                     //采样率
static float32_t Freq;                            //计算出的最大频率
static float32_t Amplitude;                      //最大频率的幅值
static float32_t DC_Component;                   //直流分量

/*定义使用中的数组大小和采样点数*/
#define sample_lenth 128                         //采样点数
static float32_t InPutBuffer[sample_lenth];       //定义输入数组 InPutBuffer     该数组是外部变量 
static float32_t MidBuffer[sample_lenth];         //定义中间数组
static float32_t OutPutBuffer[sample_lenth/2];    //定义输出数组 OutPutBuffer    该数组是一个静态数组
static float32_t FreqBuffer[(sample_lenth/4)-1];       //定义除了直流分量之外的频域数组

/*定义滤波中使用的中间数组*/
#define Filter_average_num 4                             //平均滤波样本数量
float32_t Mid_Filter_Freq_Buffer[Filter_average_num];    //频率平均滤波时的中间数组


/*定义fft运算中的参数*/
uint32_t fftSize = 64;                        //FFT计算点数
uint32_t ifftFlag = 0;                          //设置为fft变换   ifftFlag=1时为fft反变换
uint32_t doBitReverse = 1;                      //按位取反

/*定义计算结果存放*/
float32_t maxvalue;                             //计算出频域上最大值(包括直流分量)
uint32_t Index;                                 //最大值所在数组位置(包括直流分量)

float32_t Freq_maxvalue;                        //计算出频域上的最大值(不包括直流分量)
uint32_t Freq_Index;                            //最大值所在数组位置(不包括直流分量)

float32_t Virtual_value;                        //有效值
float32_t res;

然后我们在中断函数中加一段将采集到的数据存入数组当中。

这里有个需要注意的地方,在dsp库中的rfft函数应用较少,cfft的函数应用较多。但是复数的表示需要用两个数组元素来表示,前一个表示实部,后面的表示虚部

但是对于我们采集的信号来说,我们的信号只有实部,所以我们在存取数据的时候需要将数据的虚部设置为0。

存取代码如下:

if( j < fftSize )
		{
			
			InPutBuffer[2*j] = ((float)((short)g_tAD.usBuf[0])/32768/2);         //数据存放在输入数组的偶数位
			InPutBuffer[2*j+1] = 0;                                              //奇数位置零
			g_tAD.usWrite = 0;
			j++;
}

我们把这一段函数放在刚才的中断函数后面。

开始FFT计算

这里主要用到dsp库的这几个函数

void arm_cfft_f32(const arm_cfft_instance_f32 * S,float32_t * p1,uint8_t ifftFlag,uint8_t bitReverseFlag)

其中 *S是用于计算的结构体,

数组是需要计算的数组,计算的结果也会存放在这个数组中,

ifftFlag是傅里叶变换方向的标志, 0是正向fft,1是fft逆变换

bitReverseFlag是输出位是否翻转的标志,0为不使能,1为使能。

用于fft运算

void arm_cmplx_mag_f32(const float32_t * pSrc,float32_t pDst,uint32_t numSamples)*

其中 pSrc是输入数组,

pDst是输出数组,

numSample是数组数量

用于数组的求模运算

有了以上两个函数我们就可以进行fft运算啦,想看频谱图的话只要将最后输出的数组用图片显示就可以啦。

但是很多情况我们需要很快的知道最大的频率分量,所以我们还要用到一个函数:

void arm_max_f32(const float32_t * pSrc,uint32_t blockSize,float32_t * pResult,uint32_t * pIndex)

pSrc是输入数组,

blockSize是需要计算的大小,

pResult是数组中,

pIndex是最大值在数组中的位置。

这个函数可以求一个数组之中的最大值以及最大值的位置

计算函数程序

arm_cfft_f32(&arm_cfft_sR_f32_len64,MidBuffer,ifftFlag,doBitReverse);      //对输入数组进行FFT变换,变换结果将存放在输入数组中
	
arm_cmplx_mag_f32(MidBuffer,OutPutBuffer,fftSize);                         //对经过FFT变换的数组进行取模运算,运算结果将存放在OutPutBuffer数组中
	
arm_max_f32(OutPutBuffer,fftSize,&maxvalue,&Index);                        //输出数组中频域最大的数值和其所在数组中的位置

通过这三个函数可以得到频域上幅值最大的分量的位置

经过一系列的运算之后就可以输出啦,注释已经比较详细了,大家有不懂的可以在评论区提出

arm_cfft_f32(&arm_cfft_sR_f32_len64,MidBuffer,ifftFlag,doBitReverse);      //对输入数组进行FFT变换,变换结果将存放在输入数组中
	
arm_cmplx_mag_f32(MidBuffer,OutPutBuffer,fftSize);                         //对经过FFT变换的数组进行取模运算,运算结果将存放在OutPutBuffer数组中
	
arm_max_f32(OutPutBuffer,fftSize,&maxvalue,&Index);                        //输出数组中频域最大的数值和其所在数组中的位置

for(k=0;k<(fftSize/2-1);k++)
{
	FreqBuffer[k] = OutPutBuffer[k+1];                                       //取输出结果的一半,并且去除直流分量
	}
		
arm_max_f32(FreqBuffer,(fftSize/2-1),&Freq_maxvalue,&Freq_Index);          //去除直流分量后输出数组中频域最大的数值和其所在数组中的位置
		
Freq = (Freq_Index+1)*((float)sample_freq/(float)fftSize);                 //频率 = (N-1)*Fs/FFTSize        单位Hz
		
Amplitude = Freq_maxvalue/((float)fftSize/2)*10000;                        //频率幅度 = value/FFTSize/2*10   单位V
		
DC_Component = OutPutBuffer[0]/fftSize*10000;                              //直流分量 = value/FFTSize
		
		
res = ((Virtual_value-8)/43.3)/(4-((Virtual_value-8)/43.3))*2000;
		
		//printf("maxvalue = %f \r\n location = %d  \r\n",maxvalue,Index);
		
printf("Fmaxvalue = %f \r\n Amplitude = %f  \r\n  DC_Component = %f  \r\n  Virtual_value = %f  \r\n Res = %f  \r\n  ",Freq,Amplitude,DC_Component,Virtual_value,res);

至于有效值dsp库中也有支持的函数可以计算,但是我暂时没用到,所以这次就先不写的,挖个坑留到下次写

具体的工程文件我放在 github的仓库了,需要的同学可以自行取用,觉得有帮助的话帮我点个赞吧~

如果发现错误也希望在评论区指出,谢谢大家

  • 22
    点赞
  • 116
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值