FPGA Verilog开发实战指南:基于Intel Cyclone IV(进阶篇)
上QQ阅读APP看书,第一时间看更新

1.2 实战演练

1.2.1 实验目标

将手机或计算机中的音乐通过WM8978传输到FPGA内,然后再将音频数据从FPGA传回WM8978中播放出来。

1.2.2 硬件资源

本次实验需要使用开发板上的WM8978音频相关接口,如图1-6所示。

图1-6 音频相关接口图

其相关原理图如图1-7~图1-9所示。

图1-7 音频芯片及MIC插头图

图1-8 喇叭插座

图1-9 耳机及音频输入接口

本次实验需要使用音频输入、耳机以及喇叭接口。音频输入接口连接播放器,用于音频输入,耳机以及喇叭接口用来连接耳机以及喇叭,用于音频播放。

1.2.3 程序设计

硬件资源介绍完毕,我们开始实验工程的程序设计。在本小节,我们采用先整体概括,再局部说明的方式对实验工程的各个模块进行讲解,详细内容如下。

1. 整体说明

根据理论部分的介绍,我们知道要想使用WM8978,首先需要对其内部寄存器进行配置。通过前面的接口时序讲解可知,我们可使用I2C接口对WM8978芯片进行配置,只需加一个寄存器配置模块即可完成WM8978的配置。除了这两个子模块外,还需要一个接收WM8978传出来的音频数据的模块,同时还需要一个将音频数据发送回WM8978的模块。同时,为了生成WM8978的主时钟(MCLk),需要输出一个12MHz的时钟给WM8978,在这里我们调用IP核生成12MHz的时钟。我们将这些子模块整合在一起即可,如图1-10所示。

图1-10 WM8978音频回环整体框图

通过图1-10可以看到,该工程共分7个模块,各模块简介见表1-2。

表1-2 audio_loopback工程模块简介

下面分模块为大家讲解。

2. 时钟生成模块

频率为12MHz的时钟用代码较难生成,所以在此我们通过调用IP核来生成,在基础篇IP核使用章节已经讲解了详细的调用方法,在此就不再说明。其模块框图如图1-11所示。

图1-11 时钟分频模块框图

如表1-3所示,inclk0输入系统时钟即可;而复位信号由于是异步复位,所以需要将其取反后才能接入系统复位;c0为输出时钟,locked为时钟稳定标志信号,当输出的c0时钟稳定后locked将拉高。这是调用的IP核,最后将其实例化在顶层模块即可。

表1-3 clk_gen模块输入输出信号描述

3. I2C驱动模块

关于I2C驱动模块i2c_ctrl,基础篇第32章中已经详细地讲解了驱动方法,在此就不再叙述,直接调用这个模块即可。

4. I2C配置模块

(1)模块框图

如图1-12所示为I2C配置模块i2c_reg_cfg的框图,可将寄存器的配置通过I2C驱动模块写入WM8978,从而完成对WM8978的配置。

图1-12 I2C配置模块框图

此模块是产生让I2C驱动模块写入WM8978的配置,其各个信号说明如表1-4所示。

表1-4 I2C配置模块输入输出信号描述

i2c_end和i2c_clk由i2c_ctrl模块输入,而复位信号由顶层模块输入。cfg_start由该模块产生,用于控制I2C驱动模块配置单个寄存器;cfg_data为配置寄存器的数据,前面说过WM8978寄存器是16位长,高7位([15:9]bit)用于表示寄存器地址,低9位用于表示数据。cfg_done为寄存器配置完成信号。

(2)波形图绘制

下面通过波形图来详细讲解该模块的时序,如图1-13所示。

图1-13 i2c_reg_cfg波形图

▪ i2c_clk、sys_rst_n:模块的时钟信号和复位信号。

▪ cfg_end:单个寄存器配置完成信号,从I2C驱动模块输入。从I2C驱动模块可知,只要产生开始信号和写入寄存器的数据,I2C驱动模块即可将寄存器数据写入WM8978中。

▪ cnt_wait:寄存器配置上电等待计数器。在这里我们上电等待1ms后开始配置寄存器,由于系统时钟为I2C驱动模块传来的时钟,频率为1MHz,单位时钟为1μs,所以计数到999即为1ms,需要将计数器计到1000时停止计数,当计数器的值小于计数器最大值CNT_WAIT_MAX(1000)时,每来一个时钟让其自加1。

▪ cfg_start:单个寄存器配置触发信号,上电后延迟一段时间,等待WM8978工作在稳定状态后产生第一个开始信号,所以需要一个产生延迟信号的计数器。计数到1ms(999)时产生第一个开始配置信号。

▪ cfg_end:单个寄存器配置完成信号,由I2C驱动模块传来。单个寄存器配置完成后,会产生一个结束信号。每当检测到结束信号后,紧接着发送下一个开始信号,直到所有寄存器配置完成。然而我们如何判断所有寄存器配置完成了呢?在这里可以产生一个寄存器个数计数器来进行判断。

▪ reg_num:配置寄存器个数。当第一个开始信号发出后,此时reg_num的值为0,配置第一个寄存器,当接收到单个寄存器配置完成信号后,reg_num加1,同时开始配置第二个寄存器,此时开始配置信号(cfg_start)所采集的值刚好是reg_num变化后的值。

▪ cfg_done:寄存器配置完成信号。当寄存器配置完成后,将cfg_data置为0。

▪ cfg_data_reg:寄存器数据暂存器,一上电就将所有需要配置的寄存器值寄存在这个暂存器中。

▪ cfg_data:寄存器配置值。寄存器触发信号产生后,将暂存器里存储的值赋给cfg_data,当reg_num等于0时赋予第一个寄存器的值,因为reg_num为寄存器的个数,所以当配置最后一个寄存器时,reg_num的值为reg_num-1。此时cfg_data的值为cfg_data_reg[REG_NUM-1]。同时当最后一个寄存器配置完成后,将寄存器配置完成信号(cfg_done)拉高。

(3)代码编写

参照绘制的波形图编写模块代码,具体参见代码清单1-1。

代码清单1-1 i2c_reg_cfg模块参考代码(i2c_reg_cfg.v)


 1 module  i2c_reg_cfg
 2 (
 3     input   wire            i2c_clk     ,   // 系统时钟,由I2C模块传入
 4     input   wire            sys_rst_n   ,   // 系统复位,低有效
 5     input   wire            cfg_end     ,   // 单个寄存器配置完成
 6
 7     output  reg             cfg_start   ,   // 单个寄存器配置触发信号
 8     output  wire    [15:0]  cfg_data    ,   // 寄存器地址7位+数据9位
 9     output  reg             cfg_done        // 寄存器配置完成
10 );
11
12 // ******************************************************************** //
13 // ****************** Parameter and Internal Signal ******************* //
14 // ******************************************************************** //
15
16 // parameter  define
17 parameter   REG_NUM       =   6'd18    ;    // 总共需要配置的寄存器个数
18 parameter   CNT_WAIT_MAX  =   10'd1000 ;    // 上电等待1ms后开始配置寄存器
19
20 parameter   LOUT1VOL      =   6'd40    ;    // 耳机左声道音量设置(0~63)
21 parameter   ROUT1VOL      =   6'd40    ;    // 耳机右声道音量设置(0~63)
22
23 parameter   SPK_LOUT2VOL  =   6'd45    ;    // 扬声器左声道音量设置(0~63)
24 parameter   SPK_ROUT2VOL  =   6'd45    ;    // 扬声器右声道音量设置(0~63)
25
26 // wire   define
27 wire    [15:0]  cfg_data_reg[REG_NUM-1:0];  // 寄存器配置数据暂存
28
29 // reg    define
30 reg     [9:0]  cnt_wait     ;               // 寄存器配置上电等待计数器
31 reg     [5:0]   reg_num     ;               // 配置寄存器个数
32
33 // ******************************************************************** //
34 // ***************************** Main Code **************************** //
35 // ******************************************************************** //
36
37 // cnt_wait:寄存器配置等待计数器
38 always@(posedge i2c_clk or negedge sys_rst_n)
39     if(sys_rst_n == 1'b0)
40         cnt_wait    <=  10'd0;
41     else    if(cnt_wait < CNT_WAIT_MAX)
42         cnt_wait    <=  cnt_wait + 1'b1;
43
44 // reg_num:配置寄存器个数
45 always@(posedge i2c_clk or negedge sys_rst_n)
46     if(sys_rst_n == 1'b0)
47         reg_num <=  6'd0;
48     else    if(cfg_end == 1'b1)
49         reg_num <=  reg_num + 1'b1;
50
51 // cfg_start:单个寄存器配置触发信号
52 always@(posedge i2c_clk or negedge sys_rst_n)
53     if(sys_rst_n == 1'b0)
54         cfg_start   <=  1'b0;
55     else    if(cnt_wait == (CNT_WAIT_MAX - 1'b1))
56         cfg_start   <=  1'b1;
57     else    if((cfg_end == 1'b1) && (reg_num < (REG_NUM-1)))
58         cfg_start   <=  1'b1;
59     else
60         cfg_start   <=  1'b0;
61
62 // cfg_done:寄存器配置完成信号
63 always@(posedge i2c_clk or negedge sys_rst_n)
64     if(sys_rst_n == 1'b0)
65         cfg_done    <=  1'b0;
66     else    if((reg_num == REG_NUM - 1'b1) && (cfg_end == 1'b1))
67         cfg_done    <=  1'b1;
68
69 // cfg_data:7位地址+ 9位数据
70 assign  cfg_data = (cfg_done == 1'b1) ? 16'b0 : cfg_data_reg[reg_num];
71
72 // ----------------------------------------------------
73 // cfg_data_reg:寄存器配置数据暂存
74 // 各寄存器功能配置详见文档介绍
75 assign  cfg_data_reg[00]  =       {7'd0 , 9'b0                  };
76 assign  cfg_data_reg[01]  =       {7'd1 , 9'b1_0010_1111        };
77 assign  cfg_data_reg[02]  =       {7'd2 , 9'b1_1011_0011        };
78 assign  cfg_data_reg[03]  =       {7'd4 , 9'b0_0101_0000        };
79 assign  cfg_data_reg[04]  =       {7'd6 , 9'b0_0000_0001        };
80 assign  cfg_data_reg[05]  =       {7'd10, 9'b0_0000_1000        };
81 assign  cfg_data_reg[06]  =       {7'd14, 9'b1_0000_1000        };
82 assign  cfg_data_reg[07]  =       {7'd43, 9'b0_0001_0000        };
83 assign  cfg_data_reg[08]  =       {7'd47, 9'b0_0111_0000        };
84 assign  cfg_data_reg[09]  =       {7'd48, 9'b0_0111_0000        };
85 assign  cfg_data_reg[10]  =       {7'd49, 9'b0_0000_0110        };
86 assign  cfg_data_reg[11]  =       {7'd50, 9'b0_0000_0001        };
87 assign  cfg_data_reg[12]  =       {7'd51, 9'b0_0000_0001        };
88 assign  cfg_data_reg[13]  =       {7'd52, 3'b110 , LOUT1VOL     };
89 assign  cfg_data_reg[14]  =       {7'd53, 3'b110 , ROUT1VOL     };
90 assign  cfg_data_reg[15]  =       {7'd54, 3'b110 , SPK_LOUT2VOL };
91 assign  cfg_data_reg[16]  =       {7'd55, 3'b110 , SPK_ROUT2VOL };
92 // 更新完耳机和扬声器的音量后再开启音频的输出使能,防止出现“嘎哒”声
93 assign  cfg_data_reg[17]  =       {7'd3 , 9'b0_0110_1111        };
94 // -------------------------------------------------------
95
96 endmodule

模块参考代码是参照绘制波形图进行编写的,在波形图绘制部分已经对模块各信号进行了详细说明,需要注意的是第91行,我们配置完需要的所有寄存器后再开启耳机和扬声器的输出使能,若是先开启耳机和扬声器的启动信号再更新音量,音量变化时可能会出现“嘎哒”声,所以最后再配置R3寄存器,以防止出现这种声音。

5. 寄存器配置模块

(1)模块框图

该模块就是对I2C驱动模块以及I2C配置进行实例化,从而得到一个完整的寄存器配置模块,其模块框图如图1-14所示。

图1-14 寄存器配置模块框图

由图1-14可看到,配置寄存器的数据(cfg_data)分别传输给了wr_data(输入数据)和byte_addr(输入地址)。由于I2C数据发送一次会发送8位,而WM8978寄存器的地址只有7位,所以需要将数据的最高位整合到地址变量的第0位先发送,接下来再发送cfg_data的低8位。由于本次实验只有写入而没有读取,所以我们将I2C驱动模块的写使能(wr_en)置为1,读使能(rd_en)置为0即可。同时由于我们的地址位只有7位,所以将输入的I2C字节地址字节数(addr_num)置为0,表示写入1个字节的地址。i2c_ctrl模块各输入输出信号的具体描述可参考基础篇第32章。

(2)代码编写

根据以上介绍,我们基本已经知道了各个子模块输入输出信号的连接方式,下面即可编写实例化代码,具体参见代码清单1-2。

代码清单1-2 寄存器配置模块参考代码(wm8978_cfg.v)


 1 module  wm8978_cfg
 2 (
 3     input   wire    sys_clk     ,           // 系统时钟,频率为50MHz
 4     input   wire    sys_rst_n   ,           // 系统复位,低电平有效
 5
 6     output  wire    i2c_scl     ,           // 输出至WM8978的串行时钟信号scl
 7     inout   wire    i2c_sda                 // 输出至WM8978的串行数据信号sda
 8
 9 );
10
11 // ******************************************************************** //
12 // ****************** Parameter and Internal Signal ******************* //
13 // ******************************************************************** //
14
15 // wire   define
16 wire            cfg_start   ;               // 输入I2C触发信号
17 wire    [15:0]  cfg_data    ;               // 寄存器地址7位+数据9位
18 wire            i2c_clk     ;               // I2C驱动时钟
19 wire            i2c_end     ;               // I2C一次读/写操作完成
20 wire            cfg_done    ;               // 寄存器配置完成信号
21
22 // ******************************************************************** //
23 // ***************************** Main Code **************************** //
24 // ******************************************************************** //
25
26 // ------------------------ i2c_ctrl_inst -----------------------
27 i2c_ctrl
28 #(
29     . DEVICE_ADDR     (7'b0011_010   )  ,   // I2C设备地址
30     . SYS_CLK_FREQ    (26'd50_000_000)  ,   // 输入系统时钟频率
31     . SCL_FREQ        (18'd250_000   )      // I2C设备scl时钟频率
32 )
33 i2c_ctrl_inst
34 (
35     .sys_clk        (sys_clk       ),       // 输入系统时钟,频率为50MHz
36     .sys_rst_n      (sys_rst_n     ),       // 输入复位信号,低电平有效
37     .wr_en          (1'b1          ),       // 输入写使能信号
38     .rd_en          (1'b0          ),       // 输入读使能信号
39     .i2c_start      (cfg_start     ),       // 输入I2C触发信号
40     .addr_num       (1'b0          ),       // 输入I2C字节地址字节数
41     .byte_addr      (cfg_data[15:8]),       // 输入I2C字节地址+数据最高位
42     .wr_data        (cfg_data[7:0] ),       // 输入I2C设备数据低8位
43
44     .rd_data        (              ),       // 输出I2C设备读取数据
45     .i2c_end        (i2c_end       ),       // I2C一次读/写操作完成
46     .i2c_clk        (i2c_clk       ),       // I2C驱动时钟
47     .i2c_scl        (i2c_scl       ),       // 输出至I2C设备的串行时钟信号scl
48     .i2c_sda        (i2c_sda       )        // 输出至I2C设备的串行数据信号sda
49 );
50
51 // ---------------------- i2c_reg_cfg_inst ---------------------
52 i2c_reg_cfg     i2c_reg_cfg_inst
53 (
54     .i2c_clk     (i2c_clk   ),              // 系统时钟,由I2C模块传入
55     .sys_rst_n   (sys_rst_n ),              // 系统复位,低电平有效
56     .cfg_end     (i2c_end   ),              // 单个寄存器配置完成
57
58     .cfg_start   (cfg_start ),              // 单个寄存器配置触发信号
59     .cfg_data    (cfg_data  ),              // 寄存器的地址和数据
60     .cfg_done    (cfg_done  )               // 寄存器配置完成信号
61 );
62
63 endmodule

关于模块的实例化,在模块框图部分已经做了讲解,这里需要注意的是代码第29行,我们将设备地址改为WM8978的设备地址,在1.1.3节已经讲到WM8978芯片的固定地址为0011010,此处将设备地址直接改为0011010即可。

(3)仿真代码编写

寄存器模块已经完成,我们现在可以编写仿真文件代码,通过仿真去看配置寄存器的代码是否和绘制的波形图一致。仿真参考代码详见代码清单1-3。

代码清单1-3 WM8978寄存器配置仿真参考代码(tb_wm8978_cfg.v)


 1 module  tb_wm8978_cfg();
 2
 3 // ******************************************************************** //
 4 // ****************** Parameter and Internal Signal ******************* //
 5 // ******************************************************************** //
 6
 7 // wire   define
 8 wire       i2c_scl  ;
 9 wire       i2c_sda  ;
10
11 // reg    define
12 reg     sys_clk     ;
13 reg     sys_rst_n   ;
14
15 // ******************************************************************** //
16 // ***************************** Main Code **************************** //
17 // ******************************************************************** //
18
19 // 对sys_clk,sys_rst_n赋初始值
20 initial
21     begin
22         sys_clk     =   1'b0;
23         sys_rst_n   <=  1'b0;
24         #100
25         sys_rst_n   <=  1'b1;
26     end
27
28 // clk:产生时钟
29 always  #10 sys_clk =  ~sys_clk;
30
31 // ******************************************************************** //
32 // *************************** Instantiation ************************** //
33 // ******************************************************************** //
34
35 // ------------- wm89078_cfg_inst -------------
36 wm8978_cfg  wm8978_cfg_inst
37 (
38     .sys_clk     (sys_clk   ),   // 系统时钟,频率为50MHz
39     .sys_rst_n   (sys_rst_n ),   // 系统复位,低电平有效
40
41     .i2c_scl     (i2c_scl   ),   // 输出至WM8978的串行时钟信号scl
42     .i2c_sda     (i2c_sda   )    // 输出至WM8978的串行数据信号sda
43
44 );
45
46 endmodule

该模块仿真文件只要生成时钟复位即可。

I2C配置模块的ACK应答信号是由我们控制的设备产生的,并不是由FPGA产生的,为了使仿真能进行下去,在仿真时需要我们给应答信号,当仿真完成再改回去即可,如图1-15中的框所示。

图1-15 仿真测试更改代码图

我们只需要将I2C驱动模块代码中的ack置0应答,当仿真完成后改回接收设备应答即可。

(4)仿真波形分析

配置好仿真文件,使用ModelSim对参考代码进行仿真,仿真结果如图1-16所示。

图1-16 I2C配置仿真波形图

由于I2C驱动模块在之前已经过验证,是无误的,所以在这里可以不需要观看其波形图。我们只需要观看编写的I2C配置模块是否正确即可,如图1-16所示,可以看到波形图整体时序和我们绘制的波形图时序是一致的,这说明我们编写的代码总体是正确的,下面通过局部仿真波形图观看是否正确。

图1-17所示为I2C配置模块开始配置时的仿真波形图,图1-18所示为I2C配置模块结束配置时的仿真波形图。可以发现这两个仿真波形图与我们绘制的波形图都是一致的,这说明我们编写的代码与绘制的波形图功能是一致的。

图1-17 I2C配置局部仿真波形图(一)

图1-18 I2C配置局部仿真波形图(二)

6. 音频接收模块

(1)模块框图

该模块的作用是接收WM8978输出的ADCDAT音频数据,模块框图如图1-19所示。

图1-19 音频接收模块框图

该模块的输入输出信号描述如表1-5所示。

表1-5 音频接收模块输入输出信号描述

由于我们接收的是WM8978输出的数据,而WM8978是根据图1-5进行输出的,所以,无论是接收数据还是发送数据,都需要遵从这个时序。根据音频时序接口图,可以先画出接收音频数据的时序波形图。

(2)波形图绘制

根据I2S音频接口时序图可以知道WM8978输出的ADCDAT音频数据是在位时钟(BCLK)的下降沿变化的,所以我们需要用位时钟的上升沿去采集数据。而数据是在对齐时钟(LRC)电平变化后的第二个位时钟脉冲处开始发送的,所以我们需要在对齐时钟电平变化后位时钟第二个上升沿处开始采集第一个数据。那如何设计才能达到这个目的呢?如图1-20所示,首先我们需要获得对齐时钟电平变化的标志信号。

图1-20 音频接收模块波形图

▪ lrc_edge:对齐时钟(audio_lrc)电平变化信号。对audio_lrc延迟一拍后得到信号audio_lrc_d1,将audio_lrc_d1信号与audio_lrc信号按位异或即可得到如图1-20所示的lrc_edge标志信号。

在配置寄存器(R4)时,我们设置了音频的量化位数为24位,所以WM8978一次会发送24位的音频数据,从高位开始发送,所以我们需要一个发送位数的计数器,而这个计数器需满足以下条件:当采集第一个数据时,计数器为0;采集第二个数据时,计数器为1,依次类推。这样设计的目的是方便我们将接收的24位音频数据拼接成一个音频数据,以便发送。所以就有了如图1-20所示的计数器设计,介绍如下。

▪ adcdat_cnt:WM8978 ADCDAT数据输出位数计数器。当lrc_edge为高电平时,计数器为0;当计数器的值小于26时(这个值设为26是保证能完全接收24位计数,同时方便产生一个时钟的接收完成高电平信号),让其每次加1。这样的话,当采集第一位数据(最高位,第23位)时,计数器的值为0;采集第二位数据(次高位,第22位)时,计数器的值为1,依次类推,当采集最后一位数据(最低位,第0位)时,计数器的值为23。当计数器为26时停止计数(即小于26计数器才计数),直到下一个对齐时钟电平变化信号到来,让其归0,重新开始计数。

▪ data_reg:adc_data数据寄存器。当采集到第一个数据时,将第一个数据寄存在寄存器的最高位(因为数据是从高位开始输出的),即当计数器等于0时是寄存在寄存器的最高位第23位;等于1时寄存的数据为第22位。可以发现它们的关系是寄存位数为23-adcdat_cnt。因为我们只需要接收24位音频数据,所以当计数器小于等于23时才接收。

▪ adc_data:一次接收的数据。当计数器等于24时,表明一次数据接收完毕,此时将寄存器里接收的一次发送的音频数据值赋给adc_data,同时拉高一个时钟的一次数据接收完成标志信号(rcv_done)。若计数器的最大值设为24,则rcv_done信号不方便产生一个时钟的高电平信号。

(3)代码编写

参照绘制的波形图编写模块代码,具体参见代码清单1-4。

代码清单1-4 音频接收模块参考代码(audio_rcv.v)


 1 module  audio_rcv
 2 (
 3     input   wire        audio_bclk     ,   // WM8978输出的位时钟
 4     input   wire        sys_rst_n      ,   // 系统复位,低电平有效
 5     input   wire        audio_lrc      ,   // WM8978输出的数据左/右对齐时钟
 6     input   wire        audio_adcdat   ,   // WM8978 ADC数据输出
 7
 8     output  reg [23:0]  adc_data       ,   // 一次接收的数据
 9     output  reg         rcv_done           // 一次数据接收完成
10
11 );
12
13 // ******************************************************************** //
14 // ****************** Parameter and Internal Signal ******************* //
15 // ******************************************************************** //
16
17 // reg    define
18 reg             audio_lrc_d1;              // 对齐时钟延迟一拍信号
19 reg     [4:0]   adcdat_cnt  ;              // WM8978ADC数据输出位数计数器
20 reg     [23:0]  data_reg    ;              // adc_data数据寄存器
21
22 // wire  define
23 wire            lrc_edge    ;              // 对齐时钟信号沿标志信号
24
25 // ******************************************************************** //
26 // ***************************** Main Code **************************** //
27 // ******************************************************************** //
28
29 // 使用异或运算符产生信号沿标志信号
30 assign  lrc_edge    =   audio_lrc   ^   audio_lrc_d1;
31
32 // 对audio_lrc信号延迟一拍以方便获得信号沿标志信号
33 always@(posedge audio_bclk  or  negedge sys_rst_n)
34     if(sys_rst_n == 1'b0)
35         audio_lrc_d1    <=  1'b0;
36     else
37         audio_lrc_d1    <=  audio_lrc;
38
39 // adcdat_cnt:当信号沿标志信号为高电平时,计数器清零
40 always@(posedge audio_bclk  or  negedge sys_rst_n)
41     if(sys_rst_n == 1'b0)
42         adcdat_cnt    <=  5'b0;
43     else    if(lrc_edge == 1'b1)
44         adcdat_cnt    <=  5'b0;
45     else    if(adcdat_cnt < 5'd26)
46         adcdat_cnt  <=  adcdat_cnt + 1'b1;
47     else
48         adcdat_cnt  <=  adcdat_cnt;
49
50 // 将WM8978输出的ADC数据寄存在data_reg中,一次寄存24位
51 always@(posedge audio_bclk  or  negedge sys_rst_n)
52     if(sys_rst_n == 1'b0)
53         data_reg    <=  24'b0;
54     else    if(adcdat_cnt <= 5'd23)
55         data_reg[23-adcdat_cnt] <=  audio_adcdat;
56     else
57         data_reg    <=  data_reg;
58
59 // 当最后一位数据传完之后,读出寄存器的值给adc_data
60 always@(posedge audio_bclk  or  negedge sys_rst_n)
61     if(sys_rst_n == 1'b0)
62         adc_data    <=  24'b0;
63     else    if(adcdat_cnt == 5'd24)
64         adc_data    <=  data_reg;
65     else
66         adc_data    <=  adc_data;
67
68 // 当最后一位数据传完之后,输出一个时钟的完成标志信号
69 always@(posedge audio_bclk  or  negedge sys_rst_n)
70     if(sys_rst_n == 1'b0)
71         rcv_done    <=  1'b0;
72     else    if(adcdat_cnt == 5'd24)
73         rcv_done    <=  1'b1;
74     else
75         rcv_done    <=  1'b0;
76
77 endmodule

模块参考代码是参照模块波形图进行编写的,在波形图绘制部分已经对模块中的各信号进行了详细的说明,在此不再赘述。

7. 音频发送模块

(1)模块框图

该模块是将接收到的音频数据发送回WM8978,通过WM8978将音频数据播放出来,模块框图如图1-21所示。

图1-21 音频发送模块框图

该模块的输入输出信号描述如表1-6所示。

表1-6 音频发送模块输入输出信号描述

音频发送模块的时序与接收模块有非常多的相似之处,用的都是WM8978输出的位时钟,传输的时序要与WM8978的I2S音频接口时序相对应,这样才能使WM8978接收到正确的数据。不同的是接收模块是将各个位的数据接收后拼接成一个完整的音频数据,发送模块是将拼接完成的音频数据逐位发送回WM8978。

下面通过波形图详细讲解各信号的时序关系图。

(2)波形图绘制

音频发送模块的波形图如图1-22所示。

图1-22 音频发送模块波形图

发送模块的时序与接收时序的思路大致相同,这里主要介绍它们的不同之处。

首先是数据的发送,由WM8978的I2S音频接口时序可知,音频是在位时钟下降沿发送的,只不过我们接收时是使用位时钟上升沿接收的,所以我们发送回去的音频数据也需要使用位时钟的下降沿进行发送,如图1-22所示。对齐时钟变化沿的产生方法与计数器的设计思路和接收模块是一致的,在此不再过多讲解。

其次是接收模块,接收模块将接收的数据组成了一个24位的音频数据,而我们发送的模块是将接收的24位音频数据发送回去,如图1-22所示,对齐时钟变化后的第一个下降沿开始发送接收数据的最高位数据(第23位),此时计数器的值是0,第二个下降沿发送接收数据的第22位数据,此时计数器的值是1,这与接收模块也是一样的。所以,发送的数据audio_dacdat = data_reg[23-0],一次数据发送完成后拉高一个时钟的标志信号(send_done),表示一次数据发送完毕。

(3)代码编写

根据图1-22,我们可以编写出发送模块的模块代码,详见代码清单1-5。

代码清单1-5 音频发送模块参考代码(audio_send.v)


 1 module  audio_send
 2 (
 3     input   wire            audio_bclk    ,  // WM8978输出的位时钟
 4     input   wire            sys_rst_n     ,  // 系统复位,低电平有效
 5     input   wire            audio_lrc     ,  // WM8978输出数据左/右对齐时钟
 6     input   wire    [23:0]  dac_data      ,  // 往WM8978发送的数据
 7
 8     output  reg             audio_dacdat  ,  // 发送DACDAT数据给WM8978
 9     output  reg             send_done        // 一次数据发送完成
10
11 );
12
13 // ******************************************************************** //
14 // ****************** Parameter and Internal Signal ******************* //
15 // ******************************************************************** //
16
17 // reg    define
18 reg             audio_lrc_d1;                // 对齐时钟信号延迟一拍
19 reg     [4:0]   dacdat_cnt  ;                // DACDAT数据发送位数计数器
20 reg     [23:0]  data_reg    ;                // dac_data数据寄存器
21
22 // wire   define
23 wire            lrc_edge    ;                // 对齐时钟信号沿标志信号
24
25 // ******************************************************************** //
26 // ***************************** Main Code **************************** //
27 // ******************************************************************** //
28
29 // 使用异或运算符产生信号沿标志信号
30 assign  lrc_edge = audio_lrc ^ audio_lrc_d1;
31
32 // 对audio_lcr信号延迟一拍以便获得信号沿标志信号
33 always@(posedge audio_bclk  or  negedge sys_rst_n)
34     if(sys_rst_n == 1'b0)
35         audio_lrc_d1    <=  1'b0;
36     else
37         audio_lrc_d1    <=  audio_lrc;
38
39 // dacdat_cnt:当信号沿标志信号为高电平时,计数器清零
40 always@(posedge audio_bclk  or  negedge sys_rst_n)
41     if(sys_rst_n == 1'b0)
42         dacdat_cnt    <=  5'b0;
43     else    if(lrc_edge == 1'b1)
44         dacdat_cnt    <=  5'b0;
45     else    if(dacdat_cnt < 5'd26)
46         dacdat_cnt  <=  dacdat_cnt + 1'b1;
47     else
48         dacdat_cnt  <=  dacdat_cnt;
49
50 // 将要发送的dac_data数据寄存在data_reg中
51 always@(posedge audio_bclk  or  negedge sys_rst_n)
52     if(sys_rst_n == 1'b0)
53         data_reg    <=  24'b0;
54     else    if(lrc_edge == 1'b1)
55         data_reg <=  dac_data;
56     else
57         data_reg    <=  data_reg;
58
59 // 下降沿到来时,将data_reg的数据逐位传递给audio_dacdat
60 always@(negedge audio_bclk  or  negedge sys_rst_n)
61     if(sys_rst_n == 1'b0)
62         audio_dacdat    <=  1'b0;
63     else    if(dacdat_cnt <= 5'd23)
64         audio_dacdat    <=  data_reg[23 - dacdat_cnt];
65     else
66         audio_dacdat    <=  audio_dacdat;
67
68 // 当最后一位数据传完之后,输出一个时钟的发送完成标志信号
69 always@(posedge audio_bclk  or  negedge sys_rst_n)
70     if(sys_rst_n == 1'b0)
71         send_done    <=  1'b0;
72     else    if(dacdat_cnt == 5'd24)
73         send_done    <=  1'b1;
74     else
75         send_done    <=  1'b0;
76
77 endmodule

8. 顶层模块

(1)模块框图

音频回环顶层模块的框图如图1-23所示。

图1-23 音频回环顶层模块框图

该模块的输入输出信号描述如表1-7所示。

表1-7 音频回环顶层模块输入输出信号描述

audio_lookback顶层模块主要是对各个子功能模块进行实例化,以及连接对应信号,对应信号的连接可根据系统整体框图进行,信号代码较容易编写,无须绘制波形图。

(2)代码编写

顶层模块参考代码具体见代码清单1-6。

代码清单1-6 顶层模块参考代码(audio_lookback.v)


 1 module  audio_loopback
 2 (
 3     input   wire    sys_clk         ,   // 系统时钟,频率为50MHz
 4     input   wire    sys_rst_n       ,   // 系统复位,低电平有效
 5     input   wire    audio_bclk      ,   // WM8978输出的位时钟
 6     input   wire    audio_lrc       ,   // WM8978输出的数据左/右对齐时钟
 7     input   wire    audio_adcdat    ,   // WM8978ADC数据输出
 8
 9     output  wire    scl             ,   // 输出至WM8978的串行时钟信号scl
10     output  wire    audio_mclk      ,   // 输出WM8978主时钟,频率为12MHz
11     output  wire    audio_dacdat    ,   // 输出DAC数据给WM8978
12
13     inout   wire    sda                 // 输出至WM8978的串行数据信号sda
14
15 );
16
17 // ******************************************************************** //
18 // ******************** Parameter And Internal Signal ***************** //
19 // ******************************************************************** //
20
21 // wire   define
22 wire    [23:0]  adc_data    ;   // 接收的音频数据
23
24 // ******************************************************************** //
25 // *************************** Instantiation ************************** //
26 // ******************************************************************** //
27
28 // ------------------- clk_gen_inst ----------------------
29 clk_gen     clk_gen_inst
30 (
31     .areset (~sys_rst_n ),             // 异步复位,取反连接
32     .inclk0 (sys_clk    ),             // 输入时钟(50MHz)
33
34     .c0     (audio_mclk ),             // 输出时钟(12MHz)
35     .locked ()                         // 输出稳定时钟标志信号
36
37     );
38
39 // ------------------- audio_rcv_inst -------------------
40 audio_rcv   audio_rcv_inst
41 (
42     .audio_bclk      (audio_bclk  ),   // WM8978输出的位时钟
43     .sys_rst_n       (sys_rst_n   ),   // 系统复位,低电平有效
44     .audio_lrc       (audio_lrc   ),   // WM8978输出的数据左/右对齐时钟
45     .audio_adcdat    (audio_adcdat),   // WM8978 ADC数据输出
46
47     .adc_data        (adc_data    ),   // 一次接收的数据
48     .rcv_done        ()                // 一次数据接收完成
49
50 );
51
52 // ------------------ audio_send_inst ------------------
53 audio_send  audio_send_inst
54 (
55     .audio_bclk      (audio_bclk  ),   // WM8978输出的位时钟
56     .sys_rst_n       (sys_rst_n   ),   // 系统复位,低电平有效
57     .audio_lrc       (audio_lrc   ),   // WM8978输出数据左/右对齐时钟
58     .dac_data        (adc_data    ),   // 往WM8978发送的数据
59
60     .audio_dacdat    (audio_dacdat),   // 发送DAC数据给WM8978
61     .send_done       ()                // 一次数据发送完成
62
63 );
64
65 // ----------------- wm8978_cfg_inst --------------------
66 wm8978_cfg  wm8978_cfg_inst
67 (
68     .sys_clk     (sys_clk   ),  // 系统时钟,频率为50MHz
69     .sys_rst_n   (sys_rst_n ),  // 系统复位,低电平有效
70
71     .i2c_scl     (scl       ),  // 输出至WM8978的串行时钟信号scl
72     .i2c_sda     (sda       )   // 输出至WM8978的串行数据信号sda
73
74 );
75
76 endmodule

在实例化时,有些预留的信号接口如果没有用到,将其悬空即可。

9. RTL视图

使用Quartus II软件对工程进行编译,编译通过后,查看RTL视图,如图1-24和图1-25所示,实验工程的RTL视图与实验整体框图相同,各信号线均已正确连接。

图1-24 audio_lookback顶层RTL视图

图1-25 寄存器配置RTL视图

10. SignalTap波形抓取

由于音频接收模块和音频发送模块的位时钟、对齐时钟都是由WM8978输出得来的,由仿真文件产生这两个时钟比较麻烦,因此我们利用Quartus软件自带的SignalTap工具对波形进行实时抓取,以验证我们的设计是否正确。

如图1-26所示,抓取的为接收模块信号的波形图,可以看到该图与我们绘制的波形图是一致的。

图1-26 接收模块信号波形图

如图1-27所示为发送模块信号的波形图,可以看到该图与我们绘制的波形图是一致的。

图1-27 发送模块信号波形图

由图1-28可看到主时钟、位时钟和对齐时钟都产生了,同时,箭头所指处对应的是FPGA接收和发送的音频数据,由于太过紧密,看得不是很真切,不过可以看到大致是相同的。下面我们随机放大截取一个对应数据来看看接收和发送的数据是否一致,如图1-29所示。

图1-28 主时钟、位时钟和对齐时钟波形

图1-29 放大后波形

图1-29中箭头所指对应的是FPGA接收和发送的数据,可以很清楚地看到其发送的音频数据就是WM8978传过来的音频数据,这说明我们接收和发送的模块可满足设计要求,能实现音频回环的功能。

11. 上板验证

仿真验证通过后,绑定引脚,对工程进行重新编译。如图1-30所示,将开发板连接至12V直流电源和USB-Blaster下载器的JTAG端口,连接耳机、音频线(如图1-31所示)、扬声器,线路正确连接后,打开开关为板卡上电,随后为开发板下载程序。

图1-30 程序下载连线图

图1-31 3.5mm公对公音频连接线

程序下载成功后即可以开始验证,我们从播放器(计算机或者手机即可)中播放一曲音乐,若耳机和喇叭上能听到所播放的音乐,则说明验证成功。