要计算一个N点的离散傅立叶变换需要同一个N * N点的W矩阵(关于W矩阵请参阅信号与系统方面的书籍)相运算,随着N值的增大,运算次数显著上升,当点数达到1024时,需要进行复数乘法运算1, 048 ,576次,显然这种算法在实际运用中无法保证当点数较大时的运算速度,无法满足对信号的实时处理。
根据W矩阵中W元素的周期性和对称性我们可以将一个N点的DFT运算分解为两组N / 2点的DFT运算,然后取和即可,为进一步提高效率,将上述两个矩阵按奇偶顺序逐级分解下去。当采样点数为2的指数次方M时,可分解为M级子矩阵运算,全部工作量仅为:
复数乘法:M * N / 2次
复数加法:N * M次
而直接DFT需要的运算量为:
复数乘法:N * N次
复数加法:N * (N - 1 )次
当点数N为几十个点时FFT的优势还不明显,而一旦达到几千、几百个点时优势是十分明显的:
N = 1024时:DFT需1048576次运算,FFT仅需5120次运算,改善比204. 8 。
N = 2048时:DFT需4194304次运算,FFT仅需11264次运算,改善比达到372. 4 。
" 时间抽选奇偶分解快速离散傅立叶变换 " 的程序实现
当采样点数较多时,如变换前和变换后的序列都按自然顺序排列,则中间运算过程会占用大量的中间存储单元,造成效率的低下和存储单元的浪费。根据FFT的实现原理我们可以对采样序列进行逐次奇偶抽选,打乱以前的次序重新排序,然后按此顺序参加运算,可以实现 " 即位运算 " 提高存储单元的利用率。
(一)复数的描述方法
进行傅立叶变换时不可避免的要用到复数,而在VC中并没有现成的可用于表示复数的数据类型,可以自己定义一个含有两个成员变量的数据结构来表示复数,这两个成员变量可分别用于表示复数的实部与虚部:
typedef struct tagComplex ... {
floatRe;//复数的实部
floatIm;//复数的虚部
} Complex;
(二)倒序的实现
在进行快速傅立叶变换时,可以将输入的时域序列和输出的频域序列都按照自然顺序排列;也可以按照 " 蝴蝶图 " 所描述的计算方法对输入的时域序列按奇偶分解后的序列排序而输出的频域序列仍是按自然顺序排列的;还有一中方式是输入的时域序列是不进行抽选的自然序列,而输出的频域序列则是按奇偶分解后的顺序排列的。这三种方式各有优点,第一种对输入、输出不需要进一步排序,但由于自然排序不符合 " 蝴蝶图 " 运算规律,会占用大量中间存储单元。而后两种则无须中间存储单元,但需要倒一次序。权衡利弊,当采样点较多时还是采用后两种方式好,多一次倒序运算对现在的高性能计算机而言并不是什么负担。下面代码用于对原始采样序列的时间抽选奇偶分解工作,其中A、N分别表示指向采样序列复数数组的指针和序列的长度。
int NV2 = N / 2 ;
int NM1 = N - 1 ;
int I,J,K = 0 ;
ComplexT; // 用于中介的复数变量T
I = J = 1 ;
while (I <= NM1)
... {
if(I<J)
...{
T=A[J-1];//将A[J-1]的内容和A[I-1]的内容互换,借助于中间变量T
A[J-1]=A[I-1];
A[I-1]=T;
}
K=NV2;
while(K<J)
...{
J-=K;
K/=2;
}
J+=K;
I++;
}
(三)时域信号的频谱分析
首先要将从外设输入或采集的时域波形数据经抽样量化后,通过CFile类的Open(……)、Read(……)等成员函数将其读取到缓存中,并将其转化为复变量存放于复变量数组A中,同时需要验证以下数据量的长度是否为2的整数次幂,如若不是则必须用0来补齐,否则无法用 " 蝴蝶图 " 进行分解运算。下面代码用于完成对原始采样时域序列的快速傅立叶变换,A、M分别表示指向原始采样数据数组的指针和序列长度的2的整数次幂:
……
ComplexU,W,T;
int LE,LE1,I,J,IP;
int N = ( int )pow( 2 ,M);
// 在此采用的是时间抽选奇偶分解方式,所以在参加运算前首先要对时间序列进行倒序
ReverseOrder(A,N);
int L = 1 ;
while (L <= M)
... {
LE=(int)pow(2,L);
LE1=LE/2;
U.Re=1.0f;
U.Im=0.0f;
W.Re=(float)cos(PI/(1.0*LE1));//计算W算子的值
W.Im=(float)-1.0*sin(PI/(1.0*LE1));
if(abs(W.Re)<1.0e-12)
W.Re=0.0f;
if(abs(W.Im)<1.0e-12)
W.Im=0.0f;
J=1;
while(J<=LE1)
...{
I=J;
while(I<=N)
...{
IP=I+LE1;
T.Re=(float)A[IP-1].Re*U.Re-A[IP-1].Im*U.Im;//计算复数运算A*U
T.Im=(float)A[IP-1].Re*U.Im+A[IP-1].Im*U.Re;
A[IP-1].Re=(float)A[I-1].Re-T.Re;//计算复数运算A-T
A[IP-1].Im=(float)A[I-1].Im-T.Im;
A[I-1].Re+=T.Re;//计算复数运算A+T
A[I-1].Im+=T.Im;
I+=LE;
}
floattemp=U.Re;
U.Re=(float)U.Re*W.Re-U.Im*W.Im;//计算复数运算U*W
U.Im=(float)temp*W.Im+U.Im*W.Re;
J++;
}
L++;
}
……
上述代码执行完毕时,原先存放着时域数值的复变量数组内存放的就是经过分析后的频域值了,对此数据可以通过绘图将频域波形直观的显示出来,也可以将其存成数据文件,以备进一步使用。