7. wm8978录音与回放

前面《WM8978音频回环》章节我们已经讲了WM8978芯片的音频回环功能,同时WM8978芯片也具有录音的功能,本章节我们将学习如何实现WM8978的录音功能以及对录音的回放。

7.1. 理论学习

7.1.1. WM8978录音功能简介

在《WM8978音频回环》章节我们已经对声音的基本概念、WM8978芯片及其通信方式、音频回环做了详细的介绍,在此就不再过多介绍,没学习的读者请大家回到《WM8978音频录音》进行完整的学习后再来学习本章节,因为本章节是基于音频回环的章节进行设计的。

若要实现录音功能就需要打开WM8978芯片的录音功能,而通过音频回环章节的学习我们知道是通过对WM8978芯片的寄存器配置来设置WM8978芯片的功能的,所以这里相较于音频回环章节我们需要更改一些寄存器配置以打开录音功能。

实现录音功能的寄存器配置如下:

  • 寄存器R0(00h)

该寄存器用于控制WM8978的软复位,通过写任意值到R0寄存器即可实现该功能。

  • 寄存器R3(03h)

该寄存器用于设置DACENL(bit0)、DACENR(bit1)为1,左右通道DAC使能,使数字音频信号转换为模拟音频信号;设置LMIXEN(bit2)、RMIXEN(bit3)为1,左右输出通道混合器使能;设置ROUT2EN(bit5)、LOUT2EN(bit6)为1 ,左右扬声器使能。

  • 寄存器R6(06h)

该寄存器可设置WM8978的工作模式,前面我们说到WM8978有两种工作模式。在这里我们将其设置为主模式,左右对齐时钟(LRC)和位时钟(BCLK)由WM8978产生,我们直接用即可,这样可以节省资源。寄存器R6的bit0为0时为从模式,为1时为主模式,所以这里我们设置bit0为1即可。

  • 寄存器R10(0Ah)

该寄存器用于设置DAC的过采样率,通过对该寄存器的DACOSR128(bit3)设置为1可得到最好的信噪比。

  • 寄存器R14(0Eh)

该寄存器用于设置ADC的过采样率,同样设置该寄存器ADCOSR128(bit3)为1可得到最好的信噪比。同时设置HPFEN(bit8)为1,高通滤波器使能,可去掉信号中不必要的低频成分或者说去掉了低频干扰。

  • 寄存器R43(2Bh)

该寄存器需设置INVROUT2(bit4)为1,反转ROUT2输出,使扬声器输出的音效更好。

  • 寄存器R49(31h)

该寄存器的SPKBOOST(bit2)可用于设置扬声器增益,我们将其设置为1即可。WM8978有个过热保护功能,通过将该寄存器的TSDEN(bit1)设置为1,开启过热保护。

  • 寄存器R50(32h)

该寄存器需要设置DACL2LMIX(bit0)为1,将左DAC输出到左输出混合器。

  • 寄存器R51(33h)

该寄存器需要设置DACR2RMIX(bit0)为1,将右DAC输出到右输出混合器。

  • 寄存器R52(34h)

该寄存器可用于设置耳机左声道的音量,通过对LOUT1VOL(bit[5:0])的设置,从6’b000000到6’b111111依次增大。同时将bit7,bit8都设置为1,用于更新增益和音量。

  • 寄存器R53(35h)

该寄存器用于设置耳机右声道的音量,同上面寄存器R52的设置一致即可。

  • 寄存器R54(36h)

该寄存器用于设置左扬声器的音量,同上面寄存器R52的设置方法一样。

  • 寄存器R55(37h)

该寄存器用于设置右扬声器的音量,同上面寄存器R54的设置一样即可。

以上寄存器设置与音频回环设置是一样的,这里需要更改或增加的寄存器为R1、R2、R4、R47、R48,设置如下:

  • 寄存器R1(01h)

该寄存器设置bit[1:0] 为2’b11,让其以最快时间启动。禁用BUFIOEN(bit[2] = 0)或BUFDCOPEN(bit[8] = 0)时,可能会导致爆音,所以需将bit[2] 和bit[8] 设置为1;同时需启动BIASEN(bit3 = 1),否则模拟放大器不会工作。bit[4]为设置MIC使能,我们需将其设置为1,使能MIC以实现录音功能。WM8978具有片上锁相环(PLL)电路,可用于从另一个外部时钟为WM8978音频功能生成主时钟。PLL可以由R1(bit5)寄存器位启用或禁用。这里我们需启用(bit[5] = 1)来配置内部MCLK时钟。

  • 寄存器R2(02h)

该寄存器用于设置ADCENL(bit0)、ADCENR(bit1)为1,使能左右声道的ADC功能;设置INPPGAENL(bit2)、INPPGAENR(bit3)为1,左右声道输入PGA使能;设置LOUT1EN(bit7)、ROUT1EN(bit8)为1,耳机输出使能。

  • 寄存器R4(04h)

该寄存器用于设置FMT(bit[4:3])为2’b10,音频数据模式选择为I2S模式,其中2’b00为右对齐模式、2’b01位左对齐模式、2’b11位DSP/PCM模式。设置WL(bit[6:5])WL为字长,即一个声音数据的量化位数,2’b00为16位、2’b01为20位、2’b10为24位、2’ b11为32位,这里我们需要设置为16位(原因容我后面为大家道来)。

  • 寄存器R45(2Dh)

该寄存器用于设置左声道输入PGA的音量,可通过bit[5:0]进行设置,从6’b000000到6’d111111一次增大,这里我们直接设置为111111,以最大音量输入。同时将bit7,bit8都设置为1,用于更新增益和音量。

  • 寄存器R46(2Eh)

该寄存器用于设置右声道输入PGA的音量,可通过bit[5:0]进行设置,从6’b000000到6’d111111一次增大,这里我们直接设置为111111,以最大音量输入。同时将bit7,bit8都设置为1,用于更新增益和音量。

  • 寄存器R47(2Fh)

该寄存器可用于设置左通道输入PGA增益,将bit[8]设置为1,输出以+20dB增益通过输入BOOST。

  • 寄存器R48(30h)

该寄存器可用于设置右通道输入PGA增益,同R47将bit[8]设置为1即可。

以上就是录音功能的寄存器配置。上面没有描述到的寄存器按默认配置即可,其余寄存器的功能感兴趣的读者可参考WM8978芯片的数据手册进行了解。

7.2. 实战演练

7.2.1. 实验目标

使用WM8978芯片,通过开发板上的两个按键实现录音与播放,具体要实现的功能为:按下按键KEY1后开始录音,再次按下按键KEY1后录音结束。按下按键KEY2开始播放录音。在录音期间按下播放按键(KEY3)不会播放录音,需录音完之后才能进行播放;在播放录音时,按下录音按键可停止播放录音开始新的录音。

7.2.2. 硬件资源

本次实验我们需使用到开发板上的WM8978音频相关接口,开发板上的音频相关接口如图 57‑1所示。

WM8978002

图 57‑1 音频相关接口图

其相关原理图如下所示:

WM8978003

图 57‑2 音频芯片及MIC插头图

WM8978004

图 57‑3 喇叭插座

WM8978005

图 57‑4 耳机及音频输入接口

本次实验需要使用到MIC插头,耳机以及喇叭接口。MIC插头是用于录音输入,耳机以及喇叭接口是用来连接耳机以及喇叭用于播放录音。

7.2.3. 程序设计

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

7.2.3.1. 整体说明

根据实验目标可知,要实现WM8978的录音、播放功能,首先得需要存放我们录音数据的存储器,这里我们使用前面章节所讲的SDRAM存储器对录音文件进行存储。同时需要一个录音的控制模块对WM8978芯片输入的录音数据进行存储,以及对SDRAM存储的录音文件进行读取通过WM8978进行播放。大致的流程框图如 图 57‑5所示:

WM8978006

图 57‑5 录音机流程图

从流程图中可以看到:WM8978芯片会一直读取外界的声音到FPGA中,而只有当录音按键(KEY2)有效时,才会将WM8978传来的声音数据存入SDRAM中。当录音按键无效时,是没有数据存入SDRAM的。同理播放时也是如此,当播放按键有效时,FPGA就会读取SDRAM中存储的声音数据,将声音数据传入W M8978芯片中通过扬声器或者耳机播放出来。当播放按键无效时,FPGA传入WM8978的声音数据为0,没有声音播放。

结合前面对WM8978音频回环以及SDRAM章节的学习,我们可画出该实验工程的整体框图,如图 57‑6所示。

WM8978007

图 57‑6 WM8978录音与回放整体框图

由图 57‑6所示:sdram_top模块的输入wr_fifo_wr_data的位宽是16bit的,也就是说存入sdram的数据的位宽为16位,而在《WM8978音频回环》章节我们设置的音频数据位宽为24bit,与其不匹配,所以我们需要将音频数据位宽设为16位,这就是我们我们需要将音频字长(WL)设 置为16位的原因。工程各模块的功能描述如表格 57‑1所示。

表格 57‑1 audio_recrd工程模块简介

模块名称

功能描述

clk_gen

时钟生成模块

key_filter

按键消抖模块

sdram_top

SDRAM读写控制模块

record_ctrl

录音控制模块

wm8978_ctrl

WM8978音频控制模块

audio_record

顶层模块

下面对各模块做详细介绍。

7.2.3.2. 时钟生成模块

该模块通过调用PLL IP核来实现,总共输出4个时钟,频率分别为12MHz、50MHz、100MHz、100MHz(相位偏移-30度)。其中12MHz时钟作为wm8978的主时钟、50MHz作为系统时钟、100MHz时钟和100MHz(相位偏移-30度)时钟作为SDRAM读写控制模块的驱动时钟。

调用IP核的方法在IP核章节已经做了详细介绍,我们只需按照调用步骤生成这四个时钟即可,在此就不再过多介绍。

7.2.3.3. 按键消抖模块

该模块我们直接调用《按键消抖》章节的按键消抖模块代码即可。在此就不再过多叙述。

7.2.3.4. SDRAM读写控制模块

该模块我们直接调用《OV7725摄像头VGA显示》章节的sdram_top模块,在《OV7725摄像头VGA显示》章节已经对该模块做了详细的说明,在此就不再过多叙述了。

7.2.3.5. WM8978音频控制模块

模块框图

WM8978008

图 57‑7 WM8978音频控制模块框图

由上图可以看到该模块中的子模块与音频回环模块的子模块是一样的,唯一不同的是寄存器的配置与字长变为16位。下面给出寄存器的配置模块的参考代码,如代码清单 57‑1所示。

代码编写

代码清单 57‑1 录音寄存器配置参考代码(i2c_reg_cfg.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
module i2c_reg_cfg
(
input wire i2c_clk , //系统时钟,由i2c模块传入
input wire sys_rst_n , //系统复位,低有效
input wire cfg_end , //单个寄存器配置完成

output reg cfg_start , //单个寄存器配置触发信号
output wire [15:0] cfg_data , //寄存器地址7bit+数据9bit
output reg cfg_done //寄存器配置完成
 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //parameter define
 parameter REG_NUM = 6'd20 ; //总共需要配置的寄存器个数
 parameter CNT_WAIT_MAX = 10'd1000 ; //上电等待1ms后开始配置寄存器

 parameter LOUT1VOL = 6'd60 ; //耳机左声道音量设置(0~63)
 parameter ROUT1VOL = 6'd60 ; //耳机右声道音量设置(0~63)

 parameter SPK_LOUT2VOL = 6'd50 ; //扬声器左声道音量设置(0~63)
 parameter SPK_ROUT2VOL = 6'd50 ; //扬声器右声道音量设置(0~63)

 //wire define
 wire [15:0] cfg_data_reg[REG_NUM-1:0]; //寄存器配置数据暂存

 //reg define
 reg [9:0] cnt_wait ; //寄存器配置上电等待计数器
 reg [5:0] reg_num ; //配置寄存器个数

 ////
 //\* Main Code \//
 ////

 //cnt_wait:寄存器配置等待计数器
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_wait <= 10'd0;
 else if(cnt_wait < CNT_WAIT_MAX)
 cnt_wait <= cnt_wait + 1'b1;

 //reg_num:配置寄存器个数
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 reg_num <= 6'd0;
 else if(cfg_end == 1'b1)
 reg_num <= reg_num + 1'b1;

 //cfg_start:单个寄存器配置触发信号
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cfg_start <= 1'b0;
 else if(cnt_wait == (CNT_WAIT_MAX - 1'b1))
 cfg_start <= 1'b1;
 else if((cfg_end == 1'b1) && (reg_num < (REG_NUM-1)))
 cfg_start <= 1'b1;
 else
 cfg_start <= 1'b0;

 //cfg_done:寄存器配置完成
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cfg_done <= 1'b0;
 else if((reg_num == REG_NUM - 1'b1) && (cfg_end == 1'b1))
 cfg_done <= 1'b1;

 //cfg_data:7bit地址+9bit数据
 assign cfg_data = (cfg_done == 1'b1) ? 16'b0 : cfg_data_reg[reg_num];

 //----------------------------------------------------
 //cfg_data_reg:寄存器配置数据暂存
 //各寄存器功能配置详见文档介绍
 assign cfg_data_reg[00] = {7'd0 , 9'b0 };
 assign cfg_data_reg[01] = {7'd1 , 9'b1_0011_1111 };
 assign cfg_data_reg[02] = {7'd2 , 9'b1_1000_1111 };
 assign cfg_data_reg[03] = {7'd4 , 9'b0_0001_0000 };
 assign cfg_data_reg[04] = {7'd6 , 9'b0_0000_0001 };
 assign cfg_data_reg[05] = {7'd10, 9'b0_0000_1000 };
 assign cfg_data_reg[06] = {7'd14, 9'b1_0000_1000 };
 assign cfg_data_reg[07] = {7'd43, 9'b0_0001_0000 };
 assign cfg_data_reg[08] = {7'd45, 9'b1_1011_1111 };
 assign cfg_data_reg[09] = {7'd46, 9'b1_1011_1111 };
 assign cfg_data_reg[10] = {7'd47, 9'b1_0000_0000 };
 assign cfg_data_reg[11] = {7'd48, 9'b1_0000_0000 };
 assign cfg_data_reg[12] = {7'd49, 9'b0_0000_0110 };
 assign cfg_data_reg[13] = {7'd50, 9'b0_0000_0001 };
 assign cfg_data_reg[14] = {7'd51, 9'b0_0000_0001 };
 assign cfg_data_reg[15] = {7'd52, 3'b110 , LOUT1VOL };
 assign cfg_data_reg[16] = {7'd53, 3'b110 , ROUT1VOL };
 assign cfg_data_reg[17] = {7'd54, 3'b110 , SPK_LOUT2VOL };
 assign cfg_data_reg[18] = {7'd55, 3'b110 , SPK_ROUT2VOL };
 //更新完耳机和扬声器的音量后再开启音频的输出使能,防止出现“嘎达”声
 assign cfg_data_reg[19] = {7'd3 , 9'b0_0110_1111 };
 //-------------------------------------------------------

 endmodule

以上代码就是增加了几个寄存器的配置以及更改了一些寄存器的配置,配置方法与音频回环章节的配置方法一致,大家可参考《WM8978音频回环章节》寄存器配置进行了解。这里需要注意的是,因为配置字长(WL)发送了变化,音频回环配置的是24bit,这里我们配置的是16bit,所以我们需要将各个模块的相关参数以及 条件改成对应16bit的参数以及条件。下面给出各需要更改模块的更改后的代码。

代码清单 57‑2 录音音频接收模块参考代码(audio_rcv.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
module audio_rcv
(
input wire audio_bclk , //WM8978输出的位时钟
input wire sys_rst_n , //系统复位,低有效
input wire audio_lrc , //WM8978输出的数据左/右对齐时钟
input wire audio_adcdat , //WM8978ADC数据输出

output reg [15:0] adc_data , //一次接收的数据
output reg rcv_done //一次数据接收完成

 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //reg define
 reg audio_lrc_d1; //对齐时钟打一拍信号
 reg [4:0] adcdat_cnt ; //WM8978ADC数据输出位数计数器
 reg [15:0] data_reg ; //adc_data数据寄存器

 //wire define
 wire lrc_edge ; //对齐时钟信号沿标志信号

 ////
 //\* Main Code \//
 ////

 //使用异或运算符产生信号沿标志信号
 assign lrc_edge = audio_lrc ^ audio_lrc_d1;

 //对audio_lrc信号打一拍以方便获得信号沿标志信号
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 audio_lrc_d1 <= 1'b0;
 else
 audio_lrc_d1 <= audio_lrc;

 //adcdat_cnt:当信号沿标志信号为高电平时,计数器清零
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 adcdat_cnt <= 5'b0;
 else if(lrc_edge == 1'b1)
 adcdat_cnt <= 5'b0;
 else if(adcdat_cnt < 5'd18)
 adcdat_cnt <= adcdat_cnt + 1'b1;
 else
 adcdat_cnt <= adcdat_cnt;

 //将WM8978输出的ADC数据寄存在data_reg中,一次寄存24位
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 data_reg <= 16'b0;
 else if(adcdat_cnt <= 5'd15)
 data_reg[15-adcdat_cnt] <= audio_adcdat;
 else
 data_reg <= data_reg;

 //当最后一位数据传完之后,读出寄存器的值给adc_data
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 adc_data <= 16'b0;
 else if(adcdat_cnt == 5'd16)
 adc_data <= data_reg;
 else
 adc_data <= adc_data;

 //当最后一位数据传完之后,输出一个时钟的完成标志信号
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rcv_done <= 1'b0;
 else if(adcdat_cnt == 5'd16)
 rcv_done <= 1'b1;
 else
 rcv_done <= 1'b0;

 endmodule

相较于音频回环的音频接收代码这里更改了接收数据、数据寄存器、数据计数器的位宽以及用到这些参数的相关代码的位宽及数据。更改这些参数就是为了匹配16bit字长的音频量化位数,如果大家对音频回环章节已经弄明白了,那么修改这些参数是轻而易举的。

代码清单 57‑3 录音音频发送模块参考代码(audio_send.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
module audio_send
(
input wire audio_bclk , //WM8978输出的位时钟
input wire sys_rst_n , //系统复位,低有效
input wire audio_lrc , //WM8978输出数据左/右对齐时钟
input wire [15:0] dac_data , //往WM8978发送的数据

output reg audio_dacdat , //发送DACDAT数据给WM8978
output reg send_done //一次数据发送完成

 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //reg define
 reg audio_lrc_d1; //对齐时钟打一拍信号
 reg [4:0] dacdat_cnt ; //DACDAT数据发送位数计数器
 reg [15:0] data_reg ; //dac_data数据寄存器

 //wire define
 wire lrc_edge ; //对齐时钟信号沿标志信号

 ////
 //\* Main Code \//
 ////

 //使用异或运算符产生信号沿标志信号
 assign lrc_edge = audio_lrc ^ audio_lrc_d1;

 //对audio_lcr信号打一拍以方便获得信号沿标志信号
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 audio_lrc_d1 <= 1'b0;
 else
 audio_lrc_d1 <= audio_lrc;

 //dacdat_cnt:当信号沿标志信号为高电平时,计数器清零
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 dacdat_cnt <= 1'b0;
 else if(lrc_edge == 1'b1)
 dacdat_cnt <= 1'b0;
 else if(dacdat_cnt < 5'd18)
 dacdat_cnt <= dacdat_cnt + 1'b1;
 else
 dacdat_cnt <= dacdat_cnt;

 //将要发送的dac_data数据寄存在data_reg中
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 data_reg <= 16'b0;
 else if(lrc_edge == 1'b1)
 data_reg <= dac_data;
 else
 data_reg <= data_reg;

 //下降沿到来时将data_reg的数据一位一位传给audio_dacdat
 always@(negedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 audio_dacdat <= 1'b0;
 else if(dacdat_cnt <= 5'd15)
 audio_dacdat <= data_reg[15 - dacdat_cnt];

 //当最后一位数据传完之后,输出一个时钟的发送完成标志信号
 always@(posedge audio_bclk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 send_done <= 1'b0;
 else if(dacdat_cnt == 5'd16)
 send_done <= 1'b1;
 else
 send_done <= 1'b0;

 endmodule

同理该模块与接收模块的修改方法是一致的,在此不再过多介绍。

剩下的模块与音频回环中对应的模块是一致的我们直接调用即可。这里我们将这些模块例化在wm8978_ctrl模块中即可,代码如代码清单 57‑4所示。

代码清单 57‑4 音频控制模块顶层参考代码(wm8978_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
module wm8978_ctrl
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //系统复位,低电平有效
input wire audio_bclk , //WM8978输出的位时钟
input wire audio_lrc , //WM8978输出的数据左/右对齐时钟
input wire audio_adcdat, //WM8978ADC数据输出
input wire [15:0] dac_data , //输入音频数据

 output wire scl , //输出至wm8978的串行时钟信号scl
 output wire audio_dacdat, //输出DAC数据给WM8978
 output wire rcv_done , //一次数据接收完成
 output wire send_done , //一次数据发送完成
 output wire [15:0] adc_data , //输出音频数据

 inout wire sda //输出至wm8978的串行数据信号sda

 );

 ////
 //\* Instantiation \//
 ////

 //------------- audio_rcv_inst -------------
 audio_rcv audio_rcv_inst
 (
 .audio_bclk (audio_bclk ), //WM8978输出的位时钟
 .sys_rst_n (sys_rst_n ), //系统复位,低有效
 .audio_lrc (audio_lrc ), //WM8978输出的数据左/右对齐时钟
 .audio_adcdat (audio_adcdat), //WM8978ADC数据输出

 .adc_data (adc_data ), //一次接收的数据
 .rcv_done (rcv_done ) //一次数据接收完成

 );

 //------------- audio_send_inst -------------
 audio_send audio_send_inst
 (
 .audio_bclk (audio_bclk ), //WM8978输出的位时钟
 .sys_rst_n (sys_rst_n ), //系统复位,低有效
 .audio_lrc (audio_lrc ), //WM8978输出数据左/右对齐时钟
 .dac_data (dac_data ), //往WM8978发送的数据

 .audio_dacdat (audio_dacdat), //发送DAC数据给WM8978
 .send_done (send_done ) //一次数据发送完成

 );

 //------------- wm8978_cfg_inst -------------
 wm8978_cfg wm8978_cfg_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //系统复位,低电平有效

 .i2c_scl (scl ), //输出至wm8978的串行时钟信号scl
 .i2c_sda (sda ) //输出至wm8978的串行数据信号sda

 );

 endmodule

该模块为音频控制的顶层模块,将录音实验所需的端口引出来供其他模块使用。

7.2.3.6. 录音控制模块

模块框图

该模块的作用是控制录音的数据存入SDRAM内以及控制SDRAM里的录音数据读出传入WM8978中播放出来。其模块框图如图 57‑8所示。

WM8978009

图 57‑8 录音控制模块框图

该模块的输入输出信号描述如下表所示:

表格 57‑2 录音控制模块输入输出信号描述

信号

位宽

类型

功能模块

clk

1bit

input

模块时钟

sys_clk

1bit

input

系统时钟,50MHz

sys_rst_n

1bit

input

复位信号,低有效

record_flag

1bit

input

录音按键有效信号

broadcast_flag

1bit

input

播放按键有效信号

rcv_done

1bit

input

一次音频数据接收完成信号

send_done

1bit

input

一次音频数据发送完成信号

key_record

1bit

input

录音按键信号

key_broadcast

1bit

input

播放按键信号

rd_data

16bit

input

SDRAM读数据

sdram_init_end

1bit

input

SDRAM初始化完成信号

adc_data

16bit

input

输入音频数据

wr_en

1bit

output

SDRAM写使能信号

rd_en

1bit

output

SDRAM读使能信号

p_broadcast_flag

1bit

output

录音按键上升沿

p_record_flag

1bit

output

播放按键上升沿

wr_data

16bit

output

SDRAM写数据

dac_data

16bit

output

输出音频数据

从模块整体框图中可以看到模块时钟(clk)是由WM8978中传来的音频时钟(audio_bclk),为什么要用此时钟作为模块时钟呢?我们知道音频数据是从WM8978中传来的,而WM8978传输音频的时钟为audio_bclk,所以我们需使用这个时钟去接收WM8978中传来的音频数据并且用此时钟将音频 数据传入SDRAM,才能使数据匹配。有了模块时钟,那为什么我们还需要输入一个系统时钟(sys_clk)呢?那是因为我们录音与播放按键的按键消抖信号是由系统时钟产生的,而且有效信号只持续了一个系统时钟。所以如果我们用更低频率的时钟去对这个有效信息去采样的话,那么将很难采到这个信号,如图 57‑9所示。

WM8978010

图 57‑9 低频采高频信号图

由上图可以看到我们使用低频时钟上升沿去采高频时钟产生的一个时钟有效标志信号,那么是有很大概率采不到的。

对使用时钟讲解完毕之后,我们通过绘制波形图来帮助我们更加清晰的了解各个信号的时序关系。

波形图绘制

WM8978011

图 57‑10 录音控制信号波形图

针对图 57‑10我们先了解一下如何产生我们的录音标志信号。根据我们的实验任务可知当按下录音按键时开始录音,再次按下录音按键时录音停止。通过按键消抖模块我们知道,我们按键消抖后的标志信号为一个系统时钟(50MHZ)的高电平。所以我们可从按键消抖模块输入消抖后的按键有效信号,再使用系统时钟(50MHz )对这个信号进行采样,当采到录音按键消抖有效信号(record_flag)时,拉高录音按键有效信号采样信号(record_flag_t),在当按键信号(key_record)为1时拉低record_flag_t。

这些信号产生完成之后,我们即可用模块时钟(clk)对record_flag_t进行打拍获得打一拍(record_flag_t_d1),打两拍(record_flag_t_d2)信号,通过取反按位与操作获得上升沿信号(p_record_flag),该上升沿可看作是适用于该模块时钟采样的按键消抖有效信号 。再通过此消抖有效信号与按键信号产生录音信号(record,高有效):当上升沿信号为1时拉高录音信号,再次检测到上升沿信号(p_record_flag)为高时,拉低录音信号。

WM8978012

图 57‑11 播放控制信号波形图

如图 57‑11所示,播放控制信号的产生的方式与录音控制信号的产生大致相同,不同的是拉低播放信号的条件(后面为大家讲述拉低条件)。因为播放的是录音数据,所以播放结束信号与录音时长是息息相关的。下面为大家介绍录音与播放时的控制时序。

WM8978013

图 57‑12 录音时序波形图

如图 57‑12所示为录音时各信号的时序关系图,下面分信号为大家讲解。

sdram_init_end:SDRAM初始化完成信号,我们录音时首先得先确定SDRAM初始化完成了之后才能开始录音,而SDRAM是由sdram模块100MHZ时钟产生的,在输入该模块后先对其打两拍以消除亚稳态。

record:录音信号,前面已经讲述了该信号的产生时序。

rcv_done、adc_data:这三个信号由音频模块输入,rcv_done为一次音频数据接收完成信号(即WM8978芯片传来的一次16bit音频数据接收完成信号)、adc_data为WM8978芯片传来的一个16bit音频数据。这些信号的具体的时序大家可参阅《WM8978音频回环》章节中各信号的 波形图。为什么在该模块要输入这些信号?下面为大家讲解:

wr_en、wr_data:写使能信号、写数据。wr_en信号为写入SDRAM的使能信号,通过对SDRAM的学习可知道当写使能为高时,可将写数据写入SDRAM中,所以如何产生写使能信号就变得尤为关键。我们是在录音时将音频数据存入SDRAM中,所以在录音信号有效时需将WM8978芯片传来的音频数据ad c_data存入SDRAM中。当rcv_done拉高时说明一次音频数据接收完成,此时我们拉高写使能信号,同时将音频数据赋值给wr_data(写数据)。有了写使能和写数据我们即可将音频数据写入SDRAM中了,需要注意的是写使能和写数据需在录音时(record为高)赋值,在不录音时让其为0即可。

reocrd_cnt:录音时长计数器,前面我们说到播放信号何时拉低的问题,这个计数器就是针对这个问题所产生的。我们每存入一个音频数据让计数器加一,当录音完成时该计数器的值就代表录音时长,那么每次录音时长都不一样,这个计数器该如何清零重新计数呢?如图 57‑13所示。

WM8978014

图 57‑13 录音时长计数器清零波形图

当录音结束(record=0)后,再按下录音按键(p_record_flag=1)时,表示此时我们要重新开始录音了,这时我们让计数器清零即可。那我们如何利用这个数值去控制播放信号拉低呢?时序图如图 57‑14所示。

WM8978015

图 57‑14 播放时波形图

图 57‑14为播放时各信号的时序波形图,该波形图的各信号时序与录音时各信号时序的产生方法是大致相同的。

send_done、rd_en、rd_data、dac_data:当播放信号为高且send_done为高时表示WM8978一次音频数据接收完成可以开始接收下一个音频数据。这里我们只需读出SDRAM内存入的数据rd_data赋给dac_data通过WM8978输出即可,所以当播放信号为高且send_d one为高时拉高一个时钟的读使能信号(rd_en)。rd_en信号为高时,SDRAM就会读出数据rd_data,将rd_data赋给dac_data即可。

broadcast_cnt:播放时长计数器,产生方法与录音时长计数器相同。每读出一个数据计数器加1,当其与录音时长计数器的值相等时,说明我们播完了我们录音的数据,此时即可拉低播放信号(broadcast),同时让播放计数器清零等待下一次播放信号的到来时再进行计数。

这里需要说明的是当录音信号为高(正在录音时),是不能播放的,即播放信号要在录音信号为低时才能拉高。同时在播放时若按下录音按键时,则会停止播放并开始录音。如图 57‑15所示。

WM8978016

图 57‑15 录音、播放信号关系图

同时若在播放时,再次按下播放键,则录音会从头开始播放,如图 57‑16所示。

WM8978017

图 57‑16 播放控制图

如图 57‑16所示,当播放时按下播放按键,则让播放时长计数器清零即可达到从头播放的目的。

以上就是该模块全部信号的时序波形图,大家跟着波形图去编写代码可达到事半功倍的效果,下面给出模块参考代码。

代码编写

参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 57‑5。

代码清单 57‑5 录音控制模块参考代码(record_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
module record_ctrl
#(
parameter TIME_RECORD = 24'd11520000 //定义最大录音时长,120s
)
(
input wire sys_clk , //系统时钟
input wire clk , //模块驱动时钟
input wire sys_rst_n , //复位信号
input wire key_record , //录音按键
input wire key_broadcast , //播放按键
input wire record_flag , //录音按键有效信号
input wire broadcast_flag , //播放按键有效信号
input wire sdram_init_end , //SDRAM初始化完成标志信号
input wire rcv_done , //一次音频数据接收完成
input wire send_done , //一次音频数据发送完成
input wire [15:0] adc_data , //输入音频数据
input wire [15:0] rd_data , //SDRAM读出的数据

output reg [15:0] wr_data , //写入SDRAM数据
output reg [15:0] dac_data , //输出音频数据
output reg wr_en , //SDRAM写FIFO写请求
output reg rd_en , //SDRAM读FIFO读请求
output wire p_record_flag , //录音按键上升沿
output wire p_broadcast_falg //播放按键上升沿

);

////
//\* Parameter and Internal Signal \//
////

//reg define
reg [23:0] record_cnt ; //录音时长计数器
reg [23:0] broadcast_cnt ; //播放时长计数器
reg sdram_init_end_d1 ; //sdram初始化完成信号打一拍信号
reg sdram_init_end_d2 ; //sdram初始化完成信号打两拍信号
reg record_flag_t ; //录音按键有效信号采样信号
reg record_flag_t_d1 ; //录音按键有效信号打一拍信号
reg record_flag_t_d2 ; //录音按键有效信号打两拍信号
reg broadcast_flag_t ; //播放按键有效信号采样信号
reg broadcast_flag_t_d1 ; //播放按键信号打一拍信号
reg broadcast_flag_t_d2 ; //播放按键信号打两拍信号
reg record ; //录音信号
reg broadcast ; //播放信号

////
//\* Main Code \//
////

//对录音、播放按键有效信号采上升沿即为该时钟下的按键有效采样信号
assign p_record_flag = record_flag_t_d1 & ~record_flag_t_d2;
assign p_broadcast_falg = broadcast_flag_t_d1 & ~broadcast_flag_t_d2;

//对sdram_init_end打拍,消除亚稳态
always@(posedge clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
sdram_init_end_d1 <= 1'b0;
sdram_init_end_d2 <= 1'b0;
end
else
begin
sdram_init_end_d1 <= sdram_init_end;
sdram_init_end_d2 <= sdram_init_end_d1;
end

//record_flag_t:用系统时钟对录音按键有效信号采样
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
record_flag_t <= 1'b0;
else if(record_flag == 1'b1)
record_flag_t <= 1'b1;
else if(key_record == 1'b1)
record_flag_t <= 1'b0;
else
record_flag_t <= record_flag_t;

// broadcast_flag_t :用系统时钟对播放按键有效信号采样
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
broadcast_flag_t <= 1'b0;
else if(broadcast_flag == 1'b1)
broadcast_flag_t <= 1'b1;
else if(key_broadcast == 1'b1)
broadcast_flag_t <= 1'b0;
else
broadcast_flag_t <= broadcast_flag_t;

//对采样有效信号延时打拍,获得上升沿标志信号
//该标志信号即为能被该模块时钟沿采到的按键有效信号
always@(posedge clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
record_flag_t_d1 <= 1'b0;
record_flag_t_d2 <= 1'b0;
broadcast_flag_t_d1 <= 1'b0;
broadcast_flag_t_d2 <= 1'b0;
end
else
 begin
 record_flag_t_d1 <= record_flag_t ;
 record_flag_t_d2 <= record_flag_t_d1;
 broadcast_flag_t_d1 <= broadcast_flag_t;
 broadcast_flag_t_d2 <= broadcast_flag_t_d1;
 end

 //record:录音信号
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 record <= 1'b0;
 else if(record_cnt == TIME_RECORD)
 record <= 1'b0;
 else if(p_record_flag == 1'b1)
 record <= ~record;
 else
 record <= record;

 //broadcast:播放信号
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 broadcast <= 1'b0;
 else if(p_broadcast_falg == 1'b1 && record == 1'b0)
 broadcast <= 1'b1;
 else if( broadcast_cnt == record_cnt \|\| p_record_flag == 1'b1)
 broadcast <= 1'b0;
 else
 broadcast <= broadcast;

 //wr_en:写使能信号的拉高
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 wr_en <= 1'b0;
 else if(record == 1'b1 && sdram_init_end_d2 == 1'b1 &&
 record_cnt < TIME_RECORD && rcv_done == 1'b1)
 wr_en <= 1'b1;
 else
 wr_en <= 1'b0;

 //record_cnt:录音时长计数器
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 record_cnt <= 1'b0;
 else if(p_record_flag == 1'b1 && record == 1'b0)
 record_cnt <= 1'b0;
 else if(wr_en == 1'b1 )
 record_cnt <= record_cnt + 1'b1;
 else
 record_cnt <= record_cnt;

 //wr_data:录音有效时写入的数据为WM8978传来的录音数据
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 wr_data <= 16'd0;
 else if(record == 1'b1)
 wr_data <= adc_data;
 else
 wr_data <= 16'd0;

 //rd_en:写使能信号的拉高
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rd_en <= 1'b0;
 else if(broadcast == 1'b1 && sdram_init_end_d2 == 1'b1
 && send_done == 1'b1)
 rd_en <= 1'b1;
 else
 rd_en <= 1'b0;

 //broadcast_cnt:播放时长计数器
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 broadcast_cnt <= 1'b0;
 else if(p_broadcast_falg == 1'b1 \|\| broadcast == record_cnt)
 broadcast_cnt <= 1'b0;
 else if(rd_en == 1'b1)
 broadcast_cnt <= broadcast_cnt + 1'b1;
 else
 broadcast_cnt <= broadcast_cnt;

 //dac_data:播放有效时读出的数据给WM8978播放
 always@(posedge clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 dac_data <= 16'd0;
 else if(broadcast == 1'b1)
 dac_data <= rd_data;
 else
 dac_data <= 16'd0;

endmodule

考虑到SDRAM的存储容量有限,这里我们定义一个录音最大时长,如代码第3、134行所示,当录音时长计数器小于我们设置的最大时长时才能写入数据。录音时长为音频采样的频率*2(左右两个声道)*时长(s),音频的采样率由寄存器R7的bit[3:1]设置,默认为000(48kHz),由于这个寄存器我们是按默 认配置的,即其采样率就为48kHz。这里我们设置录音时长最大为120s,故TIME_RECORD = 48000 * 2 * 120s = 11520000 。

7.2.3.7. 顶层模块

模块框图

WM8978018

图 57‑17 录音与回放顶层模块框图

该模块的输入输出信号描述如表格 51‑7所示。

表格 57‑3 录音与回放顶层模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

input

系统时钟,频率50MHz

sys_rst_n

1bit

input

复位信号,低有效

key_record

1bit

input

录音按键

key_broadcast

1bit

input

播放按键

audio_bclk

1bit

input

WM8978输出的位时钟

audio_lrc

1bit

input

WM8978输出的数据左/右对齐时钟

audio_adcdat

1bit

input

WM8978 ADC数据输出

audio_mclk

1bit

output

输出WM8978主时钟,频率12MHz

audio_dacdat

1bit

output

输出DAC数据给WM8978

scl

1bit

output

输出至wm8978的串行时钟信号scl

sda

1bit

inout

输出至wm8978的串行数据信号sda

sdram_clk

1bit

output

SDRAM芯片时钟

sdram_cke

1bit

output

SDRAM时钟有效信号

sdram_cs_n

1bit

output

SDRAM片选信号

sdram_cas_n

1bit

output

SDRAM列地址选通脉冲

sdram_ras_n

1bit

output

SDRAM行地址选通脉冲

sdram_we_n

1bit

output

SDRAM写允许位

sdram_ba

2bit

output

SDRAM的L-Bank地址线

sdram_addr

13bit

output

SDRAM地址总线

sdram_dqm

2bit

output

SDRAM数据掩码

sdram_dq

16bit

output

SDRAM数据总线

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

代码编写代码清单 57‑6。

代码编写

代码清单 57‑6 录音与回放顶层模块参考代码(record.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
module audio_record
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //系统复位,低电平有效
input wire key_record , //录音按键
input wire key_broadcast, //播放按键
input wire audio_bclk , //WM8978输出的位时钟
input wire audio_lrc , //WM8978输出的数据左/右对齐时钟
input wire audio_adcdat , //WM8978ADC数据输出

inout wire sda , //输出至wm8978的串行数据信号sda

output wire scl , //输出至wm8978的串行时钟信号scl
output wire audio_mlck , //输出WM8978主时钟,频率12MHz
output wire audio_dacdat , //输出DAC数据给WM8978

//SDRAM接口信号
output wire sdram_clk , //SDRAM芯片时钟
output wire sdram_cke , //SDRAM时钟有效信号
output wire sdram_cs_n , //SDRAM片选信号
output wire sdram_ras_n , //SDRAM行地址选通脉冲
output wire sdram_cas_n , //SDRAM列地址选通脉冲
output wire sdram_we_n , //SDRAM写允许位
output wire [1:0 ] sdram_ba , //SDRAM的L-Bank地址线
output wire [12:0] sdram_addr , //SDRAM地址总线
output wire [1:0 ] sdram_dqm , //SDRAM数据掩码
inout wire [15:0] sdram_dq //SDRAM数据总线

);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter TIME_RECORD = 24'd11520000; //录音时长,120s

//wire define
wire rst_n ; //当时钟输出不稳定时一直处于复位状态
wire wr_en ; //SDRAM写FIFO写请求
wire rd_en ; //SDRAM读FIFO读请求
wire [15:0] wr_data ; //SDRAM写数据
wire [15:0] rd_data ; //SDRAM读数据
wire [15:0] dac_data ; //SDRAM读FIFO读保存的录音数据
wire init_end ; //SDRAM初始化完成信号
wire record_flag ; //录音按键标志信号
wire broadcast_flag ; //播放按键标志信号
wire [15:0] adc_data ; //wm8978输出录音数据
wire rcv_done ; //一次数据接收完成
wire send_done ; //一次数据发送完成
wire clk_50m ; //输出50MHz时钟
wire clk_100m ; //输出100MHz时钟
wire clk_100m_shift ; //输出100MHZ,偏移-30度
wire locked ; //拉高表示锁相环开始稳定输出时钟信号
wire p_record_flag ; //录音按键上升沿
wire p_broadcast_falg; //播放按键上升沿

//rst_n:复位信号
assign rst_n = sys_rst_n & locked;

////
//\* Instantiation \//
////

//------------- clk_gen_inst -------------
clk_gen clk_gen_inst
(
.areset (~sys_rst_n ), //异步复位
.inclk0 (sys_clk ), //输入时钟
.c0 (audio_mlck ), //输出WM8978主时钟,频率12MHz
.c1 (clk_50m ), //输出50MHz时钟
.c2 (clk_100m ), //输出100MHz时钟
.c3 (clk_100m_shift ), //输出100MHZ,偏移-30度
.locked (locked ) //拉高表示锁相环开始稳定输出时钟信号

);

//------------- key_filter_inst -------------
key_filter record_key_filter
(
.sys_clk (clk_50m ), //系统时钟50MHz
.sys_rst_n (rst_n ), //全局复位
.key_in (key_record ), //录音按键

.key_flag (record_flag ) //录音按键有效标志信号

);

//------------- key_filter_inst -------------
key_filter broadcast_key_filter
(
.sys_clk (clk_50m ), //系统时钟50MHz
.sys_rst_n (rst_n ), //全局复位
.key_in (key_broadcast ), //播放按键

.key_flag (broadcast_flag) //播放按键有效标志信号

);

 //------------- record_ctrl_inst -------------
 record_ctrl
 #(
 .TIME_RECORD (TIME_RECORD ) //定义最大录音时长,120s
 )
 record_ctrl_inst
 (
 .sys_clk (clk_50m ), //系统时钟50MHz
 .clk (audio_bclk ), //模块时钟
 .sys_rst_n (rst_n ), //复位信号
 .key_record (key_record ), //录音按键
 .key_broadcast (key_broadcast ), //播放按键
 .record_flag (record_flag ), //录音按键消抖后信号
 .broadcast_flag (broadcast_flag), //播放按键消抖后信号
 .sdram_init_end (init_end ), //SDRAM初始化完成信号
 .rcv_done (rcv_done ), //一次音频数据接收完成
 .send_done (send_done ), //一次音频数据发送完成
 .adc_data (adc_data ), //输入音频数据
 .rd_data (rd_data ), //SDRAM读出的数据

 .wr_en (wr_en ), //SDRAM写FIFO写请求
 .rd_en (rd_en ), //SDRAM读FIFO读请求
 .p_record_flag (p_record_flag ), //录音按键上升沿
 .p_broadcast_falg(p_broadcast_falg), //播放按键上升沿
 .wr_data (wr_data ), //写入SDRAM数据
 .dac_data (dac_data ) //输出音频数据

 );

 //------------- sdram_top_inst -------------
 sdram_top sdram_top_inst
 (
 .sys_clk (clk_100m ), //sdram控制器参考时钟
 .clk_out (clk_100m_shift), //用于输出的相位偏移时钟
 .sys_rst_n (rst_n ), //复位信号,低有效
 //写FIFO信号
 .wr_fifo_wr_clk (audio_bclk ), //写FIFO写时钟
 .wr_fifo_wr_req (wr_en ), //写FIFO写请求
 .wr_fifo_wr_data (wr_data ), //写FIFO写数据
 .sdram_wr_b_addr (24'd0 ), //写SDRAM首地址
 .sdram_wr_e_addr (TIME_RECORD), //写SDRAM末地址
 .wr_burst_len (10'd512 ), //写SDRAM数据突发长度
 .wr_rst (p_record_flag), //写复位信号
 //读FIFO信号
 .rd_fifo_rd_clk (audio_bclk ), //读FIFO读时钟
 .rd_fifo_rd_req (rd_en ), //读FIFO读请求
 .sdram_rd_b_addr (24'd0 ), //读SDRAM首地址
 .sdram_rd_e_addr (TIME_RECORD), //读SDRAM末地址
 .rd_burst_len (10'd512 ), //读SDRAM数据突发长度
 .rd_fifo_rd_data (rd_data ), //读FIFO读数据
 .rd_rst (p_broadcast_falg), //读清零信号

 .read_valid (1'b1 ), //SDRAM读使能
 .pingpang_en (1'b0 ), //SDRAM乒乓操作使能
 .init_end (init_end ), //SDRAM初始化完成标志

 //SDRAM接口信号
 .sdram_clk (sdram_clk ), //SDRAM芯片时钟
 .sdram_cke (sdram_cke ), //SDRAM时钟有效信号
 .sdram_cs_n (sdram_cs_n ), //SDRAM片选信号
 .sdram_ras_n (sdram_ras_n), //SDRAM行地址选通脉冲
 .sdram_cas_n (sdram_cas_n), //SDRAM列地址选通脉冲
 .sdram_we_n (sdram_we_n ), //SDRAM写允许位
 .sdram_ba (sdram_ba ), //SDRAM的L-Bank地址线
 .sdram_addr (sdram_addr ), //SDRAM地址总线
 .sdram_dqm (sdram_dqm ), //SDRAM数据掩码
 .sdram_dq (sdram_dq ) //SDRAM数据总线

 );

 //------------- wm8978_ctrl_inst -------------
 wm8978_ctrl wm8978_ctrl_inst
 (
 .sys_clk (clk_50m ), //系统时钟,频率50MHz
 .sys_rst_n (rst_n ), //系统复位,低电平有效
 .audio_bclk (audio_bclk ), //WM8978输出的位时钟
 .audio_lrc (audio_lrc ), //WM8978输出的数据左/右对齐时钟
 .audio_adcdat (audio_adcdat), //WM8978ADC数据输出
 .dac_data (dac_data ), //输入音频数据

 .scl (scl ), //输出至wm8978的串行时钟信号scl
 .audio_dacdat (audio_dacdat), //输出DAC数据给WM8978
 .rcv_done (rcv_done ), //一次数据接收完成
 .send_done (send_done ), //一次数据发送完成
 .adc_data (adc_data ), //输出音频数据

 .sda (sda ) //输出至wm8978的串行数据信号sda

 );

 endmodule

在SDRAM的例化中,我们将读写地址的首地址设置为0,末地址设置为录音最大时长TIME_RECORD,突发长度设置为512。同时SDRAM的读写清零信号我们接入播放、录音信号按键的上升沿,这样我们每次录音、播放都能从头开始了。

7.2.3.8. RTL视图

使用Quartus II软件对工程进行编译,编译通过后,查看RTL视图如下图 57‑18所示。由图可知,实验工程的RTL视图与实验整体框图相同,各信号线均已正确连接。

WM8978019

图 57‑18 audio_record顶层RTL视图

7.2.3.9. SignalTap波形抓取

这里我们利用quartus软件自带的SignalTap工具对波形进行实时抓取来验证我们的设计是否正确,如下图所示。

WM8978020

图 57‑19 SignalTap抓取波形图(一)

如图 57‑19所示,抓取的信号为录音相关的信号,如图可以看到抓取的波形图与我们所绘制的录音时序波形图是一致的。

WM8978021

图 57‑20 SignalTap抓取波形图(二)

如图 57‑20所示,抓取的信号为播放相关的信号,如图可以看到抓取的波形图与我们所绘制的播放时序波形图是一致的。

WM8978022

图 57‑21 SignalTap抓取波形图(三)

WM8978023

图 57‑22 SignalTap抓取波形图(四)

图 57‑21与图 57‑22为录音与播放时的波形图,我们抓取的播放波形图,播放的是我们录音波形图所对应的录音。如图可以看到录音时,开始抓取的音频数据为:FFD4h、FFD6h、FFC6h、FFD0h、FFD2h、FFD7;播放时,开始抓取的音频文件为:FFD4h、FFD6h、FFC6h、FFD0h 、FFD2h、FFD7。可以发现播放的音频文件正是写入SDRAM的录音文件。

7.3. 上板调试

7.3.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 57‑4所示。

表格 57‑4 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

input

E1

时钟

sys_rst_n

input

M15

复位

key_record

input

M2

录音按键

key_broadcast

input

M1

播放按键

audio_bclk

input

D12

音频位时钟

audio_lrc

input

E9

音频对齐时钟

audio_adcdat

input

C14

音频ADC数据

audio_mclk

output

D14

音频主时钟

audio_dacdat

output

D11

音频DAC数据

scl

output

P15

I2C时钟线

sda

inout

N14

I2C数据线

sdram_clk

output

R4

SDRAM芯片时钟

sdram_cke

output

R9

SDRAM时钟有效信号

sdram_cs_n

output

R12

SDRAM片选信号

sdram_cas_n

output

R10

SDRAM列地址选通脉冲

sdram_ras_n

output

R11

SDRAM行地址选通脉冲

sdram_we_n

output

L9

SDRAM写允许位

sdram_ba[0]

output

R13

SDRAM的L-Bank地址线

sdram_ba[1]

output

R14

SDRAM的L-Bank地址线

sdram_addr[0]

output

P11

SDRAM地址总线

sdram_addr[1]

output

P14

SDRAM地址总线

sdram_addr[2]

output

N9

SDRAM地址总线

sdram_addr[3]

output

N11

SDRAM地址总线

sdram_addr[4]

output

T14

SDRAM地址总线

sdram_addr[5]

output

T13

SDRAM地址总线

sdram_addr[6]

output

T12

SDRAM地址总线

sdram_addr[7]

output

T11

SDRAM地址总线

sdram_addr[8]

output

T10

SDRAM地址总线

sdram_addr[9]

output

P9

SDRAM地址总线

sdram_addr[10]

output

T15

SDRAM地址总线

sdram_addr[11]

output

N12

SDRAM地址总线

sdram_addr[12]

output

M11

SDRAM地址总线

sdram_dqm[0]

output

M10

SDRAM数据掩码

sdram_dqm[1]

output

M9

SDRAM数据掩码

sdram_dq[0]

output

R3

SDRAM数据总线

sdram_dq[1]

output

T9

SDRAM数据总线

sdram_dq[2]

output

R5

SDRAM数据总线

sdram_dq[3]

output

R6

SDRAM数据总线

sdram_dq[4]

output

R7

SDRAM数据总线

sdram_dq[5]

output

M8

SDRAM数据总线

sdram_dq[6]

output

R8

SDRAM数据总线

sdram_dq[7]

output

N8

SDRAM数据总线

sdram_dq[8]

output

P8

SDRAM数据总线

sdram_dq[9]

output

T8

SDRAM数据总线

sdram_dq[10]

output

T7

SDRAM数据总线

sdram_dq[11]

output

T6

SDRAM数据总线

sdram_dq[12]

output

T5

SDRAM数据总线

sdram_dq[13]

output

T4

SDRAM数据总线

sdram_dq[14]

output

T3

SDRAM数据总线

sdram_dq[15]

output

T2

SDRAM数据总线

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 57‑23所示。

WM8978024

图 57‑23 管脚分配

7.3.1.1. 结果验证

管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好;连接耳机、扬声器,连接好后为板卡上电,如图 57‑24所示。

WM8978025

图 57‑24 程序下载连线图

打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 57‑25所示。

WM8978026

图 57‑25 下载成功界面

下载成功后即可以开始验证了。首先我们按下录音按键KEY1,然后开始录音,当再次按下按键1时,录音结束。(这里需要注意的是录音时长最大为两分钟,当录音到两分钟时,录音会自动停止,停止后当再次按下录音按键KEY1就是重新录音了)录完音后我们按下播放按键KEY2,此时耳机和喇叭中就会播放我们刚才的录音,直 到结束。若在录音时我们按下播放按键则录音是不会播放的;若在播放时按下播放按键则录音会重头开始播放,直到结束;若在播放时按下录音按键则录音会停止播放开始新的录音。若测试满足以上现象则说明验证成功。

7.4. 章末总结

该章节除了FPGA芯片外,我们使用了SDRAM以及WM8978音频芯片,而这两块芯片在前面章节都有详细的讲解,只要大家掌握了这两块芯片的控制时序,完成本章节的实验就轻而易举了。