matlab数字信号频谱图_正点原子开拓者FPGA开发板资料连载第五十一章 音频频谱仪实验...

1)实验平台:正点原子开拓者FPGA 开发板

2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子

3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html

c27c04ca8d68f4a614352376a04dff51.png

第五十一章 基于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上显示频谱。

6b6d93c2695118137675aef46799fc57.png

图 51.4.1 IP核之FFT实验系统框图

程序中各模块端口及信号连接如图 51.4.2所示

ad6283c14add21d01c31f8acfca8b3ef.png

图 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),模块结构如下所示:

5f2a28966da2f8f57633bcf070ca7b70.png

图 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信号。

2cb9e59c67278cd44c7b8c057bd286d9.png

图 51.4.4 streaming数据流输入时序

在讲解完FFT控制模块(fft_ctrl)后,接下来说明一下怎么配置FFT IP核。我们先打开

MegaWizard Plug-In Manager界面(详细步骤可参考“IP核之RAM实验”章节的程序设计部分),

在搜索框中输入FFT,界面中便会显示我们需要的FFT IP核,如下图所示:

26773340a6b480397edae5ca7d521077.png

图 51.4.5 FFT IP核配置界面

此时,我们需要点击界面中的FFT v13.1来选中这个IP核,然后选择IP核的生成路径。我

们一般习惯将IP核放置在工程文件夹的paripcore下。然后点击Next,出现以下界面:

f26b1e02eb734ccc778d6b2595403c8e.png

图 51.4.6 FFT IP核主界面

然后点击parameterize(参数化),进入参数配置界面,如下所示:

fdb4a22c85ff760c448638bf3c0f8002.png

图 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界面如下所示:

197e9c113edc3d3ea5b8b2d759e9d435.png

图 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所示。

3933844b8b73586f7716f7f5caab134e.png

图 51.4.9 Implementation Options界面

我们依然保留默认设置就好了,图中的配置说明FFT使用了4个乘法器以及2个加法器、以

及DSP块和逻辑单元。这里点击Finish回到FFT IP核主界面,如图 51.4.6所示。接着点击第2

个选项Set Up Simulation选项,出现以下界面:

0332d655026889de90753626272ed842.png

图 51.4.10 Set Up Simulation界面

如果需要仿真FFT IP核,则需要勾选Generate Simulation Model选项。勾选这个选项后,就会依据选择的language来生成仿真IP核所需的一系列文件。接下来点击OK,回到如图 51.4.6

所示的主界面。

最后点击Generate选项,就会生成配置好的FFT IP核,若在生成的过程长时间卡顿在下图

所示的界面 ,这个时候只需点击Cancel按钮,回到主界面再次点击Generate选项就好了,若

还是不行,请多重复几次,这是正常的情况。

f111fb8cf60a8fa8add3ab3e2e05ce53.png

最后出现下图所示的界面就表示IP核生成成功,点击Next就完成IP核的创建了。

2952081343987aea38b3c8f81d4538a2.png

图 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核选项:

6c9f78c2473d1addfe53ae47c62e2b0d.png

图 51.4.12 sqrt IP核选择界面

我们这里使用的是选项中的第三个IP核(ALTSQRT IP核),然后进入IP核的配置界面,配

置后的界面如下图所示:

aaad376f9c395cf719ad4ccb8b4a384b.png

图 51.4.13 ALTSQRT IP核配置界面

在代码77行至85行,为了将sqrt IP核输出的数据与source_valid、source_sop、

source_eop信号对齐,对这三个信号进行了打拍处理。

我们在顶层例化了LCD模块(LCD_top),其内部结构如下所示:

5f5f6f99052484ce87e63ec27f46bed5.png

图 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的状态,这样一直循环下去。

c988b10e060af40c4361a9c169803142.png

图 51.4.15 往fifo内写数据状态机示意图

107行至135行代码所示的状态机如图 51.4.16所示:

24eb17fd1613cae7ae6fe158e66b883e.png

图 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)的内部结构图如下所示:

95464fd8ef0587239fab009768ae6460.png

图 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示:

62255f9f5fc03676b8a68e7579b1ce34.png

图 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文

件下载到开发板中,如下图所示:

470ff2286796f8280047a7cce7cc03f4.png

图 51.5.2 下载界面

下载完成后,打开工程目录下的“音频文件”文件夹,里面有个名为“SHT_noise_96k.wav”

的音频文件。该音频是掺杂了噪声的一小段“上海滩”音乐,噪声频率为9.6KHz。在电脑上使

用播放器播放这段音频,我们可以听到开发板背面的喇叭在播放上海滩的音乐,音乐中混杂了

一个尖锐的类似蜂鸣器的声音,同时我们可以在LCD上看到如图 51.5.3所示的音频频谱图。

9eed9af047828e58385b7976df0acb5c.png

图 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开发板上下载验证成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值