赞
踩
一、什么是FFT
1.1 简介与我的理解
FFT是离散傅立叶变换的快速算法,可以将一个信号变换到频域。有些信号在时域上是很难看出什么特征的,但是如果变换到频域之后,就很容易看出特征了。这就是很多信号分析采用FFT变换的原因。另外,FFT可以将一个信号的频谱提取出来,这在频谱分析方面也是经常用的。
下面说说具体物理意义。一个模拟信号,经过ADC采样之后,就变成了数字信号。采样定理告诉我们,采样频率要大于信号频率的两倍,这些我就不在此罗嗦了。采样得到的数字信号,就可以做FFT变换了。N个采样点,经过FFT之后,就可以得到N个点的FFT结果。为了方便进行FFT运算,通常N取2的整数次方。假设采样频率为Fs,信号频率F,采样点数为N。那么FFT之后结果就是一个为N点的复数。每一个点就对应着一个频率点。这个点的模值,就是该频率值下的幅度特性。
假设FFT之后某点n用复数a+bi表示,那么这个复数的模就是An=根号a*a+b*b,相位就是Pn=atan2(b,a)。根据以上的结果,就可以计算出n点(n≠1,且n<=N/2)对应的信号的表达式为:An/(N/2)*cos(2*pi*Fn*t+Pn),即2*An/N*cos(2*pi*Fn*t+Pn)。对于n=1点的信号,是直流分量,幅度即为A1/N。 由于FFT结果的对称性,通常我们只使用前半部分的结果,即小于采样频率一半的结果。atan2(a,b)返回以弧度表示的 y/x 的反正切.相位的计算可用函数atan2(b,a)计算。atan2(b,a)是求坐标为(a,b)点的角度值,范围从-pi到pi。
1.2 实际例子介绍(网络照搬)
以一个实际信号为例,求取其FFT。
信号为:S=2+3*cos(2*pi*50*t-pi*30/180)+1.5*cos(2*pi*75*t+pi*90/180)
式中cos参数为弧度,所以-30度和90度要分别换算成弧度。
我们以256Hz的采样率对这个信号进行采样,总共采样256点。按照我们上面的分析,Fn=(n-1)*Fs/N,我们可以知道,每两个点之间的间距就是1Hz,第n个点的频率就是n-1。
我们的信号有3个频率:0Hz(直流2v)、50Hz、75Hz,应该分别在第1个点、第51个点、第76个点上出现峰值,其它各点应该接近0
1点: 512+0i
2点: -2.6195E-14 - 1.4162E-13i
3点: -2.8586E-14 - 1.1898E-13i
50点:-6.2076E-13 - 2.1713E-12i
51点:332.55 - 192i
52点:-1.6707E-12 - 1.5241E-12i
75点:-2.2199E-13 -1.0076E-12i
76点:3.4315E-12 + 192i
77点:-3.0263E-14 +7.5609E-13i
很明显,1点、51点、76点的值都比较大,它附近的点值都很小,可以认为是0,即在那些频率点上的信号幅度为0。接着,我们来计算各点的幅度值。分别计算这三个点的模值,结果如下:
1点 : 512
51点:384
76点:192
按照公式,可以计算出直流分量为:512/N=512/256=2(第一个点是除以N其他的除2);50Hz信号的幅度为:384/(N/2)=384/(256/2)=3;75Hz信号的幅度为192/(N/2)=192/(256/2)=1.5。可见,从频谱分析出来的幅度是正确的。
然后再来计算相位信息。直流信号没有相位可言,不用管它。先计算50Hz信号的相位,atan2(-192, 332.55)=-0.5236,结果是弧度,换算为角度就是180*-0.5236)/pi=-30.0001。再计算75Hz信号的相位,atan2(192, 3.4315E-12)=1.5708弧度,换算成角度就是180*1.5708/pi=90.0002。可见,相位也是对的。根据FFT结果以及上面的分析计算,我们就可以写出信号的表达式了,它就是我们开始提供的信号。
附上MATLAB仿真代码
- close all; %先关闭所有图片
- Adc=2; %直流分量幅度
- A1=3; %频率F1信号的幅度
- A2=1.5; %频率F2信号的幅度
- F1=50; %信号1频率(Hz)
- F2=75; %信号2频率(Hz)
- Fs=256; %采样频率(Hz)
- P1=-30; %信号1相位(度)
- P2=90; %信号相位(度)
- N=256; %采样点数
- t=(0:1/Fs:N/Fs); %采样时刻
-
- %信号
- S=Adc+A1*cos(2*pi*F1*t+pi*P1/180)+A2*cos(2*pi*F2*t+pi*P2/180);
- %显示原始信号
- plot(S);
- title('原始信号');
-
- figure;
- Y = fft(S,N); %做FFT变换
- Ayy = (abs(Y)); %取模
- plot(Ayy(1:N)); %显示原始的FFT模值结果
- title('FFT 模值');
-
- figure;
- Ayy=Ayy/(N/2); %换算成实际的幅度
- Ayy(1)=Ayy(1)/2;
- F=([1:N]-1)*Fs/N; %换算成实际的频率值
- plot(F(1:N/2),Ayy(1:N/2)); %显示换算后的FFT模值结果
- title('幅度-频率曲线图');
1.3总结
假设采样频率为Fs,采样点数为N,做FFT之后,某一点n(n从1开始)表示的频率为:Fn=(n-1)*Fs/N;该点的模值除以N/2就是对应该频率下的信号的幅度(对于直流信号是除以N);该点的相位即是对应该频率下的信号的相位。相位的计算可用函数atan2(b,a)计算。atan2(b,a)是求坐标为(a,b)点的角度值,范围从-pi到pi。要精确到xHz,则需要采样长度为1/x秒的信号,并做FFT。要提高频率分辨率,就需要增加采样点数,这在一些实际的应用中是不现实的,需要在较短的时间内完成分析。解决这个问题的方法有频率细分法,比较简单的方法是采样比较短时间的信号,然后在后面补充一定数量的0,使其长度达到需要的点数,再做FFT,这在一定程度上能够提高频率分辨力。
二、VIVADO之FFT ip核使用
fft的ip核使用对新手来说显得十分复杂。这里直接从ip核设置每个地方开始介绍。
2.1 调出ip核设置
接下来是具体参数介绍
(1)number of channels :变换通道,可以选择多通道,实现多帧数据同时进行FFT运算
(2)transform lenfgth : FFT变换长度,如果选择了最下面的‘run time configurable transdorm legth’,则该参数是FFT变化的最大长度,一般不选。
(3)采样时钟核数据时钟:这个设置越大速度越快,且可以fft测得clk/2大小的频率,如100mhz时钟可以算出50M以内得频率。
(4)architecure choice:FFT架构选择,消耗资源依次递减。第一个是自动选择。选完架构可以从左边框中的latency(隐藏起来了)看一次FFT需要的时间,即求完这N个点(FFT变换长度)的时间。资源消耗越高耗时越短,根据工程需求选择。一般选中间的。
(5)data format:1. 定点全精度 2. 定点缩减位宽 一般就默认Fixed Point
(6) scaling optios :缩放选项 :
1、 block floating point :不管输入的格式如何,FFT变化内部都采用浮点,会根据每一级的的数据情况自动缩放。 这个模式的输入输出位宽一致,便于调用。没有特殊需求就用这个。
2、scaled :在m_axis_data_tuser中会有5BIT表示每一级的缩放情况,在s_axis_config_data中会有相应的字段配置配置缩放因子.每一级别包含2个stage ,2个bit 表示一级缩放,一般0-3可选,如果log(NFFT)不是2的倍数,则最高一级的缩放只能在0-1之间选取。
3、unscaled :不用担心变化过程中会出现溢出,但是输出位宽会非常大,一般情况为了后续的信号处理,不建议用。
(7) Aresten : 复位信号要勾选,至少保持两个时钟的低电平。FFT IP核的复位引脚(ARESETn)最好使用,怎么使用?–把时钟IP核输出的locked引脚赋给FFT的复位引脚,并且在locked引脚为低时给FFT IP核一系列输入引脚均赋予初值。时钟IP核locked引脚代表意义:时钟IP核输出的时钟是否稳定有效,低时不稳定,高时表明时钟IP核输出时钟稳定
(8) output odering options:默认是倒序但是我们一般选自然输出。
(9)optional output fileds :选项输出字段:
1、xk_index:FFT 变幻的结果索引,在m_axis_data_user中有相应的字段。
2、OVFLO是变换中溢出的指示信号,对应event_fft_overflow.
其他的一般不用设置。
我们可以看到假设我们输入的数据位宽是10,则FFT之后,还有虚数部分,[9:0]是实数,[25:16]是虚数。一共有2048个点,一次FFT。
2.2 IP核参数的意义
这里面引脚很多,最重要的当然就是输入输出了。
.aclk(aclk), 输入时钟
.aresetn(aresetn), 复位 ,接PLL的locked
Input[N:0]: s_axis_config_tdata:控制输入模式,最低位控制。1fft 0ifft。如果我们只需要FFT就直接设置N'b1就ok,这里在配置scale模式的时候需要使用,之后再专门介绍。
Input: s_axis_config_tvalid: 设置为1就行
Output: s_axis_config_tready:不接不管他,总之如果你的IP核没启动这个就为0.
Output: s_axis_data_tready:aresetn拉高两个时钟周期后,该口输出1;此时ip核初始化完成,可进行数据输入。
Input: s_axis_data_tvalid:数据有效 当s_axis_data_tready高电平后,将s_axis_data_tvalid拉高L个周期,输入L个数据进行fft;L是FFT的点数。
Input[M:0]: s_axis_data_tdata:看配置界面会提示如下图低16就是实部高16虚部,我们ADC的数据是实信号放实部就好,新手注意这里必须是有符号数。
Input: s_axis_data_tlast:输入L个数据后拉高,停止数据输入。TLAST是起到一个输入数据末端指示作用,比如你作8点FFT运算,你要给8个数据给IP核吧,在最后一个数据期间你把TLAST拉高就行了,意思是告诉IP核,这一组的8个数据都给你带过来了,这是最后一个。IP核一验算,整整齐齐8个,就不会找你麻烦了。
做fft需要耗费的时钟周期计算如下s_axis_data_tlast- s_axis_data_tvalid
Output[M1:0]:m_axis_data_tdata:高位为虚部,低位为实部。 在FPGA里不好开方所以我们一般求他的功率谱即实部和虚部平方相加。
Output: m_axis_data_tvalid:当fft结果输出时拉高,输出2最后一个数据时拉低,这个是对齐最后一个数据的。
Output[M2:0]:m_axis_data_tuser:这里的OVFLO是溢出指示位,这里的XK_INDEX就是索引了,是对齐谱线的。XK_INDEX*fs/N=频率(fs就是你的采样率,N就是FFT点数)
Input: m_axis_data_tready:需保持高电平,保证FFT单元处在计算模式,并且能够输出结算结果;接 1 .m_axis_data_tready(1)
Output: m_axis_data_tlast:当fft结果输出到最后一个结果时拉高,紧接着下一个时钟就拉低。
主要就是时钟、复位、配置、数据、和事件报告。s是从机,m是主机,也好理解,对于FPGA来说是要接受主句的输出来作为输入数据,去FFT。
转自 数字信号处理(三):Xilinx FFT IP核详解(二) - 知乎 (zhihu.com)
20230109补充代码和解释
本代码是把FFTIP例化在模块内方便调用。
要注意的是,我输入datach1是无符号补码,都是正数。在送给fft ip时,实际上要补上一个符号位0,s_axis_data_tdata <= {22'd0,input_data_ch1};这里面就是我补了0,实际使用根据你的输入来自己设置。总之,ip输入带符号。而输出肯定也是带符号的。用ILA看的时候设置为有符号数看。
我下面还例化了别的IP,自行去除。本模块只需要设置好你的输入,即可跑出fft 后的实部和虚部。
- module fft_fix_4096(
- input aclk ,
- input aresetn ,
-
- input [9:0] input_data_ch1 , //adc数据输入
- output signed [15:0] fft_real ,
- output signed [15:0] fft_imag ,
- output signed [32:0] amp , //输出平方和,可以不要
- output reg [11:0] freq , //由于multisine fs/N=1,谱线间隔为1HZ
- output fft_out_valid , //fft输出有效。可以通过这个接口取4096个值。
- output [23:0] abs_fft , //开方
- output abs_tvalid //开方数据输出有效
-
- );
- //配置数据
- reg [10:0] input_data_ch1_reg; //和ADC输出格式有关,总之ip core 需要带符号位
- wire [7:0] s_axis_config_tdata;
- reg s_axis_config_tvalid;
- //输入数据
- wire s_axis_data_tready;
- reg [31:0] s_axis_data_tdata;
- reg s_axis_data_tvalid;
- reg s_axis_data_tlast;
- //输出数据
- wire [31:0] m_axis_data_tdata;
- wire [23:0] m_axis_data_tuser;
- wire m_axis_data_tvalid;
- wire m_axis_data_tlast;
- reg [7:0] cfg_cnt;
- reg [13:0] sink_ctl_cnt;
- reg [15:0] fft_real,fft_imag;
- reg fft_out_valid;
- wire [7:0] m_axis_status_tdata;
- wire m_axis_status_tvalid;
- wire event_frame_started;
- wire event_tlast_unexpected;
- wire event_tlast_missing;
- wire event_data_in_channel_halt;
-
- wire s_axis_config_tready;
-
-
-
- xfft_0 fixed_fft (
- .aclk(aclk), // input wire aclk
- .aresetn(aresetn), // input wire aresetn
-
- .s_axis_config_tdata(s_axis_config_tdata), // input wire [7 : 0] s_axis_config_tdata
- .s_axis_config_tvalid(s_axis_config_tvalid), // input wire s_axis_config_tvalid
- .s_axis_config_tready(s_axis_config_tready), // output wire s_axis_config_tready
-
- .s_axis_data_tdata(s_axis_data_tdata), // input wire [31 : 0] s_axis_data_tdata
- .s_axis_data_tvalid(s_axis_data_tvalid), // input wire s_axis_data_tvalid
- .s_axis_data_tready(s_axis_data_tready), // output wire s_axis_data_tready
- .s_axis_data_tlast(s_axis_data_tlast), // input wire s_axis_data_tlast
-
- .m_axis_data_tdata(m_axis_data_tdata), // output wire [31 : 0] m_axis_data_tdata
- .m_axis_data_tuser(m_axis_data_tuser), // output wire [23 : 0] m_axis_data_tuser
- .m_axis_data_tvalid(m_axis_data_tvalid), // output wire m_axis_data_tvalid
- .m_axis_data_tlast(m_axis_data_tlast), // output wire m_axis_data_tlast
-
- .m_axis_status_tdata(m_axis_status_tdata), // output wire [7 : 0] m_axis_status_tdata
- .m_axis_status_tvalid(m_axis_status_tvalid), // output wire m_axis_status_tvalid
-
- .event_frame_started(event_frame_started), // output wire event_frame_started
- .event_tlast_unexpected(event_tlast_unexpected), // output wire event_tlast_unexpected
- .event_tlast_missing(event_tlast_missing), // output wire event_tlast_missing
- .event_data_in_channel_halt(event_data_in_channel_halt) // output wire event_data_in_channel_halt
- );
-
- //fft core config
- always@(posedge aclk or negedge aresetn)
- begin
- if(!aresetn)
- cfg_cnt <= 8'd0;
- else
- begin
- if(cfg_cnt < 8'd200)
- cfg_cnt <= cfg_cnt + 1'b1;
- else
- cfg_cnt <= cfg_cnt;
- end
- end
- always@(posedge aclk or negedge aresetn)
- begin
- if(!aresetn)
- s_axis_config_tvalid <= 1'b0;
- else
- begin
- if(cfg_cnt < 8'd200)
- s_axis_config_tvalid <= 1'b1;
- else
- s_axis_config_tvalid <= 1'b0;
- end
- end
- assign s_axis_config_tdata = 8'd1;
-
- //fft sink_in ctl//
- always@(posedge aclk or negedge aresetn)
- begin
- if(!aresetn)
- s_axis_data_tdata <= 32'b0;
- else
- s_axis_data_tdata <= {22'd0,input_data_ch1}; //本人adc 10位无符号补码,补一位符号位0,ip设置11位输入
- end
-
- always@(posedge aclk or negedge aresetn)
- begin
- if(!aresetn)
- sink_ctl_cnt <= 14'd8194;
- else if(s_axis_config_tvalid)
- sink_ctl_cnt <= 14'd0;
- else if(sink_ctl_cnt == 14'd8192)
- sink_ctl_cnt <= 14'd1;
- else
- sink_ctl_cnt <= sink_ctl_cnt + 1'b1;
- end
- //s_axis_data_tvalid
- always@(posedge aclk or negedge aresetn)
- begin
- if(!aresetn)
- s_axis_data_tvalid <= 1'b0;
- else if(sink_ctl_cnt < 14'd1)
- s_axis_data_tvalid <= 1'b0;
- else if(sink_ctl_cnt < 14'd4097)
- s_axis_data_tvalid <= 1'b1;
- else
- s_axis_data_tvalid <= 1'b0;
- end
- //s_axis_data_tlast
- always@(posedge aclk or negedge aresetn)
- begin
- if(!aresetn)
- s_axis_data_tlast <= 1'b0;
- else
- begin
- if(sink_ctl_cnt == 14'd4096)
- s_axis_data_tlast <= 1'b1;
- else
- s_axis_data_tlast <= 1'b0;
- end
- end
- //fft sink_in ctl/ / 对齐
- always@(posedge aclk)
- begin
- fft_real <= m_axis_data_tdata[15:0];
- end
- always@(posedge aclk)
- begin
- fft_imag <= m_axis_data_tdata[31:16];
- end
- always@(posedge aclk)
- begin
- fft_out_valid <= m_axis_data_tvalid;
- end
-
- always@(posedge aclk)
- begin
- freq <= m_axis_data_tuser[11:0]; //这个位宽取n-1个,2^n=N,由于MATLAB里FS=N,所以谱线间隔是1HZ
- end
- /********** 计算频谱的幅值信号 **********/
- wire [31:0] xkre_square, xkim_square;
- mult_gen_0 inst1 (
- .CLK(aclk), // input wire CLK
- .A(fft_real), // input wire [15 : 0] A
- .B(fft_real), // input wire [15 : 0] B
- .P(xkre_square) // output wire [31 : 0] P
- );
- mult_gen_0 inst2 (
- .CLK(aclk), // input wire CLK
- .A(fft_imag), // input wire [15 : 0] A
- .B(fft_imag), // input wire [15 : 0] B
- .P(xkim_square) // output wire [31 : 0] P
- );
- assign amp = xkre_square+ xkim_square;
- wire abs_tvalid;
- cordic_0 your_instance_name (
- .aclk(aclk), // input wire aclk
- .aresetn(aresetn), // input wire aresetn
- .s_axis_cartesian_tvalid(m_axis_data_tvalid), // input wire s_axis_cartesian_tvalid
- .s_axis_cartesian_tdata(amp[31:0]), // input wire [31 : 0] s_axis_cartesian_tdata
- .m_axis_dout_tvalid(abs_tvalid), // output wire m_axis_dout_tvalid
- .m_axis_dout_tdata(abs_fft) // output wire [23 : 0] m_axis_dout_tdata
- );
- endmodule
注意事项补充:
1.始终均可拉高的引脚:S_AXIS_CONFIG_TVALID,S_AXIS_DATA_TREADY。
2.我们要注意采样频率和FFT采样点数,这两个关系到分辨率,而且采样频率还得满足奈奎斯特定理。FFT采样点数越大越好,即分辨率会高。但是太大了运算时间就会长,分辨率分辨的就是频率,第m个点对应频率m*FS/N。
3.当FFT计算结果输出完成后,信号fft_m_data_tlast变为高电平,代表数据输出结束,并在延时一小段时间后,fft_s_data_tready重新变为低电平,代表IP核重新进入到空闲状态。可以进行对IP核下一组数据的输入。
4.输入输出如果不到8bit会补上,例如12bit的实数输入,输出会有32位,[11:0]是实数部分[25:16]是虚数,实数虚数前各补4个0,输出也是一样的
2024.03.22补充:
这个地方有坑。一般我们只希望我们拉高多少个电平时读多少数据来FFT,在有一次设计中,我把FIFO空信号设置为读使能来将数据读到FFTip中,但是发现不对!!!我以为是FFT ip只支持连续送数据进去,结果是我不小心设置成实时的了,而空信号是断断续续的………………我超服了。
过几天把缩放的介绍一下再把多通道的介绍一下。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。