第四十八章 音乐播放器实验
[mw_shl_code=c,true]1.硬件平台:正点原子探索者STM32F407开发板
2.软件平台:MDK5.1
3.固件库版本:V1.4.0[/mw_shl_code]
ALIENTEK探索者STM32F4开发板拥有全双工I2S,且外扩了一颗HIFI级CODEC芯片:WM8978G,支持最高192K 24BIT的音频播放,并且支持录音(下一章介绍)。本章,我们将利用探索者STM32F4开发板实现一个简单的音乐播放器(仅支持WAV播放)。本章分为如下几个部:
48.1 WAV&WM8978&I2S简介
48.2 硬件设计
48.3 软件设计
48.4 下载验证
48.1 WAV&WM8978&I2S简介
本章新知识点比较多,包括:WAV、WM8978和I2S等三个知识点。下面我们将分别向大家介绍。
48.1.1 WAV简介
WAV即WAVE文件,WAV是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),由于其扩展名为"*.wav"。它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!
WAV一般采用线性PCM(脉冲编码调制)编码,本章,我们也主要讨论PCM的播放,因为这个最简单。
WAV是由若干个Chunk组成的。按照在文件中的出现位置包括:RIFF WAVE Chunk、 Format Chunk、 Fact Chunk(可选)和 Data
Chunk。每个Chunk由块标识符、数据大小和数据三部分组成,如图48.1.1.1所示:
图48.1.1.1 Chunk结构示意图
其中块标识符由4个ASCII码构成,数据大小则标出紧跟其后的数据的长度(单位为字节),注意这个长度不包含块标识符和数据大小的长度,即不包含最前面的8个字节。所以实际Chunk的大小为数据大小加8。
首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:
//RIFF块
typedef __packed struct
{
u32
ChunkID; //chunk
id;这里固定为"RIFF",即0X46464952
u32
ChunkSize ; //集合大小;文件总大小-8
u32
Format; //格式;WAVE,即0X45564157
}ChunkRIFF ;
接着,我们看看Format块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:
//fmt块
typedef __packed struct
{
u32
ChunkID; //chunk
id;这里固定为"fmt ",即0X20746D66
u32
ChunkSize ; //子集合大小(不包括ID和Size);这里为:20.
u16
AudioFormat; //音频格式;0X10,表示线性PCM;0X11表示IMA ADPCM
u16
NumOfChannels; //通道数量;1,表示单声道;2,表示双声道;
u32
SampleRate; //采样率;0X1F40,表示8Khz
u32
ByteRate; //字节速率;
u16
BlockAlign; //块对齐(字节);
u16
BitsPerSample; //单个采样数据大小;4位ADPCM,设置为4
}ChunkFMT;
接下来,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:
//fact块
typedef __packed struct
{
u32
ChunkID; //chunk id;这里固定为"fact",即0X74636166;
u32
ChunkSize ; //子集合大小(不包括ID和Size);这里为:4.
u32 DataFactSize; //数据转换为PCM格式后的大小
}ChunkFACT;
DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。
最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”'作为该Chunk的标示,然后是数据的大小。数据块的Chunk结构如下:
//data块
typedef __packed struct
{
u32
ChunkID; //chunk
id;这里固定为"data",即0X61746164
u32
ChunkSize ; //子集合大小(不包括ID和Size);文件大小-60.
}ChunkDATA;
ChunkSize后紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如表48.1.1.1所示的几种形式:
单声道
|
取样1
|
取样2
|
取样3
|
取样4
|
取样5
|
取样6
|
8位量化
|
声道0
|
声道0
|
声道0
|
声道0
|
声道0
|
声道0
|
双声道
|
取样1
|
取样2
|
取样3
|
8位量化
|
声道0(左)
|
声道1(右)
|
声道0(左)
|
声道1(右)
|
声道0(左)
|
声道1(右)
|
单声道
|
取样1
|
取样2
|
取样3
|
16位量化
|
声道0
(低字节)
|
声道0
(高字节)
|
声道0
(低字节)
|
声道0
(高字节)
|
声道0
(低字节)
|
声道0
(高字节)
|
双声道
|
取样1
|
取样2
|
16位量化
|
声道0
|
声道0
|
声道1
|
声道1
|
声道0
|
声道0
|
(低字节)
|
(高字节)
|
(低字节)
|
(高字节)
|
(低字节)
|
(高字节)
|
单声道
|
取样1
|
取样2
|
24位量化
|
声道0
(低字节)
|
声道0
(中字节)
|
声道0
(高字节)
|
声道0
(低字节)
|
声道0
(中字节)
|
声道0
(高字节)
|
双声道
|
取样1
|
24位量化
|
声道0
|
声道0
|
声道0
|
声道1
|
声道1
|
声道1
|
(低字节)
|
(中字节)
|
(高字节)
|
(低字节)
|
(中字节)
|
(高字节)
|
表48.1.1.1
WAVE文件数据采样格式
本章,我们播放的音频支持:16位和24位,立体声,所以每个取样为4/6个字节,低字节在前,高字节在后。在得到这些wav数据以后,通过I2S丢给WM8978,就可以欣赏音乐了。
48.1.2 WM8978简介
WM8978是欧胜(Wolfson)推出的一款全功能音频处理器。它带有一个HI-FI级数字信号处理内核,支持增强3D硬件环绕音效,以及5频段的硬件均衡器,可以有效改善音质;并有一个可编程的陷波滤波器,用以去除屏幕开、切换等噪音。
WM8978同样集成了对麦克风的支持,以及用于一个强悍的扬声器功放,可提供高达900mW的高质量音响效果扬声器功率。
一个数字回放限制器可防止扬声器声音过载。WM8978进一步提升了耳机放大器输出功率,在推动16欧姆耳机的时候,每声道最大输出功率高达40毫瓦!可以连接市面上绝大多数适合随身听的高端HI-FI耳机。
WM8988的主要特性有:
●I2S接口,支持最高192K,24bit音频播放
●DAC信噪比98dB;ADC信噪比90dB
●支持无电容耳机驱动(提供40mW@16Ω的输出能力)
●支持扬声器输出(提供0.9W@8Ω的驱动能力)
●支持立体声差分输入/麦克风输入
●支持左右声道音量独立调节
●支持3D效果,支持5路EQ调节
WM8978的控制通过I2S接口(即数字音频接口)同MCU进行音频数据传输(支持音频接收和发送),通过两线(MODE=0,即IIC接口)或三线(MODE=1)接口进行配置。WM8978的I2S接口,由4个引脚组成:
1,ADCDAT:ADC数据输出
2,DACDAT:DAC数据输入
3,LRC:数据左/右对齐时钟
4,BCLK:位时钟,用于同步
WM8978可作为I2S主机,输出LRC和BLCK时钟,不过我们一般使用WM8978作为从机,接收LRC和BLCK。另外,WM8978的I2S接口支持5中不同的音频数据模式:左(MSB)对齐标准、右(LSB)对齐标准、飞利浦(I2S)标准、DSP模式A和DSP模式B。本章,我们用飞利浦标准来传输I2S数据。
飞利浦(I2S)标准模式,数据在跟随LRC传输的BCLK的第二个上升沿时传输MSB,其他位一直到LSB按顺序传输。传输依赖于字长、BCLK频率和采样率,在每个采样的LSB和下一个采样的MSB之间都应该有未用的BCLK周期。飞利浦标准模式的I2S数据传输协议如图48.1.2.1所示:
图48.1.2.1 飞利浦标准模式I2S数据传输图
图中,fs即音频信号的采样率,比如44.1Khz,因此可以知道,LRC的频率就是音频信号的采样率。另外,WM8978还需要一个MCLK,本章我们采用STM32F4为其提供MCLK时钟,MCLK的频率必须等于256fs,也就是音频采样率的256倍。
WM8978的框图如图48.1.2.2所示:
图48.1.2.2
WM8978框图
从上图可以看出,WM8978内部有很多的模拟开关,用来选择通道,同时还有很多调节器,用来设置增益和音量。
本章,我们通过IIC接口(MODE=0)连接WM8978,不过WM8978的IIC接口比较特殊:1,只支持写,不支持读数据;2,寄存器长度为7位,数据长度为9位。3,寄存器字节的最低位用于传输数据的最高位(也就是9位数据的最高位,7位寄存器的最低位)。WM8978的IIC地址固定为:0X1A。关于WM8978的IIC详细介绍,请看其数据手册第77页。
这里我们简单介绍一下要正常使用WM8978来播放音乐,应该执行哪些配置。
1,寄存器R0(00h),该寄存器用于控制WM8978的软复位,写任意值到该寄存器地址,即可实现软复位WM8978。
2,寄存器R1(01h),该寄存器主要要设置BIASEN(bit3),该位设置为1,模拟部分的放大器才会工作,才可以听到声音。
3,寄存器R2(02h),该寄存器要设置ROUT1EN(bit8),LOUT1EN(bit7)和SLEEP(bit6)等三个位,ROUT1EN和LOUT1EN,设置为1,使能耳机输出,SLEEP设置为0,进入正常工作模式。
4,寄存器R3(03h),该寄存器要设置LOUT2EN(bit6),ROUT2EN(bit5),RMIXER(bit3),LMIXER(bit2),DACENR(bit1)和DACENL(bit0)等6个位。LOUT2EN和ROUT2EN,设置为1,使能喇叭输出;LMIXER和RMIXER设置为1,使能左右声道混合器;DACENL和DACENR则是使能左右声道的DAC了,必须设置为1。
5,寄存器R4(04h),该寄存器要设置WL(bit6:5)和FMT(bit4:3)等4个位。WL(bit6:5)用于设置字长(即设置音频数据有效位数),00表示16位音频,10表示24位音频;FMT(bit4:3)用于设置I2S音频数据格式(模式),我们一般设置为10,表示I2S格式,即飞利浦模式。
6,寄存器R6(06h),该寄存器我们直接全部设置为0即可,设置MCLK和BCLK都来自外部,即由STM32F4提供。
7,寄存器R10(0Ah),该寄存器我们要设置SOFTMUTE(bit6)和DACOSR128(bit3)等两个位,SOFTMUTE设置为0,关闭软件静音;DACOSR128设置为1,DAC得到最好的SNR。
8,寄存器R43(2Bh),该寄存器我们只需要设置INVROUT2为1即可,反转ROUT2输出,更好的驱动喇叭。
9,寄存器R49(31h),该寄存器我们要设置SPKBOOST(bit2)和TSDEN(bit1)这两个位。SPKBOOST用于设置喇叭的增益,我们默认设置为0就好了(gain=-1),如想获得更大的声音,设置为1(gain=+1.5)即可;TSDEN用于设置过热保护,设置为1(开启)即可。
10,寄存器R50(32h)和R51(33h),这两个寄存器设置类似,一个用于设置左声道(R50),另外一个用于设置右声道(R51)。我们只需要设置这两个寄存器的最低位为1即可,将左右声道的DAC输出接入左右声道混合器里面,才能在耳机/喇叭听到音乐。
11,寄存器R52(34h)和R53(35h),这两个寄存器用于设置耳机音量,同样一个用于设置左声道(R52),另外一个用于设置右声道(R53)。这两个寄存器的最高位(HPVU)用于设置是否更新左右声道的音量,最低6位用于设置左右声道的音量,我们可以先设置好两个寄存器的音量值,最后设置其中一个寄存器最高位为1,即可更新音量设置。
12,寄存器R54(36h)和R55(37h),这两个寄存器用于设置喇叭音量,同R52,R53设置一模一样,这里就不细说了。
以上,就是我们用WM8978播放音乐时的设置,按照以上所述,对各个寄存器进行相应的配置,即可使用WM8978正常播放音乐了。还有其他一些3D设置,EQ设置等,我们这里就不再介绍了,大家参考WM8978的数据手册自行研究下即可。
48.1.3 I2S简介
I2S(Inter IC Sound)总线, 又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专责于音频设备之间的数据传输,广泛应用于各种多媒体系统。它采用了沿独立的导线传输时钟与数据信号的设计,通过将数据和时钟信号分离,避免了因时差诱发的失真,为用户节省了购买抵抗音频抖动的专业设备的费用。
STM32F4自带了2个全双工I2S接口,其特点包括:
●支持全双工/半双工通信
●主持主/从模式设置
●8位可编程线性预分频器,可实现精确的音频采样频率(8~192Khz)
●支持16位/24位/32位数据格式
●数据包帧固定为16位(仅16位数据帧)或32位(可容纳16/24/32位数据帧)
●可编程时钟极性
●支持MSB对齐(左对齐)、LSB对齐(右对齐)、飞利浦标准和PCM标准等I2S协议
●支持DMA数据传输(16位宽)
●数据方向固定位MSB在前
●支持主时钟输出(固定为256*fs,fs即音频采样率)
STM32F4的I2S框图如图48.1.3.1所示:
图48.1.3.1 I2S框图
STM32F4的I2S是与SPI部分共用的,通过设置SPI_I2SCFGR寄存器的I2SMOD位即可开启I2S功能,I2S接口使用了几乎与SPI相同的引脚、标志和中断。
I2S用到的信号有:
1,SD:串行数据(映射到 MOSI 引脚),用于发送或接收两个时分复用的数据通道上的数据(仅半双工模式)。
2,WS:字选择(映射到NSS引脚),即帧时钟,用于切换左右声道的数据。WS频率等于音频信号采样率(fs)。
3,CK:串行时钟(映射到SCK引脚),即位时钟,是主模式下的串行时钟输出以及从模式下的串行时钟输入。CK频率=WS频率(fs)*2*16(16位宽),如果是32位宽,则是:CK频率=WS频率(fs)*2*32(32位宽)
4,I2S2ext_SD和I2S3ext_SD:用于控制I2S全双工模式的附加引脚(映射到MISO引脚)。
5,MCK:即主时钟输出,当I2S配置为主模式(并且SPI_I2SPR寄存器中的MCKOE位置1)时,使用此时钟,该时钟输出频率 256×fs,fs即音频信号采样频率(fs)。
为支持I2S全双工模式,除了I2S2和I2S3,还可以使用两个额外的I2S,它们称为扩展I2S(I2S2_ext、I2S3_ext),如图48.1.3.2:
图48.1.3.2
I2S全双工框图
因此,第一个I2S全双工接口基于I2S2和I2S2_ext,第二个基于I2S3和I2S3_ext。注意:I2S2_ext和I2S3_ext仅用于全双工模式。
I2Sx可以在主模式下工作。因此:
1,只有I2Sx可在半双工模式下输出SCK和WS
2,只有I2Sx可在全双工模式下向I2S2_ext和I2S3_ext提供SCK和WS。
扩展I2S (I2Sx_ext)只能用于全双工模式。I2Sx_ext始终在从模式下工作。I2Sx和I2Sx_ext 均可用于发送和接收。
STM32F4的I2S支持4种数据和帧格式组合,分别是:1,将16位数据封装在16位帧中;2,将16位数据封装在32位帧中;3,将24位数据封装在32位帧中;4,将32位数据封装在32位帧中。
将16位数据封装在32位帧中时,前16位(MSB)为有效位,16位LSB被强制清零,无需任何软件操作或DMA请求(只需一个读/写操作)。如果应用程序首选DMA,则24位和32位数据帧需要对SPI_DR执行两次CPU读取或写入操作,或者需要两次DMA操作。24位的数据帧,硬件会将8位非有效位扩展到带有0位的32位。
对于所有数据格式和通信标准而言,始终会先发送最高有效位(MSB优先)。
STM32F4的I2S支持:MSB对齐(左对齐)标准、LSB对齐(右对齐)标准、飞利浦标准和PCM标准等4种音频标准,本章我们用飞利浦标准,仅针对该标准进行介绍,其他的请大家参考《STM32F4xx中文参考手册》第27.4节。
I2S飞利浦标准,使用WS信号来指示当前正在发送的数据所属的通道。该信号从当前通道数据的第一个位(MSB)之前的一个时钟开始有效。发送方在时钟信号(CK)的下降沿改变数据,接收方在上升沿读取数据。WS信号也在CK的下降沿变化。这和我们48.1.2节介绍的是一样的。
本章我们使用16位/24位数据格式,16位时采用扩展帧格式(即将16位数据封装在32位帧中),以24位帧为例,I2S波形(飞利浦标准)如图48.1.3.3所示:
图48.1.3.3
I2S飞利浦标准24位帧格式波形
这个图和图48.1.2.1是一样的时序,在24位模式下数据传输,需要对SPI_DR执行两次读取或写入操作。比如我们要发送0X8EAA33这个数据,就要分两次写入SPI_DR,第一次写入:0X8EAA,第二次写入0X33xx(xx可以为任意数值),这样就把0X8EAA33发送出去了。
顺便说一下SD卡读取到的24位WAV数据流,是低字节在前,高字节在后的,比如,我们读到一个声道的数据(24bit),存储在buf[3]里面,那么要通过SPI_DR发送这个24位数据,过程如下:
SPI_DR=((u16)buf[2]<<8)+buf[1];
SPI_DR=(u16)buf[0]<<8;
这样,第一次发送高16为数据,第二次发送低8位数据,完成一次24bit数据的发送。
接下来,我们介绍下STM32F4的I2S时钟发生器,其架构如图48.1.3.4所示:
图48.1.3.4 I2S时钟发生器架构
图中I2SxCLK可以来自PLLI2S输出(通过R系数分频)或者来自外部时钟(I2S_CKIN引脚),一般我们使用前者作为I2SxCLK输入时钟。
一般我们需要根据音频采样率(fs,即CK的频率)来计算各个分频器的值,常用的音频采样率有:22.05Khz、44.1Khz、48Khz、96Khz、196Khz等。
根据是否使能MCK输出,fs频率的计算公式有2种情况。不过,本章只考虑MCK输出使能时的情况,当MCK输出使能时,fs频率计算公式如下:
fs=I2SxCLK/[256*(2*I2SDIV+ODD)]
其中:I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR。HSE我们是8Mhz,而pllm在系统时钟初始化就确定了,是8,这样结合以上2式,可得计算公式如下:
fs= (1000*PLLI2SN/PLLI2SR )/[256*(2*I2SDIV+ODD)]
fs单位是:Khz。其中:PLL2SN取值范围:192~432;PLLI2SR取值范围:2~7;I2SDIV取值范围:2~255;ODD取值范围:0/1。根据以上约束条件,我们便可以根据fs来设置各个系数的值了,不过很多时候,并不能取得和fs一模一样的频率,只能近似等于fs,比如44.1Khz采样率,我们设置PLL2SN=271,PLL2SR=2,I2SDIV=6,ODD=0,得到fs=44.108073Khz,误差为:0.0183%。晶振频率决定了有时无法通过分频得到我们所要的fs,所以,某些fs如果要实现0误差,大家必须得选用外部时钟才可以。
如果要通过程序去计算这些系数的值,是比较麻烦的,所以,我们事先计算好常用fs对应的系数值,建立一个表,这样,用的时候,只需要查表取值就可以了,大大简化了代码,常用fs对应系数表如下:
//表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD
const u16 I2S_PSC_TBL[][5]=
{
{800
,256,5,12,1}, //8Khz采样率
{1102,429,4,19,0}, //11.025Khz采样率
{1600,213,2,13,0}, //16Khz采样率
{2205,429,4,
9,1}, //22.05Khz采样率
{3200,213,2,
6,1}, //32Khz采样率
{4410,271,2,
6,0}, //44.1Khz采样率
{4800,258,3,
3,1}, //48Khz采样率
{8820,316,2,
3,1}, //88.2Khz采样率
{9600,344,2,
3,1}, //96Khz采样率
{17640,361,2,2,0}, //176.4Khz采样率
{19200,393,2,2,0}, //192Khz采样率
};
有了上面的fs-系数对应表,我们可以很方便的完成I2S的时钟配置。
接下来,我们看看本章需要用到的一些相关寄存器。
首先,是SPI_I2S配置寄存器:SPI_I2SCFGR,该寄存器各位描述如图48.1.3.5所示:
图48.1.3.5 寄存器SPI_I2SCFGR各位描述
I2SMOD位,设置为1,选择I2S模式,注意,必须在I2S/SPI禁止的时候,设置该位。
I2SE位,设置为1,使能I2S外设,该位必须在I2SMOD位设置之后再设置。
I2SCFG[1:0]位, 这两个位用于配置I2S模式,设置为10,选择主模式(发送)。
I2SSTD[1:0]位,这两个位用于选择I2S标准,设置为00,选择飞利浦模式。
CKPOL位,用于设置空闲时时钟电平,设置为0,空闲时时钟低电平。
DATLEN[1:0]位,用于设置数据长度,00,表示16位数据;01表示24位数据。
CHLEN位,用于设置通道长度,即帧长度,0,表示16位;1,表示32位。
第二个是SPI_I2S预分配器寄存器:SPI_I2SPR,该寄存器各位描述如图48.1.3.6所示:
图48.1.3.6 寄存器SPI_
I2SPR各位描述
本章我们设置MCKOE为1,开启MCK输出,ODD和I2SDIV则根据不同的fs,查表进行设置。
第三个是PLLI2S配置寄存器:RCC_PLLI2SCFGR,该寄存器各位描述如图48.1.3.7所示:
图48.1.3.7 寄存器RCC_
PLLI2SCFGR各位描述
该寄存器用于配置PLLI2SR和PLLI2SN两个系数,PLLI2SR的取值范围是:2~7,PLLI2SN的取值范围是:192~432。同样,这两个也是根据fs的值来设置的。
此外,还要用到SPI_CR2寄存器的bit1位,设置I2S TX DMA数据传输,SPI_DR寄存器用于传输数据,本章用DMA来传输,所以直接设置DMA的外设地址位SPI_DR即可。
最后,我们看看要通过STM32F4的I2S,驱动WM8978播放音乐的简要步骤。这里需要说明一下,I2S相关的库函数申明和定义跟SPI是同文件的,在stm32f4xx_spi.c以及头文件stm32f4xx_spi.h中。具体步骤如下:
1)初始化WM8978
这个过程就是在48.1.2节最后那十几个寄存器的配置,包括软复位、DAC设置、输出设置和音量设置等。在我们实验工程中是在文件wm8978.c中,大家可以打开实验工程参考。
2)初始化I2S
此过程主要设置SPI_I2SCFGR寄存器,设置I2S模式、I2S标准、时钟空闲电平和数据帧长等,最后开启I2S
TX DMA,使能I2S外设。
在库函数中初始化I2S调用的函数为:
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef*
I2S_InitStruct);
第一个参数比较好理解,我们来着重看下第二个参数,这里我们主要讲解结构体I2S_InitTypeDef各个成员变量的含义。结构体I2S_InitTypeDef的定义为:
typedef struct
{
uint16_t
I2S_Mode;
uint16_t
I2S_Standard;
uint16_t
I2S_DataFormat;
uint16_t
I2S_MCLKOutput;
uint32_t
I2S_AudioFreq;
uint16_t
I2S_CPOL;
}I2S_InitTypeDef;
第一个参数用来设置I2S的模式,也就是设置SPI_I2SCFGR寄存器的I2SCFG相关位。可以配置为主模式发送I2S_Mode_MasterTx,主模式接受I2S_Mode_MasterRx,从模式发送I2S_Mode_SlaveTx以及从模式接受I2S_Mode_SlaveRx四种模式。
第二个参数I2S_Standard用来设置I2S标准,这个前面已经讲解过。可以设置为:飞利浦标准I2S_Standard_Phillips,MSB对齐标准I2S_Standard_MSB,LSB对齐标准I2S_Standard_LSB以及PCM标准I2S_Standard_PCMShort。
第三个参数I2S_DataFormat用来设置I2S的数据通信格式。这里实际包含设置SPI_I2SCFGR寄存器的HCLEN位(通道长度)以及DATLEN位(传输的数据长度)。当我们设置为16位标准格式I2S_DataFormat_16b的时候,实际上传输的数据长度为16位,通道长度为16位。当我们设置为其他值的时候,通道长度都为32位。
第四个参数I2S_MCLKOutput用来设置是否使能主时钟输出。我们实验会使能主时钟输出。
第五个参数I2S_AudioFreq用来设置I2S频率。实际根据输入的频率值,会来计算SPI预分频寄存器SPI_I2SPR的预分频奇数因子以及I2S线性预分频器的值。这里支持10中频率:
#define I2S_AudioFreq_192k ((uint32_t)192000)
#define I2S_AudioFreq_96k ((uint32_t)96000)
#define I2S_AudioFreq_48k ((uint32_t)48000)
#define I2S_AudioFreq_44k ((uint32_t)44100)
#define I2S_AudioFreq_32k ((uint32_t)32000)
#define I2S_AudioFreq_22k ((uint32_t)22050)
#define I2S_AudioFreq_16k ((uint32_t)16000)
#define I2S_AudioFreq_11k ((uint32_t)11025)
#define I2S_AudioFreq_8k ((uint32_t)8000)
#define I2S_AudioFreq_Default ((uint32_t)2)
第六个参数I2S_CPOL用来设置空闲状态时钟电平,这个比较好理解。取值为高电平I2S_CPOL_High以及低电平I2S_CPOL_Low。
3)解析WAV文件,获取音频信号采样率和位数并设置I2S时钟分频器
这里,要先解析WAV文件,取得音频信号的采样率(fs)和位数(16位或32位),根据这两个参数,来设置I2S的时钟分频,这里我们用前面介绍的查表法来设置即可。这是我们单独写了一个设置频率的函数为I2S2_SampleRate_Set,我们后面程序章节会讲解。
4)设置DMA
I2S播放音频的时候,一般都是通过DMA来传输数据的,所以必须配置DMA,本章我们用I2S2,其TX是使用的DMA1数据流4的通道0来传输的。并且,STM32F4的DMA具有双缓冲机制,这样可以提高效率,大大方便了我们的数据传输,本章将DMA1数据流4设置为:双缓冲循环模式,外设和存储器都是16位宽,并开启DMA传输完成中断(方便填充数据)。DMA具体配置过程请参考我们光盘工程代码,前面DMA实验我们已经讲解过DMA相关配置过程。
5)编写DMA传输完成中断服务函数
为了方便填充音频数据,我们使用DMA传输完成中断,每当一个缓冲数据发送完后,硬件自动切换为下一个缓冲,同时进入中断服务函数,填充数据到发送完的这个缓冲。过程如图48.1.3.8所示:
图
48.1.3.8 DMA双缓冲发送音频数据流框图
6)开启DMA传输,填充数据
最后,我们就只需要开启DMA传输,然后及时填充WAV数据到DMA的两个缓存区即可。此时,就可以在WM8978的耳机和喇叭通道听到所播放音乐了。操作方法为:
DMA_Cmd(DMA1_Stream4,ENABLE);//开启DMA TX传输,开始播放
48.2 硬件设计
本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始循环播放SD卡MUSIC文件夹里面的歌曲(必须在SD卡根目录建立一个MUSIC文件夹,并存放歌曲(仅支持wav格式)在里面),在TFTLCD上显示歌曲名字、播放时间、歌曲总时间、歌曲总数目、当前歌曲的编号等信息。KEY0用于选择下一曲,KEY2用于选择上一曲,KEY_UP用来控制暂停/继续播放。DS0还是用于指示程序运行状态。
本实验用到的资源如下:
1) 指示灯DS0
2) 三个按键(KEY_UP/KEY0/KEY1)
3) 串口
4) TFTLCD模块
5) SD卡
6) SPI FLASH
7) WM8978
8) I2S2
这些硬件我们都已经介绍过了,不过WM8978和STM32F4的连接,还没有介绍,连接如图48.2.1所示:
图48.2.1
WM8978与STM32F4连接原理图
图中,PHONE接口,可以用来插耳机,P1接口,可以外接喇叭(1W@8Ω,需自备)。硬件上,IIC接口和24C02,MPU6050等共用,另外I2S_MCLK和DCMI_D0共用,所以I2S和DCMI不可以同时使用。
本实验,大家需要准备1个SD卡(在里面新建一个MUSIC文件夹,并存放一些wav歌曲在MUSIC文件夹下)和一个耳机(或喇叭),分别插入SD卡接口和耳机接口(喇叭接P1接口),然后下载本实验就可以通过耳机来听歌了。
48.3 软件设计
打开本章实验工程目录可以看到,我们在工程根目录文件夹下新建APP和AUDIOCODEC两个文件夹。在APP文件夹里面新建了audioplay.c和audioplay.h两个文件。在AUDIOCODEC文件夹里面新建了wav文件夹,然后在其中新建了wavplay.c和wavplay.h两个文件。同时,我们把相关的源文件引入工程相应分组,同时将APP和wav文件夹加入头文件包含路径。
然后,我们在HARDWARE文件夹下新建了WM8978和I2S两个文件夹,在WM8978文件夹里面新建了wm8978.c和wm8978.h两个文件,在I2S文件夹里面新建了i2s.c和i2s.h两个文件。 最后将wm8978.c和i2s.c添加到工程HARDWARE组下。同时相应的头文件加入到PATH中。
本章代码比较多,我们就不全部贴出来给大家介绍了,这里仅挑一些重点函数给大家介绍下。首先是i2s.c里面,重点函数代码如下:
//I2S2初始化
//参数I2S_Standard: @ref SPI_I2S_Standard I2S标准,
//参数I2S_Mode: @ref SPI_I2S_Mode
//参数I2S_Clock_Polarity @ref SPI_I2S_Clock_Polarity:
//参数I2S_DataFormat:@ref SPI_I2S_Data_Format :
void I2S2_Init(u16 I2S_Standard,u16 I2S_Mode,u16
I2S_Clock_Polarity,u16 I2S_DataFormat)
{
I2S_InitTypeDef I2S_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,
ENABLE);//使能SPI2时钟
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,ENABLE);
//复位SPI2
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,DISABLE);//结束复位
I2S_InitStructure.I2S_Mode=I2S_Mode;//IIS模式
I2S_InitStructure.I2S_Standard=I2S_Standard;//IIS标准
I2S_InitStructure.I2S_DataFormat=I2S_DataFormat;//IIS数据长度
I2S_InitStructure.I2S_MCLKOutput=I2S_MCLKOutput_Disable;//主时钟输出禁止
I2S_InitStructure.I2S_AudioFreq=I2S_AudioFreq_Default;//IIS频率设置
I2S_InitStructure.I2S_CPOL=I2S_Clock_Polarity;//空闲状态时钟电平
I2S_Init(SPI2,&I2S_InitStructure);//初始化IIS
SPI_I2S_DMACmd(SPI2,SPI_I2S_DMAReq_Tx,ENABLE);//SPI2
TX DMA请求使能.
I2S_Cmd(SPI2,ENABLE);//SPI2 I2S EN使能.
} //采样率计算公式:Fs=I2SxCLK/[256*(2*I2SDIV+ODD)]
//I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR
//一般HSE=8Mhz
//pllm:在Sys_Clock_Set设置的时候确定,一般是8
//PLLI2SN:一般是192~432
//PLLI2SR:2~7
//I2SDIV:2~255
//ODD:0/1
//I2S分频系数表@pllm=8,HSE=8Mhz,即vco输入频率为1Mhz
//表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD
const u16 I2S_PSC_TBL[][5]=
{
……//省略部分代码,见48.1.3节介绍
};
//设置IIS的采样率(@MCKEN)
//samplerate:采样率,单位:Hz
//返回值:0,设置成功;1,无法设置.
u8 I2S2_SampleRate_Set(u32 samplerate)
{
u8
i=0;
u32
tempreg=0;
samplerate/=10;//缩小10倍
for(i=0;i<(sizeof(I2S_PSC_TBL)/10);i++)//看看改采样率是否可以支持
{
if(samplerate==I2S_PSC_TBL[0])break;
}
RCC_PLLI2SCmd(DISABLE);//先关闭PLLI2S
if(i==(sizeof(I2S_PSC_TBL)/10))return
1;//搜遍了也找不到
RCC_PLLI2SConfig((u32)I2S_PSC_TBL[1],(u32)I2S_PSC_TBL[2]);
//设置I2SxCLK的频率(x=2) 设置PLLI2SN
PLLI2SR
RCC->CR|=1<<26; //开启I2S时钟
while((RCC->CR&1<<27)==0); //等待I2S时钟开启成功.
tempreg=I2S_PSC_TBL[3]<<0; //设置I2SDIV
tempreg|=I2S_PSC_TBL[3]<<8; //设置ODD位
tempreg|=1<<9; //使能MCKOE位,输出MCK
SPI2->I2SPR=tempreg; //设置I2SPR寄存器
return
0;
}
//I2S2 TX DMA配置
//设置为双缓冲模式,并开启DMA传输完成中断
//buf0:M0AR地址.
//buf1:M1AR地址.
//num:每次传输数据量
void I2S2_TX_DMA_Init(u8* buf0,u8 *buf1,u16 num)
{
NVIC_InitTypeDef
NVIC_InitStructure;
DMA_InitTypeDef
DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
DMA_DeInit(DMA1_Stream4);
while (DMA_GetCmdStatus(DMA1_Stream4) != DISABLE){}//等待可配置
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = DMA_Channel_0; //通道0 SPI2_TX通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&SPI2->DR;//外设地址
DMA_InitStructure.DMA_Memory0BaseAddr
= (u32)buf0;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式
DMA_InitStructure.DMA_BufferSize = num;//数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
DMA_InitStructure.DMA_MemoryInc
= DMA_MemoryInc_Enable;//存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_HalfWord;
//外设数据长度:16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
//存储器数据长度:16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//高优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //不使用FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
//外设突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
//存储器突发单次传输
DMA_Init(DMA1_Stream4, &DMA_InitStructure);//初始化DMA Stream
DMA_DoubleBufferModeConfig(DMA1_Stream4,(u32)buf1,DMA_Memory_0);
//双缓冲模式配置
DMA_DoubleBufferModeCmd(DMA1_Stream4,ENABLE);//双缓冲模式开启
DMA_ITConfig(DMA1_Stream4,DMA_IT_TC,ENABLE);//开启传输完成中断
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;//响应优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
NVIC_Init(&NVIC_InitStructure);//配置}
//I2S DMA回调函数指针
void (*i2s_tx_callback)(void); //TX回调函数
//DMA1_Stream4中断服务函数
void DMA1_Stream4_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_Stream4,DMA_IT_TCIF4)==SET)//传输完成标志
{
DMA_ClearITPendingBit(DMA1_Stream4,DMA_IT_TCIF4);
i2s_tx_callback(); //执行回调函数,读取数据等操作在这里面处理
}
}
其中,I2S2_Init完成I2S2的初始化,通过4个参数设置I2S2的详细配置信息。另外一个函数:I2S2_SampleRate_Set,则是用前面介绍的查表法,根据音频采样率来设置I2S的时钟部分。函数I2S2_TX_DMA_Init,用于设置I2S2的DMA发送,使用双缓冲循环模式,发送数据给WM8978,并开启了发送完成中断。而DMA1_Stream4_IRQHandler函数,则是DMA1数据流4发送完成中断的服务函数,该函数调用i2s_tx_callback函数(函数指针,使用前需指向特定函数)实现DMA数据填充。在i2s.c里面,还有2个函数:I2S_Play_Start和I2S_Play_Stop,用于开启和关闭DMA传输,这里我们没贴出来了,请大家参考光盘本例程源码。
再来看wm8978.c里面的几个函数,代码如下:
//WM8978初始化
//返回值:0,初始化正常
// 其他,错误代码
u8 WM8978_Init(void)
{
u8
res;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB|RCC_AHB1Periph_GPIOC,
ENABLE); //使能外设GPIOB,GPIOC时钟
//PB12/13 复用功能输出
GPIO_InitStructure.GPIO_Pin
= GPIO_Pin_12 | GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽
GPIO_InitStructure.GPIO_Speed =
GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
//PC2/PC3/PC6复用功能输出
GPIO_InitStructure.GPIO_Pin
= GPIO_Pin_2 | GPIO_Pin_3|GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽
GPIO_InitStructure.GPIO_Speed =
GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化
GPIO_PinAFConfig(GPIOB,GPIO_PinSource12,GPIO_AF_SPI2);//PB12,
I2S_LRCK
GPIO_PinAFConfig(GPIOB,GPIO_PinSource13,GPIO_AF_SPI2);//PB13
I2S_SCLK
GPIO_PinAFConfig(GPIOC,GPIO_PinSource3,GPIO_AF_SPI2);//PC3,I2S_DACDATA
GPIO_PinAFConfig(GPIOC,GPIO_PinSource6,GPIO_AF_SPI2);//PC6,AF5
I2S_MCK
GPIO_PinAFConfig(GPIOC,GPIO_PinSource2,GPIO_AF6_SPI2);//PC2,I2S_ADCDATA
IIC_Init();//初始化IIC接口
res=WM8978_Write_Reg(0,0); //软复位WM8978
if(res)return
1; //发送指令失败,WM8978异常
//以下为通用设置
WM8978_Write_Reg(1,0X1B); //R1,MICEN设置为1(MIC使能),BIASEN设置为1
//(模拟器工作),VMIDSEL[1:0]设置为:11(5K)
WM8978_Write_Reg(2,0X1B0); //R2,ROUT1,LOUT1输出使能(耳机可以工作)
//,BOOSTENR,BOOSTENL使能
WM8978_Write_Reg(3,0X6C); //R3,LOUT2,ROUT2,喇叭输出,RMIX,LMIX使能
WM8978_Write_Reg(6,0); //R6,MCLK由外部提供
WM8978_Write_Reg(43,1<<4); //R43,INVROUT2反向,驱动喇叭
WM8978_Write_Reg(47,1<<8); //R47设置,PGABOOSTL,左通道MIC获得20倍增益
WM8978_Write_Reg(48,1<<8); //R48设置,PGABOOSTR,右通道MIC获得20倍增益
WM8978_Write_Reg(49,1<<1); //R49,TSDEN,开启过热保护
WM8978_Write_Reg(10,1<<3); //R10,SOFTMUTE关闭,128x采样,最佳SNR
WM8978_Write_Reg(14,1<<3); //R14,ADC 128x采样率
return
0;
}
//WM8978 DAC/ADC配置
//adcen:adc使能(1)/关闭(0)
//dacen:dac使能(1)/关闭(0)
void WM8978_ADDA_Cfg(u8 dacen,u8 adcen)
{
u16
regval;
regval=WM8978_Read_Reg(3); //读取R3
if(dacen)regval|=3<<0; //R3最低2个位设置为1,开启DACR&DACL
else
regval&=~(3<<0); //R3最低2个位清零,关闭DACR&DACL.
WM8978_Write_Reg(3,regval); //设置R3
regval=WM8978_Read_Reg(2); //读取R2
if(adcen)regval|=3<<0; //R2最低2个位设置为1,开启ADCR&ADCL
else
regval&=~(3<<0); //R2最低2个位清零,关闭ADCR&ADCL.
WM8978_Write_Reg(2,regval); //设置R2
}
//WM8978 输出配置
//dacen AC输出(放音)开启(1)/关闭(0)
//bpsen:Bypass输出(录音,包括MIC,LINE IN,AUX等)开启(1)/关闭(0)
void WM8978_Output_Cfg(u8 dacen,u8 bpsen)
{
u16
regval=0;
if(dacen)regval|=1<<0; //DAC输出使能
if(bpsen)
{
regval|=1<<1; //BYPASS使能
regval|=5<<2; //0dB增益
}
WM8978_Write_Reg(50,regval);//R50设置
WM8978_Write_Reg(51,regval);//R51设置
}
//设置I2S工作模式
//fmt:0,LSB(右对齐);1,MSB(左对齐);2,飞利浦标准I2S;3,PCM/DSP;
//len:0,16位;1,20位;2,24位;3,32位;
void WM8978_I2S_Cfg(u8 fmt,u8 len)
{
fmt&=0X03;
len&=0X03;//限定范围
WM8978_Write_Reg(4,(fmt<<3)|(len<<5)); //R4,WM8978工作模式设置
}
以上代码WM8978_Init用于初始化WM8978,这里只是通用配置(ADC&DAC),初始化之后,并不能正常播放音乐,还需要通过WM8978_ADDA_Cfg函数,使能DAC,然后通过WM8978_Output_Cfg选择DAC输出,通过WM8978_I2S_Cfg配置I2S工作模式,最后设置音量才可以接收I2S音频数据,实现音乐播放。这里设置音量、EQ、音效等函数,没有贴出了,请大家参考光盘本例程源码。
接下来,看看wavplay.c里面的几个函数,代码如下:
__wavctrl wavctrl; //WAV控制结构体
vu8 wavtransferend=0; //i2s传输完成标志
vu8 wavwitchbuf=0; //i2sbufx指示标志
//WAV解析初始化
//fname:文件路径+文件名
//wavx:wav 信息存放结构体指针
//返回值:0,成功;1,打开文件失败;2,非WAV文件;3,DATA区域未找到.
u8 wav_decode_init(u8* fname,__wavctrl* wavx)
{
FIL*ftemp;
u32 br=0;
u8
*buf; u8 res=0;
ChunkRIFF
*riff; ChunkFMT *fmt;
ChunkFACT
*fact; ChunkDATA *data;
ftemp=(FIL*)mymalloc(SRAMIN,sizeof(FIL));
buf=mymalloc(SRAMIN,512);
if(ftemp&&buf) //内存申请成功
{
res=f_open(ftemp,(TCHAR*)fname,FA_READ);//打开文件
if(res==FR_OK)
{
f_read(ftemp,buf,512,&br); //读取512字节在数据
riff=(ChunkRIFF
*)buf; //获取RIFF块
if(riff->Format==0X45564157)//是WAV文件
{
fmt=(ChunkFMT
*)(buf+12); //获取FMT块
fact=(ChunkFACT
*)(buf+12+8+fmt->ChunkSize);//读取FACT块
if(fact->ChunkID==0X74636166||fact->ChunkID==0X5453494C)
wavx->datastart=12+8+fmt->ChunkSize+8+fact->ChunkSize;
//具有fact/LIST块的时候(未测试)
else
wavx->datastart=12+8+fmt->ChunkSize;
data=(ChunkDATA
*)(buf+wavx->datastart); //读取DATA块
if(data->ChunkID==0X61746164)//解析成功!
{
wavx->audioformat=fmt->AudioFormat; //音频格式
wavx->nchannels=fmt->NumOfChannels; //通道数
wavx->samplerate=fmt->SampleRate; //采样率
wavx->bitrate=fmt->ByteRate*8; //得到位速
wavx->blockalign=fmt->BlockAlign; //块对齐
wavx->bps=fmt->BitsPerSample; //位数,16/24/32位
wavx->datasize=data->ChunkSize; //数据块大小
wavx->datastart=wavx->datastart+8; //数据流开始的地方.
}else
res=3;//data区域未找到.
}else
res=2;//非wav文件
}else
res=1;//打开文件错误
}
f_close(ftemp);
myfree(SRAMIN,ftemp);
myfree(SRAMIN,buf); //释放内存
return
0;
}
//填充buf
//buf:数据区
//size:填充数据量
//bits:位数(16/24)
//返回值:读到的数据个数
u32 wav_buffill(u8 *buf,u16 size,u8 bits)
{
u16
readlen=0; u32 bread;
u16 i;
u8 *p;
if(bits==24)//24bit音频,需要处理一下
{
readlen=(size/4)*3; //此次要读取的字节数
f_read(audiodev.file,audiodev.tbuf,readlen,(UINT*)&bread); //读取数据
p=audiodev.tbuf;
for(i=0;i<size;)
{
buf[i++]=p[1];
buf=p[2];
i+=2;
buf[i++]=p[0];
p+=3;
}
bread=(bread*4)/3; //填充后的大小.
}else
{
f_read(audiodev.file,buf,size,(UINT*)&bread);//16bit音频,直接读取数据
if(bread<size)
for(i=bread;i<size-bread;i++)buf=0;//不够数据了,补充0
}
return
bread;
}
//WAV播放时,I2S DMA传输回调函数
void wav_i2s_dma_tx_callback(void)
{
u16 i;
if(DMA1_Stream4->CR&(1<<19))
{
wavwitchbuf=0;
if((audiodev.status&0X01)==0)
//暂停
for(i=0;i<WAV_I2S_TX_DMA_BUFSIZE;i++)audiodev.i2sbuf1=0;//填0
}else
{
wavwitchbuf=1;
if((audiodev.status&0X01)==0)
//暂停
for(i=0;i<WAV_I2S_TX_DMA_BUFSIZE;i++)audiodev.i2sbuf2=0;//填0
}
wavtransferend=1;
}
//播放某个WAV文件
//fname:wav文件路径.
//返回值:
//KEY0_PRES:下一曲
//KEY1_PRES:上一曲
//其他:错误
u8 wav_play_song(u8* fname)
{
u8
key; u8 t=0; u8 res; u32 fillnum;
audiodev.file=(FIL*)mymalloc(SRAMIN,sizeof(FIL));
audiodev.i2sbuf1=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
audiodev.i2sbuf2=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
audiodev.tbuf=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
if(audiodev.file&&audiodev.i2sbuf1&&audiodev.i2sbuf2&&audiodev.tbuf)
{
res=wav_decode_init(fname,&wavctrl);//得到文件的信息
if(res==0)//解析文件成功
{
if(wavctrl.bps==16)
{
WM8978_I2S_Cfg(2,0); //飞利浦标准,16位数据长度
I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,
I2S_DataFormat_16bextended);
//飞利浦标准,主机发送,时钟低电平,16位扩展帧长度
}else
if(wavctrl.bps==24)
{
WM8978_I2S_Cfg(2,2); //飞利浦标准,24位数据长度
I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,
I2S_DataFormat_24b);//飞利浦标准,主机发送,时钟低,24位扩展帧长度
}
I2S2_SampleRate_Set(wavctrl.samplerate);//设置采样率
I2S2_TX_DMA_Init(audiodev.i2sbuf1,audiodev.i2sbuf2,
WAV_I2S_TX_DMA_BUFSIZE/2); //配置TX DMA
i2s_tx_callback=wav_i2s_dma_tx_callback;//回调函数指wav_i2s_dma_callback
audio_stop();
res=f_open(audiodev.file,(TCHAR*)fname,FA_READ);//打开文件
if(res==0)
{
f_lseek(audiodev.file,
wavctrl.datastart);//跳过文件头
fillnum=wav_buffill(audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,
wavctrl.bps);
fillnum=wav_buffill(audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,
wavctrl.bps);
audio_start();
while(res==0)
{
while(wavtransferend==0);//等待wav传输完成;
wavtransferend=0;
if(fillnum!=WAV_I2S_TX_DMA_BUFSIZE)//播放结束?
{
res=KEY0_PRES; break; }
if(wavwitchbuf)fillnum=wav_buffill(audiodev.i2sbuf2,
WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充buf2
else
fillnum=wav_buffill(audiodev.i2sbuf1,
WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充buf1
while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)//暂停
{
if(audiodev.status&0X01)audiodev.status&=~(1<<0);
else
audiodev.status|=0X01;
}
if(key==KEY2_PRES||key==KEY0_PRES)//下一曲/上一曲
{
res=key; break; }
wav_get_curtime(audiodev.file,&wavctrl);//得到播放和总时间
audio_msg_show(wavctrl.totsec,wavctrl.cursec,wavctrl.bitrate);
t++;
if(t==20)
{ t=0; LED0=!LED0; }
if((audiodev.status&0X01)==0)delay_ms(10);
else
break;
}
}
audio_stop();
}else
res=0XFF;
}else
res=0XFF;
}else
res=0XFF;
myfree(SRAMIN,audiodev.tbuf);
myfree(SRAMIN,audiodev.file); //释放内存
myfree(SRAMIN,audiodev.i2sbuf1);
myfree(SRAMIN,audiodev.i2sbuf2); //释放内存
return
res;
}
以上,wav_decode_init函数,用来对wav文件进行解析,得到wav的详细信息(音频采样率,位数,数据流起始位置等);wav_buffill函数,用f_read读取数据,填充数据到buf里面,注意24位音频的时候,读出的数据需要经过转换后才填充到buf;wav_i2s_dma_tx_callback函数,则是DMA发送完成的回调函数(i2s_tx_callback函数指针指向该函数),这里面,我们并没有对数据进行填充处理(暂停时进行了填0处理),而是采用2个标志量:wavtransferend和wavwitchbuf,来告诉wav_play_song函数是否传输完成,以及应该填充哪个数据buf(i2sbuf1或i2sbuf2);
最后,wav_play_song函数,是播放WAV的最终执行函数,该函数解析完WAV文件后,设置WM8978和I2S的参数(采样率,位数等),并开启DMA,然后不停填充数据,实现WAV播放,该函数还进行了按键扫描控制,实现上下取切换和暂停/播放等操作。该函数通过判断wavtransferend是否为1来处理是否应该填充数据,而到底填充到哪个buf(i2sbuf1或i2sbuf2),则是通过wavwitchbuf标志来确定的,当wavwitchbuf=0时,说明DMA正在使用i2sbuf2,程序应该填充i2sbuf1;当wavwitchbuf=1时,说明DMA正在使用i2sbuf1,程序应该填充i2sbuf2;
接下来,看看audioplay.c里面的几个函数,代码如下:
//播放音乐
void audio_play(void)
{
u8
res; u8 key; u16 temp;
DIR wavdir;
//目录
FILINFO
wavfileinfo; //文件信息
u8
*fn; //长文件名
u8
*pname; //带路径的文件名
u16
totwavnum; //音乐文件总数
u16
curindex; //图片当前索引
u16
*wavindextbl; //音乐索引表
WM8978_ADDA_Cfg(1,0); //开启DAC
WM8978_Input_Cfg(0,0,0);//关闭输入通道
WM8978_Output_Cfg(1,0); //开启DAC输出
while(f_opendir(&wavdir,"0:/MUSIC"))//打开音乐文件夹
{
Show_Str(60,190,240,16,"MUSIC文件夹错误!",16,0); delay_ms(200);
LCD_Fill(60,190,240,206,WHITE);
delay_ms(200);//清除显示
}
totwavnum=audio_get_tnum("0:/MUSIC");
//得到总有效文件数
while(totwavnum==NULL)//音乐文件总数为0
{
Show_Str(60,190,240,16,"没有音乐文件!",16,0); delay_ms(200);
LCD_Fill(60,190,240,146,WHITE);
delay_ms(200); //清除显示
}
wavfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度
wavfileinfo.lfname=mymalloc(SRAMIN,wavfileinfo.lfsize);//为长文件缓存区分配内存
pname=mymalloc(SRAMIN,wavfileinfo.lfsize); //为带路径的文件名分配内存
wavindextbl=mymalloc(SRAMIN,2*totwavnum); //申请内存,用于存放音乐文件索引
while(wavfileinfo.lfname==NULL||pname==NULL||wavindextbl==NULL)//内存分配出错
{
Show_Str(60,190,240,16,"内存分配失败!",16,0); delay_ms(200);
LCD_Fill(60,190,240,146,WHITE);
delay_ms(200);//清除显示
}
//记录索引
res=f_opendir(&wavdir,"0:/MUSIC"); //打开目录
if(res==FR_OK)
{
curindex=0;//当前索引为0
while(1)//全部查询一遍
{
temp=wavdir.index; //记录当前index
res=f_readdir(&wavdir,&wavfileinfo); //读取目录下的一个文件
if(res!=FR_OK||wavfileinfo.fname[0]==0)break; //错误了/到末尾了,退出
fn=(u8*)(*wavfileinfo.lfname?wavfileinfo.lfname:wavfileinfo.fname);
res=f_typetell(fn);
if((res&0XF0)==0X40)//取高四位,看看是不是音乐文件
{
wavindextbl[curindex]=temp;//记录索引
curindex++;
}
}
}
curindex=0; //从0开始显示
res=f_opendir(&wavdir,(const
TCHAR*)"0:/MUSIC"); //打开目录
while(res==FR_OK)//打开成功
{
dir_sdi(&wavdir,wavindextbl[curindex]); //改变当前目录索引
res=f_readdir(&wavdir,&wavfileinfo); //读取目录下的一个文件
if(res!=FR_OK||wavfileinfo.fname[0]==0)break; //错误了/到末尾了,退出
fn=(u8*)(*wavfileinfo.lfname?wavfileinfo.lfname:wavfileinfo.fname);
strcpy((char*)pname,"0:/MUSIC/"); //复制路径(目录)
strcat((char*)pname,(const
char*)fn); //将文件名接在后面
LCD_Fill(60,190,240,190+16,WHITE); //清除之前的显示
Show_Str(60,190,240-60,16,fn,16,0); //显示歌曲名字
audio_index_show(curindex+1,totwavnum);
key=audio_play_song(pname);
//播放这个音频文件
if(key==KEY2_PRES) //上一曲
{
if(curindex)curindex--;
else
curindex=totwavnum-1;
}else if(key==KEY0_PRES)//下一曲
{
curindex++;
if(curindex>=totwavnum)curindex=0;//到末尾的时候,自动从头开始
}else break; //产生了错误
}
myfree(SRAMIN,wavfileinfo.lfname); //释放内存
myfree(SRAMIN,pname); //释放内存
myfree(SRAMIN,wavindextbl); //释放内存
}
//播放某个音频文件
u8 audio_play_song(u8* fname)
{
u8
res;
res=f_typetell(fname);
switch(res)
{
case
T_WAV:
res=wav_play_song(fname);
break;
default://其他文件,自动跳转到下一曲
printf("can't
play:%s\r\n",fname);
res=KEY0_PRES; break;
}
return
res;
}
这里,audio_play函数在main函数里面被调用,该函数首先设置WM8978相关配置,然后查找SD卡里面的MUSIC文件夹,并统计该文件夹里面总共有多少音频文件(统计包括:WAV/MP3/APE/FLAC等),然后,该函数调用audio_play_song函数,按顺序播放这些音频文件。
在audio_play_song函数里面,通过判断文件类型,调用不同的解码函数,本章,只支持WAV文件,通过wav_play_song函数实现WAV解码。其他格式:MP3/APE/FLAC等,在综合实验我们会实现其解码函数,大家可以参考综合实验代码,这里就不做介绍了。
最后,我们看看主函数代码:
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
LED_Init(); //初始化LED
usmart_dev.init(84); //初始化USMART
LCD_Init(); //LCD初始化
KEY_Init(); //按键初始化
W25QXX_Init(); //初始化W25Q128
WM8978_Init(); //初始化WM8978
WM8978_HPvol_Set(40,40); //耳机音量设置
WM8978_SPKvol_Set(50); //喇叭音量设置
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMCCM); //初始化CCM内存池
exfuns_init(); //为fatfs相关变量申请内存
f_mount(fs[0],"0:",1); //挂载SD卡
POINT_COLOR=RED;
while(font_init())
//检查字库
{
LCD_ShowString(30,50,200,16,16,"Font
Error!");
delay_ms(200);
LCD_Fill(30,50,240,66,WHITE);//清除显示
delay_ms(200);
}
POINT_COLOR=RED;
Show_Str(60,50,200,16,"Explorer
STM32F4开发板",16,0);
Show_Str(60,70,200,16,"音乐播放器实验",16,0);
Show_Str(60,90,200,16,"正点原子@ALIENTEK",16,0);
Show_Str(60,110,200,16,"2014年5月24日",16,0);
Show_Str(60,130,200,16,"KEY0:NEXT KEY2 REV",16,0);
Show_Str(60,150,200,16,"KEY_UP AUSE/PLAY",16,0);
while(1)
{
audio_play();
}
}
该函数就相对简单了,在初始化各个外设后,通过audio_play函数,开始音频播放。软件部分就介绍到这里,其他未贴出代码,请参考光盘本例程源码。
48.4 下载验证
在代码编译成功之后,我们下载代码到ALIENTEK探索者STM32F4开发板上,程序先执行字库检测,然后当检测到SD卡根目录的MUSIC文件夹有有效音频文件(WAV格式音频)的时候,就开始自动播放歌曲了,如图48.4.1所示:
图48.4.1 MP3播放中
从上图可以看出,当前正在播放第4首歌曲,总共4首歌曲,歌曲名、播放时间、总时长、码率、音量等信息等也都有显示。此时DS0会随着音乐的播放而闪烁。
只要我们在开发板的PHONE端子插入耳机(或者在P1接口插入喇叭),就能听到歌曲的声音了。同时,我们可以通过按KEY0和KEY2来切换下一曲和上一曲,通过KEY_UP控制暂停和继续播放。
本实验,我们还可以通过USMART来测试WM8978的其他功能,通过将wm8978.c里面的部分函数加入USMART管理,我们可以很方便的设置wm8978的各种参数(音量、3D、EQ等都可以设置),达到验证测试的目的。有兴趣的朋友,可以实验测试一下。
至此,我们就完成了一个简单的音乐播放器了,虽然只支持WAV文件,但是大家可以在此基础上,增加其他音频格式解码器(可参考综合实验),便可实现其他音频格式解码了。
正点原子探索者STM32F407开发板购买地址:http://item.taobao.com/item.htm?id=41855882779
|