之前从来没有接触过FFT,但是工作需求,让我不得不对它下手。
具体的FFT的原理我不做过多说明,大概说下是做什么用的。简单说就是频谱分析,说的通俗点就是:一个50Hz的正弦波,对它进行FFT计算,可以得到以下一些信息: 1.该正弦波的频率 2.该正弦波的幅值
你可能会觉得这有什么用?但是你要知道,我输入进FFT的正弦波,事先你是不知道它的任何信息的,包括频率和幅值。FFT就相当于一个盲盒,你给我一杯水我还你一片湖泊。
好,你大概知道了FFT是干嘛的就好了,接下来说怎么用?本文是基于ALTERA的Quartus 18.0版本设计。
step1:产生正弦波
当然,如果你有信号发生器,那就简单多了,不必像我这么麻烦的调DAC,我是使用的查表法,利用ROM中存放的mif文件,依次通过DAC输出,产生的正弦波。(DAC部分的内容会单独给一个专题)
step2:采样正弦波
这个也是一个关键步骤,你需要足够了解ADC的工作时序,采样频率。将上一步产生的正弦波输入进ADC进行采样,转化后的数据保存到一个RAM中,供FFT调用。(ADC部分的内容同样会单独给一个专题)
step3:FFT计算
如果上面比较顺利,那么这里估计会耽误一点时间。因为调用FFT IP核的时候,会有很多的参数,需要很清楚这些参数的功能以及工作时序。
假设你现在已经采样到正弦波数据并且保存到RAM中,准备给FFT使用了,我们开始配置。
①找到FFT IP核
②配置相关参数
主要参数包括采样点数 数据模式 位宽等。这里需要注意,红框中的两个选项都要执行,不然运行会有报错。
IP核的配置基本就这些了,然后会生成可以例化的代码,如下
module fft3 (
input wire clk, // clk.clk IP核的参考时钟
input wire reset_n, // rst.reset_n 复位线
input wire sink_valid, // sink.sink_valid 输入数据有效信号
output wire sink_ready, // .sink_ready 表示FFT IP核准备好接收数据了,实测的时候该信号一直是高电平
input wire [1:0] sink_error, // .sink_error 输入报错标志 输入手动置0,不会有影响
input wire sink_sop, // .sink_sop 输入数据起始
input wire sink_eop, // .sink_eop 输入数据终止
input wire [15:0] sink_real, // .sink_real 输入数据的实部
input wire [15:0] sink_imag, // .sink_imag 输入数据的虚部 一般都会直接置0
input wire [0:0] inverse, // .inverse FFT模式选择 正计算置0 逆运算置1
output wire source_valid, // source.source_valid 输出数据有效
input wire source_ready, // .source_ready 准备好输出数据 一般直接拉高,表示一直准备着,随时可以输出数据
output wire [1:0] source_error, // .source_error 输出报错
output wire source_sop, // .source_sop 输出数据的起始
output wire source_eop, // .source_eop 输出数据终止
output wire [15:0] source_real, // .source_real 输出数据实部
output wire [15:0] source_imag, // .source_imag 输出数据虚部
output wire [5:0] source_exp // .source_exp 输出数据缩放因子,实际输出的实部和虚部需要乘以2^source_exp
);
还是再上个图看看吧
这些就是在FFT计算的时候需要考虑的一些变量,需要做的就是控制几个输入的信号,来完成整个的FFT计算过程,那么这个就是需要参考各个信号的时序图了。
我们在配置IP核的时候,选择的模式是streaming,因此,找到官方文档查看时序图,如下:
将输入的波形放大,如下:
简单分析一下数据输入的时序流程: 当reset拉高后,sink_valid和sink_ready都被直接拉高,sink_valid是需要自己拉高的,sink_ready是IP核自己拉高的,这个值我实测好像是一直是高电平;inverse需要手动拉低;然后给sink_sop一个高脉冲,这时候数据就会开始往FFT中传输了;但是图中没有给出什么时候停止,当sink_eop拉高一个脉冲时,这一帧数据输入完成。FFT就会对sink_sop和sink_eop两个高脉冲之间输入的数据进行FFT计算。这之间的数据点数就是上面设置的1024,当然这个点数由你自己定,数据越多,精度会越高,占用内存也就越高。
然后看一下数据输出的时序图,如下:
和输入数据类似的:source_ready和sink_valid拉高,表示可以输出了,注意这里的source_ready是手动拉高的,默认一直是高,sink_valid是FFT核拉高的,实测好像一直是高;然后当数据可以输出时,FFT核会将source_sop拉高一个高脉冲,当输出完成1024个数据时,FFT核会将source_eop拉高一个高脉冲,表示一帧数据完成输出。这样就完成了对一帧数据的FFT计算,请注意,这里的一帧数据实际是要计算1024次的,因为有1024个点啊。
最终需要用到的数据就是source_real和source_imag。将这两个值进行平方和计算,然后再开根号,得到的结果我们暂时记作fft_out吧,就是我们需要使用的值。
具体怎么用呢?一开始我们设置了对1024个输入的点进行FFT计算,每一点都会计算得到一组source_real和source_imag,对应的就可以计算出一个fft_out;那么最终就会有1024个fft_out,我们需要在这1024个数据中找到fft_out最大的那个点,假设是第n个点(点号从0-1023计算),那么采样的这个波形的频率就是Fout=(n-1)*Fs/N;其中,Fout是需要测量的信号的频率,n是计算的点号,Fs是ADC的采样频率(这个会在ADC专题中说明),N是采样点数,就是这里的1024。
简单的流程说完了,上示例:
module FFT_Control_Module
(
//输入端口
CLK_50M,RST_N,data_real_in_int,
//输出端口
fft_bit_cnt,fft_x_cnt,
vga_freq,vga_fengzhi,
fft_out,sink_sop,
sink_ready,sink_valid,
sink_eop,fft_start,source_error,source_sop,source_eop,source_valid,max_cnt,position,data_max
);
//---------------------------------------------------------------------------
//-- 外部端口声明
//---------------------------------------------------------------------------
input CLK_50M;
input RST_N;
input [ 15:0] data_real_in_int;//[ 9:0]
output [10:0] fft_bit_cnt;
output [10:0] fft_x_cnt;
output [31:0] vga_freq;
output [31:0] vga_fengzhi;
output [ 15:0] fft_out;//
output sink_sop;
output sink_ready;
output reg sink_valid;
output sink_eop;
output reg fft_start;
output [1:0] source_error;
output source_sop;
output source_eop;
output source_valid;
output reg [10:0] max_cnt;
output reg [10:0] position;
output reg [15:0] data_max;
//---------------------------------------------------------------------------
//-- 内部端口声明
//---------------------------------------------------------------------------
wire inverse;
wire [ 15:0] data_imag_in_int;//[ 9:0]
wire [ 9:0] fftpts_array;
wire [ 1:0] sink_error;
//wire sink_ready;
//reg sink_valid;
reg sink_valid_n;
wire fft_ready_valid;
reg [10:0] fft_bit_cnt;
reg [10:0] fft_bit_cnt_n;
//wire sink_sop;
//wire sink_eop;
reg [ 15:0] sink_real;
reg [ 15:0] sink_real_n;
reg [ 15:0] sink_imag;
reg [ 15:0] sink_imag_n;
wire end_input;
reg end_test;
reg end_test_n;
wire source_ready;
wire source_valid;
wire [ 15:0] source_real;
reg [ 15:0] fft_real_out_int;
reg [ 15:0] fft_real_out_int_n;
wire [ 15:0] source_imag;
reg [ 15:0] fft_imag_out_int;
reg [ 15:0] fft_imag_out_int_n;
wire [ 5:0] source_exp;
reg [ 5:0] exponent_out_int;
reg [ 5:0] exponent_out_int_n;
reg [31:0] fft_real_image_data;
reg [31:0] fft_real_image_data_n;
wire [15:0] sqrt_data_out;
wire fft_read;
//wire source_sop;
reg [ 10:0] fft_x_cnt;
reg [ 10:0] fft_x_cnt_n;
//reg [ 15:0] data_max;
reg [ 15:0] data_max_n;
reg [ 15:0] data_max2;
reg [ 15:0] data_max2_n;
//reg [ 10:0] max_cnt;
reg [ 10:0] max_cnt_n;
reg [ 10:0] max_cnt2;
reg [ 10:0] max_cnt2_n;
reg [15:0] fengzhi_max;
reg [15:0] fengzhi_max_n;
reg [15:0] fengzhi_min;
reg [15:0] fengzhi_min_n;
wire [31:0] vga_freq;
wire [31:0] vga_fengzhi;
wire [ 10:0] fft_out;
reg [15:0] frames_in_index;
reg [15:0] frames_out_index;
reg [9:0] position_r;
//reg fft_start;
reg fft_start_n;
//---------------------------------------------------------------------------
//-- 逻辑功能实现
//---------------------------------------------------------------------------
/* FFT控制信号,1:IFFT,0:FFT */
assign inverse = 1'b0;
/* FFT输入数据信号 */
assign data_imag_in_int = 16'b0;
/* FFT数据的个数为1024 */
assign fftpts_array = 10'd1023;
/* 输入错误信号 */
assign sink_error = 2'b0;
//start valid for first cycle to indicate that the file reading should start.
always@(posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fft_start <= 1'b1;
else
fft_start <= fft_start_n;
end
always@(*)
begin
if(fft_ready_valid)//sink_valid为1时该式成立,则fft_start_n变为0,也就是两个信号同时会有脉冲信号产生
fft_start_n = 1'b0;
else
fft_start_n = 1'b1;
end
//输入数据帧数
// count the input frames and increment the index
always @(posedge CLK_50M or negedge RST_N)
begin
if (RST_N == 1'b0)
frames_in_index <= 'd0;
else
begin
if (sink_eop == 1'b1 & sink_valid == 1'b1 & sink_ready == 1'b1 & frames_in_index < 3000)输入数据的次数,即输入进1024个点就算一帧数据
frames_in_index <= frames_in_index + 1'b1;
end
end
// count the output frames and increment the index 这个定义的是输入数据帧数,实际我没使用,而是一直输入进行FFT计算
always @(posedge CLK_50M or negedge RST_N)
begin
if (RST_N == 1'b0)
frames_out_index <= 'd0;
else
begin
if (source_eop == 1'b1 & source_valid == 1'b1 & source_ready == 1'b1 & frames_out_index < 3000)
frames_out_index <= frames_out_index + 1'b1;
end
end
/* 时序电路,用来给sink_valid寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
sink_valid <= 1'b0;
else
sink_valid <= sink_valid_n;
end
/* 组合电路,输入的有效信号 */
//***********************无效
always @ (*)
begin
if(end_test || end_input)//end_input = (sink_eop && fft_ready_valid) ? 1'b1 : 1'b0;
sink_valid_n = 1'b0;
else if(fft_ready_valid || (fft_start == 1'b1 & !(sink_valid == 1'b1 & sink_ready == 1'b0)) )//(fft_start & !(sink_valid && sink_ready == 1'b0)))
sink_valid_n <= 1'b1;
else
sink_valid_n = 1'b1;
end
//*********************************/
//always@(*)//组合逻辑一般用阻塞赋值
//begin
// if(end_test || end_input)
// sink_valid_n = 1'b1;
// else
// sink_valid_n = 1'b1;
//end
/* 输入开始进行FFT转换运算 */
assign fft_ready_valid = (sink_valid && sink_ready);//sink_ready貌似都是正的
/* 时序电路,用来给fft_bit_cnt寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fft_bit_cnt <= 10'b0;
else
fft_bit_cnt <= fft_bit_cnt_n;
end
/* 组合电路,FFT位计数器 */
always @ (*)
begin
if(fft_ready_valid && (fft_bit_cnt == fftpts_array))
fft_bit_cnt_n = 1'b0;
else if(fft_ready_valid)//这个判断,一开始这里因为sink_valid为0进不来
fft_bit_cnt_n = fft_bit_cnt + 1'b1;
else
fft_bit_cnt_n = fft_bit_cnt;
end
/* 输入流的开始 */
assign sink_sop = (fft_bit_cnt == 1'b0) ? 1'b1 : 1'b0 ;
/* 输入流的结束 */
assign sink_eop = (fft_bit_cnt == fftpts_array) ? 1'b1 : 1'b0;
/* 时序电路,用来给sink_real寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
sink_real <= 10'b0;
else
sink_real <= sink_real_n;
end
/* 组合电路,输入的实部信号 */
always @ (*)
begin
if(end_test || end_input)//1024个数据已经输入完成了
sink_real_n = 10'b0;
else if (fft_ready_valid)//(sink_valid && sink_ready)
sink_real_n = data_real_in_int;
else
sink_real_n = sink_real;
end
/* 时序电路,用来给sink_imag寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
sink_imag <= 10'b0;
else
sink_imag <= sink_imag_n;
end
/* 组合电路,输入的虚部信号 */
always @ (*)
begin
if(end_test || end_input)
sink_imag_n = 10'b0;
else if (fft_ready_valid)
sink_imag_n = data_imag_in_int;
else
sink_imag_n = sink_imag;
end
/* 停止输入信号 */ //signal when all of the input data has been sent to the DUT
assign end_input = (sink_eop && fft_ready_valid&& frames_in_index == 3000) ? 1'b0 : 1'b0;
/* 时序电路,用来给end_test寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
end_test <= 1'b0;
else
end_test <= end_test_n;
end
/* 组合电路,结束测试信号 */
always @ (*)
begin
if(end_input)
end_test_n = 1'b1;
else
end_test_n = end_test;
end
/* 源准备信号 */
assign source_ready = 1'b1;
/* 时序电路,用来给fft_real_out_int寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fft_real_out_int <= 10'b0;
else
fft_real_out_int <= fft_real_out_int_n;
end
/* 组合电路,FFT输出的实部信号 */
always @ (*)
begin
if(source_valid && source_ready)
fft_real_out_int_n = source_real[15] ? (~source_real[15:0]+1) : source_real;
else
fft_real_out_int_n = fft_real_out_int;
end
/* 时序电路,用来给fft_imag_out_int寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fft_imag_out_int <= 10'b0;
else
fft_imag_out_int <= fft_imag_out_int_n;
end
/* 组合电路,FFT输出的虚部信号 */
always @ (*)
begin
if(source_valid && source_ready)
fft_imag_out_int_n = source_imag[15] ? (~source_imag[15:0]+1) : source_imag;
else
fft_imag_out_int_n = fft_imag_out_int;
end
/* 时序电路,用来给exponent_out_int寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
exponent_out_int <= 10'b0;
else
exponent_out_int <= exponent_out_int_n;
end
/* 组合电路,用于生成FFT输出的指数信号 */
always @ (*)
begin
if(source_valid && source_ready)
exponent_out_int_n = source_exp;
else
exponent_out_int_n = exponent_out_int;
end
/* 时序电路,用来给fft_real_image_data寄存器赋值 */
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fft_real_image_data <= 32'b0;
else
fft_real_image_data <= fft_real_image_data_n;
end
/* 组合电路,用于生成FFT输出的数据信号 */
always @ (*)
begin
if(source_valid && source_ready)
fft_real_image_data_n = fft_real_out_int*fft_real_out_int + fft_imag_out_int*fft_imag_out_int;
else
fft_real_image_data_n = fft_real_image_data;
end
/* 平方根模块 */
SQRT_Module SQRT_Init
(
.radical (fft_real_image_data ),
.q (sqrt_data_out )
);
/* 开始进行FFT转换运算 */
assign fft_read = (source_valid && source_ready);
/* 时序电路,用来给fft_x_cnt寄存器赋值 */
//***************找最大值可以用,但是最大位置值会复位累计
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N) begin
position <= 1'b0;
fft_x_cnt <= 1'b0;
data_max <= 1'b0;
end
else begin
position <= position_r;
data_max <= data_max_n;
fft_x_cnt <= fft_x_cnt_n;
end
end
//********/
/* 组合电路,用于生成FFT输出数据的计数器 */
//**********找最大值可以用,但是最大位置值会复位累计
always @ (*)
begin
if(fft_ready_valid && (source_sop || (fft_x_cnt == fftpts_array)))begin//输出完成后
fft_x_cnt_n = 1'b0;
position_r = 1'b0;
data_max_n= 1'b0;
end
else if(fft_read) begin //assign fft_read = (source_valid && source_ready);
fft_x_cnt_n = fft_x_cnt + 1'b1;
if(sqrt_data_out > data_max && fft_x_cnt > 2 && fft_x_cnt < 512) begin
data_max_n= sqrt_data_out;
position_r= fft_x_cnt -2'd2;
end
else
data_max_n= data_max_n;
position_r= position_r;
end
else
fft_x_cnt_n = fft_x_cnt;
end
//****************/
*//
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fengzhi_max <= 16'b0;
else
fengzhi_max <= fengzhi_max_n;
end
always @ (*)
begin
if(data_real_in_int > fengzhi_max)
fengzhi_max_n = data_real_in_int;
else
fengzhi_max_n = fengzhi_max;
end
always @ (posedge CLK_50M or negedge RST_N)
begin
if(!RST_N)
fengzhi_min <= 16'd255;
else
fengzhi_min <= fengzhi_min_n;
end
always @ (*)
begin
if(fengzhi_min > data_real_in_int)
fengzhi_min_n = data_real_in_int;
else
fengzhi_min_n = fengzhi_min;
end
assign vga_freq = (position) * 32'd147000 >> 8'd10;//ADC的采样频率147KHz F=Fs*n/N;Fs是采样频率,n是第几个点,N是采样点数,本例中是1024
assign vga_fengzhi = ((fengzhi_max - fengzhi_min) * 10'd50) >> 8'd8;
assign fft_out = sqrt_data_out;
fft3 fft_init
(
.clk (CLK_50M ),//
.reset_n (RST_N ),//
.inverse (inverse ),//
.sink_valid (sink_valid ),//input
.sink_sop (sink_sop ),//
.sink_eop (sink_eop ),//
.sink_real (sink_real ),//
.sink_imag (sink_imag ),//
.sink_error (sink_error ),//
.source_ready (source_ready ),//
.sink_ready (sink_ready ),//output
.source_error (source_error ),//
.source_sop (source_sop ),//
.source_eop (source_eop ),//
.source_valid (source_valid ),//
.source_exp (source_exp ),//
.source_real (source_real ),//
.source_imag (source_imag )//
);
endmodule
代码中有很多的测试代码也放在里面了,也是自己调试过程中的记录,当然,这个是FFT部分的代码,并不是整个工程的,也仅供参考。在计算最大值方面还存在一些问题,就是每次有新的数据帧输入的时候,上一步中计算出来的最大值会被复位,导致输出不会一直稳定,所以在输入一个稳定正弦波的时候,无法一直输出稳定的频率,而是一直在查找过程,不过基本是验证了该方法的可行性,高手可以帮修改一下。
下面是测试波形图:
我输入的是2000Hz的正弦波,计算出来的值是2009,允许误差。可以增加采样点数,提高精度。
以上就是FFT调试过程的记录,供以后查阅。