22. wm8978音频回环实验¶
WM8978是一个低功耗、高质量的立体声多媒体数字信号编译码器。它主要应用于便携式应用,比如数码照相机、可携式数码摄像机。它结合了立体声差分麦克风的前置放大与扬声器、耳机和差分、立体声线输出的驱动,减少了应用时必需的外部组件,比如不需要单独的麦克风或者耳机的放大器。
本章节将通过一个音频回环实验,带大家去了解WM8978的驱动方法。
22.1. 理论学习¶
22.1.1. 声音的基本概念¶
声音是通过一定介质传播的连续的波。声音信号可分为两种类型,一种是模拟信号(自然界、物理),一种是数字信号(计算机)。声音信号的数字化过程如下图所示。
图 51‑1 声音数字化过程图
图 51‑2 声音数字化过程波形示意图
声音数字化的三要素:
表格 51‑1 声音数字化三要素
采样频率 | 量化 |
数 | 声道数 |
|
每秒钟抽 | 每个取声波幅度样本的次数 | 二进制位表示数据 |
样点用多少 | 使用声音通道的个数 围 | |
|
采样频率越高声音 | 量化位数越多质量越好数据量也越大 | 音质越好数据量也 |
立体声比单声道的表 大 | 现力丰富,单数量翻倍 |
|
|
11.025 KHz22.05 KHz44.1 KHz |
8位 = 256 | 16位 = 65536 | |
单声道 | 立体声 | |
22.2. i2s音频总线¶
22.2.1. i2s总线概述¶
音频数据的采集、处理和传输是多媒体技术的重要组成部分。众多的数字音频系统已经进入消费市场,例如数字音频录音带、数字声音处理器等。对于设备和生产厂家来说,标准化的信息传输结构可以提高系统的适应性。I2S(Inter-IC Sound)总线,又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专门用于音频设备之间的数据传输,广泛应用于各种多媒体系统。
22.2.1.1. I2S总线接口¶
I2S总线接口有三个主要接口
SD(Serial Data):串行数据线,用于发送或接受两个时分复用的数据通道上的数据,数据的最高位总是被最先传输。
SCK(continuous serial clock):串行时钟,对应数字音频的每一位数据,SCK都有一个脉冲。SCK的频率=2 × 采样频率 ×采样位数。
WS(Word Select):字段选择线,也称帧时钟(LRC)线,表明当前传输数据的声道,不同标准有不同的定义。WS线的帧率等于采样频率(Fs)。
22.2.2. wm8978芯片¶
WM8978是欧胜(Wolfson)推出的一款全功能音频处理器。它带有一个HI-FI级数字信号处理内核,支持增强3D硬件环绕音效,以及5频段的硬件均衡器,可以有效改善音质。
WM8978具有高级的片上数字信号处理功能,包含一个 5 路均衡功能,一个用于 ADC 和麦克风或者线路输入之间的混合信号的电平自动控制功能,一个纯粹的录音或者重放的数字限幅功能。另外在 ADC 的线路上提供了一个数字滤波的功能,可以更好的应用滤波,比如“减少风噪声”。
图 51‑3为 WM8978 芯片内部结构示意图,参考来自WM8978数据手册。该图给人的第一印象感觉就是很复杂,密密麻麻很多内容,特别有很多“开关”。实际上,每个开关对应着 WM8978 内部寄存器的一个位,通过控制寄存器的就可以控制开关的状态。
图 51‑3 WM8978内部结构
22.2.2.1. 输入部分¶
WM8978 结构图的左边部分是输入部分,可用于模拟声音输入,即用于录音输入。有三个输入接口,一个是由 LIN 和 LIP、 RIN 和 RIP 组合而成的伪差分立体声麦克风输入,一个是由 L2 和 R2 组合的立体声麦克风输入,还有一个是由 AUXL 和 AUXR 组合的线输入或用来传输警告声的输入。
22.2.2.2. 输出部分¶
WM8978 结构图的右边部分是声音放大输出部分, LOUT1 和 ROUT1 用于耳机驱动, LOUT2 和 ROUT2 用于扬声器驱动, OUT3 和 OUT4 也可以配置成立体声线输出, OUT4也可以用于提供一个左右声道的单声道混合。
22.2.2.3. ADC和DAC¶
WM8978 结构图的中边部分是芯片核心内容,处理声音的 AD 和 DA 转换。 ADC 部分对声音输入进行处理,包括 ADC 滤波处理、音量控制、输入限幅器/电平自动控制等等。DAC 部分控制声音输出效果,包括 DAC5 路均衡器、 DAC 3D 放大、 DAC 输出限幅以及音量控制等等。
22.2.2.4. 通信接口¶
WM8978 有两个通信接口,一个是数字音频通信接口,另外一个是控制接口。音频通信接口可选用 I2S 接口,支持左对齐、右对齐和 I2S 标准模式,以及 DSP 模式 A 和模式 B。控制接口用于控制器发送控制命令配置 WM8978 运行状态,它提供 2 线或 3 线控制接口,对于我们使用的FPGA控制器,我们选择 2 线接口方式,其芯片地址固定为0011010。通过控制接口可以访问 WM8978 内部寄存器,实现芯片工作环境配置。对于 2 线接口方式,其时序图如下所示:
图 51‑4 2线接口时序图
如图 51‑4所示,该模式只有两个控制信号,我们只需控制SDIN和SCLK即可。通过时序图可以发现该时序就是我们前面章节讲的I2C接口时序,所以在这里我们可以调用我们前面写的I2C的控制时序来给WM8978配置寄存器。
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频率输出。
本次实验选用的音频通信接口为I2S音频总线接口,其传输书序图如下图所示。
图 51‑5 I2S音频接口(假设n位字长)
如图 51‑5所示,fs为采样频率,LRC为左右声道的对齐时钟。当LRC为低电平时传输左声道的音频数据,LRC为高电平时传输右声道的数据。如图所示1bit的音频数据的持续时间是一个BCLK位时钟,且从高位开始传送,所以位时钟BCLK的频率=2×采样频率×采样位数。如果我们将WM8978的工作模式设置 为主模式,那么BCLK和LRC就由WM8978产生,我们可直接使用;若设置为从模式,BCLK和LRC则需由外部设备产生。所以如果我们设置为主模式那就能省去很多麻烦。从图中可以看到,DACDAT/ADCDAT音频数据是在LRC变化后的第二个BCLK脉冲处产生,且在LRC变化后BCLK第二个上升沿开始采 数据采数据。
22.2.2.5. WM8978寄存器配置¶
WM8978 寄存器为 16bit 长度,高 7 位 ( [15:9]bit ) 用于表示寄存器地址,低 9 位 ( [8:0]bit ) 用于表示数据。所以在控制器向芯片发送控制命令时,必须传输长度为 16bit 的指令,芯片会根据接收命令高 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(bit3 = 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,音频数据模式选择为I2S模式,其中2’b00为右对齐模式、2’b01位左对齐模式、2’b11位DSP/PCM模式,这里我们选择I2S音频接口(其他模式的时序图可查看数据手册了解)。设置WL(bit[6:5]),WL为字长,即为表格 51‑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的设置一致即可。
以上为该实验的的参考寄存器设置,大家也可尝试更改部分配置,只要能达到我们的实验功能即可。
22.3. 实战演练¶
22.3.1. 实验目标¶
将耳机或电脑上的音乐通过WM8978传输到FPGA内,然后再将音频数据从FPGA传回WM8978中播放出来。
22.3.2. 硬件资源¶
本次实验我们需使用到开发板上的WM8978音频相关接口,开发板上的音频相关接口如图 51‑6所示。
图 51‑6 音频相关接口图
其相关原理图如下所示:
图 51‑7 音频芯片及MIC插头图
图 51‑8 喇叭插座
图 51‑9 耳机及音频输入接口
本次实验需要使用到音频输入,耳机以及喇叭接口。音频输入接口是连接播放器用于音频输入,耳机以及喇叭接口是用来连接耳机以及喇叭用于音频播放。
22.3.3. 程序设计¶
硬件资源介绍完毕,我们开始实验工程的程序设计。在本小节,我们采用先整体概括,再局部说明的方式对实验工程的各个模块进行讲解,详细内容如下。
22.3.3.1. 整体说明¶
根据理论部分的介绍,我们知道要想使用WM8978,首先需要对其内部寄存器进行配置。通过前面的接口时序讲解可知,我们可使用I2C接口对WM8978芯片进行配置,我们只需加个寄存器配置模块即可对WM8978完成配置。除了这两个子模块外我们还需要一个接收WM8978传出来的音频数据模块,同时还需要一个将音 频数据发送回WM8978的模块。同时为了生成WM8978的主时钟(MCLk),我们需要输出一个12MHz的时钟给WM8978,在这里我们调用IP核去生成12MHz的时钟。有了这些子模块,我们将这些子模块整合在一起即可,如图 51‑10所示。
图 51‑10 WM8978音频回环整体框图
通过上图可以看到,该工程共分七个模块,各模块简介见表格 51‑2。
表格 51‑2 audio_loopback工程模块简介
模块名称 |
功能描述 |
---|---|
clk_gen |
时钟生成模块,产生12MHz时钟 |
i2c_ctrl |
i2c驱动模块 |
i2c_reg_cfg |
i2c配置模块 |
wm8978_cfg |
寄存器配置模块 |
audio_rcv |
音频接收模块 |
audio_send |
音频发送模块 |
audio_lookback |
顶层模块 |
下面分模块为大家讲解。
22.3.3.2. 时钟生成模块¶
频率为12MHz的时钟产生用代码编写较难生成,所以在此我们通过调用IP核来生成,在IP核使用章节已经讲解了详细的调用方法,在此就不再说明。其模块框图如图 51‑11所示。
图 51‑11 时钟分频模块
表格 51‑3 clk_gen模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
~areset |
1bit |
input |
复位信号,异步复位 |
inclk0 |
1bit |
input |
输入时钟,频率50MHz |
c0 |
1bit |
output |
输出时钟,12MHz |
locked |
1bit |
output |
时钟稳定标志信号 |
如表格 51‑3所示,inclk0输入系统时钟即可;而复位信号由于是异步复位,所以需将其取反后才能接入系统复位;c0为输出时钟,locked为时钟稳定标志信号,当输出的c0时钟稳定后locked将拉高。这是调用的IP核,最后将其例化在顶层模块即可。
22.3.3.3. i2c驱动模块¶
在《基于I2C协议的 EEPROM驱动控制》章节已经详细地讲解了驱动方法,在此就不再叙述,直接调用这个模块即可。
22.3.3.4. i2c配置模块¶
模块框图
该模块为i2c配置模块,将寄存器的配置通过i2c驱动模块写入WM8978从而完成对WM8978的配置,其模块框图如图 51‑12所示。
图 51‑12 i2c配置模块框图
此模块是产生让i2c驱动模块写入WM8978的配置,其各个信号说明如表格 51‑4所示。
表格 51‑4 i2c配置模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_rst_n |
1bit |
input |
复位信号,低有效 |
i2c_end |
1bit |
input |
i2c一次读/写操作完成 |
i2c_clk |
1bit |
input |
i2c驱动时钟 |
cfg_start |
1bit |
output |
单个寄存器配置触发信号 |
cfg_data |
16bit |
output |
寄存器地址7bit+数据9bit |
cfg_done |
1bit |
output |
寄存器配置完成信号 |
i2c_end和i2c_clk由i2c_ctrl模块输入,而复位信号由顶层模块输入。cfg_start由该模块产生去控制i2c驱动模块配置单个寄存器;cfg_data为配置寄存器的数据,前面说到WM8978 寄存器是 16bit 长度,高 7 位([15:9]bit)用于表示寄存器地址,低 9 位表示数据。cfg_done为寄存器配置完成信号。
下面通过波形图来详细讲解该模块的时序。
波形图绘制
图 51‑13 i2c_reg_cfg波形图
由图 51‑13所示:i2c_clk和sys_rst_n为模块的模块时钟信号和模块复位信号,cfg_end为单个寄存器配置完成信号,从i2c驱动模块输入。从i2c驱动模块可知,我们只要产生开始信号和写入寄存器的数据,i2c驱动模块即可将寄存器数据写入WM8978中。
cfg_start:单个寄存器配置触发信号,上电后延迟一段时间等待WM8978工作在稳定状态后产生第一个开始信号,所以我们需要一个产生延迟信号的计数器。计数到1ms(999)时产生第一个开始配置信号。
cnt_wait:寄存器配置上电等待计数器。在这里我们上电等待1ms后开始配置寄存器,由于系统时钟为i2c驱动模块传来的时钟,频率为1MHz,单位时钟为1us,所以计数到999即为1ms,需将计数器计到1000时停止计数,当计数器的值小于计数器最大值CNT_WAIT_MAX(1000)时,每来一个时 钟让其自加1。
i2c_end:单个寄存器配置完成后,会产生一个结束信号。每当检测到结束信号后,紧接着发送下一个开始信号,直到所有寄存器配置完成。然而我们如何判断所有寄存器配置完成了呢?在这里我们可以产生一个寄存器个数计数器来进行判断。
reg_num:配置寄存器个数。当第一个开始信号发出后,此时reg_num的值为0,配置第一个寄存器,当接收到单个寄存器配置完成信号后,reg_num加1,同时开始配置第二个寄存器,此时开始配置信号(cfg_start)所采的值刚好是reg_num变化后的值。
cfg_data_reg:寄存器数据暂储器,一上电我们就将所有需要配置的寄存器值寄存在这个暂存器中。
cfg_data:寄存器配置值。寄存器触发信号产生后,将暂存器里存储的值赋给寄存器配置值(cfg_data),当reg_num等于0时赋予第一个寄存器的值,REG_NUM为寄存器的个数,所以当配置最后一个寄存器时reg_num的值为REG_NUM-1。此时cfg_data的值为cfg_data_re g[REG_NUM-1]。同时当最后一个寄存器配置完成后,将寄存器配置完成信号(cfg_done)拉高。
cfg_done:寄存器配置完成信号。当寄存器配置完成后,将寄存器配置值(cfg_data)置为0。
代码编写
参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 51‑1。
代码清单 51‑1 i2c_reg_cfg模块参考代码(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 | 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'd18 ; //总共需要配置的寄存器个数
parameter CNT_WAIT_MAX = 10'd1000 ; //上电等待1ms后开始配置寄存器
parameter LOUT1VOL = 6'd40 ; //耳机左声道音量设置(0~63)
parameter ROUT1VOL = 6'd40 ; //耳机右声道音量设置(0~63)
parameter SPK_LOUT2VOL = 6'd45 ; //扬声器左声道音量设置(0~63)
parameter SPK_ROUT2VOL = 6'd45 ; //扬声器右声道音量设置(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_0010_1111 };
assign cfg_data_reg[02] = {7'd2 , 9'b1_1011_0011 };
assign cfg_data_reg[03] = {7'd4 , 9'b0_0101_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'd47, 9'b0_0111_0000 };
assign cfg_data_reg[09] = {7'd48, 9'b0_0111_0000 };
assign cfg_data_reg[10] = {7'd49, 9'b0_0000_0110 };
assign cfg_data_reg[11] = {7'd50, 9'b0_0000_0001 };
assign cfg_data_reg[12] = {7'd51, 9'b0_0000_0001 };
assign cfg_data_reg[13] = {7'd52, 3'b110 , LOUT1VOL };
assign cfg_data_reg[14] = {7'd53, 3'b110 , ROUT1VOL };
assign cfg_data_reg[15] = {7'd54, 3'b110 , SPK_LOUT2VOL };
assign cfg_data_reg[16] = {7'd55, 3'b110 , SPK_ROUT2VOL };
//更新完耳机和扬声器的音量后再开启音频的输出使能,防止出现“嘎达”声
assign cfg_data_reg[17] = {7'd3 , 9'b0_0110_1111 };
//-------------------------------------------------------
endmodule
|
模块参考代码是参照绘制波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,需要注意的是代码91行,我们配置完我们需要的所有寄存器后再开启耳机和扬声器的输出使能,若是先开启耳机和扬声器的使能再更新音量的话,音量变化时可能会出现“嘎达”声,所以我们最后再配置R3寄存器,防止出现“嘎达”声。
22.3.3.5. 寄存器配置模块¶
模块框图
该模块就是对i2c驱动模块以及i2c配置进行实例化从而得到一个完整的寄存器配置模块,其模块框图如图 51‑14所示。
图 51‑14 寄存器配置模块框图
由上图可看到,配置寄存器的数据(cfg_data)分别传输给了wr_data(输入数据)和byte_addr(输入地址)。由于i2c数据发送一次只能发送8bit,而我们WM8978寄存器的地址只有7bit,所以我们需要将数据的最高位整合到地址变量的第0位先发送,接下来再发送cfg_data的低八位。 由于本次实验只有写入而没有读取,所以我们将i2c驱动模块的写使能(wr_en)置为1,读使能(rd_en)置为0即可。同时由于我们的地址位只有7bit,所以将输入i2c字节地址字节数(addr_num)置为0,表示写入1个字节的地址。i2c_ctrl模块各输入输出信号具体描述,大家可参考“基于I2C 协议的 EEPROM驱动控制”章节详细了解。
根据以上的讲述,我们基本已经知道了各个子模块输入输出信号的连接,下面即可编写实例化代码。
代码编写
代码清单 51‑2 寄存器配置模块参考代码(wm8978_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 | module wm8978_cfg
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //系统复位,低有效
output wire i2c_scl , //输出至WM8978的串行时钟信号scl
inout wire i2c_sda //输出至WM8978的串行数据信号sda
);
////
//\* Parameter and Internal Signal \//
////
//wire define
wire cfg_start ; //输入i2c触发信号
wire [15:0] cfg_data ; //寄存器地址7bit+数据9bit
wire i2c_clk ; //i2c驱动时钟
wire i2c_end ; //i2c一次读/写操作完成
wire cfg_done ; //寄存器配置完成信号
////
//\* Main Code \//
////
//------------------------ i2c_ctrl_inst -----------------------
i2c_ctrl
#(
.
VICE_ADDR (7'b0011_010 ) , //i2c设备地址
.
S_CLK_FREQ (26'd50_000_000) , //输入系统时钟频率
.
L_FREQ (18'd250_000 ) //i2c设备scl时钟频率
)
i2c_ctrl_inst
(
.sys_clk (sys_clk ), //输入系统时钟,50MHz
.sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效
.wr_en (1'b1 ), //输入写使能信号
.rd_en (1'b0 ), //输入读使能信号
.i2c_start (cfg_start ), //输入i2c触发信号
.addr_num (1'b0 ), //输入i2c字节地址字节数
.byte_addr (cfg_data[15:8]), //输入i2c字节地址+数据最高位
.wr_data (cfg_data[7:0] ), //输入i2c设备数据低八位
.rd_data ( ), //输出i2c设备读取数据
.i2c_end (i2c_end ), //i2c一次读/写操作完成
.i2c_clk (i2c_clk ), //i2c驱动时钟
.i2c_scl (i2c_scl ), //输出至i2c设备的串行时钟信号scl
.i2c_sda (i2c_sda ) //输出至i2c设备的串行数据信号sda
);
//---------------------- i2c_reg_cfg_inst ---------------------
i2c_reg_cfg i2c_reg_cfg_inst
(
.i2c_clk (i2c_clk ), //系统时钟,由i2c模块传入
.sys_rst_n (sys_rst_n ), //系统复位,低有效
.cfg_end (i2c_end ), //单个寄存器配置完成
.cfg_start (cfg_start ), //单个寄存器配置触发信号
.cfg_data (cfg_data ), //寄存器的地址和数据
.cfg_done (cfg_done ) //寄存器配置完成信号
);
endmodule
|
关于模块的实例化在模块框图处已经做了讲解,这里需要注意的是代码第29行,我们需要将设备地址改为WM8978的设备地址,在通信接口小节已经讲到WM8978芯片的固定地址为0011010,将设备地址直接改为0011010即可。
仿真代码编写
寄存器模块已经完成,我们可以编写仿真文件代码,通过仿真去看我们编写的配置寄存器的代码,是否和我们绘制的波形图一致。仿真参考代码详见代码清单 51‑3。
代码清单 51‑3 WM8978寄存器配置仿真参考代码(tb_wm8978_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 | module tb_wm8978_cfg();
////
//\* Parameter and Internal Signal \//
////
//wire define
wire i2c_scl ;
wire i2c_sda ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
////
//\* Main Code \//
////
//对sys_clk,sys_rst_n赋初始值
initial
begin
sys_clk = 1'b0;
sys_rst_n <= 1'b0;
#100
sys_rst_n <= 1'b1;
end
//clk:产生时钟
always #10 sys_clk = ~sys_clk;
////
//\* Instantiation \//
////
//------------- wm89078_cfg_inst -------------
wm8978_cfg wm8978_cfg_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //系统复位,低有效
.i2c_scl (i2c_scl ), //输出至WM8978的串行时钟信号scl
.i2c_sda (i2c_sda ) //输出至WM8978的串行数据信号sda
);
endmodule
|
该模块仿真文件我们只要生成时钟复位即可。
在前面章节对i2c驱动的学习,我们知道i2c配置模块的ACK应答信号是由我们控制的设备产生的,并不是由我们FPGA产生的,为了使仿真能进行下去,在仿真时我们需要自己去给应答信号,当仿真完成再改回去即可。如图 51‑15中框所示。
图 51‑15 仿真测试更改代码图
我们只需要将i2c驱动模块代码中ack置0应答,当仿真完成后改回接收设备应答即可。
仿真波形分析
配置好仿真文件,使用ModelSim对参考代码进行仿真,仿真结果如下。
图 51‑16 i2c配置仿真波形图
由于i2c驱动模块是前面章节使用过无错误的,所以在这里可以不需要去观看其波形图。我们只需要观看我们编写的i2c配置模块是否正确即可,如图 51‑16所示:可以看到波形图整体时序和我们绘制的波形图时序是一致的,这说明我们编写的代码总体是正确的,下面通过局部仿真波形图观看是否正确。
图 51‑17 i2c配置局部仿真波形图(一)
图 51‑18 i2c配置局部仿真波形图(二)
图 51‑17为i2c配置模块开始配置时的仿真波形图,图 51‑18为i2c配置模块结束配置时的仿真波形图。可以发现这两个仿真波形图与我们绘制的波形图都是一致的,这说明我们编写的代码与我们绘制的波形图功能是一致的。
22.3.3.6. 音频接收模块¶
模块框图
该模块的作用是接收WM8978输出的ADCDAT音频数据,模块框图如图 51‑19所示。
图 51‑19 音频接收模块框图
该模块的输入输出信号描述如下表所示:
表格 51‑5 音频接收模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_rst_n |
1bit |
input |
输入复位,低有效 |
audio_lrc |
1bit |
input |
WM8978输出的数据左/右对齐时钟 |
audio_bclk |
1bit |
input |
WM8978输出的位时钟 |
audio_adcdat |
1bit |
input |
WM8978ADC数据输出 |
rcv_done |
1bit |
output |
一次数据接收完成 |
adc_data |
24bit |
output |
一次接收的数据 |
由于我们接收的是WM8978输出的数据,而WM8978是根据图 51‑5 I2S音频接口时序进行输出的,所以,无论是接收还是发送我们都需要遵从这个时序。根据音频时序接口图,我们可以先画出接收音频数据的时序波形图。
波形图绘制
图 51‑20 音频接收模块波形图
根据I2S音频接口时序图,可以知道WM8978输出的ADCDAT音频数据是在位时钟(BCLK)的下降沿变化的,所以我们需要用位时钟的上升沿去采集数据。而数据是在对齐时钟(LRC)电平变化后的第二个位时钟脉冲处开始发送,所以我们需要在对齐时钟电平变化后位时钟第二个上升沿处开始采得第一个数据。那我们如何 设计才能达到这个目的呢?如图 51‑20所示,首先我们需获得对齐时钟电平变化标志信号。
lrc_edge:对齐时钟(audio_lrc)电平变化信号。对audio_lrc打一拍后得到打一拍信号audio_lrc_d1,将audio_lrc_d1信号与audio_lrc信号按位异或即可得到如波形图所示的lrc_edge标志信号。
在配置寄存器(R4)时,我们设置了音频的量化位数为24位,所以WM8978一次会发送24bit的音频数据,从高位开始发送。所以我们需要一个发送位数的计数器,而这个计数器需满足:当采第一个数据时,计数器为0,采第二个数据时,计数器为1,依次类推,这样设计的目的是方便我们将接收的24bit音频数据拼接成 一个音频数据,方便发送。所以就有了如图所示的计数器设计:
adcdat_nct:WM8978ADCDAT数据输出位数计数器。当lrc_edge为高电平时计数器为0,当计数器的值小于26时(这个值设为26是保证能完全接收我们24bit计数,同时方便产生一个时钟的接收完成高电平信号),让其每次加1。这样的话当采第一位数据(最高位,第23bit)时计数器的值为0 ;依次类推,当采最后一位数据(最低位,第0bit)时计数器的值为23。当计数器为26停止计数(即小于26计数器才计数),直到下一个对齐时钟电平变化信号来到,让其归0重新开始计数。
data_reg:adc_data数据寄存器。当采到第一个数据时我们将第一个数据寄存在寄存器的最高位(因为数据是从高位开始输出的)。即当计数器等于0时是寄存在寄存器的最高位第23bit;等于1时寄存的数据为第22bit。可以发现他们的关系为寄存位数为:23-adcdat_cnt。因为我们只需要接收2 4位音频数据,所以当计数器小于等于23时才接收。
adc_data:一次接收的数据。当计数器等于24时,表明一次数据接收完毕,此时将寄存器里接收的一次发送的音频数据值赋给adc_data,同时拉高一个时钟的一次数据接收完成标志信号(rcv_done)。若计数器最大值设为24,则rcv_done信号不方便产生一个时钟的高电平信号。
代码编写
参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 51‑4。
代码清单 51‑4 音频接收模块参考代码(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 [23:0] adc_data , //一次接收的数据
output reg rcv_done //一次数据接收完成
);
////
//\* Parameter and Internal Signal \//
////
//reg define
reg audio_lrc_d1; //对齐时钟打一拍信号
reg [4:0] adcdat_cnt ; //WM8978ADC数据输出位数计数器
reg [23: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'd26)
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 <= 24'b0;
else if(adcdat_cnt <= 5'd23)
data_reg[23-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 <= 24'b0;
else if(adcdat_cnt == 5'd24)
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'd24)
rcv_done <= 1'b1;
else
rcv_done <= 1'b0;
endmodule
|
模块参考代码是参照模块波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,在此就不再次介绍了。
22.3.3.7. 音频发送模块¶
模块框图
该模块是将接收到的音频数据发送回WM8978,通过WM8978将音频数据播放出来,模块框图如图 51‑21所示。
图 51‑21 音频发送模块框图
该模块的输入输出信号描述如下表所示:
表格 51‑6 音频发送模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_rst_n |
1bit |
input |
复位信号,低电平有效 |
audio_lrc |
1bit |
input |
WM8978输出的数据左/右对齐时钟 |
audio_bclk |
1bit |
input |
WM8978输出的位时钟 |
dac_data |
24bit |
input |
往WM8978发送的数据 |
audio_dacdat |
1bit |
output |
发送DAC数据给WM8978 |
send_done |
1bit |
output |
一次数据发送完成 |
音频发送模块的时序与接收模块有非常多的相似之处,用的都是WM8978输出的位时钟,传输的时序要与WM8978的I2S音频接口时序相对应,才能使WM8978接收到正确的数据。不同的是接收模块是将各bit的数据接收后拼接成一个完整的音频数据,发送模块是将拼接完成的音频数据1bit、1bit地发送回给WM 8978。
下面通过波形图详细给大家讲解各信号时序关系图。
波形图绘制
图 51‑22 音频发送模块波形图
发送模块的时序与接收时序的思路是大致相同的。大家若是清楚的了解了接收模块的时序,发送时序一看就能明白了。这里就只为大家讲解一下它们的不同之处。
首先是数据的发送,由WM8978的I2S音频接口时序我们知道音频是在位时钟下降沿发送的,只不过我们接收是使用位时钟上升沿进行接收。所以我们发送回去的音频数据也需要使用位时钟的下降沿进行发送,如上图所示。对齐时钟变化沿的产生方法与计数器的设计思路与接收模块是一致的,在此不在过多讲解。
其次是接收模块是将接收的一位一位数据组成了一个24位的音频数据,而我们的发送模块是将接收的24位音频数据发送回去,如图所示,对齐时钟变化后的第一个下降沿开始发送接收数据最高位数据(第23bit),此时计数器的值是0;第二个下降沿发送接收数据第22bit数据,此时计数器的值是1;这与接收模块也是一样的 。所以发送的数据audio_dacdat = data_reg[23-0],一次数据发送完成后拉高一个时钟的标志信号(send_done)表示一次数据发送完毕。
代码编写
根据上面的波形图,我们可编写出发送模块的模块代码,详见代码清单 51‑5。
代码清单 51‑5 音频发送模块参考代码(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 76 77 | module audio_send
(
input wire audio_bclk , //WM8978输出的位时钟
input wire sys_rst_n , //系统复位,低有效
input wire audio_lrc , //WM8978输出数据左/右对齐时钟
input wire [23: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 [23: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 <= 5'b0;
else if(lrc_edge == 1'b1)
dacdat_cnt <= 5'b0;
else if(dacdat_cnt < 5'd26)
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 <= 24'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'd23)
audio_dacdat <= data_reg[23 - dacdat_cnt];
else
audio_dacdat <= audio_dacdat;
//当最后一位数据传完之后,输出一个时钟的发送完成标志信号
always@(posedge audio_bclk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
send_done <= 1'b0;
else if(dacdat_cnt == 5'd24)
send_done <= 1'b1;
else
send_done <= 1'b0;
endmodule
|
模块参考代码是参照模块波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,在此就不再次介绍了。
22.3.3.8. 顶层模块¶
模块框图
图 51‑23 音频回环顶层模块框图
该模块的输入输出信号描述如表格 51‑7所示。
表格 51‑7 音频回环顶层模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_clk |
1bit |
input |
系统时钟,频率50MHz |
sys_rst_n |
1bit |
input |
系统复位,低有效 |
audio_lrc |
1bit |
input |
WM8978输出的位时钟 |
audio_bclk |
1bit |
input |
WM8978输出的数据左/右对齐时钟 |
audio_adcdat |
1bit |
input |
WM8978ADC数据输出 |
audio_dacdat |
1bit |
output |
输出DAC数据给WM8978 |
mclk |
1bit |
output |
输出WM8978主时钟,频率12MHz |
scl |
1bit |
output |
输出至wm8978的串行时钟信号scl |
sda |
1bit |
inout |
输出至wm8978的串行数据信号sda |
audio_lookback顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,对应信号连接可根据系统整体框图进行连接,信号代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 51‑6。
代码编写
代码清单 51‑6 顶层模块参考代码(audio_lookback.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 | module audio_loopback
(
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数据输出
output wire scl , //输出至wm8978的串行时钟信号scl
output wire audio_mclk , //输出WM8978主时钟,频率12MHz
output wire audio_dacdat , //输出DAC数据给WM8978
inout wire sda //输出至wm8978的串行数据信号sda
);
////
//\* Parameter And Internal Signal \//
////
//wire define
wire [23:0] adc_data ; //接收的音频数据
////
//\* Instantiation \//
////
//------------------- clk_gen_inst ----------------------
clk_gen clk_gen_inst
(
.areset (~sys_rst_n ), //异步复位,取反连接
.inclk0 (sys_clk ), //输入时钟(50MHz)
.c0 (audio_mclk ), //输出时钟(12MHz)
.locked () //输出稳定时钟标志信号
);
//------------------- 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 () //一次数据接收完成
);
//------------------ 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 (adc_data ), //往WM8978发送的数据
.audio_dacdat (audio_dacdat), //发送DAC数据给WM8978
.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
|
在例化时,有些预留的信号接口,而我们在这个工程中没有使用到时,我们将其悬空不连接即可。
22.3.3.9. RTL视图¶
使用Quartus II软件对工程进行编译,编译通过后,查看RTL视图如下图 51‑24、图 51‑25。由图可知,实验工程的RTL视图与实验整体框图相同,各信号线均已正确连接。
图 51‑24 audio_lookback顶层RTL视图
图 51‑25 寄存器配置RTL视图
22.3.3.10. SignalTap波形抓取¶
由于音频接收模块和音频发送模块的位时钟,对齐时钟都是由WM8978输出而得来的,我们由仿真文件产生这两个时钟比较麻烦,所以在此我们就利用quartus软件自带的SignalTap工具对波形进行实时抓取来验证我们的设计是否正确,如下图所示。
图 51‑26 SignalTap抓取波形图(一)
如图 51‑26所示抓取的为接收模块信号的波形图,由图可以看到该图与我们绘制的波形图是一致的。
图 51‑27 SignalTap抓取波形图(二)
如图 51‑27所示抓取的为发送模块信号的波形图,由图可以看到该图与我们绘制的波形图是一致的。
图 51‑28 SignalTap抓取波形图(三)
如图 51‑28可看到主时钟,位时钟和对齐时钟都有产生。同时箭头所指对应的是FPGA接收和发送的音频数据,由于太过紧密看的不是很真切,不过可以看到大致是相同的。下面我们随机放大截取一个对应数据来看看接收和发送的数据是不是一致。如图 51‑29所示。
图 51‑29 SignalTap抓取波形图(四)
如图 51‑29箭头所对应的FPGA接收和发送的数据,可以很清楚的看到其发送的音频数据就是WM8978传过来的音频数据,这说明我们的接收和发送的模块可实现我们设计的要求,能达到音频回环的功能。
22.4. 上板调试¶
22.4.1. 引脚约束¶
仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 51‑8所示。
表格 51‑8 引脚分配表
信号名 |
信号类型 |
对应引脚 |
备注 |
---|---|---|---|
sys_clk |
input |
E1 |
时钟 |
sys_rst_n |
input |
M15 |
复位 |
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数据线 |
下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 51‑30所示。
图 51‑30 管脚分配
22.4.1.1. 结果验证¶
管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好;连接耳机、音频线(如图 51‑31所示)、扬声器,连接好后为板卡上电,如图 51‑32所示。
图 51‑31 3.5mm公对公音频连接线
图 51‑32 程序下载连线图
打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 51‑33所示。
图 51‑33 下载成功界面
下载成功后即可以开始验证了。我们从播放器(电脑或者手机即可)中播放一首音乐,若耳机和喇叭上能听到我们所播放的音乐则说明验证成功。
22.5. 章末总结¶
到这里,本章节讲解完毕,该实验的关键是对寄存器的配置,只有寄存器配置正确了,WM8978才能正常的工作。只要大家熟练了解了WM8978内部的时序,代码设计编写起来就较为轻松。
22.6. 拓展训练¶
若将音频字长设置为16位,代码该如何更改?
若要将音频字长设置为参数,更改参数即可更改音频字长,代码该如何更改?