1)实验平台:正点原子开拓者FPGA 开发板
2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第五十一章 基于FFT IP核的音频频谱仪实验
FFT的英文全称是Fast Fourier Transformation,即快速傅里叶变换,它是根据离散傅
里叶变换(DFT)的奇、偶、虚、实等特性,在离散傅里叶变换的基础上改进得到的。FFT主要
用于频谱分析,可以将时域信号转化为频域信号,在滤波、图象处理和数据压缩等领域具有普
遍应用。本章我们将使用Quartus II软件自带的FFT IP核来分析音频信号的频谱,作为一个简
单的例程,向大家介绍Altera FFT IP核的使用方法。
本章包括以下几个部分:
51.1 FFT IP 核简介
51.2 实验任务
51.3 硬件设计
51.4 程序设计
51.5 下载验证
FFT IP核简介
首先,我们简单介绍下FFT:FFT即快速傅里叶变换,是1965年由J.W.库利和T.W.图基提出
的。采用这种算法能使计算机计算离散傅里叶变换(DFT)所需要的乘法次数大为减少,被变
换的抽样点数N越多,FFT算法计算量的节省就越显著。
FFT可以将一个时域信号变换到频域。因为有些信号在时域上是很难看出什么特征的,但
是如果变换到频域之后,就很容易看出特征了,这就是很多信号分析采用FFT变换的原因。另
外,FFT可以将一个信号的频谱提取出来,这在频谱分析方面也是经常用的。简而言之,FFT就
是将一个信号从时域变换到频域方便我们分析处理。在实际应用中,一般的处理过程是先对一
个信号在时域进行采集,比如我们通过ADC,按照一定大小采样频率F去采集信号,采集N个点,
那么通过对这N个点进行FFT运算,就可以得到这个信号的频谱特性。
这里还涉及到一个采样定理的概念:在进行模拟/数字信号的转换过程中,当采样频率F大
于信号中最高频率fmax的2倍时(F>2*fmax),采样之后的数字信号完整地保留了原始信号中的
信息,采样定理又称奈奎斯特定理。举个简单的例子:比如我们正常人发声,频率范围一般在
8KHz以内,那么我们要通过采样之后的数据来恢复声音,我们的采样频率必须为8KHz的2倍以
上,也就是必须大于16KHz才行。
模拟信号经过ADC采样之后,就变成了数字信号,采样得到的数字信号,就可以做FFT变换
了。N个采样点数据,在经过FFT之后,就可以得到N个点的FFT结果。为了方便进行FFT运算,
通常N取2的整数次方。
假设采样频率为F,对一个信号采样,采样点数为N,那么FFT之后结果就是一个N点的复数,
每一个点就对应着一个频率点(以基波频率为单位递增),这个点的模值(sqrt(实部
2
+虚部
2
))
就是该频点频率值下的幅度特性。具体跟原始信号的幅度有什么关系呢?假设原始信号的峰值
为A,那么FFT的结果的每个点(除了第一个点直流分量之外)的模值就是A的N/2倍,而第一个
点就是直流分量,它的模值就是直流分量的N倍。
这里还有个基波频率,也叫频率分辨率的概念,就是如果我们按照F的采样频率去采集一
个信号,一共采集N个点,那么基波频率(频率分辨率)就是fk=F/N。这样,第n个点对应信号
频率为:F*(n-1)/N;其中n≥1,当n=1时为直流分量。关于FFT我们就介绍到这。如果我们要
自己实现FFT算法,对于不懂数字信号处理的朋友来说,是比较难的。不过,Quartus II提供
的IP核里面就有FFT IP核可以给我们使用,因此我们只需要知道如何使用这个IP核,就可以迅
速的完成FFT计算,而不需要自己学习数字信号处理,去编写代码了,大大方便了我们的开发。
实验任务
本节实验任务是先将电脑或手机的音乐通过开拓者开发板上的WM8978器件输给FPGA,然后
使用Altera FFT IP核分析WM8978输出的音频信号的频谱,并将采样点的幅度特性显示到4.3寸
RGB TFT-LCD上。
硬件设计
音频WM8978接口部分的硬件设计与“音频环回实验”完全相同,请参考“音频环回实验”
中的硬件设计部分。RGB TFT-LCD接口部分的硬件设计请参考“RGB TFT-LCD彩条显示实验”中
的硬件设计部分。
由于WM8978接口和RGB TFT-LCD引脚数目较多且在前面相应的章节中已经给出它们的管脚
列表,这里不再列出管脚分配。
程序设计
图 51.4.1是根据本章实验任务画出的系统框图。首先,WM8978模块通过控制接口配置
WM8978相关的寄存器。WM8978在接收电脑传来的音频数据后,将一路音频数据送给喇叭播放,
将另一路经ADC采集过的数据送给WM8978模块。WM8978模块紧接着将音频数据送给FFT模块做频
谱分析,得到频谱幅度数据。LCD模块则负责读取频谱幅度数据,并在RGB TFT-LCD上显示频谱。
图 51.4.1 IP核之FFT实验系统框图
程序中各模块端口及信号连接如图 51.4.2所示
图 51.4.2 模块连接图
FPGA顶层(FFT_audio_lcd)例化了以下四个模块:pll时钟模块(pll)、wm8978模块
(wm8978_ctrl)、FFT模块(FFT_top)、LCD模块(LCD_top)。
pll时钟模块(pll):本实验中WM8978模块所需要的时钟为12MHz,FFT模块的驱动时钟为
50MHz,另外LCD模块需要50Mhz的时钟来处理、缓存FFT模块输出的数据,并在10MHz的驱动时
钟下驱动RGB TFT-LCD显示。因此需要一个PLL模块用于产生系统各个模块所需的时钟频率。
wm8978模块(wm8978_ctrl):WM8978控制模块主要完成WM8978的配置和WM8978接收的录
音音频数据的接收处理,以及FPGA发送的音频数据的发送处理。该模块和“音频环回实验”章
节中用到的wm8978_ctrl模块为同一个模块,本实验对该模块有少许更改,我们会在后面进行
讲解,有关该模块的详细介绍请大家参考“音频环回实验”章节。
FFT模块(FFT_top):FFT模块将wm8978模块传输过来的音频信号进行缓存,然后将其送
给FFT IP核进行频谱分析。接着计算FFT IP核输出复数的平方根,即频谱的幅度值,然后将其
输出给LCD模块显示。
LCD模块(LCD_top):LCD模块取FFT模块传输过来的一帧数据的一半(也就是64个数据)
进行缓存,并驱动RGB TFT-LCD液晶屏显示频谱。
顶层模块的代码如下:
1 module FFT_audio_lcd(
2 input sys_clk,
3 input rst_n,
4
5 // WM8978接口
6 output aud_mclk,
7 input aud_bclk,
8 input aud_lrc,
9 input aud_adcdat,
10 output aud_dacdat,
11 output aud_scl,
12 inout aud_sda,
13
14 //LCD接口
15 output lcd_hs,
16 output lcd_vs,
17 output lcd_de,
18 output [15:0] lcd_rgb,
19 output lcd_bl,
20 output lcd_rst,
21 output lcd_pclk
22 );
23
24 //wire define
25 wire clk50M;
26 wire clk10M;
27
28 wire audio_valid;
29 wire [15:0] audio_data;
30
31 wire fft_sop;
32 wire fft_eop;
33 wire fft_valid;
34 wire [15:0] fft_data;
35
36 //*****************************************************
37 //** main code
38 //*****************************************************
39
40 //锁相环模块
41 pll pll_inst (
42 .inclk0 (sys_clk),
43
44 .c0 (aud_mclk),
45 .c1 (clk50M),
46 .c2 (clk10M)
47 );
48
49 //例化WM8978控制模块
50 wm8978_ctrl u_wm8978_ctrl(
51 .clk (clk50M),
52 .rst_n (rst_n),
53
54 .aud_bclk (aud_bclk), // WM8978位时钟
55 .aud_lrc (aud_lrc), // 对齐信号
56 .aud_adcdat (aud_adcdat), // 音频输入
57 .aud_dacdat (aud_dacdat), // 音频输出
58
59 .aud_scl (aud_scl), // WM8978的SCL信号
60 .aud_sda (aud_sda), // WM8978的SDA信号
61
62 .dac_data (audio_data), // 输出的音频数据
63 .adc_data (audio_data), // 输入的音频数据
64 .rx_done (audio_valid), // 一次接收完成
65 .tx_done () // 一次发送完成
66 );
67
68 //对输入的音频数据进行傅里叶变换
69 FFT_top FFT_u(
70 .clk_50m (clk50M),
71 .rst_n (rst_n),
72
73 .audio_clk (aud_bclk),
74 .audio_data (audio_data),
75 .audio_valid (audio_valid),
76
77 .data_modulus (fft_data),
78 .data_sop (fft_sop),
79 .data_eop (fft_eop),
80 .data_valid (fft_valid)
81 );
82
83 //RGB_LCD 显示模块
84 LCD_top LCD_u(
85 .clk50M (clk50M),
86 .clk10M (clk10M),
87 .rst_n (rst_n),
88
89 .lcd_hs (lcd_hs),
90 .lcd_vs (lcd_vs),
91 .lcd_de (lcd_de),
92 .lcd_rgb (lcd_rgb),
93 .lcd_bl (lcd_bl),
94 .lcd_rst (lcd_rst),
95 .lcd_pclk (lcd_pclk),
96
97 .fft_data (fft_data),
98 .fft_sop (fft_sop),
99 .fft_eop (fft_eop),
100 .fft_valid (fft_valid)
101 );
102
103 endmodule
顶层模块主要完成了对各个子模块的例化、接收外部传输给FPGA的数据、以及输出数据给外设。
WM8978模块里例化了三个子模块:音频接收模块(audio_receive)、音频发送模块
(audio_send)、WM8978配置模块(wm8978_config)。
WM8978模块(wm8978_ctrl)只是在“音频环回实验”章节中的WM8978控制模块(wm8978_ctrl
模块)的基础上做了两处修改,下面我们会说明作出修改的地方,以及这么修改的原因。
wm8978_ctrl模块的详细介绍,还请查看“音频环回实验”章节里相应的部分。
第一个要修改的地方是wm8978_ctrl模块内部一个常量的定义,代码如下所示:
1 module wm8978_ctrl(
2 //system clock
3 input clk , // 时钟信号
4 input rst_n , // 复位信号
5
6 //wm8978 interface
7 //audio interface(master mode)
8 input aud_bclk , // WM8978位时钟
9 input aud_lrc , // 对齐信号
10 input aud_adcdat , // 音频输入
11 output aud_dacdat , // 音频输出
12 //control interface
13 output aud_scl , // WM8978的SCL信号
14 inout aud_sda , // WM8978的SDA信号
15
16 //user interface
17 input [31:0] dac_data , // 输出的音频数据
18 output [31:0] adc_data , // 录音的数据
19 output rx_done , // 一次采集完成
20 output tx_done // 一次发送完成
21 );
22
23 //parameter define
24 parameter WL = 6'd16; // word length音频字长定义
25
26 //*****************************************************
27 //** main code
28 //*****************************************************
……省略部分代码……
67 endmodule
我们在代码的第24行对音频字长做了修改,将常量WL的值由32改为了16。这是因为如果这
里依然保持字长为32位,那么后面FFT模块占用的资源将会比较大,所以这里将字长修改为16
位。
第二个要修改地方的是音频接收模块(audio_receive)内部,lrc_edge信号的定义。代
码如下所示:
1 module audio_receive #(parameter WL = 6'd32) ( // WL(word length音频字长定义)
2 //system clock 50MHz
3 input rst_n , // 复位信号
4
5 //wm8978 interface
6 input aud_bclk , // WM8978位时钟
7 input aud_lrc , // 对齐信号
8 input aud_adcdat, // 音频输入
9
10 //user interface
11 output reg rx_done , // FPGA接收数据完成
12 output reg [31:0] adc_data // FPGA接收的数据
13 );
14
15 //reg define
16 reg aud_lrc_d0; // aud_lrc延迟一个时钟周期
17 reg [ 5:0] rx_cnt; // 发送数据计数
18 reg [31:0] adc_data_t; // 预输出的音频数据的暂存值
19
20 //wire define
21 wire lrc_edge ; // 边沿信号
22
23 //*****************************************************
24 //** main code
25 //*****************************************************
26
27 //assign lrc_edge = aud_lrc ^ aud_lrc_d0; // LRC信号的边沿检测
28 assign lrc_edge = aud_lrc & (~aud_lrc_d0); // LRC信号的边沿检测
29
……省略部分代码……
73 endmodule
第27行的代码是修改前lrc_edge信号的定义,第28行代码则是修改后的信号定义,这里将
原来信号的双边沿检测,修改为上升沿检测。LRC信号的下降/上升沿将用于采集左/右两个通
道的音频数据;修改成上升沿检测之后,程序只采集单个通道(右通道)的音频。这么做的原
因是如果我们同时采集两个通道的音频数据,那么在通道切换的时候,会给信号的频谱带来一
个高频的噪声。
接下来我们介绍FFT模块(FFT_top)的相关内容。FFT模块(FFT_top)内部例化了4个模
块:音频数据缓存模块(audio_in_fifo)、FFT控制模块(fft_ctrl)、FFT IP核(FFT)、
数据取模模块(data_modulus),模块结构如下所示:
图 51.4.3 FFT模块内部结构图
由图可知音频数据进入FFT模块(FFT_top)后,先经音频数据缓存模块(audio_in_fifo)
缓存数据,然后再将数据送给FFT IP核。音频数据经FFT IP核处理后,输出形式为复数的数据。
紧接着复数数据经过数据取模模块(data_modulus)处理后得到复数的模值,最后将模值从FFT
模块(FFT_top)输出出去。
音频数据缓存模块(audio_in_fifo):音频数据缓存模块是一个fifo,这里它的深度设
置为64,宽度为16bit。它负责缓存WM8978模块传输过来的音频数据。另外,当FFT控制模块
(fft_ctrl)输出的读请求信号拉高时,音频数据缓存模块会将缓存的数据输出给FFT IP核做
频谱分析。
FFT控制模块(fft_ctrl):FFT控制模块依据FFT IP核的数据输入时序原理,产生数据传
输的控制信号,来驱动FFT IP核不断地进行FFT分析。
FFT IP核(FFT):这里直接例化Quartus II软件提供的FFT IP核,我们只需按照IP核的
数据传输时序,将音频数据送给FFT IP核,它会自动输出经过FFT分析后的复数数据。
数据取模模块(data_modulus):数据取模模块负责计算FFT IP核输出的复数的模值,也
就是这个频率点的幅度模值。
FFT模块(FFT_top)的代码如下所示,它完成了各个子模块的例化以及信号交互:
1 module FFT_top(
2 input clk_50m,
3 input rst_n,
4
5 input audio_clk,
6 input audio_valid,
7 input [15:0] audio_data,
8
9 output data_sop,
10 output data_eop,
11 output data_valid,
12 output [15:0] data_modulus
13 );
14
15 //wire define
16 wire [15:0] audio_data_w;
17 wire fifo_rdreq;
18 wire fifo_rd_empty;
19
20 wire fft_rst_n;
21 wire fft_ready;
22 wire fft_sop;
23 wire fft_eop;
24 wire fft_valid;
25
26 wire source_sop;
27 wire source_eop;
28 wire source_valid;
29 wire [15:0] source_real;
30 wire [15:0] source_imag;
31
32 //*****************************************************
33 //** main code
34 //*****************************************************
35
36 //例化fifo,缓存wm8978输出的音频数据
37 audio_in_fifo fifo_inst(
38 .aclr (~rst_n),
39
40 .wrclk (audio_clk),
41 .wrreq (audio_valid),
42 .data (audio_data),
43 .wrfull (),
44
45 .rdclk (clk_50m),
46 .rdreq (fifo_rdreq),
47 .q (audio_data_w),
48 .rdempty (fifo_rd_empty)
49 );
50
51 //FFT控制模块,控制FFT的输入端口
52 fft_ctrl u_fft_ctrl(
53 .clk_50m (clk_50m),
54 .rst_n (rst_n),
55
56 .fifo_rd_empty (fifo_rd_empty),
57 .fifo_rdreq (fifo_rdreq),
58
59 .fft_ready (fft_ready),
60 .fft_rst_n (fft_rst_n),
61 .fft_valid (fft_valid),
62 .fft_sop (fft_sop),
63 .fft_eop (fft_eop)
64 );
65
66 //例化 FFT IP核
67 FFT FFT_u(
68 .clk (clk_50m),
69 .reset_n (fft_rst_n),
70
71 .sink_ready (fft_ready), //FFT准备好信号,此信号为高表示可输入变换数据
72 .sink_real (audio_data_w), //实部
73 .sink_imag (16'd0), //虚部
74 .sink_sop (fft_sop), //输入数据起始信号,与第一个数据对齐
75 .sink_eop (fft_eop), //输入数据结束信号,与最后一个数据对齐
76 .sink_valid (fft_valid), //输入数据有效信号,在输入数据期间要保持高电平有效
77 .inverse (1'b0), //高电平为FFT反变换
78 .sink_error (1'b0), //输入错误信号,置0即可
79
80 .source_ready (1'b1), //后端模块准备好信号,置1即可
81 .source_real (source_real), //实部 有符号数
82 .source_imag (source_imag), //虚部 有符号数
83 .source_sop (source_sop), //起始信号
84 .source_eop (source_eop), //终止信号
85 .source_valid (source_valid), //输出有效信号,FFT变换完成后,此信号置高开始输出数据
86 .source_exp (), //数据的缩放因子 有符号数
87 .source_error () //输出错误信号,若输入的数据格式有误,则不进行FFT变
88 ); //换,并给出错误值
89
90 //对FFT输出的实部和虚部进行取模运算
91 data_modulus u_sqrt_top(
92 .clk_50m (clk_50m),
93 .rst_n (rst_n),
94
95 .source_real (source_real),
96 .source_imag (source_imag),
97 .source_sop (source_sop),
98 .source_eop (source_eop),
99 .source_valid (source_valid),
100
101 .data_modulus (data_modulus),
102 .data_sop (data_sop),
103 .data_eop (data_eop),
104 .data_valid (data_valid)
105 );
106
107 endmodule
我们在代码的52行至64行例化了FFT控制模块(fft_ctrl),它负责产生驱动FFT IP核输
入端口的控制信号,代码如下所示:
1 module fft_ctrl(
2 input clk_50m,
3 input rst_n,
4
5 input fifo_rd_empty,
6 output fifo_rdreq,
7
8 input fft_ready,
9 output reg fft_rst_n,
10 output reg fft_valid,
11 output fft_sop,
12 output fft_eop
13 );
14
15 //reg define
16 reg state;
17 reg [4:0] delay_cnt;
18 reg [9:0] fft_cnt;
19 reg rd_en;
20
21 //*****************************************************
22 //** main code
23 //*****************************************************
24
25 assign fifo_rdreq = rd_en && (~fifo_rd_empty); //fifo读请求信号
26 assign fft_sop = (fft_cnt==10'd1) ? fft_valid : 1'b0; //生成sop信号
27 assign fft_eop = (fft_cnt==10'd128) ? fft_valid : 1'b0; //生成eop信号
28
29 //产生驱动FFT ip核的控制信号
30 always @ (posedge clk_50m or negedge rst_n) begin
31 if(!rst_n) begin
32 state <= 1'b0;
33 rd_en <= 1'b0;
34 fft_valid <= 1'b0;
35 fft_rst_n <= 1'b0;
36 fft_cnt <= 10'd0;
37 delay_cnt <= 5'd0;
38 end
39 else begin
40 case(state)
41 1'b0: begin
42 fft_valid <= 1'b0;
43 fft_cnt <= 10'd0;
44
45 if(delay_cnt < 5'd31) begin //延时32个时钟周期,用于FFT复位
46 delay_cnt <= delay_cnt + 1'b1;
47 fft_rst_n <= 1'b0;
48 end
49 else begin
50 delay_cnt <= delay_cnt;
51 fft_rst_n <= 1'b1;
52 end
53
54 if((delay_cnt==5'd31)&&(fft_ready))
55 state <= 1'b1;
56 else
57 state <= 1'b0;
58 end
59 1'b1: begin
60 if(!fifo_rd_empty)
61 rd_en <= 1'b1;
62 else
63 rd_en <= 1'b0;
64
65 if(fifo_rdreq) begin
66 fft_valid <= 1'b1;
67 if(fft_cnt < 10'd128)
68 fft_cnt <= fft_cnt + 1'b1;
69 else
70 fft_cnt <= 10'd1;
71 end
72 else begin
73 fft_valid <= 1'b0;
74 fft_cnt <= fft_cnt;
75 end
76 end
77 default: state <= 1'b0;
78 endcase
79 end
80 end
81
82 endmodule
我们接下来会在介绍FFT IP核的数据输入时序的同时,对代码进行讲解,如图 51.4.4所
示为IP核的数据输入时序。在让FFT IP核工作之前,需要先让IP核复位一段时间,这里让IP核
复位了32个时钟周期,对应于代码的45行至52行。在复位操作完成后,需要先等FFT IP核拉高
sink_ready信号(表示IP核可以接受数据了),才能进行下一步操作,这步操作对应于代码的
54行至58行。
sink_ready信号拉高后在给FFT IP核送数据的时候,需要同时拉高sink_valid信号。大家
可以看到图 51.4.4中,在发送第一个数据的时候,sink_sop信号(startofpacket,数据包的
开始信号)需要拉高一个时钟周期。相应的,在发送最后一个数据的时候,需要拉高fft_eop
信号(endofpacket,数据包的结束信号)一个时钟周期(图中未展示)。如代码的60行至63
行所示,在FFT IP核拉高sink_ready信号后,先判断音频数据缓存模块(audio_in_fifo)内
是否有数据,若模块内无数据则保持等待。当模块内有数据的时候,会拉高rd_en信号。此时
大家可以看到代码第25行,由于fifo不为空且rd_en信号拉高了,fifo_rdreq信号(fifo的读
使能信号)也会跟着一起拉高。代码65行至75行,fifo_rdreq信号拉高后,fft_cnt计数器开
始计数,计数满128的时候(发送了128个数据),一帧数据(一次数据传输的总量)传输完毕,
接着判断sink_valid信号是否为高电平,这样周而复始下去。需要注意的是,在代码的第26行
和第27行,我们依据fft_cnt计数器的值产生了fft_sop和fft_eop信号。
图 51.4.4 streaming数据流输入时序
在讲解完FFT控制模块(fft_ctrl)后,接下来说明一下怎么配置FFT IP核。我们先打开
MegaWizard Plug-In Manager界面(详细步骤可参考“IP核之RAM实验”章节的程序设计部分),
在搜索框中输入FFT,界面中便会显示我们需要的FFT IP核,如下图所示:
图 51.4.5 FFT IP核配置界面
此时,我们需要点击界面中的FFT v13.1来选中这个IP核,然后选择IP核的生成路径。我
们一般习惯将IP核放置在工程文件夹的paripcore下。然后点击Next,出现以下界面:
图 51.4.6 FFT IP核主界面
然后点击parameterize(参数化),进入参数配置界面,如下所示:
图 51.4.7 FFT IP核Parameterize配置界面
实际上,我们只需要配置这个界面下的Parameters窗口里的参数就可以了。我们开发板上
使用的是Cyclone IV系列的FPGA芯片,所以不用修改Target Device Family中的选项。由于我
们这次实验的采样点数是128个,所以,这里Transform Length设置为128。传输给FFT IP核的
音频数据位宽为16bit,所以这里Data Input Precision(输入数据位宽)设置为16bits。
Twiddle Width是旋转因子的数据位宽,只要比输入数据的位宽低就可以了,这里将其设置成
8bits。Data Output Precision(输出数据位宽)选项这里无法修改。
接下来我们看一下Architecture界面下的选项,Architecture界面如下所示:
图 51.4.8 Architecture配置界面
在I/O Data Flow配置界面下有4个选项,分别为:流模式(Streaming)、缓存突发模式
(Buffered Burst)、可变流模式(Variable Streaming)、突发模式(Burst)。流模式(Streaming)
运算速度大于缓存突发模式(Buffered Burst),突发模式(Buffered Burst) 运算速度大于突
发模式(Burst),且占用资源也依次减少。Variable Streaming模式可用于在线改变Transform
Length的大小。速度和流模式差不多,资源占用更多。
这里我们使用默认的流模式(Streaming)。
然后,我们看一下Implementation Options界面,如图 51.4.9所示。
图 51.4.9 Implementation Options界面
我们依然保留默认设置就好了,图中的配置说明FFT使用了4个乘法器以及2个加法器、以
及DSP块和逻辑单元。这里点击Finish回到FFT IP核主界面,如图 51.4.6所示。接着点击第2
个选项Set Up Simulation选项,出现以下界面:
图 51.4.10 Set Up Simulation界面
如果需要仿真FFT IP核,则需要勾选Generate Simulation Model选项。勾选这个选项后,就会依据选择的language来生成仿真IP核所需的一系列文件。接下来点击OK,回到如图 51.4.6
所示的主界面。
最后点击Generate选项,就会生成配置好的FFT IP核,若在生成的过程长时间卡顿在下图
所示的界面 ,这个时候只需点击Cancel按钮,回到主界面再次点击Generate选项就好了,若
还是不行,请多重复几次,这是正常的情况。
最后出现下图所示的界面就表示IP核生成成功,点击Next就完成IP核的创建了。
图 51.4.11 FFT IP核生成完成界面
我们在FFT模块(FFT_top)代码的97行至110行例化了data_modulus模块,data_modulus
模块的代码如下所示:
1 module data_modulus(
2 input clk_50m,
3 input rst_n,
4
5 input [15:0] source_real,
6 input [15:0] source_imag,
7 input source_sop,
8 input source_eop,
9 input source_valid,
10
11 output [15:0] data_modulus,
12 output reg data_sop,
13 output reg data_eop,
14 output reg data_valid
15 );
16
17 //reg define
18 reg [31:0] source_data;
19 reg [15:0] data_real;
20 reg [15:0] data_imag;
21 reg data_sop1;
22 reg data_sop2;
23 reg data_eop1;
24 reg data_eop2;
25 reg data_valid1;
26 reg data_valid2;
27
28 //*****************************************************
29 //** main code
30 //*****************************************************
31
32 //取实部和虚部的平方和
33 always @ (posedge clk_50m or negedge rst_n) begin
34 if(!rst_n) begin
35 source_data <= 32'd0;
36 data_real <= 16'd0;
37 data_imag <= 16'd0;
38 end
39 else begin
40 if(source_real[15]==1'b0) //由补码计算原码
41 data_real <= source_real;
42 else
43 data_real <= ~source_real + 1'b1;
44
45 if(source_imag[15]==1'b0) //由补码计算原码
46 data_imag <= source_imag;
47 else
48 data_imag <= ~source_imag + 1'b1;
49 //计算原码平方和
50 source_data <= (data_real*data_real) + (data_imag*data_imag);
51 end
52 end
53
54 //例化sqrt模块,开根号运算
55 sqrt sqrt_inst (
56 .clk (clk_50m),
57 .radical (source_data),
58
59 .q (data_modulus),
60 .remainder ()
61 );
62
63 //数据取模运算共花费了三个时钟周期,此处延时三个时钟周期
64 always @ (posedge clk_50m or negedge rst_n) begin
65 if(!rst_n) begin
66 data_sop <= 1'b0;
67 data_sop1 <= 1'b0;
68 data_sop2 <= 1'b0;
69 data_eop <= 1'b0;
70 data_eop1 <= 1'b0;
71 data_eop2 <= 1'b0;
72 data_valid <= 1'b0;
73 data_valid1 <= 1'b0;
74 data_valid2 <= 1'b0;
75 end
76 else begin
77 data_valid1 <= source_valid;
78 data_valid2 <= data_valid1;
79 data_valid <= data_valid2;
80 data_sop1 <= source_sop;
81 data_sop2 <= data_sop1;
82 data_sop <= data_sop2;
83 data_eop1 <= source_eop;
84 data_eop2 <= data_eop1;
85 data_eop <= data_eop2;
86 end
87 end
88
89 endmodule
我们在代码的40行至48行将FFT IP核输出的复数的实部与虚部进行了处理,求得了它们的
原码,并在第50行计算了原码的平方和。在代码的55行至61行,例化了sqrt IP核(求平方根),
我们将前面计算得到的平方和输送给sqrt IP核,进行平方根运算,得到的结果将在后面用于
在LCD上显示频谱。
我们接下来了解一下sqrt IP核的配置,方法和前面配置FFT IP核一样。先在MegaWizard
Plug-In Manager界面搜索框内输入sqrt,出现下图所示的三个IP核选项:
图 51.4.12 sqrt IP核选择界面
我们这里使用的是选项中的第三个IP核(ALTSQRT IP核),然后进入IP核的配置界面,配
置后的界面如下图所示:
图 51.4.13 ALTSQRT IP核配置界面
在代码77行至85行,为了将sqrt IP核输出的数据与source_valid、source_sop、
source_eop信号对齐,对这三个信号进行了打拍处理。
我们在顶层例化了LCD模块(LCD_top),其内部结构如下所示:
图 51.4.14 LCD模块的内部结构图
如图所示LCD模块(LCD_top)内部例化了3个模块:fifo控制模块(fifo_ctrl)、fifo缓
存模块(FFT_LCD_FIFO)、LCD显示模块(lcd_rgb_top)。FFT模块(FFT_top)传输过来的幅
度数据经过fifo控制模块(fifo_ctrl)处理,送到fifo缓存模块(FFT_LCD_FIFO)进行缓存,
然后送给LCD用于频谱显示。
fifo控制模块(fifo_ctrl):fifo控制模块负责fifo缓存模块(FFT_LCD_FIFO)的读写
控制。由于经过FFT得到的频谱是对称的,所以只需要显示频谱的一半即可,因此这里缓存的
一帧数据的长度为64,也就是采样长度128的一半。此外,由于LCD读取数据的速度较慢,为了
防止fifo缓存模块(FFT_LCD_FIFO)写满,这里对fifo缓存模块(FFT_LCD_FIFO)的写数据使
能做了一些处理。此外,当LCD显示模块(lcd_rgb_top)请求数据的时候,fifo控制模块
(fifo_ctrl)负责拉高fifo缓存模块(FFT_LCD_FIFO)的读数据使能。
fifo缓存模块(FFT_LCD_FIFO):fifo缓存模块负责缓存频谱幅度数据,当读数据使能拉
高的时候,输出数据给LCD显示模块(lcd_rgb_top)。
LCD显示模块(lcd_rgb_top):LCD显示模块负责依据读取到的幅度数据,在RGB TFT-LCD
上显示频谱。
LCD模块(LCD_top)的代码如下所示:
1 module LCD_top(
2 input clk50M,
3 input clk10M,
4 input rst_n,
5
6 output lcd_hs,
7 output lcd_vs,
8 output lcd_de,
9 output [15:0] lcd_rgb,
10 output lcd_bl,
11 output lcd_rst,
12 output lcd_pclk,
13
14 input [15:0] fft_data,
15 input fft_sop,
16 input fft_eop,
17 input fft_valid
18 );
19
20 //wire define
21 wire [6:0] line_cnt;
22 wire [15:0] line_length;
23 wire data_req;
24 wire wr_over;
25
26 wire fifo_wr_req;
27 wire fifo_rd_req;
28 wire [15:0] fifo_wr_data;
29 wire fifo_empty;
30
31 //*****************************************************
32 //** main code
33 //*****************************************************
34
35 //fifo读写控制模块
36 fifo_ctrl u_fifo_ctrl(
37 .clk_50m (clk50M),
38 .lcd_clk (clk10M),
39 .rst_n (rst_n),
40
41 .fft_data (fft_data),
42 .fft_sop (fft_sop),
43 .fft_eop (fft_eop),
44 .fft_valid (fft_valid),
45
46 .data_req (data_req),
47 .wr_over (wr_over),
48 .rd_cnt (line_cnt), //频谱的序号
49
50 .fifo_wr_data (fifo_wr_data),
51 .fifo_wr_req (fifo_wr_req),
52 .fifo_rd_req (fifo_rd_req)
53 );
54
55 //例化fifo
56 FFT_LCD_FIFO FFT_LCD_FIFO_inst (
57 .aclr (~rst_n),
58 //写端口
59 .wrclk (clk50M),
60 .wrreq (fifo_wr_req),
61 .data (fifo_wr_data),
62 //读端口
63 .rdclk (clk10M),
64 .rdreq (fifo_rd_req),
65 .q (line_length), //频谱的幅度
66
67 .rdempty (fifo_empty)
68 );
69
70 //LCD驱动显示模块
71 lcd_rgb_top u_lcd_rgb_top(
72 .lcd_clk (clk10M),
73 .sys_rst_n (rst_n &(~fifo_empty)),
74
75 .lcd_hs (lcd_hs),
76 .lcd_vs (lcd_vs),
77 .lcd_de (lcd_de),
78 .lcd_rgb (lcd_rgb),
79 .lcd_bl (lcd_bl),
80 .lcd_rst (lcd_rst),
81 .lcd_pclk (lcd_pclk),
82
83 .line_cnt (line_cnt), //频谱的序号(0~63)
84 .line_length (line_length[15:3]),//频谱的幅度,缩小8倍以适应屏幕尺寸
85 .data_req (data_req), //请求频谱数据输入
86 .wr_over (wr_over) //”一条频谱绘制完成”标志信号
87 );
88
89 endmodule
LCD模块(LCD_top)完成了三个子模块的例化。不过需要注意的是,在代码的第84行,为
了在播放音乐的时候能够看到合适的频谱,我们对频谱的幅度进行了缩放处理。
接下来,我们看一下fifo控制模块(fifo_ctrl),fifo控制模块的代码如下所示:
1 module fifo_ctrl(
2 input clk_50m,
3 input lcd_clk,
4 input rst_n,
5
6 input [15:0] fft_data,
7 input fft_sop,
8 input fft_eop,
9 input fft_valid,
10
11 input data_req, //外部数据请求信号
12 input wr_over,
13 output reg [6:0] rd_cnt,
14
15 output [15:0] fifo_wr_data,
16 output fifo_wr_req,
17 output reg fifo_rd_req
18 );
19
20 //parameter define
21 parameter Transform_Length = 128;
22
23 //reg define
24 reg [1:0] wr_state;
25 reg [1:0] rd_state;
26 reg [6:0] wr_cnt;
27 reg wr_en;
28 reg fft_valid_r;
29 reg [15:0] fft_data_r;
30
31 //*****************************************************
32 //** main code
33 //*****************************************************
34
35 //产生fifo写请求信号
36 assign fifo_wr_req = fft_valid_r && wr_en;
37 assign fifo_wr_data = fft_data_r;
38
39 //将数据与有效信号延时一个时钟周期
40 always @ (posedge clk_50m or negedge rst_n) begin
41 if(!rst_n) begin
42 fft_data_r <= 16'd0;
43 fft_valid_r <= 1'b0;
44 end
45 else begin
46 fft_data_r <= fft_data;
47 fft_valid_r <= fft_valid;
48 end
49 end
50
51 //控制FIFO写端口,每次向FIFO中写入前半帧(64个)数据
52 always @ (posedge clk_50m or negedge rst_n) begin
53 if(!rst_n) begin
54 wr_state <= 2'd0;
55 wr_en <= 1'b0;
56 wr_cnt <= 7'd0;
57 end
58 else begin
59 case(wr_state)
60 2'd0: begin //等待一帧数据的开始信号
61 if(fft_sop) begin
62 wr_state <= 2'd1;
63 wr_en <= 1'b1;
64 end
65 else begin //进入写数据过程,拉高写使能wr_en
66 wr_state <= 2'd0;
67 wr_en <= 1'b0;
68 end
69 end
70 2'd1: begin
71 if(fifo_wr_req) //对写入FIFO中的数据计数
72 wr_cnt <= wr_cnt + 1'b1;
73 else
74 wr_cnt <= wr_cnt;
75 //由于FFT得到的数据具有对称性,因此只取一帧数据的一半
76 if(wr_cnt < Transform_Length/2 - 1'b1) begin
77 wr_en <= 1'b1;
78 wr_state <= 2'd1;
79 end
80 else begin
81 wr_en <= 1'b0;
82 wr_state <= 2'd2;
83 end
84 end
85 2'd2: begin //当FIFO中的数据被读出一半的时候,进入下一帧数据写过程
86 if((rd_cnt == Transform_Length/4)&& wr_over) begin
87 wr_cnt <= 7'd0;
88 wr_state <= 2'd0;
89 end
90 else
91 wr_state <= 2'd2;
92 end
93 default:
94 wr_state <= 2'd0;
95 endcase
96 end
97 end
98
99 //控制FIFO读端口,每次输出一个数据用于绘制频谱
100 always @ (posedge lcd_clk or negedge rst_n) begin
101 if(!rst_n) begin
102 rd_state <= 2'd0;
103 rd_cnt <= 7'd0;
104 fifo_rd_req <= 1'b0;
105 end
106 else begin
107 case(rd_state)
108 2'd0: begin //外部请求频谱数据时,拉高读FIFO请求信号
109 if(data_req) begin
110 fifo_rd_req <= 1'b1;
111 rd_state <= 2'd1;
112 end
113 else begin
114 fifo_rd_req <= 1'b0;
115 rd_state <= 2'd0;
116 end
117 end
118 2'd1: begin //读FIFO请求仅拉高一个时钟周期
119 fifo_rd_req <= 1'b0;
120 rd_state <= 2'd2;
121 end
122 2'd2: begin //等待输出的频谱数据绘制结束
123 if(wr_over) begin
124 rd_state <= 2'd0;
125 if( rd_cnt== Transform_Length/2 -1 )
126 rd_cnt <= 7'd0;
127 else
128 rd_cnt <= rd_cnt + 1'b1;
129 end
130 else
131 rd_state <= 2'd2;
132 end
133 default:
134 rd_state <= 2'd0;
135 endcase
136 end
137 end
138
139 endmodule
在代码的59行至95行所描述的状态机如图 51.4.15所示 ,在state的值为0的时候,
fft_sop信号(数据包开始信号)一拉高,就进入state值为1的状态。此时当wr_req信号为高
电平的时候(见代码36行,此时fft_valid_r信号也为高电平,fft_valid_r为数据有效信号),开始往fifo里写数据,同时让wr_cnt计数器累加计数。当wr_cnt计数器的值等于63的时候,也
就是fifo里写入了64个数据的时候,进入下一状态。在这个状态里保持等待,直到LCD显示模
块(lcd_rgb_top)读了32个数据的时候回到state的值等于0的状态,这样一直循环下去。
图 51.4.15 往fifo内写数据状态机示意图
107行至135行代码所示的状态机如图 51.4.16所示:
图 51.4.16 从fifo读数据状态机示意图
在state的值为0的时候, draw_able信号(LCD显示模块请求数据信号)一拉高,就进入
state值为1的状态。此时读取fifo里的一个数据,并拉低rd_en信号,然后进入下一状态。当
wr_over信号为高电平的时候(LCD显示模块显示了一条频谱),让rd_cnt计数器自加1(rd_cnt
计数器的值等于63的时候清零,对应于代码的126行),并回到state的值为0的状态。
我们在LCD模块(LCD_top)中例化了fifo缓存模块(FFT_LCD_FIFO),它起到了缓存数据
的作用,我们在前面已经也对该模块的作用进行了详细的描述,这里就不再赘述了。接下来,
我们来了解一下LCD显示模块(lcd_rgb_top)。
我们在LCD显示模块中例化了LCD显示模块(lcd_rgb_top),它在内部还例化了两个模块:
lcd驱动模块(lcd_driver模块)以及lcd显示模块(lcd_display模块)。LCD显示模块
(lcd_rgb_top)的内部结构图如下所示:
图 51.4.17 LCD显示模块内部结构图
lcd驱动模块(lcd_driver模块):在像素时钟的驱动下输出数据使能信号用于数据同步,
同时还需要输出像素点的纵横坐标,供LCD显示模块(lcd_display)调用,以绘制图案。有关
LCD驱动模块的详细介绍请大家参考“RGB TFT-LCD彩条显示实验”章节。
接下来我们了解一下lcd显示模块(lcd_display模块),它的代码如下所示:
1 module lcd_display(
2 input lcd_clk, //lcd驱动时钟
3 input sys_rst_n, //复位信号
4
5 input [10:0] pixel_xpos, //像素点横坐标
6 input [10:0] pixel_ypos, //像素点纵坐标
7
8 input [6:0] line_cnt, //频点
9 input [15:0] line_length, //频谱数据
10 output data_req, //请求频谱数据
11 output wr_over, //绘制频谱完成
12 output [15:0] lcd_data //LCD像素点数据
13 );
14
15 //parameter define
16 parameter H_LCD_DISP = 11'd480; //LCD分辨率——行
17 localparam BLACK = 16'b00000_000000_00000; //RGB565 黑色
18 localparam WHITE = 16'b11111_111111_11111; //RGB565 白色
19
20 //*****************************************************
21 //** main code
22 //*****************************************************
23
24 //请求像素数据信号(这里加8是为了图像居中显示)
25 assign data_req = ((pixel_ypos == line_cnt * 4'd4 + 4'd8 - 4'd1)
26 && (pixel_xpos == H_LCD_DISP - 1)) ? 1'b1 : 1'b0;
27
28 //在要显示图像的列,显示line_length长度的白色条纹
29 assign lcd_data = ((pixel_ypos == line_cnt * 4'd4 + 4'd8)
30 && (pixel_xpos <= line_length)) ? WHITE : BLACK;
31
32 //wr_over标志着一个频点上的频谱绘制完成,该信号会触发line_cnt加1
33 assign wr_over = ((pixel_ypos == line_cnt * 4'd4 + 4'd8)
34 && (pixel_xpos == H_LCD_DISP - 1)) ? 1'b1 : 1'b0;
35
36 endmodule
正点原子4.3寸RGB TFT-LCD屏幕的分辨率是480*272的,LCD的扫描原理是扫描完一行接着
扫描下一行的。而LCD的数据来源是fifo,无法保存已经读过的数据。那么为了能在LCD上显示
频谱(在屏幕上显示64个像素条),我们将272行像素点64等分,也就是每4行显示一个频率点
的幅度图像,这样272行像素还余下16行像素不显示图像。为了让频谱能够居中显示,我们从
第8行开始显示第一个频率点的幅度图像,幅度图像(像素条)的长度由该频率点的幅值(从
fifo中读出)决定。
如代码29行所示,我们在第8行开始显示第一个频率点的幅度图像,当列像素点的值小于
处理后的频谱幅值时(line_length),显示白色像素点,其他像素点不显示。然后以4行为间
隔显示其他频率点的幅度图像。但在显示图像之前,需要先获取幅值。所以在代码第25行,我
们在显示频谱条纹的前一行的最后一列发出读请求信号,从fifo中获得幅值用于绘制频谱。此
外,如代码的第32行所示,每当一条频谱绘制完成后,将绘制完成的标志信号wr_over拉高,
通知fifo_ctrl模块当前频谱绘制完成。然后随着line_cnt计数器从0累加到63,再回到0这样
循环的变化,我们就能在LCD上观察到不断变化的频谱。
到此,程序设计部分就结束了。
下载验证
首先我们打开IP核之FFT实验工程,在工程所在的路径下打开FFT_audio_lcd/par文件
夹,在里面找到“FFT_audio_lcd.qpf”并双击打开。注意工程所在的路径名只能由字母、数
字以及下划线组成,不能出现中文、空格以及特殊字符等。工程打开后如图 51.5.1示:
图 51.5.1 IP核之FFT实验工程
将下载器一端连接电脑,另一端与开发板上对应端口连接,然后用音频线连接电脑和开发
板,最后连接电源线并打开电源开关。
需要注意的是,使用FFT IP核需要LICENSE!如果我们的LICENSE文件不包含该IP核的使用
许可,那么工程编译结束之后,将会生成一个带“_time_limited”后缀的sof文件。该sof文
件只能运行一个小时,然后自动停止运行,不过这并不影响我们本次实验的下载验证。
点击工具栏中的“Programmer”图标打开下载界面,通过点击“Add File”按钮选择
FFT_audio_lcd/par/output_files 目 录 下 的 “FFT_audio_lcd_time_limited.sof” 或 者
“FFT_audio_lcd.sof”文件。
开发板电源打开后,在程序下载界面点击“Hardware Setup”,在弹出的对话框中选择当
前的硬件连接为“USB-Blaster[USB-0]”。然后点击“Start”将工程编译完成后得到的sof文
件下载到开发板中,如下图所示:
图 51.5.2 下载界面
下载完成后,打开工程目录下的“音频文件”文件夹,里面有个名为“SHT_noise_96k.wav”
的音频文件。该音频是掺杂了噪声的一小段“上海滩”音乐,噪声频率为9.6KHz。在电脑上使
用播放器播放这段音频,我们可以听到开发板背面的喇叭在播放上海滩的音乐,音乐中混杂了
一个尖锐的类似蜂鸣器的声音,同时我们可以在LCD上看到如图 51.5.3所示的音频频谱图。
图 51.5.3 频谱图
由本章简介部分的内容可知,频谱第n个点对应信号频率为:F*(n-1)/N。我们的采样频率
F是WM8978内部ADC的采样频率,即48KHz;N是FFT IP核的一次频谱分析长度,即128;n是频谱
中白色条纹的序号。
上图中,幅度最高的频谱的序号为27(从左往右数第27个白色条纹最高),经过计算得出
该频谱对应的频率为48*(27-1)/128=9.75KHz,它就是我们在音乐播放过程中所听到的9.6KHz
的高频噪声。从频谱中计算出来的频率值误差为0.15KHz,在频率精度(48/128=0.375KHz)范
围内,说明我们本次实验在开拓者FPGA开发板上下载验证成功。