FPGA Verilog进阶开发教程:WM8978音频回环实验

Posted 谷哥-Mr.Gu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FPGA Verilog进阶开发教程:WM8978音频回环实验相关的知识,希望对你有一定的参考价值。

【FPGA Verilog进阶开发教程】@谷歌学术

WM8978是一个低功耗、高质量的立体声多
媒体数字信号编译码器,主要用于便携式应用,
比如数码照相机、便携式数码摄像机。WM8978
结合了立体声差分麦克风的前置放大器与扬声
器、耳机、差分或立体声线路输出的驱动器,减
少了应用时所必需的外部组件,比如不需要单独
的麦克风或者耳机的放大器。
本章将通过一个音频回环实验带领大家了解
WM8978的驱动方法

1.1 理论学习
1.1.1 声音的基本概念
声音是通过一定介质传播的连续的波。声音
信号可分为两种类型,一种是模拟信号(如自然
界的物理信号),一种是数字信号(如计算机中
的信号)。声音信号的数字化过程和波形示意图
分别如图1-1和图1-2所示。

图1-2 声音信号数字化过程中的波形示意图

声音信号数字化的三要素为采样频率、量化
位数和声道数,详情如表1-1所示。
表1-1 声音信号数字化三要素

 1.1.2 I 2 S音频总线
1. I 2 S总线概述
音频数据的采集、处理和传输是多媒体技术
的重要组成部分。众多数字音频系统已经进入消
费市场,例如数字音频录音带、数字声音处理器
等。对于设备和生产厂家来说,标准化的信息传
输结构可以提高系统的适应性。I 2 S(Inter-IC
Sound)总线又称集成电路内置音频总线,是飞利
浦公司为数字音频设备之间的音频数据传输而制
定的一种总线标准,该总线专门用于音频设备之
间的数据传输,广泛应用于各种多媒体系统。
2. I 2 S总线接口
I 2 S总线接口主要有三个,如下所示。
▪ SD(Serial Data):串行数据线,用于发送
或接收两个时分复用的数据通道上的数据,数据
的最高位总是被最先传输。
▪ SCK(Continuous Serial Clock):串行时
钟,对应数字音频的每一位数据,SCK都有一个
脉冲。SCK的频率= 2×采样频率×采样位数。
▪ WS(Word Select):字段选择线,也称帧
时钟(LRC)线,表明当前传输数据的声道,不
同的标准有不同的定义。WS线的帧率等于采样频
率(Fs)。

1.1.3 WM8978芯片
WM8978是欧胜(Wolfson)推出的一款全功
能音频处理器。它带有一个Hi-Fi级数字信号处理
内核,支持增强3D硬件环绕音效以及5频段的硬件
均衡器,可以有效改善音质。
WM8978具有高级的片上数字信号处理功
能,包含一个5路均衡功能,一个用于ADC和麦克
风或者线路输入之间的混合信号的电平自动控制
功能,一个纯粹的录音或者重放的数字限幅功
能。另外,在ADC的线路上提供了一个数字滤波
的功能,可以更好地应用滤波,比如“减少风噪
声”。
图1-3所示为WM8978芯片的内部结构示意
图,参考来自WM8978的数据手册。该图给人的
第一印象就是很复杂,有很多“开关”。实际上,
每个开关对应着WM8978内部寄存器的一位,通
过控制寄存器就可以控制开关的状态。

 图1-3 WM8978内部结构

1. 输入部分
WM8978结构图的左边是输入部分,可用于
模拟声音输入,即录音输入。有三个输入接口,
一个是由LIN和LIP、RIN和RIP组合而成的伪差分
立体声麦克风输入,一个是由L2和R2组合而成的
立体声麦克风输入,还有一个是由AUXL和AUXR
组合而成的线输入或用来传输警告声的输入。
2. 输出部分
WM8978结构图的右边是声音放大输出部
分,LOUT1和ROUT1用于耳机驱动,LOUT2和
ROUT2用于扬声器驱动,OUT3和OUT4也可以配
置成立体声线输出,OUT4也可以用于提供一个左
右声道的单声道混合。
3. ADC和DAC
WM8978结构图的中间部分是芯片的核心内
容,用于处理声音的AD和DA转换。ADC部分对
声音输入进行处理,包括ADC滤波处理、音量控
制、输入限幅器/电平自动控制等。DAC部分控制
声音输出效果,包括DAC 5路均衡器、DAC 3D放
大、DAC输出限幅以及音量控制等。
4. 通信接口
WM8978有两个通信接口,一个是数字音频
通信接口,另外一个是控制接口。音频通信接口
可选用I 2 S接口,支持左对齐、右对齐和I 2 S标准模
式,以及DSP模式A和模式B。控制接口用于控制
器发送控制命令配置WM8978运行状态,它提供2
线或3线控制接口,对于我们使用的FPGA控制
器,我们选择2线接口方式,其芯片地址固定为
0011010。通过控制接口可以访问WM8978内部寄
存器,实现芯片工作环境的配置。对于2线接口方
式,其时序图如图1-4所示。

 图1-4 2线接口时序图


该模式只有两个控制信号,我们只需控制
SDIN和SCLK即可。通过时序图可以发现该时序
就是I 2 C接口时序,所以在这里可以调用I 2 C的控制
时序来给WM8978配置寄存器。关于I 2 C的详细介
绍,可参考基础篇中的相关内容。
WM8978有两种工作模式,分别为主模式和
从模式。在主模式下,WM8978为主控设备,LCR
与BCLK由WM8978内部产生。而在从模式下,外
部设备(本实验中为FPGA)作为主控设备,同时
LRC和BCLK需要由外部设备产生。WM8978工作
模式可通过配置寄存器来选择。
WM8978有30多个接口,下面将介绍此次实
验用到的接口。音频有一个ADCDAT输出接口,
通过这个接口将ADC音频数据传给FPGA,同时它
有一个DACDAT输入接口,FPGA通过这个接口将
DAC音频数据传回给WM8978,从而达到音频回
环的目的。LRC为音频的左右声道的数据对齐时
钟信号。BCLK(位时钟)用于同步数据输入和输
出,音频数据就是在这个时钟沿下进行传输的。
另外,为使系统间更好地同步,还要传输一个主
时钟(MCLK),MCLK=256fs,fs为音频的采样
频率,一般为48kHz,所以其频率为
256×48kHz=12.288MHz。我们输入一个频率为
12MHz的时钟给WM8978即可,WM8978会通过其
内部的PLL和时钟选择电路将时钟配置成
12.288MHz频率输出。
本次实验选用的音频通信接口为I 2 S音频总线
接口,其传输时序图如图1-5所示,LRC为左右声
道的对齐时钟。当LRC为低电平时,传输左声道
的音频数据,LRC为高电平时,传输右声道的数
据。1位的音频数据的持续时间是一个BCLK位时
钟,且从高位开始传送,所以位时钟BCLK的频率
=2×采样频率×采样位数。如果将WM8978的工作
模式设置为主模式,那么BCLK和LRC就由
WM8978产生,可以直接使用;若设置为从模
式,BCLK和LRC则需要由外部设备产生。所以,
如果设置为主模式,那就能省去很多麻烦。从图
1-5中可以看到,DACDAT/ADCDAT音频数据是在
LRC变化后的第二个BCLK脉冲处产生的,且在
LRC变化后BCLK第二个上升沿开始采集数据。

5. WM8978寄存器配置
WM8978寄存器为16位长,高7位
(bit[15:9])用于表示寄存器地址,低9位
(bit[8:0])用于表示数据。所以在控制器向芯片
发送控制命令时,必须传输长度为16位的指令,
芯片会根据所接收命令的高7位值寻址。
使用WM8978的第一步就是对其内部的寄存
器进行配置,简单地说,就是对其进行功能设
置。WM8978内部有多个寄存器,本章实验我们
要实现音频回环播放功能,只需对其部分寄存器
进行设置即可。下面将对使用到的寄存器配置进
行介绍。
▪ 寄存器R0(00h)
该寄存器用于控制WM8978的软复位,通过
写任意值到R0寄存器即可实现该功能。
▪ 寄存器R1(01h)
该寄存器需要设置bit[1:0]为2’b11,让其以最
快的速度启动;禁用BUFIOEN(bit[2] = 0)或
BUFDCOPEN(bit[8] = 0)时,可能会导致爆音,
所以需要将bit[2]和bit[8]设置为1;同时需要启动
BIASEN(bit[3] = 1),否则模拟放大器不会工
作;WM8978具有片上锁相环(PLL)电路,可用
于从另一个外部时钟为WM8978的音频功能生成
主时钟。PLL可以由该寄存器的bit5设置启用或禁
用。这里需要启用(bit[5] = 1)来配置内部MCLK
时钟。
由上所述,设置R1为8’b1_0010_1111。
▪ 寄存器R2(02h)
该寄存器用于设置ADCENL(bit0)、
ADCENR(bit1)为1,启用左右声道的ADC功
能;设置BOOSTENR(bit5)、
BOOSTENL(bit4)为1,启用左右声道BOOST;
设置LOUT1EN(bit7)、ROUT1EN(bit8)为1,
启用耳机输出。
由上所述,设置R2为8’b1_1011_0011。
▪ 寄存器R3(03h)
该寄存器用于设置DACENL(bit0)、
DACENR(bit1)为1,启用左右声道的DAC功
能,使数字音频信号转换为模拟音频信号;设置
LMIXEN(bit2)、RMIXEN(bit3)为1,启用左
右输出通道混合器;设置ROUT2EN(bit5)、
LOUT2EN(bit6)为1,启用左右扬声器。
由上所述,设置R3为8’b0_0110_1111。
▪ 寄存器R4(04h)
该寄存器用于设置FMT(bit[4:3])为2’b10,
音频数据模式选择为I 2 S模式,其中2’b00为右对齐
模式、2’b01为左对齐模式、2’b11为DSP/PCM模
式,这里选择I 2 S音频接口(其他模式的时序图可
查看数据手册了解)。设置WL(bit[6:5]),WL
为字长,即为表1-1中的量化位数,2’b00为16位,
2’b01为20位,2’b10为24位,2’b11为32位,这里
我们选择24位即可,也就是2’b10。
由上所述,设置R4为8’b0_0101_0000。
▪ 寄存器R6(06h)
该寄存器可设置WM8978的工作模式,前面
提及WM8978有两种工作模式,此处我们将其设
置为主模式,左右对齐时钟(LRC)和位时钟
(BCLK)由WM8978产生,直接使用即可,这样
可以节省资源。该寄存器的bit0为0时为从模式,
为1时为主模式,所以这里设置bit0为1即可。
由上所述,设置R6为8’b0_0000_0001。
▪ 寄存器R10(0Ah)
该寄存器用于设置DAC的过采样率,通过将
该寄存器的DACOSR128(bit3)设置为1可得到最
好的信噪比。
由上所述,设置R10为8’b0_0000_1000。
▪ 寄存器R14(0Eh)
该寄存器用于设置ADC的过采样率,同样
地,设置该寄存器ADCOSR128(bit3)为1可得到
最好的信噪比。同时设置HPFEN(bit8)为1,启
用高通滤波器,可去掉信号中的低频干扰。
由上所述,设置R14为8’b1_0000_1000。
▪ 寄存器R43(2Bh)
该寄存器需要设置INVROUT2(bit4)为1,
反转ROUT2输出,使扬声器输出的音效更好。
由上所述,设置R43为8’b0_0001_0000。
▪ 寄存器R47(2Fh)
该寄存器可用于设置左声道输入增益,可由
该寄存器的bit[6:4]控制,在此我们将其设置为
2’b111,获得+ 6dB增益。
由上所述,设置R47为8’b0_0111_0000。
▪ 寄存器R48(30h)
该寄存器可用于设置右声道输入增益,同R47
一样,将bit[6:4]设置为2’b111即可,获得+ 6dB增
益。
由上所述,设置R48为8’b0_0111_0000。
▪ 寄存器R49(31h)
该寄存器的SPKBOOST(bit2)可用于设置扬
声器增益,我们将其设置为1即可。WM8978有过
热保护功能,通过将该寄存器的TSDEN(bit1)设
置为1,可开启过热保护。
由上所述,设置R49为8’b0_0000_0110。
▪ 寄存器R50(32h)
该寄存器需要设置DACL2LMIX(bit0)为
1,将左DAC输出到左输出混合器。
由上所述,设置R50为8’b0_0000_0001。
▪ 寄存器R51(33h)
该寄存器需要设置DACR2RMIX(bit0)为
1,将右DAC输出到右输出混合器。
由上所述,设置R51为8’b0_0000_0001。
▪ 寄存器R52(34h)
该寄存器可用于设置耳机左声道的音量,通
过对LOUT1VOL(bit[5:0])的设置,从6’b000000
到6’b111111依次增大。将bit7、bit8都设置为1,可
用于更新增益和音量。
▪ 寄存器R53(35h)
该寄存器用于设置耳机右声道的音量,同上
面寄存器R52的设置一致即可。
▪ 寄存器R54(36h)
该寄存器用于设置左扬声器的音量,同上面
寄存器R52的设置方法一致。
▪ 寄存器R55(37h)
该寄存器用于设置右扬声器的音量,同上面
寄存器R54的设置一致即可。
以上为该实验的参考寄存器设置,大家也可
尝试更改部分配置,只要能实现实验功能即可。

1.2 实战演练
1.2.1 实验目标
将手机或计算机中的音乐通过WM8978传输
到FPGA内,然后再将音频数据从FPGA传回
WM8978中播放出来。
1.2.2 硬件资源
本次实验需要使用开发板上的WM8978音频
相关接口,如图1-6所示。

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

本次实验需要使用音频输入、耳机以及喇叭
接口。音频输入接口连接播放器,用于音频输
入,耳机以及喇叭接口用来连接耳机以及喇叭,
用于音频播放。
1.2.3 程序设计
硬件资源介绍完毕,我们开始实验工程的程
序设计。在本小节,我们采用先整体概括,再局
部说明的方式对实验工程的各个模块进行讲解,
详细内容如下。
1. 整体说明
根据理论部分的介绍,我们知道要想使用
WM8978,首先需要对其内部寄存器进行配置。
通过前面的接口时序讲解可知,我们可使用I 2 C接
口对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-3所示,inclk0输入系统时钟即可;而
复位信号由于是异步复位,所以需要将其取反后
才能接入系统复位;c0为输出时钟,locked为时钟
稳定标志信号,当输出的c0时钟稳定后locked将拉
高。这是调用的IP核,最后将其实例化在顶层模
块即可。
表1-3 clk_gen模块输入输出信号描述

3. I 2 C驱动模块
关于I 2 C驱动模块i2c_ctrl,基础篇第32章中已
经详细地讲解了驱动方法,在此就不再?述,直
接调用这个模块即可。
4. I 2 C配置模块
(1)模块框图
如图1-12所示为I 2 C配置模块i2c_reg_cfg的框
图,可将寄存器的配置通过I 2 C驱动模块写入
WM8978,从而完成对WM8978的配置。

此模块是产生让I 2 C驱动模块写入WM8978的
配置,其各个信号说明如表1-4所示。
表1-4 I 2 C配置模块输入输出信号描述

i2c_end和i2c_clk由i2c_ctrl模块输入,而复位
信号由顶层模块输入。cfg_start由该模块产生,用
于控制I 2 C驱动模块配置单个寄存器;cfg_data为配
置寄存器的数据,前面说过WM8978寄存器是16
位长,高7位([15:9]bit)用于表示寄存器地址,
低9位用于表示数据。cfg_done为寄存器配置完成
信号。
(2)波形图绘制
下面通过波形图来详细讲解该模块的时序,
如图1-13所示。

 ▪ i2c_clk、sys_rst_n:模块的时钟信号和复位
信号。
▪ cfg_end:单个寄存器配置完成信号,从I 2 C
驱动模块输入。从I 2 C驱动模块可知,只要产生开
始信号和写入寄存器的数据,I 2 C驱动模块即可将
寄存器数据写入WM8978中。
▪ cnt_wait:寄存器配置上电等待计数器。在
这里我们上电等待1ms后开始配置寄存器,由于系
统时钟为I 2 C驱动模块传来的时钟,频率为
1MHz,单位时钟为1μs,所以计数到999即为
1ms,需要将计数器计到1000时停止计数,当计数
器的值小于计数器最大值
CNT_WAIT_MAX(1000)时,每来一个时钟让
其自加1。
▪ cfg_start:单个寄存器配置触发信号,上电
后延迟一段时间,等待WM8978工作在稳定状态
后产生第一个开始信号,所以需要一个产生延迟
信号的计数器。计数到1ms(999)时产生第一个
开始配置信号。
▪ cfg_end:单个寄存器配置完成信号,由I 2 C
驱动模块传来。单个寄存器配置完成后,会产生
一个结束信号。每当检测到结束信号后,紧接着
发送下一个开始信号,直到所有寄存器配置完
成。然而我们如何判断所有寄存器配置完成了
呢?在这里可以产生一个寄存器个数计数器来进
行判断。
▪ 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 m?dule i2c_reg_cfg
2 (
3 i??u? wire i2c_clk ,
// 系统时钟,由I 2 C模块传入
4 i??u? wire sys_rs?_? ,
// 系统复位,低有效
5 i??u? wire cfg_e?d ,
// 单个寄存器配置完成
6
7 ?u??u? reg cfg_s?ar? ,
// 单个寄存器配置触发信号
8 ?u??u? wire [15:0] cfg_da?a ,
// 寄存器地址7位+数据9位
9 ?u??u? reg cfg_d??e
// 寄存器配置完成
10 );
11
12 //
************************************************
******************** //
13 // ****************** Parame?er a?d I??er?al
Sig?al ******************* //
14 //
************************************************
******************** //
15
16 // ?arame?er defi?e
17 ?arame?er RE?_NUM = 6'd18 ;
// 总共需要配置的寄存器个数
18 ?arame?er CNT_WAIT_MAX = 10'd1000 ;
// 上电等待1ms后开始配置寄存器
19
20 ?arame?er LOUT1VOL = 6'd40 ;
// 耳机左声道音量设置(0~63)
21 ?arame?er ROUT1VOL = 6'd40 ;
// 耳机右声道音量设置(0~63)
22
23 ?arame?er SPK_LOUT2VOL = 6'd45 ;
// 扬声器左声道音量设置(0~63)
24 ?arame?er SPK_ROUT2VOL = 6'd45 ;
// 扬声器右声道音量设置(0~63)
25
26 // wire defi?e
27 wire [15:0] cfg_da?a_reg[RE?_NUM-1:0];
// 寄存器配置数据暂存
28
29 // reg defi?e
30 reg [9:0] c??_wai? ;
// 寄存器配置上电等待计数器
31 reg [5:0] reg_?um ;
// 配置寄存器个数
32
33 //
************************************************
******************** //
34 // ***************************** Mai? C?de
**************************** //
35 //
************************************************
******************** //
36
37 // c??_wai?:寄存器配置等待计数器
38 always@(??sedge i2c_clk ?r ?egedge sys_rs?_?)
39 if(sys_rs?_? == 1'b0)
40 c??_wai? <= 10'd0;
41 else if(c??_wai? < CNT_WAIT_MAX)
42 c??_wai? <= c??_wai? + 1'b1;
43
44 // reg_?um:配置寄存器个数
45 always@(??sedge i2c_clk ?r ?egedge sys_rs?_?)
46 if(sys_rs?_? == 1'b0)
47 reg_?um <= 6'd0;
48 else if(cfg_e?d == 1'b1)
49 reg_?um <= reg_?um + 1'b1;
50
51 // cfg_s?ar?:单个寄存器配置触发信号
52 always@(??sedge i2c_clk ?r ?egedge sys_rs?_?)
53 if(sys_rs?_? == 1'b0)
54 cfg_s?ar? <= 1'b0;
55 else if(c??_wai? == (CNT_WAIT_MAX -
1'b1))
56 cfg_s?ar? <= 1'b1;
57 else if((cfg_e?d == 1'b1) && (reg_?um
< (RE?_NUM-1)))
58 cfg_s?ar? <= 1'b1;
59 else
60 cfg_s?ar? <= 1'b0;
61
62 // cfg_d??e:寄存器配置完成信号
63 always@(??sedge i2c_clk ?r ?egedge sys_rs?_?)
64 if(sys_rs?_? == 1'b0)
65 cfg_d??e <= 1'b0;
66 else if((reg_?um == RE?_NUM - 1'b1) &&
(cfg_e?d == 1'b1))
67 cfg_d??e <= 1'b1;
68
69 // cfg_da?a:7位地址+ 9位数据
70 assig? cfg_da?a = (cfg_d??e == 1'b1) ? 16'b0
: cfg_da?a_reg[reg_?um];
71
72 // ------------------------------------------
----------
73 // cfg_da?a_reg:寄存器配置数据暂存
74 // 各寄存器功能配置详见文档介绍
75 assig? cfg_da?a_reg[00] = 7'd0 ,
9'b0 ;
76 assig? cfg_da?a_reg[01] = 7'd1 ,
9'b1_0010_1111 ;
77 assig? cfg_da?a_reg[02] = 7'd2 ,
9'b1_1011_0011 ;
78 assig? cfg_da?a_reg[03] = 7'd4 ,
9'b0_0101_0000 ;
79 assig? cfg_da?a_reg[04] = 7'd6 ,
9'b0_0000_0001 ;
80 assig? cfg_da?a_reg[05] = 7'd10,
9'b0_0000_1000 ;
81 assig? cfg_da?a_reg[06] = 7'd14,
9'b1_0000_1000 ;
82 assig? cfg_da?a_reg[07] = 7'd43,
9'b0_0001_0000 ;
83 assig? cfg_da?a_reg[08] = 7'd47,
9'b0_0111_0000 ;
84 assig? cfg_da?a_reg[09] = 7'd48,
9'b0_0111_0000 ;
85 assig? cfg_da?a_reg[10] = 7'd49,
9'b0_0000_0110 ;
86 assig? cfg_da?a_reg[11] = 7'd50,
9'b0_0000_0001 ;
87 assig? cfg_da?a_reg[12] = 7'd51,
9'b0_0000_0001 ;
88 assig? cfg_da?a_reg[13] = 7'd52,
3'b110 , LOUT1VOL ;
89 assig? cfg_da?a_reg[14] = 7'd53,
3'b110 , ROUT1VOL ;
90 assig? cfg_da?a_reg[15] = 7'd54,
3'b110 , SPK_LOUT2VOL ;
91 assig? cfg_da?a_reg[16] = 7'd55,
3'b110 , SPK_ROUT2VOL ;
92 // 更新完耳机和扬声器的音量后再开启音频的输出使能,防
止出现“嘎哒”声
93 assig? cfg_da?a_reg[17] = 7'd3 ,
9'b0_0110_1111 ;
94 // ------------------------------------------
-------------
95
96 e?dm?dule
模块参考代码是参照绘制波形图进行编写
的,在波形图绘制部分已经对模块各信号进行了
详细说明,需要注意的是第91行,我们配置完需
要的所有寄存器后再开启耳机和扬声器的输出使
能,若是先开启耳机和扬声器的启动信号再更新
音量,音量变化时可能会出现“嘎哒”声,所以最
后再配置R3寄存器,以防止出现这种声音。
5. 寄存器配置模块
(1)模块框图
该模块就是对I 2 C驱动模块以及I 2 C配置进行实
例化,从而得到一个完整的寄存器配置模块,其
模块框图如图1-14所示。

 由图1-14可看到,配置寄存器的数据
(cfg_data)分别传输给了wr_data(输入数据)和
byte_addr(输入地址)。由于I 2 C数据发送一次会
发送8位,而WM8978寄存器的地址只有7位,所
以需要将数据的最高位整合到地址变量的第0位先
发送,接下来再发送cfg_data的低8位。由于本次
实验只有写入而没有读取,所以我们将I 2 C驱动模
块的写使能(wr_en)置为1,读使能(rd_en)置
为0即可。同时由于我们的地址位只有7位,所以
将输入的I 2 C字节地址字节数(addr_num)置为
0,表示写入1个字节的地址。i2c_ctrl模块各输入
输出信号的具体描述可参考基础篇第32章。
(2)代码编写
根据以上介绍,我们基本已经知道了各个子
模块输入输出信号的连接方式,下面即可编写实
例化代码,具体参见代码清单1-2。
代码清单1-2 寄存器配置模块参考代码
(wm8978_cfg.v)
1 m?dule wm8978_cfg
2 (
3 i??u? wire sys_clk ,
// 系统时钟,频率为50MHz
4 i??u? wire sys_rs?_? ,
// 系统复位,低电平有效
5
6 ?u??u? wire i2c_scl ,
// 输出至WM8978的串行时钟信号scl
7 i??u? wire i2c_sda
// 输出至WM8978的串行数据信号sda
8
9 );
10
11 //
************************************************
******************** //
12 // ****************** Parame?er a?d I??er?al
Sig?al ******************* //
13 //
************************************************
******************** //
14
15 // wire defi?e
16 wire cfg_s?ar? ;
// 输入I 2 C触发信号
17 wire [15:0] cfg_da?a ;
// 寄存器地址7位+数据9位
18 wire i2c_clk ;
// I 2 C驱动时钟
19 wire i2c_e?d ;
// I 2 C一次读/写操作完成
20 wire cfg_d??e ;
// 寄存器配置完成信号
21
22 //
************************************************
******************** //
23 // ***************************** Mai? C?de
**************************** //
24 //
************************************************
******************** //
25
26 // ------------------------ i2c_c?rl_i?s? ---
--------------------
27 i2c_c?rl
28 #(
29 . DEVICE_ADDR (7'b0011_010 ) ,
// I 2 C设备地址
30 . SYS_CLK_?REQ (26'd50_000_000) ,
// 输入系统时钟频率
31 . SCL_?REQ (18'd250_000 )
// I 2 C设备scl时钟频率
32 )
33 i2c_c?rl_i?s?
34 (
35 .sys_clk (sys_clk ),
// 输入系统时钟,频率为50MHz
36 .sys_rs?_? (sys_rs?_? ),
// 输入复位信号,低电平有效
37 .wr_e? (1'b1 ),
// 输入写使能信号
38 .rd_e? (1'b0 ),
// 输入读使能信号
39 .i2c_s?ar? (cfg_s?ar? ),
// 输入I 2 C触发信号
40 .addr_?um (1'b0 ),
// 输入I 2 C字节地址字节数
41 .by?e_addr (cfg_da?a[15:8]),
// 输入I 2 C字节地址+数据最高位
42 .wr_da?a (cfg_da?a[7:0] ),
// 输入I 2 C设备数据低8位
43
44 .rd_da?a ( ),
// 输出I 2 C设备读取数据
45 .i2c_e?d (i2c_e?d ),
// I 2 C一次读/写操作完成
46 .i2c_clk (i2c_clk ),
// I 2 C驱动时钟
47 .i2c_scl (i2c_scl ),
// 输出至I 2 C设备的串行时钟信号scl
48 .i2c_sda (i2c_sda )
// 输出至I 2 C设备的串行数据信号sda
49 );
50
51 // ---------------------- i2c_reg_cfg_i?s? --
-------------------
52 i2c_reg_cfg i2c_reg_cfg_i?s?
53 (
54 .i2c_clk (i2c_clk ),
// 系统时钟,由I 2 C模块传入
55 .sys_rs?_? (sys_rs?_? ),
// 系统复位,低电平有效
56 .cfg_e?d (i2c_e?d ),
// 单个寄存器配置完成
57
58 .cfg_s?ar? (cfg_s?ar? ),
// 单个寄存器配置触发信号
59 .cfg_da?a (cfg_da?a ),
// 寄存器的地址和数据
60 .cfg_d??e (cfg_d??e )
// 寄存器配置完成信号
61 );
62
63 e?dm?dule
关于模块的实例化,在模块框图部分已经做
了讲解,这里需要注意的是代码第29行,我们将
设备地址改为WM8978的设备地址,在1.1.3节已
经讲到WM8978芯片的固定地址为0011010,此处
将设备地址直接改为0011010即可。
(3)仿真代码编写
寄存器模块已经完成,我们现在可以编写仿
真文件代码,通过仿真去看配置寄存器的代码是
否和绘制的波形图一致。仿真参考代码详见代码
清单1-3。
代码清单1-3 WM8978寄存器配置仿真参考代码
(tb_wm8978_cfg.v)
1 m?dule ?b_wm8978_cfg();
2
3 //
************************************************
******************** //
4 // ****************** Parame?er a?d I??er?al
Sig?al ******************* //
5 //
************************************************
******************** //
6
7 // wire defi?e
8 wire i2c_scl ;
9 wire i2c_sda ;
10
11 // reg defi?e
12 reg sys_clk ;
13 reg sys_rs?_? ;
14
15 //
************************************************
******************** //
16 // ***************************** Mai? C?de
**************************** //
17 //
************************************************
******************** //
18
19 // 对sys_clk,sys_rs?_?赋初始值
20 i?i?ial
21 begi?
22 sys_clk = 1'b0;
23 sys_rs?_? <= 1'b0;
24 #100
25 sys_rs?_? <= 1'b1;
26 e?d
27
28 // clk:产生时钟
29 always #10 sys_clk = ~sys_clk;
30
31 //
************************************************
******************** //
32 // *************************** I?s?a??ia?i??
************************** //
33 //
************************************************
******************** //
34
35 // ------------- wm89078_cfg_i?s? -----------
--
36 wm8978_cfg wm8978_cfg_i?s?
37 (
38 .sys_clk (sys_clk ), // 系统时钟,
频率为50MHz
39 .sys_rs?_? (sys_rs?_? ), // 系统复位,
低电平有效
40
41 .i2c_scl (i2c_scl ), // 输出至
WM8978的串行时钟信号scl
42 .i2c_sda (i2c_sda ) // 输出至
WM8978的串行数据信号sda
43
44 );
45
46 e?dm?dule
该模块仿真文件只要生成时钟复位即可。
I 2 C配置模块的ACK应答信号是由我们控制的
设备产生的,并不是由FPGA产生的,为了使仿真
能进行下去,在仿真时需要我们给应答信号,当
仿真完成再改回去即可,如图1-15中的 框所示。

我们只需要将I 2 C驱动模块代码中的ack置0应
答,当仿真完成后改回接收设备应答即可。
(4)仿真波形分析
配置好仿真文件,使用ModelSim对参考代码
进行仿真,仿真结果如图1-16所示。

由于I 2 C驱动模块在之前已经过验证,是无误
的,所以在这里可以不需要观看其波形图。我们
只需要观看编写的I 2 C配置模块是否正确即可,如
图1-16所示,可以看到波形图整体时序和我们绘
制的波形图时序是一致的,这说明我们编写的代
码总体是正确的,下面通过局部仿真波形图观看
是否正确。
图1-17所示为I 2 C配置模块开始配置时的仿真
波形图,图1-18所示为I 2 C配置模块结束配置时的
仿真波形图。可以发现这两个仿真波形图与我们
绘制的波形图都是一致的,这说明我们编写的代
码与绘制的波形图功能是一致的。

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

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

 由于我们接收的是WM8978输出的数据,而
WM8978是根据图1-5进行输出的,所以,无论是
接收数据还是发送数据,都需要遵从这个时序。
根据音频时序接口图,可以先画出接收音频数据
的时序波形图。
(2)波形图绘制
根据I 2 S音频接口时序图可以知道WM8978输
出的ADCDAT音频数据是在位时钟(BCLK)的下
降沿变化的,所以我们需要用位时钟的上升沿去
采集数据。而数据是在对齐时钟(LRC)电平变
化后的第二个位时钟脉冲处开始发送的,所以我
们需要在对齐时钟电平变化后位时钟第二个上升
沿处开始采集第一个数据。那如何设计才能达到
这个目的呢?如图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 m?dule audi?_rc?
2 (
3 i??u? wire audi?_bclk , //
WM8978输出的位时钟
4 i??u? wire sys_rs?_? , //
系统复位,低电平有效
5 i??u? wire audi?_lrc , //
WM8978输出的数据左/右对齐时钟
6 i??u? wire audi?_adcda? , //
WM8978 ADC数据输出
7
8 ?u??u? reg [23:0] adc_da?a , //
一次接收的数据
9 ?u??u? reg rc?_d??e //
一次数据接收完成
10
11 );
12
13 //
************************************************
******************** //
14 // ****************** Parame?er a?d I??er?al
Sig?al ******************* //
15 //
************************************************
******************** //
16
17 // reg defi?e
18 reg audi?_lrc_d1; //
对齐时钟延迟一拍信号
19 reg [4:0] adcda?_c?? ; //
WM8978ADC数据输出位数计数器
20 reg [23:0] da?a_reg ; //
adc_da?a数据寄存器
21
22 // wire defi?e
23 wire lrc_edge ; //
对齐时钟信号沿标志信号
24
25 //
************************************************
******************** //
26 // ***************************** Mai? C?de
**************************** //
27 //
************************************************
******************** //
28
29 // 使用异或运算符产生信号沿标志信号
30 assig? lrc_edge = audi?_lrc ^
audi?_lrc_d1;
31
32 // 对audi?_lrc信号延迟一拍以方便获得信号沿标志信号
33 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
34 if(sys_rs?_? == 1'b0)
35 audi?_lrc_d1 <= 1'b0;
36 else
37 audi?_lrc_d1 <= audi?_lrc;
38
39 // adcda?_c??:当信号沿标志信号为高电平时,计数器清

40 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
41 if(sys_rs?_? == 1'b0)
42 adcda?_c?? <= 5'b0;
43 else if(lrc_edge == 1'b1)
44 adcda?_c?? <= 5'b0;
45 else if(adcda?_c?? < 5'd26)
46 adcda?_c?? <= adcda?_c?? + 1'b1;
47 else
48 adcda?_c?? <= adcda?_c??;
49
50 // 将WM8978输出的ADC数据寄存在da?a_reg中,一次寄存
24位
51 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
52 if(sys_rs?_? == 1'b0)
53 da?a_reg <= 24'b0;
54 else if(adcda?_c?? <= 5'd23)
55 da?a_reg[23-adcda?_c??] <=
audi?_adcda?;
56 else
57 da?a_reg <= da?a_reg;
58
59 // 当最后一位数据传完之后,读出寄存器的值给adc_da?a
60 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
61 if(sys_rs?_? == 1'b0)
62 adc_da?a <= 24'b0;
63 else if(adcda?_c?? == 5'd24)
64 adc_da?a <= da?a_reg;
65 else
66 adc_da?a <= adc_da?a;
67
68 // 当最后一位数据传完之后,输出一个时钟的完成标志信号
69 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
70 if(sys_rs?_? == 1'b0)
71 rc?_d??e <= 1'b0;
72 else if(adcda?_c?? == 5'd24)
73 rc?_d??e <= 1'b1;
74 else
75 rc?_d??e <= 1'b0;
76
77 e?dm?dule
模块参考代码是参照模块波形图进行编写
的,在波形图绘制部分已经对模块中的各信号进
行了详细的说明,在此不再赘述。

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

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

音频发送模块的时序与接收模块有非常多的
相似之处,用的都是WM8978输出的位时钟,传
输的时序要与WM8978的I 2 S音频接口时序相对
应,这样才能使WM8978接收到正确的数据。不
同的是接收模块是将各个位的数据接收后拼接成
一个完整的音频数据,发送模块是将拼接完成的
音频数据逐位发送回WM8978。
下面通过波形图详细讲解各信号的时序关系
图。
(2)波形图绘制
音频发送模块的波形图如图1-22所示。

 发送模块的时序与接收时序的思路大致相
同,这里主要介绍它们的不同之处。
首先是数据的发送,由WM8978的I 2 S音频接
口时序可知,音频是在位时钟下降沿发送的,只
不过我们接收时是使用位时钟上升沿接收的,所
以我们发送回去的音频数据也需要使用位时钟的
下降沿进行发送,如图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 m?dule audi?_se?d
2 (
3 i??u? wire audi?_bclk ,
// WM8978输出的位时钟
4 i??u? wire sys_rs?_? ,
// 系统复位,低电平有效
5 i??u? wire audi?_lrc ,
// WM8978输出数据左/右对齐时钟
6 i??u? wire [23:0] dac_da?a ,
// 往WM8978发送的数据
7
8 ?u??u? reg audi?_dacda? ,
// 发送DACDAT数据给WM8978
9 ?u??u? reg se?d_d??e
// 一次数据发送完成
10
11 );
12
13 //
************************************************
******************** //
14 // ****************** Parame?er a?d I??er?al
Sig?al ******************* //
15 //
************************************************
******************** //
16
17 // reg defi?e
18 reg audi?_lrc_d1;
// 对齐时钟信号延迟一拍
19 reg [4:0] dacda?_c?? ;
// DACDAT数据发送位数计数器
20 reg [23:0] da?a_reg ;
// dac_da?a数据寄存器
21
22 // wire defi?e
23 wire lrc_edge ;
// 对齐时钟信号沿标志信号
24
25 //
************************************************
******************** //
26 // ***************************** Mai? C?de
**************************** //
27 //
************************************************
******************** //
28
29 // 使用异或运算符产生信号沿标志信号
30 assig? lrc_edge = audi?_lrc ^ audi?_lrc_d1;
31
32 // 对audi?_lcr信号延迟一拍以便获得信号沿标志信号
33 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
34 if(sys_rs?_? == 1'b0)
35 audi?_lrc_d1 <= 1'b0;
36 else
37 audi?_lrc_d1 <= audi?_lrc;
38
39 // dacda?_c??:当信号沿标志信号为高电平时,计数器清

40 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
41 if(sys_rs?_? == 1'b0)
42 dacda?_c?? <= 5'b0;
43 else if(lrc_edge == 1'b1)
44 dacda?_c?? <= 5'b0;
45 else if(dacda?_c?? < 5'd26)
46 dacda?_c?? <= dacda?_c?? + 1'b1;
47 else
48 dacda?_c?? <= dacda?_c??;
49
50 // 将要发送的dac_da?a数据寄存在da?a_reg中
51 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
52 if(sys_rs?_? == 1'b0)
53 da?a_reg <= 24'b0;
54 else if(lrc_edge == 1'b1)
55 da?a_reg <= dac_da?a;
56 else
57 da?a_reg <= da?a_reg;
58
59 // 下降沿到来时,将da?a_reg的数据逐位传递给
audi?_dacda?
60 always@(?egedge audi?_bclk ?r ?egedge
sys_rs?_?)
61 if(sys_rs?_? == 1'b0)
62 audi?_dacda? <= 1'b0;
63 else if(dacda?_c?? <= 5'd23)
64 audi?_dacda? <= da?a_reg[23 -
dacda?_c??];
65 else
66 audi?_dacda? <= audi?_dacda?;
67
68 // 当最后一位数据传完之后,输出一个时钟的发送完成标志
信号
69 always@(??sedge audi?_bclk ?r ?egedge
sys_rs?_?)
70 if(sys_rs?_? == 1'b0)
71 se?d_d??e <= 1'b0;
72 else if(dacda?_c?? == 5'd24)
73 se?d_d??e <= 1'b1;
74 else
75 se?d_d??e <= 1'b0;
76
77 e?dm?dule
8. 顶层模块
(1)模块框图
音频回环顶层模块的框图如图1-23所示。

该模块的输入输出信号描述如表1-7所示。
表1-7 音频回环顶层模块输入输出信号描述

 audio_lookback顶层模块主要是对各个子功能
模块进行实例化,以及连接对应信号,对应信号
的连接可根据系统整体框图进行,信号代码较容
易编写,无须绘制波形图。
(2)代码编写
顶层模块参考代码具体见代码清单1-6。
代码清单1-6 顶层模块参考代码
(audio_lookback.v)
1 m?dule audi?_l???back
2 (
3 i??u? wire sys_clk , // 系
统时钟,频率为50MHz
4 i??u? wire sys_rs?_? , // 系
统复位,低电平有效
5 i??u? wire audi?_bclk , //
WM8978输出的位时钟
6 i??u? wire audi?_lrc , //
WM8978输出的数据左/右对齐时钟
7 i??u? wire audi?_adcda? , //
WM8978ADC数据输出
8
9 ?u??u? wire scl , // 输
出至WM8978的串行时钟信号scl
10 ?u??u? wire audi?_mclk , // 输
出WM8978主时钟,频率为12MHz
11 ?u??u? wire audi?_dacda? , // 输
出DAC数据给WM8978
12
13 i??u? wire sda // 输
出至WM8978的串行数据信号sda
14
15 );
16
17 //
************************************************
******************** //
18 // ******************** Parame?er A?d
I??er?al Sig?al ***************** //
19 //
************************************************
******************** //
20
21 // wire defi?e
22 wire [23:0] adc_da?a ; // 接收的音频数

23
24 //
************************************************
******************** //
25 // *************************** I?s?a??ia?i??
************************** //
26 //
************************************************
******************** //
27
28 // ------------------- clk_ge?_i?s? ---------
-------------
29 clk_ge? clk_ge?_i?s?
30 (
31 .arese? (~sys_rs?_? ), // 异
步复位,取反连接
32 .i?clk0 (sys_clk ), // 输
入时钟(50MHz)
33
34 .c0 (audi?_mclk ), // 输
出时钟(12MHz)
35 .l?cked () // 输
出稳定时钟标志信号
36
37 );
38
39 // ------------------- audi?_rc?_i?s? -------
------------
40 audi?_rc? audi?_rc?_i?s?
41 (
42 .audi?_bclk (audi?_bclk ), //
WM8978输出的位时钟
43 .sys_rs?_? (sys_rs?_? ), // 系
统复位,低电平有效
44 .audi?_lrc (audi?_lrc ), //
WM8978输出的数据左/右对齐时钟
45 .audi?_adcda? (audi?_adcda?), //
WM8978 ADC数据输出
46
47 .adc_da?a (adc_da?a ), // 一
次接收的数据
48 .rc?_d??e () // 一
次数据接收完成
49
50 );
51
52 // ------------------ audi?_se?d_i?s? -------
-----------
53 audi?_se?d audi?_se?d_i?s?
54 (
55 .audi?_bclk (audi?_bclk ), //
WM8978输出的位时钟
56 .sys_rs?_? (sys_rs?_? ), // 系
统复位,低电平有效
57 .audi?_lrc (audi?_lrc ), //
WM8978输出数据左/右对齐时钟
58 .dac_da?a (adc_da?a ), // 往
WM8978发送的数据
59
60 .audi?_dacda? (audi?_dacda?), // 发
送DAC数据给WM8978
61 .se?d_d??e () // 一
次数据发送完成
62
63 );
64
65 // ----------------- wm8978_cfg_i?s? --------
------------
66 wm8978_cfg wm8978_cfg_i?s?
67 (
68 .sys_clk (sys_clk ), // 系统时钟,频
率为50MHz
69 .sys_rs?_? (sys_rs?_? ), // 系统复位,低
电平有效
70
71 .i2c_scl (scl ), // 输出至
WM8978的串行时钟信号scl
72 .i2c_sda (sda ) // 输出至
WM8978的串行数据信号sda
73
74 );
75
76 e?dm?dule
在实例化时,有些预留的信号接口如果没有
用到,将其悬空即可。
9. RTL视图
使用Quartus II软件对工程进行编译,编译通
过后,查看RTL视图,如图1-24和图1-25所示,实
验工程的RTL视图与实验整体框图相同,各信号
线均已正确连接。

10. SignalTap波形抓取
由于音频接收模块和音频发送模块的位时
钟、对齐时钟都是由WM8978输出得来的,由仿
真文件产生这两个时钟比较麻烦,因此我们利用
Quartus软件自带的SignalTap工具对波形进行实时
抓取,以验证我们的设计是否正确。
如图1-26所示,抓取的为接收模块信号的波
形图,可以看到该图与我们绘制的波形图是一致
的。 

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

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

图1-29中箭头所指对应的是FPGA接收和发送
的数据,可以很清楚地看到其发送的音频数据就
是WM8978传过来的音频数据,这说明我们接收
和发送的模块可满足设计要求,能实现音频回环
的功能。
11. 上板验证
仿真验证通过后,绑定引脚,对工程进行重
新编译。如图1-30所示,将开发板连接至12V直流
电源和USB-Blaster下载器的JTAG端口,连接耳
机、音频线(如图1-31所示)、扬声器,线路正
确连接后,打开开关为板卡上电,随后为开发板

 

程序下载成功后即可以开始验证,我们从播
放器(计算机或者手机即可)中播放一曲音乐,
若耳机和喇叭上能听到所播放的音乐,则说明验
证成功。
1.3 章末总结
到这里,本章讲解完毕,本实验的关键是对
寄存器进行配置,只有寄存器配置正确了,
WM8978才能正常工作。只要大家熟练掌握了
WM8978内部的时序,设计、编写代码就会较为
轻松。 

FPGA教程案例29基于FPGA的DDS直接数字频率合成器之二——Verilog开发

FPGA教程目录

MATLAB教程目录

---------------------------------------------------------------------------------------

1.软件版本

vivado2019.2

2.DDS的verilog开发

        在上一课程,我们基本了解了DDS的基本实现原理以及相关技术指标的计算方法。在本课程中,将介绍如何通过Verilog实现DDS。根据上一章的原理介绍可知,DDS系统包括如下几个模块:频率控制字输入模块,相位累加器模块,ROM存储器模块。在本课题中,为了方便入门学习掌握,我们只考虑频率控制字的输入,不考虑相位控制字。下面,我们学习DDS各个模块的FPGA实现过程。

2.1ROM存储器模块

首先,通过MATLAB产生sin数据,用于存放在ROM中,运行如下的matlab程序:

clc;
clear;
close all;
warning off;


LL   = 2^20;
t    = 1/LL:1/LL:1;


y  = sin(2*pi*t); 
y  = round(2^14*y);

figure;
plot(y);


fid &

以上是关于FPGA Verilog进阶开发教程:WM8978音频回环实验的主要内容,如果未能解决你的问题,请参考以下文章

FPGA教程案例61硬件开发板调试1——初识FPGA开发板,通过verilog控制LED灯,按键开关等

[从零开始学习FPGA编程-27]:进阶篇 - 基本组合电路-数据比较器(Verilog语言)

FPGA教程案例8基于verilog的分频器设计与实现

20180805 玩玩FPGA开发,学学Verilog,学学基本操作,制作流水灯

基于FPGA的呼叫设备verilog开发

[从零开始学习FPGA编程-28]:进阶篇 - 基本组合电路-奇偶校验生成器(Verilog语言版本)