18. SD卡音乐播放¶
前面基于WM8978芯片我们讲解了它的音频回环与录音回放实验,本章节我们让其作为一个播放器,结合SD卡,对SD卡内存储的音乐进行播放。
18.1. 理论学习¶
在《WM8978音频回环》章节我们已经对声音的基本概念、WM8978芯片及其通信方式做了详细的介绍。在《SD卡数据读写控制》章节也已对其读写控制做了详细的介绍。在学习本章节前需先对以上两个章节进行学习,当学习完以上两个章节再过来学习本章节就较为简单了。
18.1.1. 音频参数简介¶
与声音有关的参数有三个,分别为采样频率、采样位数、声道数。
采样频率
采样频率即取样频率,指每秒钟取得声音样本的次数。采样频率越高,数字化音频的质量越好,还原的波形越完整,播放的声音越真实,当然所占资源也越多。根据奎特采样定理,要从采样中完全恢复原始信号的波形,采样频率要高于声音中最高频率的两倍。人耳可听到的声音的频率范围是在16Hz-20kHz之间。因此,要将听到的 原声音真实地还原出来,采样频率必须大于40kHz。常用的采样频率有8kHz、11.025kHz、44.1kHz、48kHz等几种。22.05kHz相当于普通FM广播的音质,44.1kHz理论上可达到CD的音质。对于高于48kHz的采样人耳很难分辨,没有实际意义。
采样位数
采样位数即采样值或取样值,也叫量化位数,是存储每个采样值所用的二进制数。采样值反映了声音的波动状态,采样位数决定了量化精度。采样位数越长,量化的精度就越高,还原的波形曲线越真实,产生的量化噪声越小,回放的效果就越逼真。常用的量化位数有4、8、12、16、24,量化位数与声卡的位数和编码有关。如果采样 PCM编码同时使用8位声卡,可将音频信号幅度从上限到下线分为256个音量等级,取值范围为0-255;使用16位声卡,可将音频信号幅度划分成64K个音量等级,取值范围为-32768至32767。
声道数
声道数是使用的声音通道的个数,也是采样时所产生的的声音波形的个数。播放声音时,单声道的WAV一般使用一个喇叭发声,立体声的WAV可以使两个喇叭发声。记录声音时,单声道每次产生一个波形的数据;双声道每次产生两个波形的数据,所占用的存储空间增加一倍。
18.1.2. 音乐格式简介¶
音乐格式有很多种,这里只介绍较为常见的几种。
CD格式
首先是CD格式,在大多数播放软件的“打开文件类型”中,都可以看到.cda格式的音乐文件,这就是CD格式了。标准的CD格式为44.1K的采样频率,速率为1411k/s,16位量化位数。CD音轨可以说是近似无损的,它的声音基本上是忠于原声的。一个CD音频文件是一个.cda文件,这只是一个索引信息,并不是 真正的包含声音信息,所以不论CD音乐长短,在电脑上看到的.cda文件都是44字节长。注意:不能直接复制CD格式的.cda文件到硬盘上播放,需要使用像EAC这样的抓音轨软件把CD格式的文件转换成WAV,这个转换过程如果光盘驱动器质量过关而且EAC的参数设置得当的话,可以说是基本上无损抓音频。推荐大家使用这种方法。
MP3格式
MP3格式音乐文件普遍存在我们生活中,实际上 MP3本身是一种音频编码方式,全称为 Moving Picture Experts Group Audio Layer III(MPEG Audio Layer 3)。MPEG音频文件是MPEG 标准中的声音部分,根据压缩质量和编码复杂程度划分为三层,即Layer-1、Layer2、Layer3,且分别对应MP1、MP2、MP3这三种声音文件,其中MP3压缩率可达到 10:1至12:1,可以大大减少文件占用存储空间大小。 MPEG 音频编码的层次越高,编码器越复杂,压缩率也越高。MP3是利用 人耳对高频声音信号不敏感的特性,将时域波形信号转换成频域信号,并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)对低频信号使用小压缩比,保证信号不失真。这样一来就相当于抛弃人耳基本听不到的高频声音,只保留能听到的低频部分,这样可得到很高的压缩率。
AIFF格式
AIFF是音频交换文件格式(Audio Interchange File Format)的英文缩写,是Apple公司开发的一种声音文件格式,被Macintosh平台及其应用程序所支持,Netscape Navigator浏览器中的LiveAudio也支持AIFF格式,SGI及其它专业音频软件包也同样支持AIFF格式。AIFF支持ACE2、ACE8、MAC3和MAC6压缩,支持16位44.1kHZ立体声。
WAV格式
WAV是微软公司开发的一种音频格式文件,用于保存Windows平台的音频信息资源,它符合资源互换文件格式(Resource Interchange File Format,RIFF)文件规范。标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!WAVE是录音时用的标准的WINDOWS文件格式,文件的扩展名为“WAV”,数据本身的格式为PCM或压缩型,属于无损音乐格式的一种。
WAV格式的编码分为两种,一是按一定格式存储数据,而是采用一定的算法压缩数据。WAV格式对音频流的编码没有硬性规定,支持非压缩的PCM(Puls code Modulation)脉冲编码调制格式,还支持压缩型的编码及算法。
PCM编码是直接存储声波采样被量化后所产生的非压缩数据,故被视为单纯的无损耗编码格式,其优点是可获得高质量的音频信号。基于PCM编码的WAV格式是最基本的WAV格式,被声卡直接支持,能直接存储采样的声音数据,所存储的数据能直接通过声卡播放,还原的波形曲线与原始声音波形十分接近,播放的声音质量是一流的 ,在Windows平台下被支持得最好,常常被用作在其它编码的文件之间转换的中间文件。PCM的缺点是文件体积过大(每分钟的音乐大约需要12),不适合长时间记录。正因为如此,又出现了多种在PCM编码的基础上经改进发展起来的编码格式,如:DPCM,ADPCM编码等。
本次实验播放的音乐格式为.WAV格式的非压缩数据,这样我们可直接读取音频数据,可以不用进行相应的解压缩算法处理。采样率为48kHz,量化位数为16位,声道为双声道。
表格 68‑1 16bit声音数据格式
单声道 |
采样一 |
采样二 |
…… |
||
---|---|---|---|---|---|
低字节 |
高字节 |
低字节 |
高字节 |
…… |
|
双声道 |
采样一 |
…… |
|||
左声道 |
右声道 |
…… |
|||
低字节 |
高字节 |
低字节 |
高字节 |
…… |
如表格 68‑1所示,WAV音频格式是以低字节在前高字节在后的方式存储的。
18.1.3. WM8978音乐播放寄存器配置¶
相较于录音与回放的配置,SD音乐播放可少配置PGA使能,输入PGA音量以及增益(录音相关配置)。同时因为我们只使用WM8978实现音乐播放,所以相关的音频输入功能可不用配置。下面给出SD卡音乐播放的参考配置:
寄存器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可以由R1(bit5)寄存器位启用或禁用。这里我们需启用(bit[5] = 1)来配置内部MCLK时钟。
寄存器R2(02h)
该寄存器设置LOUT1EN(bit7)、ROUT1EN(bit8)为1,耳机输出使能。
寄存器R3(03h)
该寄存器用于设置DACENL(bit0)、DACENR(bit1)为1,左右通道DAC使能,使数字音频信号转换为模拟音频信号;设置LMIXEN(bit2)、RMIXEN(bit3)为1,左右输出通道混合器使能;设置ROUT2EN(bit5)、LOUT2EN(bit6)为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位。
寄存器R6(06h)
该寄存器可设置WM8978的工作模式,前面我们说到WM8978有两种工作模式。在这里我们将其设置为主模式,左右对齐时钟(LRC)和位时钟(BCLK)由WM8978产生,我们直接用即可,这样可以节省资源。寄存器R6的bit0为0时为从模式,为1时为主模式,所以这里我们设置bit0为1即可。
寄存器R10(0Ah)
该寄存器用于设置DAC的过采样率,通过对该寄存器的DACOSR128(bit3)设置为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的设置一样即可。
18.2. 实战演练¶
18.2.1. 实验目标¶
将SD卡内存储的“.WAV”格式的音乐文件读出通过WM8978播放出来。
18.2.2. 硬件资源¶
本次实验我们需使用到开发板上的WM8978音频相关接口和SD卡接口,开发板上的音频和SD卡相关接口如图 68‑1所示。
图 68‑1 音频和SD卡相关接口图
其相关原理图如下所示:
图 68‑2 音频芯片及MIC插头图
图 68‑3 喇叭插座
图 68‑4 耳机及音频输入接口
图 68‑5 SD卡原理图
本次实验需要使用到耳机以及喇叭接口以及SD卡。SD卡用于输出播放的音乐,耳机以及喇叭接口是用来连接耳机以及喇叭用于音频播放。
18.2.3. 程序设计¶
硬件资源介绍完毕,我们开始实验工程的程序设计。在本小节,我们采用先整体概括,再局部说明的方式对实验工程的各个模块进行讲解。
18.2.3.1. 整体说明¶
根据实验目标可知,实现音乐播放首先需要有SD卡控制模块将SD内存储的音乐读取出来,同时还需要一个控制音频数据播放的模块,当然音乐要想播放出来WM8978控制模块也必不可少。因为SD卡的控制时钟sys_clk与WM8978的控制时钟audio_bclk的频率是不一样的,所以这里我们需要调用一个fifo 来实现跨时钟域的数据传输。完整的模块框图如图 68‑6所示。
图 68‑6 SD卡音乐播放工程整体框图
由图 68‑6所示:其中的sd_ctrl模快与wm8978_ctrl模块在前面章节都有讲解,clk_gen与fifo_data为调用的IP核,sd_play_ctrl模块为SD卡音乐播放控制模块。下面对工程各模块的功能描述做个说明,如表格 68‑2所示。
表格 68‑2 audio_sd_play工程模块简介
模块名称 |
功能描述 |
---|---|
clk_gen |
时钟生成模块 |
fifo_data |
fifo数据缓存模块 |
sd_ctrl |
SD卡控制模块 |
WM8978_ctrl |
WM8978音频控制模块 |
sd_play_ctrl |
SD卡音乐播放控制模块 |
audio_sd_play |
音乐播放顶层模块 |
下面对各模块做详细介绍。
18.2.3.2. 时钟生成模块¶
该模块通过调用PLL IP核来实现,总共输出3个时钟,频率分别为12MHz时钟、50MHz时钟、50MHz(相位偏移90°)。其中12MHz时钟作为wm8978的主时钟,50MHz作为系统时钟,50MHz(相位偏移90°)作为SD卡输入工作时钟。
调用IP核的方法在IP核章节已经做了详细介绍,我们只需按照调用步骤生成这四个时钟即可,在此就不再过多介绍。
18.2.3.3. FIFO数据缓存模块¶
该模块同样通过调用FIFO IP核来实现,由于SD卡的工作时钟与WM8978的工作时钟频率不一致,所以调用时我们需要选择不同时钟进行读写,同时输出fifo内剩余数据量信号以及fifo清空信号。这里我们把fifo的数据位宽设置为16bit,深度设置为1024。具体的调用步骤可参考IP核章节fifo的调用方法。
18.2.3.4. SD卡控制模块¶
我们通过该模块进行对SD卡内的的音乐数据进行读取,该模块在《SD卡数据读写控制》章节已经有了详细的讲解,模块框图如下所示:
图 68‑7 SD卡控制模块框图
因为我们只需要从里面读出音乐,所以我们只需控制其读端口信号即可。该模块我们直接调用即可,不明白的读者可先回到《SD卡数据读写控制》章节进行学习。
18.2.3.5. WM8978音频控制模块¶
模块框图
图 68‑8 WM8978音频控制模块框图
该模块与《WM8978录音与回放》章节模块几乎是一样的,不同的是寄存器的配置,其余模块是相同的。我们只需更改寄存器配置模块的代码即可,下面给出寄存器的配置模块参考代码,如代码清单 68‑1所示。
代码清单 68‑1 SD卡音乐播放WM8978寄存器配置模块参考代码(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 | 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'd15 ; //总共需要配置的寄存器个数
parameter CNT_WAIT_MAX = 10'd1000 ; //上电等待1ms后开始配置寄存器
parameter LOUT1VOL = 6'd30 ; //耳机左声道音量设置(0~63)
parameter ROUT1VOL = 6'd30 ; //耳机右声道音量设置(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'b1)))
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'd0 };
assign cfg_data_reg[01] = {7'd1 , 9'b1_0010_1111 };
assign cfg_data_reg[02] = {7'd2 , 9'b1_1000_0000 };
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'd43, 9'b0_0001_0000 };
assign cfg_data_reg[07] = {7'd49, 9'b0_0000_0110 };
assign cfg_data_reg[08] = {7'd50, 9'b0_0000_0001 };
assign cfg_data_reg[09] = {7'd51, 9'b0_0000_0001 };
assign cfg_data_reg[10] = {7'd52, 3'b110 , LOUT1VOL };
assign cfg_data_reg[11] = {7'd53, 3'b110 , ROUT1VOL };
assign cfg_data_reg[12] = {7'd54, 3'b110 , SPK_LOUT2VOL };
assign cfg_data_reg[13] = {7'd55, 3'b110 , SPK_ROUT2VOL };
//更新完耳机和扬声器的音量后再开启音频的输出使能,防止出现“嘎达”声
assign cfg_data_reg[14] = {7'd3 , 9'b0_0110_1111 };
//-------------------------------------------------------
endmodule
|
18.2.3.6. SD卡音乐播放控制模块¶
音乐扇区地址的获取
在讲解该模块之前,先为大家讲解一下如何获得SD卡内存储的音乐所在的的扇区地址。这里我们借用WinHex软件工具进行查看,该软件在该工程目录中的doc目录中,我们打开WinHex,双击该目录下的“WinHex.exe”或者“WinHex64.exe”应用程序。
图 68‑9 查看sd卡中音乐扇区地址图(一)
打开应用程序后会出现如下界面,如图 68‑10所示。
图 68‑10 查看sd卡中音乐扇区地址图(二)
借助读卡器将SD卡插在电脑上后如所示,我们双击打开工具中的打开磁盘。打开磁盘后出现所示界面。
图 68‑11 查看sd卡中音乐扇区地址图(三)
如图 68‑11所示所示,箭头1中所指的就是我们插入的读卡器,其标号为RM2。我们双击打开箭头2所指带有RM2的逻辑驱动器。
图 68‑12 查看sd卡中音乐扇区地址图(四)
如图 68‑12所示,箭头所指方框中即可看到歌曲在SD中的第一扇区数。同时双击歌曲我们可看到该歌曲占用的空间字节数,如图 68‑13所示。同时也可在歌曲文件中的属性中看到该歌曲的字节数。
图 68‑13 查看sd卡中音乐扇区地址图(五)
如图 68‑22箭头所指方框中可看到该音乐文件的大小为57299230个字节,而一个扇区地址为512个字节,这样该音乐所占的扇区数为57299230/512≈111912.56,占用111913个扇区地址,这样就可得到其末地址扇区了。
模块框图
该模块是将SD中的音乐数据读出来,输入到WM8978中去播放,模块框图如图 68‑14所示。
图 68‑14 音乐播放控制模块框图
该模块的各信号描述如下表所示:
表格 68‑3 音乐播放控制模块输入输出信号描述
信号 |
位宽 |
类型 |
功能模块 |
---|---|---|---|
sys_rst_n |
1bit |
input |
复位信号,低有效 |
sys_clk |
1bit |
input |
系统时钟,50MHz |
audio_bclk |
1bit |
input |
WM8978驱动时钟 |
sd_init_end |
1bit |
input |
sd卡初始化完成信号 |
fifo_data |
16bit |
input |
读出的fifo数据 |
rd_busy |
1bit |
input |
读忙信号 |
send_done |
1bit |
input |
一次音频数据发送完成信号 |
fifo_data_cnt |
10bit |
input |
fifo内剩余数据量 |
rd_en |
1bit |
input |
sd卡数据读使能信号 |
rd_addr |
32bit |
input |
sd卡读扇区地址 |
dac_data |
16bit |
input |
输出给WM8978播放的数据 |
从模块框图以及信号描述可知,该模块的主要功能是产生sd读使能信号(rd_en)以及sd卡读地址(rd_addr)来读出sd卡内的数据;同时产生给WM8978播放的音频数据(dac_data)。那要如何产生这些信号呢?我们通过波形图去具体了解。
波形图绘制
首先我们先看看如何产生输出给WM8978播放的音频数据,如图 68‑15所示。
图 68‑15 播放音频数据产生波形图
因为我们调用了一个fifo来缓存从sd中读出的数据,所以我们将fifo内的数据读出来播放即可。读fifo的时钟我们需要使用音频时钟audio_bclk,读使能使用音频模块的send_done信号。当send_done信号为1时我们将读出的fifo数据给dac_data输出,而从理论部分的表格 68‑1可以看到WAV格式的16bit数据组成是低字节在前的,而我们的WM8978音频数据是高位在前的,所以这里我们需要将fifo_data数据的高低字节互换。
下面我们看看如何去读取SD卡内的音频数据。
图 68‑16 读使能、读地址时序波形图(一)
根据波形图 68‑16为大家讲述时序关系以及控制过程。
sd_clk:我们是对sd卡内的数据进行读取,所以时钟信号我们使用sd_ctrl模块输出的时钟sd_clk。
state:状态机状态。这里借助一个状态机来帮助我们去产生读使能和读地址。
sd_init_end:sd卡初始化完成信号。我们读取sd卡内的存储的数据必须在sd卡初始化成功之后才能读取成功。
rd_en:读sd数据使能信号。当sd卡初始化成功后给第一个读使能信号开始对sd内的数据进行读取同时状态机跳转到1状态。从sd卡章节的学习我们知道,当使能信号拉高后,读忙信号(rd_busy)会在下一个时钟拉高,同时在后面的某个时刻sd卡的数据会开始读出来。那么什么时候开始给第二个读使能信号呢?首先 必须是在读忙信号(rd_busy)为低时我们给使能信号才有效,这里我们产生一个读忙信号的下降沿来进行判断,产生方法如图 68‑17所示。
图 68‑17 读忙信号下降沿产生波形图
下降沿的产生方法前面很多章节都有所讲解,在此就不再过多讲解。
下降沿产生之后,是不是可以在下降沿到来时就产生读使能信号?即读完1扇区(512个字节)之后就马上开始读取下一个扇区。这样的话读取的数据就会一直源源不断地写入fifo内,而我们读fifo数据的时钟(audio_bclk)和写fifo数据的时钟(sys_clk)频率是不同的,sys_clk的频率为50M Hz,而audio_bclk的时钟约为12MHz。所以写入fifo的数据要速率大约是读的4倍,而我们调用的fifo深度是1024,如果一直写的话将会超出fifo的存储容量,这显然是不行的。
音频数据必须源源不断地读取,才能保证播放音乐的连贯性。所以我们只需要保证fifo内一直有数据存在即可,而当fifo内剩余数据不多时我们再读出sd卡内的数据写入fifo内即可。
如波形图 68‑16所示。当读完一扇区的数据之后,跳转到状态2等待fifo内剩余数据量(fifo_data_cnt)小于512时,我们开始读取sd卡下一扇区的数据写入到fifo内,同时状态跳回状态1等待读完之后再跳到状态2等待fifo内剩余数据量小于512时拉高读使能。如此一直循环即可一直读出数据了。
fifo_data_cnt:fifo内剩余数据量可在调用IP核时输出该功能信号。
通过对读使能信号的讲解,大家是不是对读地址的时序该怎么给也有了比较清晰的思路了。首先生成一个扇区计数器(sector_cnt),计数器初始值0,当状态机从状态1跳到状态2的时让扇区计数加1;读地址的值即为音乐首扇区地址加扇区计数器的值,即rd_addr = INIT_ADDR + sector_cnt ,这样当下一个使能到来时读的就是音乐的下一个扇区的数据了。
当读到音乐的最后一个扇区时,各信号的时序关系又是如何的呢?如图 68‑18所示。
图 68‑18 读使能、读地址时序波形图(二)
如图 68‑18所示,当扇区计数器(sector_cnt)计数计到该音乐的所占扇区数时(AUDIO_STCTOR),即音乐播放完成时,让扇区计数器归0同时状态机回到状态0,开始重头播放。
以上就是该模块全部信号的时序波形图,大家跟着波形图去编写代码可达到事半功倍的效果,下面给出模块参考代码。
代码编写
参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 68‑2。
代码清单 68‑2 SD卡音乐播放控制模块参考代码(sd_play_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 | module sd_play_ctrl
#(
parameter INIT_ADDR = 'd472896 , //音乐存放起始扇区地址
parameter AUDIO_SECTOR = 'd111913 //播放的音乐占用的扇区数
)
(
input wire sd_clk , //SD卡时钟信号
input wire audio_bclk , //WM8978音频时钟
input wire sys_rst_n , //复位信号,低有效
input wire sd_init_end , //sd卡初始化完成信号
input wire rd_busy , //读操作忙信号
input wire send_done , //一次音频发送完成信号
input wire [15:0] fifo_data , //读fifo数据
input wire [10:0] fifo_data_cnt , //fifo内剩余数据量
input wire cfg_done , //寄存器配置完成信号
output reg rd_en , //数据读使能信号
output wire [31:0] rd_addr , //读数据扇区地址
output reg [15:0] dac_data //输出WM8978音频数据
);
////
//\* Parameter and Internal Signal \//
////
//reg define
reg rd_busy_d0 ; //读操作忙信号打一拍信号
reg rd_busy_d1 ; //读操作忙信号打两拍信号
reg [16:0] sector_cnt ; //读扇区计数器
reg [1:0] state ; //状态机状态
//wire define
wire rd_busy_fall ; //读操作忙信号下降沿
////
//\* Main Code \//
////
//sd卡读操作忙信号下降沿,用于控制扇区地址
assign rd_busy_fall = ~rd_busy_d0 & rd_busy_d1;
//rd_addr:读地址为首地址加扇区计数器值
assign rd_addr = INIT_ADDR + sector_cnt;
//对读忙操作信号打拍
always@(posedge sd_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
rd_busy_d0 <= 1'b0;
rd_busy_d1 <= 1'b0;
end
else
begin
rd_busy_d0 <= rd_busy;
rd_busy_d1 <= rd_busy_d0;
end
//dac_data:发送完一次数据后,将读出fifo的数据给dac_data
//WAV音乐数据格式低字节在前,而WM8978音频是从高字节输入,需换位
always@(posedge audio_bclk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
dac_data <= 16'd0;
else if(send_done == 1'b1)
dac_data <= {fifo_data[7:0],fifo_data[15:8]};
else
dac_data <= dac_data;
//生成读使能以及扇区计数器
always@(posedge sd_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
state <= 2'b0;
sector_cnt <= 17'd0;
rd_en <= 1'b0;
end
else
case(state)
2'd0:
//SD卡初始化话完成后拉高读使能信号
if(sd_init_end == 1'b1 && cfg_done == 1'b1 )
begin
rd_en <= 1'b1;
state <= state + 1'b1;
end
else
begin
rd_en <= 1'b0;
state <= 2'd0;
end
2'd1:
//扇区计数器计到音乐所占扇区数时,归0重新播放
if(sector_cnt == AUDIO_SECTOR)
begin
rd_en <= 1'b0;
sector_cnt <= 17'd0;
state <= 2'd0;
end
//一个扇区读完之后,扇区加1
else if(rd_busy_fall == 1'b1)
begin
rd_en <= 1'b0;
sector_cnt <= sector_cnt + 17'd1;
state <= state + 1'b1;
end
else
begin
rd_en <= 1'b0;
sector_cnt <= sector_cnt;
state <= 2'd1;
end
2'd2:
//当fifo内数据量低于256个时,拉高一个读使能信号并回到状态1
if(fifo_data_cnt < 11'd512)
begin
rd_en <= 1'd1;
state <= 2'd1;
end
else
begin
rd_en <= 1'd0;
state <= 2'd2;
end
default: state <= 2'b0;
endcase
endmodule
|
代码各信号的时序关系在波形图介绍时已经做了详细的讲解,这里需要注意的是,扇区计数器能计到的最大数值需大于所播放音乐占用的扇区数,若不够则需增大扇区计数器的位宽。
18.2.3.7. 顶层模块¶
模块框图
图 68‑19 SD卡音乐播放顶层模块框图
各输入输出信号描述如表格 68‑4所示。
表格 68‑4 SD卡音乐播放顶层模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_clk |
1bit |
input |
系统时钟,50MHz |
sys_rst_n |
1bit |
input |
复位信号,低有效 |
sd_miso |
1bit |
input |
SD卡主输入从输出信号 |
audio_bclk |
1bit |
input |
WM8978输出的位时钟 |
audio_lrc |
1bit |
input |
WM8978输出的数据左/右对齐时钟 |
audio_mclk |
1bit |
input |
输出WM8978主时钟,频率12MHz |
sd_clk |
1bit |
input |
SD卡时钟信号 |
sd_mosi |
1bit |
input |
SD卡主输出从输入信号 |
scl |
1bit |
input |
输出至wm8978的串行时钟信号scl |
sda |
1bit |
inout |
输出至wm8978的串行数据信号sda |
audio_dacdat |
1bit |
input |
输出DAC数据给WM8978 |
audio_sd_play顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,对应信号连接可根据系统整体框图进行连接,信号代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 68‑3。
代码编写
代码清单 68‑3 SD卡音乐播放顶层模块参考代码(audio_sd_play.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 | module audio_sd_play
(
//系统时钟复位
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //系统复位,低有效
//WM8978接口
input wire audio_bclk , //WM8978输出的位时钟
input wire audio_lrc , //WM8978输出的数据左/右对齐时钟
output wire audio_mclk , //输出WM8978主时钟,频率12MHz
output wire audio_dacdat , //输出DAC数据给WM8978
//i2c接口
output wire scl , //输出至wm8978的串行时钟信号scl
inout wire sda , //输出至wm8978的串行数据信号sda
//sd卡接口
input wire sd_miso , //主输入从输出信号
output wire sd_clk , //SD卡时钟信号
output wire sd_cs_n , //片选信号
output wire sd_mosi //主输出从输入信号
);
////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter INIT_ADDR = 'd472896 ; //音乐存放起始地址
parameter AUDIO_SECTOR = 'd111913 ; //播放的音乐占用的扇区数
//wire define
wire rst_n ; //当时钟输出不稳定时一直处于复位状态
wire init_end ; //sd卡初始化完成信号
wire rd_busy ; //sd卡读操作忙信号
wire send_done ; //一次数据发送完成信号
wire [15:0] fifo_data ; //fifo读出数据
wire [10:0] fifo_data_cnt ; //fifo内剩余数据量
wire rd_en ; //sd卡数据读使能信号
wire [31:0] rd_addr ; //sd卡读扇区地址
wire [15:0] dac_data ; //WM8978音频播放数据
wire clk_50m ; //输出50MHz时钟
wire clk_50m_shift ; //输出50MHZ,偏移90度
wire locked ; //拉高表示锁相环开始稳定输出时钟信号
wire [15:0] sd_rd_data ; //sd卡读出的音频数据
wire rd_data_en ; //写fifo使能信号
wire cfg_done ; //寄存器配置完成信号
//rst_n:复位信号
assign rst_n = sys_rst_n & locked;
////
//\* Instantiation \//
////
//------------- sd_play_ctrl_inst -------------
sd_play_ctrl
#(
.INIT_ADDR (INIT_ADDR ), //音乐存放起始地址
.AUDIO_SECTOR(AUDIO_SECTOR ) //播放的音乐占用的扇区数
)
sd_play_ctrl_inst
(
.sd_clk (sd_clk ), //SD卡时钟信号
.audio_bclk (audio_bclk ), //WM8978音频时钟
.sys_rst_n (rst_n ), //复位信号,低有效
.sd_init_end (init_end ), //sd卡初始化完成信号
.rd_busy (rd_busy ), //读操作忙信号
.send_done (send_done ), //一次音频发送完成信号
.fifo_data (fifo_data ), //fifo传来的音频数据
.fifo_data_cnt (fifo_data_cnt ), //fifo内剩余数据量
.cfg_done (cfg_done ), //寄存器配置完成信号
.rd_en (rd_en ), //数据读使能信号
.rd_addr (rd_addr ), //读数据扇区地址
.dac_data (dac_data ) //输入WM8978音频数据
);
//------------- clk_gen_inst -------------
clk_gen clk_gen_inst
(
.areset (~sys_rst_n ), //异步复位,高有效
.inclk0 (sys_clk ), //输入时钟
.c0 (audio_mclk ), //输出12MHz时钟
.c1 (clk_50m ), //输出50MHz时钟
.c2 (clk_50m_shift ), //输出50MHz时钟,偏移90°
.locked (locked ) //时钟稳定输出信号,高有效
);
//------------- fifo_data_inst -------------
fifo_data fifo_data_inst
(
.data (sd_rd_data ) , //写入fifo数据
.rdclk (audio_bclk ) , //读fifo时钟
.rdreq (send_done ) , //读fifo数据使能信号
.wrclk (sd_clk ) , //写fifo时钟信号
.wrreq (rd_data_en ) , //写fifo使能信号
.q (fifo_data ) , //读出fifo数据
.wrusedw(fifo_data_cnt) //fifo内剩余数据量
);
//------------- 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(), //WM8978ADC数据输出
.dac_data (dac_data ), //输入音频数据
.scl (scl ), //输出至wm8978的串行时钟信号scl
.audio_dacdat(audio_dacdat), //输出DAC数据给WM8978
.rcv_done (), //一次数据接收完成
.send_done (send_done ), //一次数据发送完成
.adc_data (), //输出音频数据
.cfg_done (cfg_done ), //寄存器配置完成信号
.sda (sda ) //输出至wm8978的串行数据信号sda
);
//------------- sd_ctrl_inst -------------
sd_ctrl sd_ctrl_inst
(
.sys_clk (clk_50m ), //输入工作时钟,频率50MHz
.sys_clk_shift (clk_50m_shift), //输入工作时钟,频率50MHz,相位偏移90度
.sys_rst_n (rst_n ), //输入复位信号,低电平有效
//SD卡接口
.sd_miso (sd_miso ), //主输入从输出信号
.sd_clk (sd_clk ), //SD卡时钟信号
.sd_cs_n (sd_cs_n ), //片选信号
.sd_mosi (sd_mosi ), //主输出从输入信号
//写SD卡接口(由于没有往sd卡内写入数据,故输入端口接0即可,输出端口可不接)
.wr_en (0), //数据写使能信号
.wr_addr (0), //写数据扇区地址
.wr_data (0), //写数据
.wr_busy (), //写操作忙信号
.wr_req (), //写数据请求信号
//读SD卡接口
.rd_en (rd_en ), //数据读使能信号
.rd_addr (rd_addr ), //读数据扇区地址
.rd_busy (rd_busy ), //读操作忙信号
.rd_data_en (rd_data_en ), //读数据标志信号
.rd_data (sd_rd_data ), //读数据
.init_end (init_end ) //SD卡初始化完成信号
);
endmodule
|
代码中WM8978_ctrl模块与sd_ctrl模块中部分输入端口我们接了0,输出端口没有接,那是因为我们只需要用WM8978来播放音频而所以我们FPGA只需输出音频数据即可,而不用接受WM8978发送的音频;SD卡的话我们只需从SD卡中读出音乐而不用往SD卡中写入数据,所以sd_ctrl写端口我们 就不需要用到。
18.2.3.8. RTL视图¶
实验工程通过仿真验证后,使用Quartus软件对实验工程进行编译,编译完成后,我们查看一下RTL视图,RTL视图展示信息与顶层模块框图一致,如图 68‑20所示。
图 68‑20 audio_sd_play顶层RTL视图
18.2.3.9. SignalTap波形抓取¶
这里我们利用quartus软件自带的SignalTap工具对波形进行实时抓取来验证我们的设计是否正确,如下图所示。
图 68‑21 SignalTap抓取波形图(一)
如图 68‑21所示为抓取的开头波形文件,由图可以看到当SD卡初始化成功后(sd_init_end=1),读使能信号(rd_en)拉高,读忙信号(rd_busy)跟着会被拉高,同时状态跳转到状态1,其余值0。这与我们所绘制的波形图是一致的,能达到我们的设计要求。
图 68‑22 SignalTap抓取波形图(二)
图 68‑22抓取的是状态机第一次从状态1跳转到状态2的波形图,可以看到我们所设计的波形图是一致的,这里要注意的是我们的dac_data是等于send_done上升沿前一个状态的fifo_data高低字节互换值,大家别混淆了。
图 68‑23 SignalTap抓取波形图(三)
如图 68‑23抓取的是音乐播放结束时的波形图,也就是状态跳回状态0的波形图。可以看到扇区计数器计到111913时表示音乐最后的一个扇区也已经读完,此时扇区计数器跳回0,开始重头播放,与我们绘制的波形图是一致的,这说明我们的设计是正确的。
18.3. 上板调试¶
18.3.1. 引脚约束¶
仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 68‑5所示。
表格 68‑5 引脚分配表
信号名 |
信号类型 |
对应引脚 |
备注 |
---|---|---|---|
sys_clk |
input |
E1 |
时钟 |
sys_rst_n |
input |
M15 |
复位 |
audio_bclk |
input |
D12 |
音频位时钟 |
audio_lrc |
input |
E9 |
音频对齐时钟 |
audio_mclk |
output |
D14 |
音频主时钟 |
audio_dacdat |
output |
D11 |
音频DAC数据 |
scl |
output |
P15 |
I2C时钟线 |
sda |
inout |
N14 |
I2C数据线 |
sd_miso |
input |
J16 |
SD卡主输入从输出信号 |
sd_clk |
output |
J12 |
SD卡时钟信号 |
sd_cs_n |
output |
K12 |
SD卡片选信号 |
sd_mosi |
output |
J14 |
SD卡主输出从输入信号 |
下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 68‑24所示。
图 68‑24 管脚分配
18.3.1.1. 结果验证¶
管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源、下载线和耳机或者喇叭与开发板连接好,插入SD卡,随后对板卡上电,如图 68‑25所示。
图 68‑25 下载连线图
打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 68‑26所示。
图 68‑26 下载成功界面
下载成功后即可以开始验证了。下载成功后若耳机以及喇叭中正在播放SD卡中的音乐,则说明验证成功。
18.4. 章末总结¶
通过该章节的学习相信大家对音频有了更深一步的了解,为大家以后学习用不同的方式控制不同的音频格式的读写打下一个扎实的基础。
18.5. 拓展训练¶
更改代码,加入按键信号,使用按键去控制音乐的播放与停止。