关于傅里叶变换的原理,可以参考以下的博文:
如何理解傅里叶变换公式.
FFT即快速傅里叶变换,是有限长序列作离散傅里叶变换(DFT)的快速算法。
DFT公式为
X [ k ] = ∑ n = 0 N − 1 x [ n ] e − j 2 π k n N 0 ≤ n ≤ N − 1 X[k]=\sum_{n=0}^{N-1}x[n]e^{-j2\pi k\frac{n}{N}} \quad \quad 0\leq n \leq N-1 X[k]=n=0∑N−1x[n]e−j2πkNn0≤n≤N−1
令 W N = e − j 2 π N W_N=e^{\frac{-j2\pi }{N}} WN=eN−j2π,则 X [ k ] X[k] X[k]重写为
X [ k ] = ∑ n = 0 N − 1 x [ n ] W N k n 0 ≤ n ≤ N − 1 X[k]=\sum_{n=0}^{N-1}x[n]W_N^{kn}\quad \quad \quad 0\leq n \leq N-1 X[k]=n=0∑N−1x[n]WNkn0≤n≤N−1
从上面的公式可以看出,我们需要计算有限长序列中每个因子与对应的旋转因子 W N W_N WN的乘积,再对求出来的乘积进行求和。
我们知道 W N = e − j 2 π N W_N=e^{\frac{-j2\pi }{N}} WN=eN−j2π,所以我们先要明白 e − j 2 π N e^{\frac{-j2\pi }{N}} eN−j2π的含义。
由欧拉公式知:对于 θ ∈ R \theta\in R θ∈R,有 e i θ = c o s θ + i s i n θ e^{i\theta}=cos\theta+isin\theta eiθ=cosθ+isinθ。那么这个公式是如何推出来的,有兴趣的可以参考
如何通俗地解释欧拉公式.
因此 W N W_N WN实际上就是一个在复平面单位圆上旋转的值,根据kn的不同, W N W_N WN的值不同,因此我们通俗地解释为旋转因子。
在这里就讲讲概念,复杂的推导就不写了。
由离散傅里叶变换(DFT)的公式可以看出,我们计算DFT序列的每个样本需要进行 N N N次复数相乘和 N − 1 N-1 N−1次复数相加,那么完整的计算DFT所有样本就需要 N 2 N^2 N2次复数相乘和 ( N − 1 ) N (N-1)N (N−1)N次复数相加。当序列长度为N时,可以证明,计算其N点DFT序列需要4 N 2 N^2 N2次实数相乘和 ( 4 N − 2 ) N (4N-2)N (4N−2)N次实数相加。
显然,随着点数增多,DFT的计算量急剧增加,因此,推导快速有效的DFT算法具有很大的实际意义。
一种方法是使用递归的计算方法,常用的方案是戈泽尔算法。而另一种方法则是用分解的思想将N点DFT的计算依次分解为尺寸较小的DFT计算并利用复数 W N k n W_N^{kn} WNkn的周期性和对称性进行计算,这就是我们的快速傅里叶变换算法(FFT)。
基于时间的FFT是在计算之前,输入序列 x [ n ] x[n] x[n]经过抽取,形成子序列,以奇偶作划分,输出的 X [ k ] X[k] X[k]是按顺序输出。
基于时间的FFT算法是依次将输入序列 x [ n ] x[n] x[n]分解为越来越小的子序列组,将这种分解的思想应用到DFT序列 X [ k ] X[k] X[k]上,就形成基于频率的FFT算法。因此,输入序列 x [ n ] x[n] x[n]以顺序输入,输出DFT序列以抽取后的形式出现。
而如果在每一级以因子 R R R进行抽取,则得到的算法称为基 R R R快速傅里叶变换算法。常用的为基2和基4算法。
明白原理后,我们使用veilog实现1024点基2按时间抽取的FFT算法。
首先,我们需要准备好输入的序列和每一级计算结果的缓存。注意:复数的实部和虚部是分开存储的
reg signed [23:0] input_data [0:N-1]; //原始输入数据,最高位为符号位
reg signed [23:0] dft_oridata [0:N-1]; //码位倒置后的输入数据,最高位为符号位
reg signed [23:0] dft_firoutreal [0:N-1]; //第一级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_firoutimg [0:N-1]; //第一级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_secoutreal [0:N-1]; //第二级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_secoutimg [0:N-1]; //第二级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_trdoutreal [0:N-1]; //第三级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_trdoutimg [0:N-1]; //第三级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_foroutreal [0:N-1]; //第四级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_foroutimg [0:N-1]; //第四级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_fifoutreal [0:N-1]; //第五级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_fifoutimg [0:N-1]; //第五级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_sixoutreal [0:N-1]; //第六级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_sixoutimg [0:N-1]; //第六级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_sevoutreal [0:N-1]; //第七级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_sevoutimg [0:N-1]; //第七级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_eigoutreal [0:N-1]; //第八级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_eigoutimg [0:N-1]; //第八级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_ninoutreal [0:N-1]; //第九级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_ninoutimg [0:N-1]; //第九级DFT输出数据虚部,最高位为符号位
reg signed [23:0] dft_tenoutreal [0:N-1]; //第十级DFT输出数据实部,最高位为符号位
reg signed [23:0] dft_tenoutimg [0:N-1]; //第十级DFT输出数据虚部,最高位为符号位
在FPGA中直接计算旋转因子是一件比较麻烦的事,因此我们使用MATLAB将旋转因子计算好后,存储在ROM中。如何使用IP核ROM可以参考我之前的博文:
vivado三种常用IP核的调用.
我们已经知道 W N k n = e − j 2 π k n N = c o s ( − 2 π k n N ) + j s i n ( − 2 π k n N ) W_N^{kn}=e^{\frac{-j2\pi kn}{N}}=cos(\frac{-2\pi kn}{N})+jsin(\frac{-2\pi kn}{N}) WNkn=eN−j2πkn=cos(N−2πkn)+jsin(N−2πkn),k是我们查表的地址,因此最终MATLAB计算旋转因子的代码为
%fft旋转因子生成表
%w代表返回值,n代表运算点数
%uint8一个字节,uint16两个字节,uint32四个字节,字节数越多,精度越高
%这里将w放大,是因为浮点运算比较消耗时间,因此将其化为整数
function [w]=fftw
clear all;
clc;
n=1024; %fft点数,根据实际调整
for i=1:n/2
%w(i)=cos(-2*pi*(i-1)/n); %正变换用到的旋转因子实部
w(i)=sin(-2*pi*(i-1)/n); %正变换用到的旋转因子虚部
end
%for i=1:i-2
% w(2*i-1)=cos(2*pi*(i-1)/n); %逆变换用到的旋转因子
% w(2*i)=sin(2*pi*(i-1)/n);
%end
w=w*256; %将w放大2^8次倍
w=int16(w);%取整
w(find(w<0))=2^11+w(find(w<0));
%fid=fopen('C:\Users\Leixx\Desktop\fftwn_real.coe','wt');
fid=fopen('C:\Users\Leixx\Desktop\fftwn_img.coe','wt');
fprintf(fid,'%d,\n',w);
fclose(fid);
因为我的ADC是10位的无符号数,计算中我会将其扩展为11位,所以这里我将旋转因子设置为11位,最高位为符号位。
注意:我将旋转因子扩大了256倍,是因为旋转因子计算出来的值为小数,浮点数运算在FPGA中非常占用资源,因此将其扩为整数方便计算。
按照前面提到的,输入序列在进行DFT运算前需要进行抽取,而抽取的方式就是码位倒置。码位倒置的原理图如下:
码位倒置即将原来的码再按高到底重新排序。verilog实现如下
5'd1:begin //装载需要计算的数据
#15 input_data[data_cnt] = inputdata_r;
inputdata_addr = inputdata_addr+1'b1;
data_cnt = data_cnt+1'b1;
if(!inputdata_addr) begin
state <= state+1;
data_cnt <= 11'd0;
end
end
5'd2:begin //码位倒置
dft_oridata[data_cnt] = input_data[{data_cnt[0],data_cnt[1],data_cnt[2],data_cnt[3],data_cnt[4],data_cnt[5],
data_cnt[6],data_cnt[7],data_cnt[8],data_cnt[9]}];
data_cnt = data_cnt+1'b1;
if(data_cnt==N)begin
data_cnt <= 0;
state <= state+1'b1;
end
end
前面的fft流图看上去就像是蝴蝶,因此fft运算也称为蝶形运算,下面是最简单的蝶形运算图
首先 x 2 [ k ] x_2[k] x2[k]乘以对应的旋转因子 W N k W_N^k WNk,再分别与 x 1 [ k ] x_1[k] x1[k]加减。非常简单。
但需要注意的是,这里的 x 1 [ k ] x_1[k] x1[k], W N k W_N^k WNk和 x 2 [ k ] x_2[k] x2[k]都是复数,因此需要遵守复数的运算法则:
加法法则
复数的加法按照以下规定的法则进行:设z1=a+bi,z2=c+di是任意两个复数,则它们的和是 (a+bi)+(c+di)=(a+c)+(b+d)i。
两个复数的和依然是复数,它的实部是原来两个复数实部的和,它的虚部是原来两个虚部的和。
复数的加法满足交换律和结合律,
即对任意复数z1,z2,z3,有: z1+z2=z2+z1;(z1+z2)+z3=z1+(z2+z3)。
减法法则
复数的减法按照以下规定的法则进行:设z1=a+bi,z2=c+di是任意两个复数,
则它们的差是 (a+bi)-(c+di)=(a-c)+(b-d)i。
两个复数的差依然是复数,它的实部是原来两个复数实部的差,它的虚部是原来两个虚部的差。
乘法法则
规定复数的乘法按照以下的法则进行:
设z1=a+bi,z2=c+di(a、b、c、d∈R)是任意两个复数,那么它们的积(a+bi)(c+di)=(ac-bd)+(bc+ad)i。
其实就是把两个复数相乘,类似两个多项式相乘,展开得: ac+adi+bci+bdi2,因为i2=-1,所以结果是(ac-bd)+(bc+ad)i 。两个复数的积仍然是一个复数。
在极坐标下,复数可用模长r与幅角θ表示为(r,θ)。对于复数a+bi,r=√(a²+b²),θ=arctan(b/a)。此时,复数相乘表现为幅角相加,模长相乘。
除法法则
复数除法定义:满足(c+di)(x+yi)=(a+bi)的复数x+yi(x,y∈R)叫复数a+bi除以复数c+di的商。
运算方法:可以把除法换算成乘法做,在分子分母同时乘上分母的共轭.。所谓共轭你可以理解为加减号的变换,互为共轭的两个复数相乘是个实常数。
除法运算规则:
①设复数a+bi(a,b∈R),除以c+di(c,d∈R),其商为x+yi(x,y∈R),
即(a+bi)÷(c+di)=x+yi
分母实数化
分母实数化
∵(x+yi)(c+di)=(cx-dy)+(dx+cy)i
∴(cx-dy)+(dx+cy)i=a+bi
由复数相等定义可知 cx-dy=a dx+cy=b
解这个方程组,得 x=(ac+bd)/(c2+d2) y=(bc-ad)/(c2+d2)
于是有:(a+bi)/(c+di)=(ac+bd)/(c2+d2) +((bc-ad)/(c2+d2))i
②利用共轭复数将分母实数化得(见右图):
点评:①是常规方法;②是利用初中我们学习的化简无理分式时,都是采用的分母有理化思想方法,而复数c+di与复数c-di,相当于我们初中学习的 的对偶式,它们之积为1是有理数,而(c+di)·(c-di)=c2+d2是正实数.所以可以分母实数化。把这种方法叫做分母实数化法。
另外,由上述乘法法则可得另一计算方法,即幅角相减,模长相除。
在verilog中实现如下:
cache_real = dft_firoutreal[data_cnt+cal_stage]*wndatareal[wndatareal_addr]-
dft_firoutimg[data_cnt+cal_stage]*wndataimg[wndataimg_addr]; //先计算旋转因子,分别计算实部和虚部
cache_img = dft_firoutreal[data_cnt+cal_stage]*wndataimg[wndataimg_addr]+
dft_firoutimg[data_cnt+cal_stage]*wndatareal[wndatareal_addr];
cache_realres[31:0] = (dft_firoutreal[data_cnt]<<8) + cache_real;
cache_imgres[31:0] = (dft_firoutimg[data_cnt]<<8) + cache_img;
dft_secoutreal[data_cnt] = cache_realres[31:8];
dft_secoutimg[data_cnt] = cache_imgres[31:8];
cache_realres[31:0] = (dft_firoutreal[data_cnt]<<8) - cache_real;
cache_imgres[31:0] = (dft_firoutimg[data_cnt]<<8)-cache_img;
dft_secoutreal[data_cnt+cal_stage] = cache_realres[31:8];
dft_secoutimg[data_cnt+cal_stage] = cache_imgres[31:8];
最上面四行是乘以旋转因子,下面8行则是两个序列值加减。关于代码中的位移操作,请注意:因为旋转因子扩大了2^8 倍,因此我们在作加减法时也需要将另一个没有乘旋转因子的序列值扩大2^8 倍,在加减完成后,再缩小2^8倍。
同时,蝶形运算中会出现负数相乘的情况,一定要保证位宽对齐!
在上面的代码中,我用到了一个个变量,cal_stage。那么这个变量是干什么的?这里就涉及到了fft流图如何循环的问题。
上面提到了,fft算法是将DFT序列分解为更小的序列进行计算。第一级DFT运算是N/2点DFT,第二级是N/4点DFT,第三级是N/8点DFT…,并且都是偶序列和奇序列分开计算。
根据这个原理,我们可以设置两个寄存器fft_stage和cal_stage,一个寄存器记录当前是第几级fft,另一个寄存器记录当前每组做几次蝶形运算。
以第二级DFT举例。此时是N/4点DFT,共有4组,每组有4个点,每组进行两次蝶形运算,此时fft_stage=2,cal_stage=2。观察图我们发现,进行蝶形运算的两个序列值,中间间隔恰好为cal_stage,这便是加上cal_stage的原因。
那么旋转因子呢,同样,观察图我们发现,不同级的旋转因子变化和fft_stage紧密相关,旋转因子间隔值是N>>fft_stage的关系。
每组有两个蝶形运算,我们用wndata_cnt对蝶形运算的个数进行计数。
偶序列和奇序列都有多个分组,我们还需要用group_cnt对组数进行计数。
理清这些后,代码就很好写了。
5'd5:begin //第二级蝶形运算,N/4点DFT,计算偶数部分
cache_real = dft_firoutreal[data_cnt+cal_stage]*wndatareal[wndatareal_addr]-
dft_firoutimg[data_cnt+cal_stage]*wndataimg[wndataimg_addr]; //先计算旋转因子,分别计算实部和虚部
cache_img = dft_firoutreal[data_cnt+cal_stage]*wndataimg[wndataimg_addr]+
dft_firoutimg[data_cnt+cal_stage]*wndatareal[wndatareal_addr];
cache_realres[31:0] = (dft_firoutreal[data_cnt]<<8) + cache_real;
cache_imgres[31:0] = (dft_firoutimg[data_cnt]<<8) + cache_img;
dft_secoutreal[data_cnt] = cache_realres[31:8];
dft_secoutimg[data_cnt] = cache_imgres[31:8];
cache_realres[31:0] = (dft_firoutreal[data_cnt]<<8) - cache_real;
cache_imgres[31:0] = (dft_firoutimg[data_cnt]<<8)-cache_img;
dft_secoutreal[data_cnt+cal_stage] = cache_realres[31:8];
dft_secoutimg[data_cnt+cal_stage] = cache_imgres[31:8];
wndatareal_addr = wndatareal_addr+(N>>fft_stage);
wndataimg_addr = wndataimg_addr+(N>>fft_stage);
wndata_cnt = wndata_cnt+1;
data_cnt = data_cnt+1;
if(wndata_cnt==cal_stage)begin //说明该分组已完成计算,切换到下一个分组
data_cnt = data_cnt+cal_stage;
wndatareal_addr = 0;
wndataimg_addr = 0;
wndata_cnt = 0;
group_cnt = group_cnt+1; //已计算完一个分组
if(group_cnt==N>>(fft_stage+1))begin //说明偶数部分已计算完成
group_cnt <= 0;
state <= state+1;
data_cnt <= N>>1;
end
end
end
5'd6:begin //第二级蝶形运算,N/4点DFT,计算奇数部分
cache_real = dft_firoutreal[data_cnt+cal_stage]*wndatareal[wndatareal_addr]-
dft_firoutimg[data_cnt+cal_stage]*wndataimg[wndataimg_addr]; //先计算旋转因子,分别计算实部和虚部
cache_img = dft_firoutreal[data_cnt+cal_stage]*wndataimg[wndataimg_addr]+
dft_firoutimg[data_cnt+cal_stage]*wndatareal[wndatareal_addr];
cache_realres[31:0] = (dft_firoutreal[data_cnt]<<8) + cache_real;
cache_imgres[31:0] = (dft_firoutimg[data_cnt]<<8) + cache_img;
dft_secoutreal[data_cnt] = cache_realres[31:8];
dft_secoutimg[data_cnt] = cache_imgres[31:8];
cache_realres[31:0] = (dft_firoutreal[data_cnt]<<8) - cache_real;
cache_imgres[31:0] = (dft_firoutimg[data_cnt]<<8)-cache_img;
dft_secoutreal[data_cnt+cal_stage] = cache_realres[31:8];
dft_secoutimg[data_cnt+cal_stage] = cache_imgres[31:8];
wndatareal_addr = wndatareal_addr+(N>>fft_stage);
wndataimg_addr = wndataimg_addr+(N>>fft_stage);
wndata_cnt = wndata_cnt+1;
data_cnt = data_cnt+1;
if(wndata_cnt==cal_stage)begin //说明该分组已完成计算,切换到下一个分组
data_cnt = data_cnt +cal_stage;
wndatareal_addr = 0;
wndataimg_addr = 0;
wndata_cnt = 0;
group_cnt = group_cnt+1; //已计算完一个分组
if(group_cnt==(N>>(fft_stage+1)))begin //说明奇数部分已计算完成
group_cnt <= 0;
state<= state+1;
cal_stage <= cal_stage<<1;
fft_stage <= fft_stage+1;
data_cnt <= 4'd0;
end
end
end
在上面的代码中,我全部使用了寄存器取代特定的数值,具有很好的移植性,只需要将输入的序列和输出的序列修改,就生成了下一级DFT计算的代码。
最后一级计算略有不同,因为最后一级是偶序列和奇序列作蝶形运算,因此不用分偶序列和奇序列
5'd21:begin //第十级蝶形运算,N/1024点DFT
cache_real = dft_ninoutreal[data_cnt+cal_stage]*wndatareal[wndatareal_addr]-
dft_ninoutimg[data_cnt+cal_stage]*wndataimg[wndataimg_addr]; //先计算旋转因子,分别计算实部和虚部
cache_img = dft_ninoutreal[data_cnt+cal_stage]*wndataimg[wndataimg_addr]+
dft_ninoutimg[data_cnt+cal_stage]*wndatareal[wndatareal_addr];
cache_realres[31:0] = (dft_ninoutreal[data_cnt]<<8) + cache_real;
cache_imgres[31:0] = (dft_ninoutimg[data_cnt]<<8) + cache_img;
dft_tenoutreal[data_cnt] = cache_realres[31:8];
dft_tenoutimg[data_cnt] = cache_imgres[31:8];
cache_realres[31:0] = (dft_ninoutreal[data_cnt]<<8) - cache_real;
cache_imgres[31:0] = (dft_ninoutimg[data_cnt]<<8) - cache_img;
dft_tenoutreal[data_cnt+cal_stage] = cache_realres[31:8];
dft_tenoutimg[data_cnt+cal_stage] = cache_imgres[31:8];
wndatareal_addr = wndatareal_addr+(N>>fft_stage);
wndataimg_addr = wndataimg_addr+(N>>fft_stage);
wndata_cnt = wndata_cnt+1;
data_cnt = data_cnt+1;
if(wndata_cnt==cal_stage)begin //说明该分组已完成计算,切换到下一个分组
data_cnt = data_cnt +cal_stage;
wndatareal_addr = 0;
wndataimg_addr = 0;
wndata_cnt = 0;
group_cnt = group_cnt+1; //已计算完一个分组
if(group_cnt==(N>>fft_stage))begin //最后一阶只有一个小组
group_cnt <= 0;
state<= state+1;
cal_stage <= cal_stage<<1;
data_cnt <= 0;
end
end
end
使用MATLAB生成输入数据,将输入数据存到ROM中读取出来并作FFT运算。
MATLAB代码如下:
%=============设置系统参数==============%
f1=1e6; %设置波形频率
f2=500e3;
f3=800e3;
Fs=20e6; %设置采样频率
L=8192; %数据长度
N=11; %数据位宽
%=============产生输入信号==============%
t=0:1/Fs:(1/Fs)*(L-1);
y1=sin(2*pi*f1*t);
y2=sin(2*pi*f2*t);
y3=sin(2*pi*f3*t);
y4=y1+y2+y3;
y_n=round(y4*(2^(N-3)-1)); %N比特量化;如果有n个信号相加,则设置(N-n)
%=================画图==================%
a=50; %改变系数可以调整显示周期
stem(t,y_n);
axis([0 L/Fs/a -2^N 2^N]); %显示
%=============写入外部文件==============%
fid=fopen('C:\Users\Leixx\Desktop\sin_test.txt','w'); %把数据写入sin_data.txt文件中,如果没有就创建该文件
%fid=fopen('C:\Users\Leixx\Desktop\input_data.coe','wt');
for k=1:1024
B_s=dec2bin(y_n(k)+((y_n(k))<0)*2^N,N);
% fprintf(fid,'%d,',y_n(k));
for j=1:N
if B_s(j)=='1'
tb=1;
else
tb=0;
end
fprintf(fid,'%d',tb);
end
fprintf(fid,',\n');
end
fprintf(fid,';');
fclose(fid);
仿真结果
可以发现,存在一些误差,主要是因为旋转因子只扩大了2^8 倍,损失了一些精度。但说明我们的算法基本是正确的。
以上就是fft原理及fpga实现。
快速傅里叶变换是通信中常用的重要算法,它将时域的信号转换到频域进行分析,具有非常重要的价值。
以往即使了解这个算法,但是将其用代码也是非常困难的事情。我在编写的过程中,遇到了很多问题,但在解决这些问题的同时,我对FFT算法的理解又进了一步。
这个代码存在很多的不足,一个很显著的可以改进的地方就是蝶形运算那里,可以将蝶形运算编写成一个模块,使用时进行调用即可,这样会大量节省代码。
如果文章有什么问题,欢迎交流!