正点原子 发表于 2019-7-28 15:24:42

【正点原子FPGA连载】第五十一章 基于FFT IP核的音频频谱仪--摘自【正点原子】开拓者 FPGA 开发指南

本帖最后由 正点原子 于 2020-10-24 15:19 编辑

1)实验平台:正点原子开拓者FPGA开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=579749209820
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-281143-1-1.html
4)对正点原子FPGA感兴趣的同学可以加群讨论:712557122点击加入:
5)关注正点原子公众号,获取最新资料更新


第五十一章 基于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 下载验证
51.1 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计算, 而不需要自己学习数字信号处理, 去编写代码了, 大大方便了我们的开发。
51.2 实验任务
本节实验任务是先将电脑或手机的音乐通过开拓者开发板上的WM8978器件输给FPGA, 然后使用Altera FFT IP核分析WM8978输出的音频信号的频谱, 并将采样点的幅度特性显示到4.3寸RGB TFT-LCD上。
51.3 硬件设计
音频WM8978接口部分的硬件设计与“音频环回实验” 完全相同, 请参考“音频环回实验”中的硬件设计部分。 RGB TFT-LCD接口部分的硬件设计请参考“RGB TFT-LCD彩条显示实验” 中的硬件设计部分。由于WM8978接口和RGB TFT-LCD引脚数目较多且在前面相应的章节中已经给出它们的管脚
列表, 这里不再列出管脚分配。
51.4 程序设计
图 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 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 audio_data;
30
31 wire fft_sop;
32 wire fft_eop;
33 wire fft_valid;
34 wire 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 dac_data , // 输出的音频数据
18 output 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 1
0 //user interface
11 output reg rx_done , // FPGA接收数据完成
12 output reg adc_data // FPGA接收的数据
13 );
14
15 //reg define
16 reg aud_lrc_d0; // aud_lrc延迟一个时钟周期
17 reg [ 5:0] rx_cnt; // 发送数据计数
18 reg 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 audio_data,
8 9
output data_sop,
10 output data_eop,
11 output data_valid,
12 output data_modulus
13 );
14
15 //wire define
16 wire 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 source_real;
30 wire 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 delay_cnt;
18 reg 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核放置在工程文件夹的par\ipcore下。 然后点击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。TwiddleWidth是旋转因子的数据位宽,只要比输入数据的位宽低就可以了,这里将其设置成8bits。DataOutput 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模式可用于在线改变TransformLength的大小。速度和流模式差不多,资源占用更多。 这里我们使用默认的流模式(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 source_real,
6 input source_imag,
7 input source_sop,
8 input source_eop,
9 input source_valid,
10
11 output data_modulus,
12 output reg data_sop,
13 output reg data_eop,
14 output reg data_valid
15 );
16
17 //reg define
18 reg source_data;
19 reg data_real;
20 reg 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==1'b0) //由补码计算原码
41 data_real <= source_real;
42 else
43 data_real <= ~source_real + 1'b1;
44
45 if(source_imag==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核一样。 先在MegaWizardPlug-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 lcd_rgb,
10 output lcd_bl,
11 output lcd_rst,
12 output lcd_pclk,
13
14 input fft_data,
15 input fft_sop,
16 input fft_eop,
17 input fft_valid
18 );
19
20 //wire define
21 wire line_cnt;
22 wire line_length;
23 wire data_req;
24 wire wr_over;
25
26 wire fifo_wr_req;
27 wire fifo_rd_req;
28 wire 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),//频谱的幅度, 缩小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 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 rd_cnt,
14
15 output 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 wr_state;
25 reg rd_state;
26 reg wr_cnt;
27 reg wr_en;
28 reg fft_valid_r;
29 reg 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 pixel_xpos, //像素点横坐标
6 input pixel_ypos, //像素点纵坐标
7 8
input line_cnt, //频点
9 input line_length, //频谱数据
10 output data_req, //请求频谱数据
11 output wr_over, //绘制频谱完成
12 output 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上观察到不断变化的频谱。
到此, 程序设计部分就结束了。
51.5 下载验证
首先我们打开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” 。 然后点击“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开发板上下载验证成功。

wzavr 发表于 2019-7-28 15:29:07

赶上2楼,支持一下

tear604922959 发表于 2019-7-28 22:41:07

fft fpga 音频
页: [1]
查看完整版本: 【正点原子FPGA连载】第五十一章 基于FFT IP核的音频频谱仪--摘自【正点原子】开拓者 FPGA 开发指南