1024點fft原理及fpga實現
關於傅里葉變換的原理,可以參考以下的博文:
如何理解傅里葉變換公式.
FFT即快速傅里葉變換,是有限長序列作離散傅里葉變換(DFT)的快速算法。
DFT公式爲
令,則重寫爲
從上面的公式可以看出,我們需要計算有限長序列中每個因子與對應的旋轉因子的乘積,再對求出來的乘積進行求和。
1. 是什麼
我們知道,所以我們先要明白的含義。
由歐拉公式知:對於,有。那麼這個公式是如何推出來的,有興趣的可以參考
因此實際上就是一個在複平面單位圓上旋轉的值,根據kn的不同,的值不同,因此我們通俗地解釋爲旋轉因子。
2.基於時間和基於頻率
在這裏就講講概念,複雜的推導就不寫了。
由離散傅里葉變換(DFT)的公式可以看出,我們計算DFT序列的每個樣本需要進行次複數相乘和次複數相加,那麼完整的計算DFT所有樣本就需要次複數相乘和次複數相加。當序列長度爲N時,可以證明,計算其N點DFT序列需要4次實數相乘和次實數相加。
顯然,隨着點數增多,DFT的計算量急劇增加,因此,推導快速有效的DFT算法具有很大的實際意義。
一種方法是使用遞歸的計算方法,常用的方案是戈澤爾算法。而另一種方法則是用分解的思想將N點DFT的計算依次分解爲尺寸較小的DFT計算並利用複數的週期性和對稱性進行計算,這就是我們的快速傅里葉變換算法(FFT)。
基於時間的FFT是在計算之前,輸入序列經過抽取,形成子序列,以奇偶作劃分,輸出的是按順序輸出。
基於時間的FFT算法是依次將輸入序列分解爲越來越小的子序列組,將這種分解的思想應用到DFT序列上,就形成基於頻率的FFT算法。因此,輸入序列以順序輸入,輸出DFT序列以抽取後的形式出現。
而如果在每一級以因子進行抽取,則得到的算法稱爲基快速傅里葉變換算法。常用的爲基2和基4算法。
基2按時間抽取的FFT算法的實現
明白原理後,我們使用veilog實現1024點基2按時間抽取的FFT算法。
(1).輸入序列和每一級
首先,我們需要準備好輸入的序列和每一級計算結果的緩存。注意:複數的實部和虛部是分開存儲的
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輸出數據虛部,最高位爲符號位
(2).旋轉因子
在FPGA中直接計算旋轉因子是一件比較麻煩的事,因此我們使用MATLAB將旋轉因子計算好後,存儲在ROM中。如何使用IP核ROM可以參考我之前的博文:
vivado三種常用IP核的調用.
我們已經知道,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中非常佔用資源,因此將其擴爲整數方便計算。
(3).碼位倒置
按照前面提到的,輸入序列在進行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
(4).蝶形運算
前面的fft流圖看上去就像是蝴蝶,因此fft運算也稱爲蝶形運算,下面是最簡單的蝶形運算圖
首先乘以對應的旋轉因子,再分別與加減。非常簡單。
但需要注意的是,這裏的,和都是複數,因此需要遵守複數的運算法則:
加法法則
複數的加法按照以下規定的法則進行:設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倍。
同時,蝶形運算中會出現負數相乘的情況,一定要保證位寬對齊!
(5).循環
在上面的代碼中,我用到了一個個變量,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算法的理解又進了一步。
這個代碼存在很多的不足,一個很顯著的可以改進的地方就是蝶形運算那裏,可以將蝶形運算編寫成一個模塊,使用時進行調用即可,這樣會大量節省代碼。
如果文章有什麼問題,歡迎交流!