FFT算法的实现(2)

上一篇文章当中已经推导出了整个频域上的表达式,这个表达式叫做蝶形运算,下面就借由蝶形运算实现FFT算法。

那么首先就要先说说蝶形运算。蝶形运算听着挺高大上的,但可以有一种简单方便的图形表示方法,为方便起见我这里再贴一次公式:


X [ k ] = X 0 [ k ] + W N k X 1 [ k ] X[k]=X_0[k]+W_N^{k}X_1[k] X[k]=X0[k]+WNkX1[k]
X [ k + N 2 ] = X 0 [ k ] − W N k X 1 [ k ] X[k+\frac N2]=X_0[k]-W_N^{k}X_1[k] X[k+2N]=X0[k]WNkX1[k]


那么这个蝶形运算可以用图形表示为

fig1

当然,可以看出一次蝶形运算虽然简化了运算,但仍然需要知道 X 0 [ k ] X_0[k] X0[k] X 1 [ k ] X_1[k] X1[k] ,就是说在进行蝶形运算之前我们仍需对 x 0 [ n ] x_0[n] x0[n] x 1 [ n ] x_1[n] x1[n] 进行DFT得到 X 0 [ k ] X_0[k] X0[k] X 1 [ k ] X_1[k] X1[k] 然后才能得到完整的 X [ k ] X[k] X[k] ,我们知道DFT运算起来是很麻烦的,但是注意到,这里DFT的运算量已经减少了一半,也就是每次只对 N 2 \large \frac N2 2N个点进行DFT,那么可以由此得到启示,能不能像得到 X [ k ] X[k] X[k] 一样通过蝶形运算来得到 X 0 [ k ] X_0[k] X0[k] X 1 [ k ] X_1[k] X1[k] 呢?这是可以的,对于 X 0 [ k ] X_0[k] X0[k] 只需将其分为奇偶两组,然后再通过一次蝶形运算即可得到 X 0 [ k ] X_0[k] X0[k] ,这时单次DFT的运算量为 N 4 \large \frac N4 4N,重复以上过程,直到DFT的运算量为1,此时就会出现一个有趣的事情:


X [ 0 ] = ∑ n = 0 0 x [ n ] e − i 2 π ∗ 0 N n = x [ 0 ] X[0]=\sum\limits_{n=0}^{0} x[n]e^{-i\frac{2\pi *0}{N}n}=x[0] X[0]=n=00x[n]eiN2π0n=x[0]


此时就不需进行DFT了,直接进行蝶形运算就可以了,这就是FFT算法。以上的说明可能比较抽象,下面举一个8点FFT的例子,假设使用 x [ 0 ] x[0] x[0]$x[7]$表示要进行FFT的8个点,用$X[0]$ X [ 7 ] X[7] X[7]表示对应频域上的点,那么根据以上思路,需先将0~7分为奇偶两组,即:{0,2,4,6}和{1,3,5,7},然后继续分组:{0,4}和{2,6}、{1,5}和{3,7},最后一次分组:{0}和{4}、{2}和{6}、{1}和{5}、{3}和{7}。经过三次分组后发现DFT的运算量已经是1了,这时再进行蝶形运算,如下图:


这里写图片描述


按照这个图我们只需要用程序实现这个图,就可以得到FFT的结果。但是在此之前还有一件事,就是分组问题,可以看到我们首先需要将01234567的顺序换为04261537的顺序,这个顺序有什么规律呢?将其转换为二进制可以看到:000、100、010、110、001、101、011、111。其实就是逆向的二进制加法,最左边为最低位,最右边为最高位,从低位开始加1,然后向高位进位。
所以要完成一个FFT分两步,第一步先将拿到的数组进行逆序,第二步进行蝶形运算。第二步的是最复杂的,但总的可以将其看成两个for循环来实现,以上图为例,8点的FFT需要进行3次蝶形运算,每一次蝶形运算又分为几个单次的蝶形运算,如第1次蝶形运算就分为4个小的单次蝶形运算。

先给出C语言版本(话说啥时候用结构体重构一下。。。)

/* linux下gcc编译 */
#define _USE_MATH_DEFINES
#include <math.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/time.h>
#include <time.h>

#define FFTN 32  //fft的点数
#define PWE 5    //2^PWE=NFFT
#define LEN FFTN*2
#define FRE 2000      //采样频率
#define RS 62.5       //分辨率=FRE/FFTN
//由于一个点是复数,因此用偶数脚标表示实部,奇数脚标表示虚部

float fft_input[LEN] = {0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,
                         8,0,9,0,10,0,11,0,12,0,13,0,14,0,15,0,
                         16,0,17,0,18,0,19,0,20,0,21,0,22,0,23,0,
                         24,0,25,0,26,0,27,0,28,0,29,0,30,0,31,0};
                         
                         /*
float fft_input[LEN] = {31,0,30,0,29,0,28,0,27,0,26,0,25,0,24,0,
                         23,0,22,0,21,0,20,0,19,0,18,0,17,0,16,0,
                         15,0,14,0,13,0,12,0,11,0,10,0,9,0,8,0,
                         7,0,6,0,5,0,4,0,3,0,2,0,1,0,0,0};
                         */
float fft_output[LEN];
float MAX[2]; //保存最后得出的结果

int ReverseArrange(void);
int Redix2FFT(void);
void IntegrateData(void);

void main(void)
{
  int i;
  clock_t begin, end;
    
  begin = clock();
  ReverseArrange(); //倒序
  Redix2FFT();  //FFT变换
  end = clock();
  printf("%lfs\n",(double)(end-begin)/CLOCKS_PER_SEC);

}
/*倒序函数,将fft_input进行逆向排序
 *               0   1   2   3   4    5     6     7
   倒序前脚标: 0-1 2-3 4-5 6-7 8-9 10-11 12-13 14-15
                 0   4   2    6    1    5    3    7
   倒序后脚标: 0-1 8-9 4-5 12-13 2-3 10-11 6-7 14-15
*/
//其中:2^PWE=NFFT,PWE表示次幂,即脚标由几位二进制组成
int ReverseArrange(void)
{
  int8_t i,j;
  int8_t tmp = 0x00;  //用来表示脚标
  //先给第一个复数赋值
  fft_output[0] = fft_input[0];
  fft_output[1] = fft_input[1];
  //对剩下的数进行倒序
  for(i=0;i<(FFTN-1)*2;i+=2){
      j = PWE-1;  //得出需要左移的位数
      //逆向二进制加法
      while((tmp & (1<<j)) != 0){
          tmp &= ~(1<<j); //把第j位置零
          j--;
      }
      tmp |= (1<<j);  //这里最后得出的是:0 4 2 6 1 5 3 7
      fft_output[i+2] = fft_input[tmp*2];   //按照新的顺序给输出赋值
      fft_output[i+3] = fft_input[tmp*2+1];
  }
  return 1;
}

/*基2fft运算,需先将fft_input倒序输入到fft_out中*/
int Redix2FFT(void)
{
  int8_t layer; //表示FFT的层数
  int8_t pmul; //= 2^(PWE - layer),表示当前层中需要进行几次小FFT,FFTN/pmul表示当前小FFT的点数
  int8_t i; //表示正在进行第几次小FFT
  int8_t j; //= 2^(layer - 1),表示当前层小FFT中蝶形运算的次数
  int8_t k; //每次小FFT中,正在进行第几次蝶形运算
  int8_t currentBase; //表示当前层的小FFT中,第一个元素的脚标
  int8_t current; //=currentBase + k,表示当前需要进行蝶形运算的元素的脚标
  int8_t another; //=current + j,表示当前需要进行蝶形运算的元素的脚标
  int8_t m;
  float Re,Im; //实部、虚部的暂存区
  float TPOA;  //=2 * pi / FFTN,FFT旋转因子WNK的w值
  float TPOATP;//=TPOA * pmul,表示小FFT中WNK的w值
  float reFactor,imFactor;  //=cos(TPOATP * k),=-sin(TPOATP * k),表示WNK的实部和虚部

  //下面的注释假设FFTN=8

  TPOA = 2 * M_PI / FFTN; //TPOA = PI/4
  for(layer=1;layer<=PWE;layer++){  //layer = 1,2,3
    j = 0x01<<(layer-1);  //使用math中的pow()会出现bug,使用左移运算减轻计算量
    pmul = 0x01<<(PWE - layer);
    for(i=0;i<pmul;i++){  //pmul=4时,i = 0,1,2,3
      currentBase = i * 4 *j;   //j = 1时,currentBase = 0,4,8,12
      TPOATP = TPOA * pmul;   //pmul=4时,TPOATP = PI
      for(k=0;k<j;k++){   //j=1时,k=0
         current = currentBase + k*2; //current = 0,4,8,12;
         another = current + j*2; //another = 2,6,10,14;
         //准备WNK
         reFactor = cos(TPOATP * k); //1
         imFactor = -sin(TPOATP * k);//0
         //乘上WNK
         Re = fft_output[another];  //备份实部
         fft_output[another] = fft_output[another] * reFactor 
            - fft_output[another+1] * imFactor; //fft_output[2] = 8
         fft_output[another+1] = fft_output[another+1] * reFactor
            + Re * imFactor;  //fft_output[3] = 0
         //蝶形加法
         Re = fft_output[current];  //Re=0
         Im = fft_output[current+1];  //Im=0
         fft_output[current] += fft_output[another];  //fft_output[0] = 8;
         fft_output[current+1] += fft_output[another+1];  //fft_output[1] = 0

         fft_output[another] = Re - fft_output[another];  //fft[2] = -8
         fft_output[another+1] = Im - fft_output[another+1];  //fft[3] = 0
      }
    }
  }
  for(m = 0; m < LEN; m += 2) {
     printf("[%d]: %f+%fi\n", m/2, fft_output[m], fft_output[m+1]);
  }
}
 /* 输出整合,计算出每个频率的幅值,取出幅值最大的点
  */
void IntegrateData(void)
{
  //计算复数的模,注意,所有的模都存到偶数项中
  //fft_output[0] = sqrt(fft_output[0]*fft_output[0]+fft_output[1]*fft_output[1])/FFTN;
  fft_output[LEN-2] = sqrt(fft_output[LEN-2]*fft_output[LEN-2]+fft_output[LEN-1]*fft_output[LEN-1])/FFTN;
  for(uint8_t i=2;i<(LEN-2);i+=2){ //最后一个点和第一个点已单独处理
    fft_output[i] = 2*sqrt(fft_output[i]*fft_output[i]+fft_output[i+1]*fft_output[i+1])/FFTN;
  }
  //找出其中模长最大的值,记录脚标和模长
  MAX[0] = 2;   //脚标
  MAX[1] = fft_output[2]; //模长
  for(uint8_t i=4;i<LEN;i+=2){
    if(MAX[1]<fft_output[i]){   //替换
      MAX[0] = i;
      MAX[1] = fft_output[i];
    }  
  }    
}

下面将这个算法在Arduino上实现,注释已经写得很完善了,这里就不多加说明。


/*从ADC读取数值后,使用FFT对波形进行分析,得出其中所含频率和对应幅度*/
/*  FFTN    fft时间(ms)  求最值时间(ms) 
 *   8         4  
 *   16        10 
 *   32        25              2
*/
#include <math.h>
#define FFTN 32  //fft的点数
#define PWE 5    //2^PWE=NFFT
#define LEN FFTN*2
#define FRE 2000      //采样频率
#define RS 62.5       //分辨率=FRE/FFTN
//由于一个点是复数,因此用偶数脚标表示实部,奇数脚标表示虚部
float fft_input[LEN] = {0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,
                         8,0,9,0,10,0,11,0,12,0,13,0,14,0,15,0,
                         16,0,17,0,18,0,19,0,20,0,21,0,22,0,23,0,
                         24,0,25,0,26,0,27,0,28,0,29,0,30,0,31,0};
                         
float fft_output[LEN];
float MAX[2]; //保存最后得出的结果
unsigned long curTime,lastTime;
void setup() {
  Serial.begin(115200);
}

void loop() {
  ReverseArrange(); //倒序
  lastTime = millis();
  Redix2FFT();  //FFT变换
  curTime = millis();
  Serial.print("fft=");
  Serial.println(curTime-lastTime);
  /* 以上是fft */
  //lastTime = millis();
  //IntegrateData();
  //curTime = millis();
  //Serial.print("int=");
  //Serial.println(curTime-lastTime);
}
/*倒序函数,将fft_input进行逆向排序
 *               0   1   2   3   4    5     6     7
   倒序前脚标: 0-1 2-3 4-5 6-7 8-9 10-11 12-13 14-15
                 0   4   2    6    1    5    3    7
   倒序后脚标: 0-1 8-9 4-5 12-13 2-3 10-11 6-7 14-15
*/
//其中:2^PWE=NFFT,PWE表示次幂,即脚标由几位二进制组成
int ReverseArrange(){
  int8_t i,j;
  int8_t tmp = 0x00;  //用来表示脚标
  //先给第一个复数赋值
  fft_output[0] = fft_input[0];
  fft_output[1] = fft_input[1];
  //对剩下的数进行倒序
  for(i=0;i<(FFTN-1)*2;i+=2){
      j = PWE-1;  //得出需要左移的位数
      //逆向二进制加法
      while((tmp & (1<<j)) != 0){
          tmp &= ~(1<<j); //把第j位置零
          j--;
      }
      tmp |= (1<<j);  //这里最后得出的是:0 4 2 6 1 5 3 7
      fft_output[i+2] = fft_input[tmp*2];   //按照新的顺序给输出赋值
      fft_output[i+3] = fft_input[tmp*2+1];
  }
  return 1;
}

/*基2fft运算,需先将fft_input倒序输入到fft_out中*/
int Redix2FFT(){
  int8_t layer; //表示FFT的层数
  int8_t pmul; //= 2^(PWE - layer),表示当前层中需要进行几次小FFT,FFTN/pmul表示当前小FFT的点数
  int8_t i; //表示正在进行第几次小FFT
  int8_t j; //= 2^(layer - 1),表示当前层小FFT中蝶形运算的次数
  int8_t k; //每次小FFT中,正在进行第几次蝶形运算
  int8_t currentBase; //表示当前层的小FFT中,第一个元素的脚标
  int8_t current; //=currentBase + k,表示当前需要进行蝶形运算的元素的脚标
  int8_t another; //=current + j,表示当前需要进行蝶形运算的元素的脚标
  float Re,Im; //实部、虚部的暂存区
  float TPOA;  //=2 * pi / FFTN,FFT旋转因子WNK的w值
  float TPOATP;//=TPOA * pmul,表示小FFT中WNK的w值
  float reFactor,imFactor;  //=cos(TPOATP * k),=-sin(TPOATP * k),表示WNK的实部和虚部
  
  //下面的注释假设FFTN=8
  
  TPOA = 2 * PI / FFTN; //TPOA = PI/4
  for(layer=1;layer<=PWE;layer++){  //layer = 1,2,3
    j = 0x01<<(layer-1);  //使用math中的pow()会出现bug,使用左移运算减轻计算量
    pmul = 0x01<<(PWE - layer);
    for(i=0;i<pmul;i++){  //pmul=4时,i = 0,1,2,3
      currentBase = i * 4 *j;   //j = 1时,currentBase = 0,4,8,12
      TPOATP = TPOA * pmul;   //pmul=4时,TPOATP = PI
      for(k=0;k<j;k++){   //j=1时,k=0
         current = currentBase + k*2; //current = 0,4,8,12;
         another = current + j*2; //another = 2,6,10,14;
         //准备WNK
         reFactor = cos(TPOATP * k); //1
         imFactor = -sin(TPOATP * k);//0
         //乘上WNK
         Re = fft_output[another];  //备份实部
         fft_output[another] = fft_output[another] * reFactor 
            - fft_output[another+1] * imFactor; //fft_output[2] = 8
         fft_output[another+1] = fft_output[another+1] * reFactor
            + Re * imFactor;  //fft_output[3] = 0
         //蝶形加法
         Re = fft_output[current];  //Re=0
         Im = fft_output[current+1];  //Im=0
         fft_output[current] += fft_output[another];  //fft_output[0] = 8;
         fft_output[current+1] += fft_output[another+1];  //fft_output[1] = 0

         fft_output[another] = Re - fft_output[another];  //fft[2] = -8
         fft_output[another+1] = Im - fft_output[another+1];  //fft[3] = 0
      }
    }
  }
  for(int8_t m=0;m<FFTN*2;m++){
    Serial.print(fft_output[m]);  
    Serial.print("\t"); 
  }
  Serial.println("");
}
 /*输出整合,计算出每个频率的幅值,取出幅值最大的点
 */
void IntegrateData(){
  //计算复数的模,注意,所有的模都存到偶数项中
  //fft_output[0] = sqrt(fft_output[0]*fft_output[0]+fft_output[1]*fft_output[1])/FFTN;
  fft_output[LEN-2] = sqrt(fft_output[LEN-2]*fft_output[LEN-2]+fft_output[LEN-1]*fft_output[LEN-1])/FFTN;
  for(uint8_t i=2;i<(LEN-2);i+=2){ //最后一个点和第一个点已单独处理
    fft_output[i] = 2*sqrt(fft_output[i]*fft_output[i]+fft_output[i+1]*fft_output[i+1])/FFTN;
  }
  //找出其中模长最大的值,记录脚标和模长
  MAX[0] = 2;   //脚标
  MAX[1] = fft_output[2]; //模长
  for(uint8_t i=4;i<LEN;i+=2){
    if(MAX[1]<fft_output[i]){   //替换
      MAX[0] = i;
      MAX[1] = fft_output[i];
    }  
  }    
}
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值