OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 2941|回复: 0

《STM32F407 探索者开发指南》第五十五章 音乐播放器实验

[复制链接]

1140

主题

1152

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4895
金钱
4895
注册时间
2019-5-8
在线时间
1248 小时
发表于 2023-9-11 17:56:39 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-9-9 12:21 编辑

第五十五章 音乐播放器实验

1)实验平台:正点原子探索者STM32F407开发板

2) 章节摘自【正点原子】STM32F407开发指南 V1.1


4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)STM32技术交流QQ群:151941872

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

正点原子探索者STM32F407开发板拥有全双工I2S接口,且外扩了一颗HIFI级CODEC芯片:ES8388,支持最高192K 24BIT的音频播放,并且支持录音(下一章介绍)。本章,我们将利用探索者STM32F407开发板实现一个简单的音乐播放器(仅支持WAV播放)。本章分为如下几个部分:
55.1 WAV&ES8388&I2S简介
55.2 硬件设计
55.3 软件设计
55.4 下载验证

55.1 WAV&ES8388&I2S简介

本章知识点比较多,包括:WAV、ES8388和I2S等三个知识点。下面我们将分别向大家介绍。

55.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由块标识符、数据大小和数据三部分组成,如图 55.1.1.1 所示:                             
image001.png
图55.1.1.1 Chunk组成结构

对于一个基本的WAVE文件而言,以下三种Chunk是必不可少的:文件中第一个Chunk是RIFF Chunk,然后是FMT Chunk,最后是Data Chunk。对于其他的Chunk,顺序没有严格的限制。使用WAVE文件的应用程序必须具有读取以上三种chunk信息的能力,如果程序想要复制WAVE文件,必须拷贝文件中所有的chunk。本章,我们主要讨论PCM,因为这个最简单,它只包含3个Chunk,我们看一下它的文件构成,如图55.1.2。   
image003.png
图55.1.2 PCM格式的wav文件构成

可以看到,不同的Chunk有不同的长度,编码文件时,按照Chunk的字节和位序排列好之后写入文件头,加上wav的后缀,就可以生成一个能被正确解析的wav文件了,对于PCM结构,我们只需要把获取到的音频数据填充到Data Chunk中即可。我们将利用VS1053实现16位,8Khz采样率的单声道WAV录音(PCM格式)。

首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:
  1. typedef __PACKED_STRUCT
  2. {
  3.     uint32_t ChunkID;         /* chunk id;这里固定为"RIFF",即0X46464952 */
  4.     uint32_t ChunkSize ;    /* 集合大小;文件总大小-8 */
  5.     uint32_t Format;         /* 格式;WAVE,即0X45564157 */
  6. }ChunkRIFF;     /* RIFF块 */
复制代码
接着,我们看看Format块(Format Chunk),该块以“fmt”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:
  1. typedef __PACKED_STRUCT
  2. {
  3.     uint32_t ChunkID;            /* chunk id;这里固定为"fmt ",即0X20746D66 */
  4.     uint32_t ChunkSize ;         /* 子集合大小(不包括ID和Size);这里为:20. */
  5.     uint16_t AudioFormat;        /* 音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM */
  6.     uint16_t NumOfChannels;     /* 通道数量;1,表示单声道;2,表示双声道; */
  7.     uint32_t SampleRate;         /* 采样率;0X1F40,表示8Khz */
  8.     uint32_t ByteRate;           /* /字节速率; */
  9.     uint16_t BlockAlign;         /* 块对齐(字节); */
  10.     uint16_t BitsPerSample;     /* 单个采样数据大小;4位ADPCM,设置为4 */
  11. // uint16_t ByteExtraData;     /* 附加的数据字节;2个; 线性PCM,没有这个参数 */
  12. }ChunkFMT;    /* fmt块 */
复制代码
接下来,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:
  1. typedef __PACKED_STRUCT
  2. {
  3.     uint32_t ChunkID;             /* chunk id;这里固定为"fact",即0X74636166; */
  4.     uint32_t ChunkSize;          /* 子集合大小(不包括ID和Size);这里为:4. */
  5.     uint32_t NumOfSamples;       /* 采样的数量; */
  6. }ChunkFACT;    /* fact块 */
复制代码
DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。
最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”作为该Chunk的标示,然后是数据的大小。数据块的Chunk结构如下:
  1. typedef __PACKED_STRUCT
  2. {
  3.     uint32_t ChunkID;            /* chunk id;这里固定为"data",即0X5453494C */
  4.     uint32_t ChunkSize ;         /* 子集合大小(不包括ID和Size) */
  5. }ChunkDATA;     /* data块 */
复制代码
ChunkSize后紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如表55.1.1.1所示的几种形式:
QQ截图20230909121645.png
表55.1.1.1 WAVE文件数据采样格式

本章,我们播放的音频支持:16位和24位,立体声,所以每个取样为4/6个字节,低字节在前,高字节在后。在得到这些wav数据以后,通过I2S丢给ES8388,就可以欣赏音乐了。

55.1.2 ES8388简介
ES8388是上海顺芯推出的一款高性能、低功耗、高性价比的音频编解码器,有2个ADC通道和2个DAC通道,麦克风放大器,耳机放大器,数字音效以及模拟混合和增益功能组成。

ES8388的主要特性有:
●I2S接口,支持最高192K,24bit音频播放
●DAC信噪比96dB;ADC信噪比95dB
●支持主机和从机模式
●支持立体声差分输入/麦克风输入
●支持左右声道音量独立调节
●支持40mW耳机输出,无爆音

ES8388的控制通过I2S接口(即数字音频接口)同MCU进行音频数据传输(支持音频接收和发送),通过两线(CE=0/1,即IIC接口)或三线(CE脚产生一个下降沿,即SPI接口)接口进行配置。ES8388的I2S接口,由4个引脚组成:
ASDOUT:ADC数据输出
DSDIN:DAC数据输入
LRC:数据左/右对齐时钟
SCLK:位时钟,用于同步

ES8388可作为I2S主机,输出LRC和SLCK时钟,不过我们一般使用ES8388作为从机,接收LRC和SLCK。另外,ES8388的I2S接口支持4种不同的音频数据模式:左(MSB)对齐标准、右(LSB)对齐标准、飞利浦(I2S)标准、DSP/PCM。本章,我们用飞利浦标准来传输I2S数据。

飞利浦(I2S)标准模式,数据在跟随LRC传输的BCLK的第二个上升沿时传输MSB,其他位一直到LSB按顺序传输。传输依赖于字长、BCLK频率和采样率,在每个采样的LSB和下一个采样的MSB之间都应该有未用的BCLK周期。飞利浦标准模式的I2S数据传输协议如图55.1.2.1所示:     
image005.jpg
图55.1.2.1 飞利浦标准模式I2S数据传输图

图中,fs即音频信号的采样率,比如44.1Khz,因此可以知道,LRC的频率就是音频信号的采样率。另外,ES8388还需要一个MCLK,本章我们采用STM32F407为其提供MCLK时钟,MCLK的频率必须等于256fs,也就是音频采样率的256倍。
ES8388的框图如图55.1.2.2所示:   
image007.png
图55.1.2.2 ES8388框图

从上图可以看出,ES8388内部有很多的模拟开关,用来选择通道,同时还有一些运放调节器,用来设置增益和音量。

本章,我们通过IIC接口(CE=0)连接ES8388,ES8388的IIC地址为:0X10。关于ES8388的IIC详细介绍,请看其数据手册第10页5.2节。

这里我们简单介绍一下要正常使用ES8388来播放音乐,应该执行哪些配置。

1,寄存器R0(00h),是芯片控制寄存器1,需要用到的位有:最高位SCPRese(bit7)用于控制ES8388的软复位,写0X80到该寄存器地址,即可实现软复位ES8388,复位后,再写0X00,ES8388恢复正常。VMIDSEL[1:0]位用于控制VMID(校正噪声用),我们一般设置为10,即用500KΩ校正。

2,寄存器R1(01h),是芯片控制寄存器2,主要要设置PdnAna(bit3),该位设置为1,模拟部分掉电,相当于复位模拟部分;设置为0,模拟部分才会工作,才可以听到声音。

3,寄存器R2(02h),是芯片电源管理控制寄存器,所有位都要用到:adc_DigPDN(bit7)和dac_DigPDN(bit6)分别用于控制ADC和DAC的DSM、DEM、滤波器和数字接口的复位,1复位,0正常;adc_stm_rst(bit5)和dac_stm_rst(bit4)分别用于控制ADC和DAC的状态机掉电,1掉电,0正常;ADCDLL_PDN(bit3)和DACDLL_PDN(bit2)分别用于控制ADC和DAC的DLL掉电,停止时钟,1掉电,0正常;adcVref_PDN(bit1)和dacVref_PDN(bit0)分别控制ADC和DAC的模拟参考电压掉电,1掉电,0正常;因此想要ADC和DAC都正常工作,R2寄存器必须全部设置为0,否则ADC或者DAC就会不能正常工作。

4,寄存器R3(03h),是ADC电源管理控制寄存器,需要用到的位有:PdnAINL(bit7)和PdnAINR(bit6)用于控制左右输入模拟通道的电源,1掉电,0正常;PdnADCL(bit5)和PdnADCR(bit4)用于控制左右通道ADC的电源,1掉电,0正常;pdnMICB(bit3)用于控制麦克风的偏置电源,1掉电,0正常;PdnADCBiasgen(bit2)用于控制偏置电源的产生,1掉电,0正常;这里6个位,我们全部设置为0,ADC部分就可以正常工作了。

5,寄存器R4(04h),是DAC电源管理控制寄存器,需要用到的位有:PdnDACL(bit7)和PdnDACR(bit6)分别用于左右声道DAC的电源控制,1掉电;0正常;LOUT1(bit5)和ROUT1(bit4)分别用于控制通道1的左右声道输出是能,1使能,0禁止;LOUT2(bit3)和ROUT2(bit2)分别用于控制通道2的左右声道输出是能,1使能,0禁止;我们一般设置PdnDACL和PdnDACR为0,使能左右声道DAC,另外,两个输出通道则根据自己的需要设置。

6,寄存器R8(08h),是主模式控制寄存器,需要用到的位有:MSC(bit7)用于控制接口模式,0从模式,1主模式;MCKDIV2(bit6)用于控制MCLK的2分频,0不分频,1二分频;BCLK_INV(bit5)用于控制BCLK的反相,0不反相;1,反相;一般设置这3个位都为0。

7,寄存器R9(09h),是ADC控制寄存器1,所有位都要用到:MicAmpL(bit7:4)和MicAmpR(bit3:0),这两个分别用于控制MIC的左右通道增益,从0开始,3dB一个档,最大增益为24dB,我们一般设置MicAmpR/L[3:0]=1000,即24dB。

8,寄存器R10(0Ah),是ADC控制寄存器2,需要用到的位有:LINSE(bit7:6)和RINSE(bit5:4)分别选择左右输入通道,0选择通道1,1选择通道2。

9,寄存器R12(0Ch),是ADC控制寄存器4,全部位都要用到:DATSEL(bit7:6)用于选择数据格式,一般设置为01,左右边数据等于左右声道ADC数据;ADCLRP(bit5)在I2S模式下用于设置数据对其方式,一般设置为0,正常极性;ADCWL(bit4:2)用于选择数据长度,我们设置011,选择16位数据长度;ADCFORMAT(bit1:0)用于设置ADC数据格式,一般设置为00,选择I2S数据格式。

10,寄存器R13(0Dh),是ADC控制寄存器5,全部位都要用到:ADCFsMode(bit7)用于设置Fs模式,0单速模式,1双倍速模式,一般设置为0;ADCFsRatio(bit4:0)用于设置ADC的MCLK和FS的比率,我们设置00010,即256倍关系。

11,寄存器R16(10h)和R17(11h),这两个寄存器分别用于控制ADC左右声道的音量衰减,LADCVOL(bit7:0)和RADCVOL(bit7:0)分别控制左声道和右声道ADC的衰减,0.5dB每步,我们一般设置为0,即不衰减。

12,寄存器R18(12h),是ADC控制寄存器10,全部位都要用到:ALCSEL(bit7:6)用于控制ALC,00表示ALC关闭,01表示ALC仅控制左声道,10表示ALC仅控制右声道11表示ALC立体声控制;我们一般设置为11。

13,寄存器R23(17h),是DAC控制寄存器1,需要用到的位有:DACLRSWAP(bit7)用于控制左右声道数据交换,0正常,1互换,一般设置为0;DACLRP(bit6) 在I2S模式下用于设置数据对其方式,一般设置为0,正常极性;DACWL(bit5:3)用于选择数据长度,我们设置011,选择16位数据长度;ADCFORMAT(bit1:0)用于设置DAC数据格式,一般设置为00,选择I2S数据格式。

14,寄存器R24(18h),是DAC控制寄存器2,全部位都要用到:DACFsMode(bit7)用于设置Fs模式,0单速模式,1双倍速模式,一般设置为0;DACFsRatio(bit4:0)用于设置DAC的MCLK和FS的比率,我们设置00010,即256倍关系。

15,寄存器R26(1Ah)和R27(1Bh),这两个寄存器分别用于控制DAC左右声道的音量衰减,LDACVOL(bit7:0)和RDACVOL(bit7:0)分别控制左声道和右声道DAC的衰减,0.5dB每步,0表示0dB衰减,192表示96dB衰减;通过这两个寄存器可以完成输出音量的调节。

16,寄存器R29(1Dh),是DAC控制寄存器7,需要用到的位有:ZeroL(bit7)和ZeroR(bit6)分别控制左右声道的全0输出,类似静音,1输出0,0正常;一般设置为0。Mono(bit5)用于单声道控制,0立体声,1单声道;一般设置为0。SE(bit4:2)用于设置3D音效,0~7表示3D效果的强弱,0表示关闭。

17,寄存器39(27h)和42(2Ah),分别控制DAC左右通道的混音器,LD2LO(bit7)和RD2RO(bit7)分别控制左右DAC的混音器开关,0关闭,1开启,需设置为1;LI2LO(bit6)和RI2RO(bit6)分别控制左右输入通道的混音器开关,0关闭,1开启,一般设置为1;LI2LOVOL(bit5:3)和RI2ROVOL(bit5:3)分别控制左右输入通道的增益,0~7表示-6 ~ -15dB的增益调节范围,默认设置为111,即-15dB。

18,寄存器43(2Bh),是DAC控制寄存器21,这里我们只关心slrck(bit7)这个位,用于控制DACLRC和ADCLRC是否共用,我们设置为1,表示共用。

以上,就是我们使用ES8388时所需要用到的一些寄存器,按照以上所述,对各个寄存器进行相应的配置,即可使用ES8388正常播放音乐了。关于ES8388更详细的寄存器设置说明,我们这里就不再介绍了,请大家参考ES8388的数据手册自行研究。

55.1.3 I2S简介
I2S(Inter ICSound)总线, 又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专责于音频设备之间的数据传输,广泛应用于各种多媒体系统。它采用了沿独立的导线传输时钟与数据信号的设计,通过将数据和时钟信号分离,避免了因时差诱发的失真,为用户节省了购买抵抗音频抖动的专业设备的费用。

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框图如图55.1.3.1所示:     
image009.png
图55.1.3.1 I2S框图

1I2S信号线
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),如图55.1.3.2:     
image011.png
图55.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均可用于发送和接收。

2I2S数据帧
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参考手册_V4(中文版).pdf》第27.4节。

I2S飞利浦标准,使用WS信号来指示当前正在发送的数据所属的通道。该信号从当前通道数据的第一个位(MSB)之前的一个时钟开始有效。发送方在时钟信号(CK)的下降沿改变数据,接收方在上升沿读取数据。WS信号也在CK的下降沿变化。这和我们55.1.2节介绍的是一样的。本章我们使用16位/24位数据格式,16位时采用扩展帧格式(即将16位数据封装在32位帧中),以24位帧为例,I2S波形(飞利浦标准)如图55.1.3.3所示:     
image013.png
图55.1.3.3 I2S飞利浦标准24位帧格式波形

这个图和图55.1.2.1是一样的时序,在24位模式下数据传输,需要对SPI_DR执行两次读取或写入操作。比如我们要发送0X8EAA33这个数据,就要分两次写入SPI_DR,第一次写入:0X8EAA,第二次写入0X33xx(xx可以为任意数值),这样就把0X8EAA33发送出去了。

顺便说一下SD卡读取到的24位WAV数据流,是低字节在前,高字节在后的,比如,我们读到一个声道的数据(24bit),存储在buf[3]里面,那么要通过SPI_DR发送这个24位数据,过程如下:
SPI_DR=((uint16_t)buf[2]<<8)+buf[1];
SPI_DR=(uint16_t)buf[0]<<8;
这样,第一次发送高16为数据,第二次发送低8位数据,完成一次24bit数据的发送。

3I2S时钟发生器
接下来,我们介绍下STM32F4的I2S时钟发生器,其架构如图55.1.3.4所示:     
image015.png
图55.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 对应系数表如下:
  1. /**
  2. * 采样率计算公式:Fs = I2SxCLK / [256 * (2 * I2SDIV + ODD)]
  3. *I2SxCLK = (HSE / pllm) * PLLI2SN / PLLI2SR
  4. * 一般HSE = 8Mhz
  5. *pllm:在sys_stm32_clock_init 设置的时候确定,一般是8
  6. *PLLI2SN:一般是192~432
  7. *PLLI2SR:2~7
  8. *I2SDIV:2~255
  9. *ODD:0/1
  10. * I2S分频系数表@pllm = 8,HSE = 8Mhz, 即vco输入频率为1Mhz
  11. * 表格式:采样率 /10,PLLI2SN,PLLI2SR,I2SDIV,ODD
  12. */
  13. const uint16_t I2S_PSC_TBL[][5]=
  14. {
  15.     {   800, 256, 5, 12, 1 },    /* 8Khz采样率 */
  16.     {  1102, 429, 4, 19, 0 },    /* 11.025Khz采样率 */
  17.     {  1600, 213, 2, 13, 0 },    /* 16Khz采样率 */
  18.     {  2205, 429, 4,  9, 1 },    /* 22.05Khz采样率 */
  19.     {  3200, 213, 2,  6, 1 },    /* 32Khz采样率 */
  20.     {  4410, 271, 2,  6, 0 },    /* 44.1Khz采样率 */
  21.     {  4800, 258, 3,  3, 1 },    /* 48Khz采样率 */
  22.     {  8820, 316, 2,  3, 1 },    /* 88.2Khz采样率 */
  23.     {  9600, 344, 2,  3, 1 },    /* 96Khz采样率 */
  24.     { 17640, 361, 2,  2, 0 },    /* 176.4Khz采样率 */
  25.     { 19200, 393, 2,  2, 0 },    /* 192Khz采样率 */
  26. };
复制代码
有了上面的 fs-系数对应表,我们可以很方便的完成 I2S 的时钟配置。

4I2S寄存器
接下来,我们看看本章需要用到的一些相关寄存器。
首先是SPI_I2S配置寄存器:SPI_I2SCFGT,该寄存器各位描述如图55.1.3.5所示:   
image017.png
图55.1.3.5寄存器SPI_I2SCFGR各位描述

第二个是SPI_I2S预分配器寄存器:SPI_I2SPR,该寄存器各位描述如图55.1.3.6所示:     
image019.png
图55.1.3.6 寄存器SPI_I2SPR各位描述

本章我们设置MCKOE为1,开启MCK输出,ODD和I2SDIV则根据不同的fs,查表进行设置。

第三个是PLLI2S配置寄存器:RCC_PLLI2SCFGR,该寄存器各位描述如图55.1.3.7所示。     
image021.png
图 55.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即可。

55.2 硬件设计
1. 例程功能
本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始循环播放SD卡MUSIC文件夹里面的歌曲(必须在SD卡根目录建立一个MUSIC文件夹,并存放歌曲在里面),在TFTLCD上显示歌曲名字、播放时间、歌曲总时间、歌曲总数目、当前歌曲的编号等信息。KEY0用于选择下一曲,KEY2用于选择上一曲,KEY_UP用来控制暂停/继续播放。LED0闪烁,提示程序运行状态。

2. 硬件资源
本实验,大家需要准备1个SD卡(在里面新建一个MUSIC文件夹,并存放一些歌曲在MUSIC文件夹下)和一个耳机(非必备),分别插入SD卡接口和耳机接口,然后下载本实验就可以通过耳机或板载喇叭来听歌了。实验用到的硬件资    如下:
1)LED灯
LED0 – PF9
LED1 – PF10

2)独立按键
KEY0 – PE4
KEY1 – PE3
KEY2 – PE2
KEY_UP – PA0  (程序中的宏名:WK_UP)

3)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)
4)正点原子 2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
5)SD卡,通过SDIO连接
6)NOR FLASH(SPI FLASH芯片,连接在SPI1上)
7)ES8388音频CODEC芯片,通过I2S驱动
8)I2S音频接口
探索者STM32F407开发板板载了ES8388解码芯片的驱动电路,原理图如图55.2.1所示:     
image023.png
图55.2.1 ES8388与STM32F407原理图

图中,PHONE接口可以用来插耳机,SPK+和SPK-连接了板载的喇叭(在开发板底部)。硬件上,IIC接口和24C02,ST480磁力计等共用,另外,I2S_MCLK和DCMI_D0共用,所以I2S和DCMI不可以同时使用。

55.3 程序设计
55.3.1 程序流程图
QQ截图20230909121947.png
图55.3.1.1 音乐播放器实验程序流程图

音乐播放我们从SD卡的指定目前读取音乐文件,解析格式正确后,通过SPI不断向ES8388发送文件数据至播放完成,ES8388解码后通过选择扬声器或直接从耳机输出音乐。为了交互性,我们设置板载的按键用于控制播放的歌曲切换及开始/暂停播放。

55.3.2 程序解析
音乐文件我们要通过SD卡来传给单片机,那我们自然要用到文件系统。LCD、按键交互这些我们也需要实现,同样我们为了快速建立工程,复制之前的《实验39 FATFS实验》来修改成我们音乐播放器实验。
由于播放功能涉及到多个外设的配合使用,用文件系统读音频文件,做播放控制等,所以我们把ES8388的硬件驱动放到BSP目录下,播放功能作为APP放到USER目录下。

1. I2S驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,I2S的驱动主要包括两个文件:i2s.c和i2s.h。
除去I2C的管脚,我们需要初始其它IO的模式,我们在头文件i2s.h中定义I2S的引脚,方便如果IO变更之后作修改:
  1. /* 引脚复用为I2S */
  2. #define GPIO_AF_I2S_SPI                   GPIO_AF5_SPI2
  3. #define I2S_LRCK_GPIO_PORT               GPIOB
  4. #define I2S_LRCK_GPIO_PIN                GPIO_PIN_12
  5. #define I2S_LRCK_GPIO_CLK_ENABLE()      do{
  6.                                                  __HAL_RCC_GPIOB_CLK_ENABLE();
  7.                                               }while(0)       /* PB口时钟使能 */
  8. #define I2S_SCLK_GPIO_PORT               GPIOB
  9. #define I2S_SCLK_GPIO_PIN                GPIO_PIN_13
  10. #define I2S_SCLK_GPIO_CLK_ENABLE()      do{
  11.                                                  __HAL_RCC_GPIOB_CLK_ENABLE();
  12.                                               }while(0)      /* PB口时钟使能 */
  13. #define I2S_SDOUT_GPIO_PORT              GPIOC
  14. #define I2S_SDOUT_GPIO_PIN               GPIO_PIN_2
  15. #define I2S_SDOUT_GPIO_CLK_ENABLE()    do{
  16.                                                  __HAL_RCC_GPIOC_CLK_ENABLE();
  17.                                               }while(0)      /* PC口时钟使能 */
  18. #define I2S_SDIN_GPIO_PORT               GPIOC
  19. #define I2S_SDIN_GPIO_PIN                GPIO_PIN_3
  20. #define I2S_SDIN_GPIO_CLK_ENABLE()      do{
  21.                                                  __HAL_RCC_GPIOC_CLK_ENABLE();
  22.                                               }while(0)       /* PC口时钟使能 */
  23. #define I2S_MCLK_GPIO_PORT               GPIOC
  24. #define I2S_MCLK_GPIO_PIN                GPIO_PIN_6
  25. #define I2S_MCLK_GPIO_CLK_ENABLE()      do{
  26.                                                  __HAL_RCC_GPIOC_CLK_ENABLE();
  27.                                               }while(0)       /* PC口时钟使能 */
  28. #define I2S_SPI                            SPI2
  29. #define I2S_SPI_CLK_ENABLE()             do{
  30.                                                  __HAL_RCC_SPI2_CLK_ENABLE();
  31.                                               }while(0)      /* I2S2时钟使能*/
复制代码
接下来开始介绍i2s.c,主要是I2S的初始化和DMA初始化,代码如下:
  1. I2S_HandleTypeDef g_i2s_handler;         /* I2S句柄 */
  2. DMA_HandleTypeDef g_i2s_txdma_handler; /* I2S发送DMA句柄 */
  3. /**
  4. *@brief        I2S初始化
  5. *@param        i2s_standard    : I2S标准
  6. *  @note       可以设置           : I2S_STANDARD_PHILIPS/I2S_STANDARD_MSB/
  7. *                                      I2S_STANDARD_LSB/I2S_STANDARD_PCM_SHORT/
  8. *                                      I2S_STANDARD_PCM_LONG
  9. *@param        i2s_mode         : I2S工作模式
  10. *  @note       可以设置          : I2S_MODE_SLAVE_TX/I2S_MODE_SLAVE_RX/
  11. *                                      I2S_MODE_MASTER_TX/I2S_MODE_MASTER_RX
  12. *@param        i2s_clock_polarity  : 显示数字的位数
  13. *@param        i2s_dataformat      : 数据长度
  14. *  @note       可以设置           : I2S_DATAFORMAT_16B/
  15. *              I2S_DATAFORMAT_16B_EXTENDED/I2S_DATAFORMAT_24B/I2S_DATAFORMAT_32B
  16. *@retval       无
  17. */
  18. void i2s_init(uint32_t i2s_standard, uint32_t i2s_mode,
  19.                 uint32_t i2s_clock_polarity, uint32_t i2s_dataformat)
  20. {
  21.    g_i2s_handler.Instance = I2S_SPI;
  22.    g_i2s_handler.Init.Mode = i2s_mode;                       /* IIS模式 */
  23.    g_i2s_handler.Init.Standard = i2s_standard;              /* IIS标准 */
  24.    g_i2s_handler.Init.DataFormat = i2s_dataformat;         /* IIS数据长度 */
  25.    g_i2s_handler.Init.MCLKOutput =I2S_MCLKOUTPUT_ENABLE; /* 主时钟输出使能 */
  26.    g_i2s_handler.Init.AudioFreq =I2S_AUDIOFREQ_DEFAULT;  /* IIS频率设置 */
  27.    g_i2s_handler.Init.CPOL = i2s_clock_polarity;            /* 空闲状态时钟电平 */
  28.    g_i2s_handler.Init.ClockSource = I2S_CLOCK_PLL;          /* IIS时钟源为PLL */
  29.    HAL_I2S_Init(&g_i2s_handler);
  30.    I2S_SPI->CR2 |= 1<<1;                 /* SPI2/I2S2 TXDMA请求使能. */
  31.    __HAL_I2S_ENABLE(&g_i2s_handler);   /* 使能I2S2 */
  32. }
  33. /**
  34. *@brief        I2S底层驱动,时钟使能,引脚配置,DMA配置
  35. *@note         此函数会被HAL_I2S_Init()调用
  36. *@param        hi2s:I2S句柄
  37. *@retval       无
  38. */
  39. voidHAL_I2S_MspInit(I2S_HandleTypeDef*hi2s)
  40. {
  41.    GPIO_InitTypeDef gpio_init_struct;
  42.    I2S_SPI_CLK_ENABLE();             /* 使能SPI2/I2S2时钟 */
  43.    I2S_LRCK_GPIO_CLK_ENABLE();      /* 使能I2S_LRCK时钟 */
  44.    I2S_SCLK_GPIO_CLK_ENABLE();      /* 使能I2S_SCLK时钟 */
  45.    I2S_SDOUT_GPIO_CLK_ENABLE();     /* 使能I2S_SDOUT时钟 */
  46.    I2S_SDIN_GPIO_CLK_ENABLE();      /* 使能I2S_SDIN时钟 */
  47.    I2S_MCLK_GPIO_CLK_ENABLE();      /* 使能I2S_MCLK时钟 */
  48.    gpio_init_struct.Pin = I2S_LRCK_GPIO_PIN;
  49.    gpio_init_struct.Mode = GPIO_MODE_AF_PP;             /* 推挽复用 */
  50.    gpio_init_struct.Pull = GPIO_PULLUP;                  /* 上拉 */
  51.    gpio_init_struct.Speed = GPIO_SPEED_HIGH;            /* 高速 */
  52.    gpio_init_struct.Alternate = GPIO_AF_I2S_SPI;        /* 复用为SPI/I2S */
  53.    HAL_GPIO_Init(I2S_LRCK_GPIO_PORT, &gpio_init_struct);/* 初始化I2S_LRCK引脚 */
  54.    gpio_init_struct.Pin = I2S_SCLK_GPIO_PIN;
  55.    HAL_GPIO_Init(I2S_SCLK_GPIO_PORT,&gpio_init_struct); /* 初始化I2S_SCLK引脚 */
  56.    
  57.    gpio_init_struct.Pin = I2S_SDOUT_GPIO_PIN;
  58.    HAL_GPIO_Init(I2S_SDOUT_GPIO_PORT,&gpio_init_struct);/*初始化I2S_SDOUT引脚*/
  59.    
  60.    gpio_init_struct.Pin = I2S_SDIN_GPIO_PIN;
  61.    HAL_GPIO_Init(I2S_SDIN_GPIO_PORT,&gpio_init_struct); /* 初始化I2S_SDIN引脚 */
  62.    
  63.    gpio_init_struct.Pin = I2S_MCLK_GPIO_PIN;
  64.    HAL_GPIO_Init(I2S_MCLK_GPIO_PORT,&gpio_init_struct); /* 初始化I2S_MCLK引脚 */
  65. }
  66. /**
  67. * 采样率计算公式:Fs=I2SxCLK/[256*(2*I2SDIV+ODD)]
  68. *I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR
  69. * 一般HSE=8Mhz
  70. *pllm:在Sys_Clock_Set设置的时候确定,一般是8
  71. *PLLI2SN:一般是192~432
  72. *PLLI2SR:2~7
  73. *I2SDIV:2~255
  74. *ODD:0/1
  75. * I2S分频系数表@pllm=8,HSE=8Mhz,即vco输入频率为1Mhz
  76. * 表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD
  77. */
  78. const uint16_t I2S_PSC_TBL[][5]=
  79. {
  80.     /*省略部分代码,见55.1.3节介绍 */
  81. };
  82. /**
  83. *@brief        开启I2S的DMA功能
  84. *@param        无
  85. *@retval       无
  86. */
  87. voidi2s_dma_enable(void)
  88. {
  89.     uint32_t tempreg = 0;
  90.    tempreg = I2S_SPI->CR2;     /* 先读出以前的设置 */
  91.    tempreg |= 1 << 1;           /* 使能DMA */
  92.    I2S_SPI->CR2 = tempreg;     /* 写入CR2寄存器中 */
  93. }
  94. /**
  95. *@brief        设置I2S的采样率
  96. *@param        samplerate:采样率, 单位:Hz
  97. *@retval       0,设置成功
  98. *                1,无法设置
  99. */
  100. uint8_t i2s_samplerate_set(uint32_t samplerate)
  101. {   
  102.     uint8_t i = 0;
  103.     uint32_t tempreg = 0;
  104.    RCC_PeriphCLKInitTypeDef rcc_i2s_clkinit_struct;
  105.     for (i = 0; i < (sizeof(I2S_PSC_TBL) / 10); i++)   /* 看看改采样率是否可以支持 */
  106.     {
  107.        if ((samplerate / 10) == I2S_PSC_TBL[0])
  108.        {
  109.            break;
  110.        }
  111.     }
  112.     if (i == (sizeof(I2S_PSC_TBL) / 10))
  113.     {
  114.        return 1;   /* 找不到 */
  115.     }
  116.     /* 外设时钟源选择 */
  117.    rcc_i2s_clkinit_struct.PeriphClockSelection = RCC_PERIPHCLK_I2S;
  118.     /* 设置PLLI2SN */
  119.    rcc_i2s_clkinit_struct.PLLI2S.PLLI2SN = (uint32_t)I2S_PSC_TBL[1];
  120.     /* 设置PLLI2SR */
  121.    rcc_i2s_clkinit_struct.PLLI2S.PLLI2SR = (uint32_t)I2S_PSC_TBL[2];
  122.    HAL_RCCEx_PeriphCLKConfig(&rcc_i2s_clkinit_struct);    /* 设置时钟 */
  123.     RCC->CR |= 1 << 26;                   /* 开启I2S时钟 */
  124.     while((RCC->CR & 1 << 27) == 0);    /* 等待I2S时钟开启成功. */
  125.    tempreg = I2S_PSC_TBL[3] << 0;   /* 设置I2SDIV */
  126.    tempreg |= I2S_PSC_TBL[4] << 8;  /* 设置ODD位 */
  127.    tempreg |= 1 << 9;                     /* 使能MCKOE位,输出MCK */
  128.     I2S_SPI->I2SPR = tempreg;             /* 设置I2SPR寄存器 */
  129.     return 0;
  130. }
  131. /**
  132. *@brief        I2S TX DMA配置
  133. * @note        设置为双缓冲模式,并开启DMA传输完成中断
  134. *@param        buf0 : M0AR地址.
  135. *@param        buf1 : M1AR地址.
  136. *@param        num  : 每次传输数据量
  137. *@retval       无
  138. */
  139. voidi2s_tx_dma_init(uint8_t* buf0, uint8_t *buf1, uint16_t num)
  140. {  
  141.    I2S_TX_DMA_CLK_ENABLE();    /* 使能I2S TX DMA时钟 */
  142.     /* 将DMA与I2S联系起来 */
  143.    __HAL_LINKDMA(&g_i2s_handler, hdmatx,g_i2s_txdma_handler);
  144.    g_i2s_txdma_handler.Instance = I2S_TX_DMASx;        /* 设置I2S TX DMA数据流 */
  145.    g_i2s_txdma_handler.Init.Channel=I2S_TX_DMASx_Channel;/*设置I2S TX DMA通道*/
  146.    g_i2s_txdma_handler.Init.Direction = DMA_MEMORY_TO_PERIPH;/*存储器到外设模式*/
  147.    g_i2s_txdma_handler.Init.PeriphInc = DMA_PINC_DISABLE;     /* 外设非增量模式 */
  148.    g_i2s_txdma_handler.Init.MemInc = DMA_MINC_ENABLE;          /* 存储器增量模式 */
  149.     /*外设数据长度:16位 */
  150.    g_i2s_txdma_handler.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
  151.     /* 存储器数据长度:16位 */
  152.    g_i2s_txdma_handler.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
  153.    g_i2s_txdma_handler.Init.Mode = DMA_CIRCULAR;                /* 使用循环模式 */
  154.    g_i2s_txdma_handler.Init.Priority = DMA_PRIORITY_HIGH;      /* 高优先级 */
  155.    g_i2s_txdma_handler.Init.FIFOMode = DMA_FIFOMODE_DISABLE;  /* 不使用FIFO */
  156.    g_i2s_txdma_handler.Init.MemBurst = DMA_MBURST_SINGLE;  /* 存储器单次突发传输 */
  157.    g_i2s_txdma_handler.Init.PeriphBurst = DMA_PBURST_SINGLE;/* 外设突发单次传输 */
  158.    HAL_DMA_DeInit(&g_i2s_txdma_handler);                        /* 先清除以前的设置 */
  159.    HAL_DMA_Init(&g_i2s_txdma_handler);                          /* 初始化DMA */
  160.    HAL_DMAEx_MultiBufferStart(&g_i2s_txdma_handler, (uint32_t)buf0,
  161.                       (uint32_t)&I2S_SPI->DR, (uint32_t)buf1, num);  /* 开启双缓冲 */
  162.    __HAL_DMA_DISABLE(&g_i2s_txdma_handler);    /* 先关闭DMA */
  163.    delay_us(10);                                     /* 10us延时,防止-O2优化出问题 */
  164.    __HAL_DMA_ENABLE_IT(&g_i2s_txdma_handler, DMA_IT_TC);   /* 开启传输完成中断 */
  165.     /* 清除DMA传输完成中断标志位 */
  166.    __HAL_DMA_CLEAR_FLAG(&g_i2s_txdma_handler, I2S_TX_DMASx_FLAG);
  167.    HAL_NVIC_SetPriority(I2S_TX_DMASx_IRQ, 0, 0);            /* DMA中断优先级 */
  168.    HAL_NVIC_EnableIRQ(I2S_TX_DMASx_IRQ);
  169. }
  170. void (*i2s_tx_callback)(void);    /* I2S DMA回调函数指针 */
  171. /**
  172. *@brief        I2S TX DMA 中断服务函数
  173. *@param        无
  174. *@retval       无
  175. */
  176. voidI2S_TX_DMASx_Handle(void)
  177. {
  178.     /*DMA传输完成 */
  179.     if(__HAL_DMA_GET_FLAG(&g_i2s_txdma_handler, I2S_TX_DMASx_FLAG)!=RESET)
  180.     {
  181.         /* 清除DMA传输完成中断标志位 */
  182.        __HAL_DMA_CLEAR_FLAG(&g_i2s_txdma_handler, I2S_TX_DMASx_FLAG);
  183.        if (i2s_tx_callback != NULL)
  184.        {
  185.            i2s_tx_callback();    /* 执行回调函数, 读取数据等操作在这里面处理 */
  186.        }
  187.     }
  188. }
  189. /**
  190. *@brief        I2S开始播放
  191. *@param        无
  192. *@retval       无
  193. */
  194. voidi2s_play_start(void)
  195. {
  196.    __HAL_DMA_ENABLE(&g_i2s_txdma_handler);    /* 开启DMA TX传输 */
  197. }
  198. /**
  199. *@brief        I2S停止播放
  200. *@param        无
  201. *@retval       无
  202. */
  203. voidi2s_play_stop(void)
  204. {
  205.    __HAL_DMA_DISABLE(&g_i2s_txdma_handler);    /* 关闭DMA TX传输 */
  206. }
复制代码
函数i2s_init完成I2S初始化,通过4个参数设置I2S的详细配置信息。函数i2s_samplerate_set则是用前面介绍的查表法,根据采样率来设置I2S的时钟。函数i2s_tx_dma_init用于设置I2S的DMA发送,使用双缓冲模式,发送数据给ES8388,并开启了发送完成中断。而函数I2S_TX_DMASx_Handle是DMA数据流发送完成中端的服务函数,该函数调用i2s_tx_callback函数(函数指针,使用前需指向特定函数)实现DMA数据填充。最后是i2s_play_start和i2s_play_stop函数,用于开启和关闭DMA中断。

2. ES8388驱动代码
ES8388主要用来将音频信号转换为数字信号或将数字信号转换为音频信号,接下来,我们开始介绍ES8388的几个函数,代码如下:
  1. /**
  2. *@brief        ES8388初始化
  3. *@param        无
  4. *@retval       0,初始化正常
  5. *               其他,错误代码
  6. */
  7. uint8_t es8388_init(void)
  8. {
  9.    iic_init();                    /* 初始化IIC接口 */
  10.    es8388_write_reg(0, 0x80);  /* 软复位ES8388 */
  11.    es8388_write_reg(0, 0x00);
  12.    delay_ms(100);                /* 等待复位 */
  13.    es8388_write_reg(0x01, 0x58);
  14.    es8388_write_reg(0x01, 0x50);
  15.    es8388_write_reg(0x02, 0xF3);
  16.    es8388_write_reg(0x02, 0xF0);
  17.    es8388_write_reg(0x03, 0x09);    /* 麦克风偏置电源关闭 */
  18.    es8388_write_reg(0x00, 0x06);    /* 使能参考 500K驱动使能 */
  19.    es8388_write_reg(0x04, 0x00);    /* DAC电源管理,不打开任何通道 */
  20.    es8388_write_reg(0x08, 0x00);    /* MCLK不分频 */
  21.    es8388_write_reg(0x2B, 0x80);    /* DAC控制    DACLRC与ADCLRC相同 */
  22. es8388_write_reg(0x09, 0x88);    /* ADC L/R PGA增益配置为+24dB */
  23.     /*ADC  数据选择为left data = left ADC,
  24.      * right data = left ADC  音频数据为16bit */
  25.      */
  26.    es8388_write_reg(0x0C, 0x4C);
  27.    es8388_write_reg(0x0D, 0x02);    /* ADC配置 MCLK/采样率=256 */
  28.    es8388_write_reg(0x10, 0x00);    /* ADC数字音量控制将信号衰减 L  设置为最小!!! */
  29.    es8388_write_reg(0x11, 0x00);    /* ADC数字音量控制将信号衰减 R  设置为最小!!! */
  30.    es8388_write_reg(0x17, 0x18);    /* DAC 音频数据为16bit */
  31.    es8388_write_reg(0x18, 0x02);    /* DAC 配置 MCLK/采样率=256 */
  32.    es8388_write_reg(0x1A, 0x00);    /* DAC数字音量控制将信号衰减 L  设置为最小!!! */
  33.    es8388_write_reg(0x1B, 0x00);    /* DAC数字音量控制将信号衰减 R  设置为最小!!! */
  34.    es8388_write_reg(0x27, 0xB8);    /* L混频器 */
  35.    es8388_write_reg(0x2A, 0xB8);    /* R混频器 */
  36.    delay_ms(100);
  37.    
  38.     return 0;
  39. }
  40. /**
  41. *@brief        ES8388写寄存器
  42. *@param        reg : 寄存器地址
  43. *@param        val : 要写入寄存器的值
  44. *@retval       0,成功
  45. *               其他,错误代码
  46. */
  47. uint8_t es8388_write_reg(uint8_t reg, uint8_t val)
  48. {
  49.    iic_start();
  50.    
  51.    iic_send_byte((ES8388_ADDR << 1)|0);    /* 发送器件地址+写命令 */
  52.     if (iic_wait_ack())
  53.     {
  54.        return 1;                      /* 等待应答(成功?/失败?) */
  55.     }
  56.    
  57.    iic_send_byte(reg);               /* 写寄存器地址 */
  58.     if (iic_wait_ack())
  59.     {
  60.        return 2;                      /* 等待应答(成功?/失败?) */
  61.     }
  62.    
  63.    iic_send_byte(val & 0xFF);      /* 发送数据 */
  64.     if (iic_wait_ack())
  65.     {
  66.        return 3;                      /* 等待应答(成功?/失败?) */
  67.     }
  68.    
  69.    iic_stop();
  70.    
  71.     return 0;
  72. }
  73. /**
  74. *@brief        ES8388读寄存器
  75. *@param        reg : 寄存器地址
  76. *@retval       读取到的数据
  77. */
  78. uint8_t es8388_read_reg(uint8_t reg)
  79. {
  80.     uint8_t temp = 0;
  81.    iic_start();
  82.    
  83.    iic_send_byte((ES8388_ADDR << 1) | 0);    /* 发送器件地址+写命令 */
  84.     if (iic_wait_ack())
  85.     {
  86.        return 1;             /* 等待应答(成功?/失败?) */
  87.     }
  88.    
  89.    iic_send_byte(reg);     /* 写寄存器地址 */
  90.     if (iic_wait_ack())
  91.     {
  92.        return 1;             /* 等待应答(成功?/失败?) */
  93.     }
  94.    
  95.    iic_start();
  96.    iic_send_byte((ES8388_ADDR << 1) | 1);    /* 发送器件地址+读命令 */
  97.     if (iic_wait_ack())
  98.     {
  99.        return 1;    /* 等待应答(成功?/失败?) */
  100.     }
  101.    
  102.    temp =iic_read_byte(0);
  103.    
  104.    iic_stop();
  105.     return temp;
  106. }
  107. /**
  108. *@brief        设置ES8388工作模式
  109. *@param        fmt : 工作模式
  110. *   @arg       0, 飞利浦标准I2S;
  111. *   @arg       1, MSB(左对齐);
  112. *   @arg       2, LSB(右对齐);
  113. *   @arg       3, PCM/DSP
  114. *@param        len : 数据长度
  115. *   @arg       0, 24bit
  116. *   @arg       1, 20bit
  117. *   @arg       2, 18bit
  118. *   @arg       3, 16bit
  119. *   @arg       4, 32bit
  120. *@retval       无
  121. */
  122. voides8388_i2s_cfg(uint8_t fmt, uint8_t len)
  123. {
  124.     fmt&= 0x03;
  125.     len&= 0x07;    /* 限定范围 */
  126.    es8388_write_reg(23, (fmt << 1) | (len << 3));  /* R23,ES8388工作模式设置 */
  127. }
  128. /**
  129. *@brief        设置耳机音量
  130. *@param        voluem : 音量大小(0 ~ 33)
  131. *@retval       无
  132. */
  133. voides8388_hpvol_set(uint8_t volume)
  134. {
  135.     if (volume > 33)
  136.     {
  137.        volume = 33;
  138.     }
  139.    
  140.    es8388_write_reg(0x2E, volume);
  141.    es8388_write_reg(0x2F, volume);
  142. }
  143. /**
  144. *@brief        设置喇叭音量
  145. *@param        volume : 音量大小(0 ~ 33)
  146. *@retval       无
  147. */
  148. voides8388_spkvol_set(uint8_t volume)
  149. {
  150.     if (volume > 33)
  151.     {
  152.        volume = 33;
  153.     }
  154.    
  155.    es8388_write_reg(0x30, volume);
  156.    es8388_write_reg(0x31, volume);
  157. }
  158. /**
  159. *@brief        设置3D环绕声
  160. *@param        depth : 0 ~ 7(3D强度,0关闭,7最强)
  161. *@retval       无
  162. */
  163. voides8388_3d_set(uint8_t depth)
  164. {
  165.    depth &= 0x7;       /* 限定范围 */
  166.    es8388_write_reg(0x1D, depth << 2);    /* R7,3D环绕设置 */
  167. }
  168. /**
  169. *@brief        ES8388 DAC/ADC配置
  170. *@param        dacen : dac使能(1)/关闭(0)
  171. *@param        adcen : adc使能(1)/关闭(0)
  172. *@retval       无
  173. */
  174. voides8388_adda_cfg(uint8_t dacen, uint8_t adcen)
  175. {
  176.     uint8_t tempreg = 0;
  177.    
  178.    tempreg |= ((!dacen) << 0);
  179.    tempreg |= ((!adcen) << 1);
  180.    tempreg |= ((!dacen) << 2);
  181.    tempreg |= ((!adcen) << 3);
  182.    es8388_write_reg(0x02, tempreg);
  183. }
  184. /**
  185. *@brief        ES8388 DAC输出通道配置
  186. *@param        o1en : 通道1使能(1)/禁止(0)
  187. *@param        o2en : 通道2使能(1)/禁止(0)
  188. *@retval       无
  189. */
  190. voides8388_output_cfg(uint8_t o1en, uint8_t o2en)
  191. {
  192.     uint8_t tempreg = 0;
  193.    tempreg |= o1en * (3 << 4);
  194.    tempreg |= o2en * (3 << 2);
  195.    es8388_write_reg(0x04, tempreg);
  196. }
  197. /**
  198. *@brief        ES8388 MIC增益设置(MIC PGA增益)
  199. *@param        gain : 0~8, 对应0~24dB  3dB/Step
  200. *@retval       无
  201. */
  202. voides8388_mic_gain(uint8_t gain)
  203. {
  204.    gain &= 0x0F;
  205.    gain |= gain << 4;
  206.    es8388_write_reg(0x09, gain);    /* R9,左右通道PGA增益设置 */
  207. }
  208. /**
  209. *@brief        ES8388 ALC设置
  210. *@param        sel
  211. *  @arg        0,关闭ALC
  212. *  @arg        1,右通道ALC
  213. *  @arg        2,左通道ALC
  214. *  @arg        3,立体声ALC
  215. *@param        maxgain : 0~7,对应-6.5~+35.5dB
  216. *@param        mingain : 0~7,对应-12~+30dB 6dB/STEP
  217. *@retval       无
  218. */
  219. voides8388_alc_ctrl(uint8_t sel, uint8_t maxgain, uint8_t mingain)
  220. {
  221.     uint8_t tempreg = 0;
  222.    tempreg = sel << 6;
  223.    tempreg |= (maxgain & 0x07) << 3;
  224.    tempreg |= mingain & 0x07;
  225.    es8388_write_reg(0x12, tempreg);    /* R18,ALC设置 */
  226. }
  227. /**
  228. *@brief        ES8388 ADC输出通道配置
  229. *@param        in : 输入通道
  230. *   @arg       0, 通道1输入
  231. *   @arg       1, 通道2输入
  232. *@retval       无
  233. */
  234. voides8388_input_cfg(uint8_t in)
  235. {
  236.    es8388_write_reg(0x0A, (5 * in) << 4);    /* ADC1 输入通道选择L/R  INPUT1 */
  237. }
复制代码
以上代码中,es8388_init函数用于初始化es8388,这里只是通用配置(ADC&DAC),初始化完成后,并不能正常播放音乐,还需要通过es8388_adda_cfg函数使能DAC,然后通过设置es8388_output_cfg选择DAC输出,通过es8388_i2s_cfg配置I2S工作模式,最后设置音量才可以接收I2S音频数据,实现音乐播放。

3. wavplay代码
wavpaly主要用于wav格式的音频文件解码,接下来看看wavplay.c里面的几个函数,代码如下:
  1. /**
  2. *@brief        WAV解析初始化
  3. *@param        fname : 文件路径+文件名
  4. *@param        wavx  : 信息存放结构体指针
  5. *@retval       0,打开文件成功
  6. *                1,打开文件失败
  7. *                2,非WAV文件
  8. *                3,DATA区域未找到
  9. */
  10. uint8_t wav_decode_init(char* fname, __wavctrl* wavx)
  11. {
  12.     FIL*ftemp;
  13.     uint8_t *buf;
  14.     uint32_t br = 0;
  15.     uint8_t res = 0;
  16.    ChunkRIFF *riff;
  17.    ChunkFMT *fmt;
  18.    ChunkFACT *fact;
  19.    ChunkDATA *data;
  20.    
  21.    ftemp = (FIL*)mymalloc(SRAMIN, sizeof(FIL));
  22.     buf= mymalloc(SRAMIN, 512);
  23.    
  24.     if (ftemp && buf)    /* 内存申请成功 */
  25.     {
  26.        res = f_open(ftemp, (TCHAR*)fname, FA_READ);    /* 打开文件 */
  27.       
  28.        if (res == FR_OK)
  29.        {
  30.            f_read(ftemp, buf, 512, &br);         /* 读取512字节在数据 */
  31.            riff = (ChunkRIFF *)buf;               /* 获取RIFF块 */
  32.            if (riff->Format == 0x45564157)       /* 是WAV文件 */
  33.            {
  34.                 fmt = (ChunkFMT *)(buf + 12);     /* 获取FMT块 */
  35.                 fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);/*读取FACT块*/
  36.                
  37.                 if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
  38.                 {
  39.                     wavx->datastart = 12 + 8 + fmt->ChunkSize + 8 +
  40.                                  fact->ChunkSize;    /* 具有fact/LIST块的时候(未测试) */
  41.                 }
  42.                 else
  43.                 {
  44.                     wavx->datastart = 12 + 8 + fmt->ChunkSize;
  45.                 }
  46.                
  47.                 data = (ChunkDATA *)(buf + wavx->datastart); /* 读取DATA块 */
  48.                
  49.                 if (data->ChunkID == 0x61746164)               /* 解析成功! */
  50.                 {
  51.                     wavx->audioformat = fmt->AudioFormat;     /* 音频格式 */
  52.                     wavx->nchannels = fmt->NumOfChannels;      /* 通道数 */
  53.                     wavx->samplerate = fmt->SampleRate;        /* 采样率 */
  54.                     wavx->bitrate = fmt->ByteRate * 8;         /* 得到位速 */
  55.                     wavx->blockalign = fmt->BlockAlign;        /* 块对齐 */
  56.                     wavx->bps = fmt->BitsPerSample;             /* 位数,16/24/32位 */
  57.                     
  58.                     wavx->datasize = data->ChunkSize;          /* 数据块大小 */
  59.                     wavx->datastart = wavx->datastart + 8;    /* 数据流开始的地方. */
  60.                     
  61.                     printf("wavx->audioformat:%d\r\n", wavx->audioformat);
  62.                     printf("wavx->nchannels:%d\r\n", wavx->nchannels);
  63.                     printf("wavx->samplerate:%d\r\n", wavx->samplerate);
  64.                     printf("wavx->bitrate:%d\r\n", wavx->bitrate);
  65.                     printf("wavx->blockalign:%d\r\n", wavx->blockalign);
  66.                     printf("wavx->bps:%d\r\n", wavx->bps);
  67.                     printf("wavx->datasize:%d\r\n", wavx->datasize);
  68.                     printf("wavx->datastart:%d\r\n", wavx->datastart);  
  69.                 }
  70.                 else
  71.                 {
  72.                     res = 3;   /* data区域未找到. */
  73.                 }
  74.            }
  75.            else
  76.            {
  77.                 res = 2;       /* 非wav文件 */
  78.            }
  79.        }
  80.        else
  81.        {
  82.            res = 1;           /* 打开文件错误 */
  83.        }
  84.     }
  85.    
  86.    f_close(ftemp);
  87.    
  88.    myfree(SRAMIN, ftemp);  /* 释放内存 */
  89.    myfree(SRAMIN, buf);
  90.    
  91.     return 0;
  92. }
  93. /**
  94. *@brief        填充buf
  95. *@param        buf  : 填充区
  96. *@param        size : 填充数据量
  97. *@param        bits : 位数(16/24)
  98. *@retval       读取到的数据长度
  99. */
  100. uint32_t wav_buffill(uint8_t *buf, uint16_t size, uint8_t bits)
  101. {
  102.     uint16_t readlen = 0;
  103.     uint32_t bread;
  104.     uint16_t i;
  105.     uint8_t *p;
  106.    
  107.     if (bits == 24)    /* 24bit音频,需要处理一下 */
  108.     {
  109.        readlen = (size / 4) * 3;    /* 此次要读取的字节数 */
  110.        /* 读取数据 */
  111.        f_read(g_audiodev.file, g_audiodev.tbuf, readlen, (UINT*)&bread);
  112.        p = g_audiodev.tbuf;
  113.       
  114.        for (i = 0; i < size;)
  115.        {
  116.            buf[i++] = p[1];
  117.            buf = p[2];
  118.            i += 2;
  119.            buf[i++] = p[0];
  120.            p += 3;
  121.        }
  122.       
  123.        bread = (bread * 4) / 3;     /* 填充后的大小. */
  124.     }
  125.     else
  126. {
  127.         /* 16bit音频,直接读取数据 */
  128.        f_read(g_audiodev.file, buf, size, (UINT*)&bread);
  129.       
  130.        if (bread < size)             /* 不够数据了,补充0 */
  131.        {
  132.            for (i = bread; i < size - bread; i++)
  133.            {
  134.                 buf = 0;
  135.            }
  136.        }
  137.     }
  138.    
  139.     return bread;
  140. }
  141. /**
  142. *@brief        WAV播放时,I2S DMA传输回调函数
  143. *@param        无
  144. *@retval       无
  145. */
  146. voidwav_i2s_dma_tx_callback(void)
  147. {
  148.     uint16_t i;
  149.     if (DMA1_Stream4->CR & (1 << 19))
  150.     {
  151.        wavwitchbuf = 0;
  152.       
  153.        if ((g_audiodev.status & 0x01) == 0)
  154.        {
  155.            for (i = 0; i < WAV_I2S_TX_DMA_BUFSIZE; i++)    /* 暂停 */
  156.            {
  157.                 g_audiodev.i2sbuf1 = 0;    /* 填充0 */
  158.            }
  159.        }
  160.     }
  161.     else
  162.     {
  163.        wavwitchbuf = 1;
  164.        if ((g_audiodev.status & 0x01) == 0)
  165.        {
  166.            for (i = 0; i < WAV_I2S_TX_DMA_BUFSIZE; i++)    /* 暂停 */
  167.            {
  168.                 g_audiodev.i2sbuf2 = 0;    /* 填充0 */
  169.            }
  170.        }
  171.     }
  172.    
  173.    wavtransferend = 1;
  174. }
  175. /**
  176. *@brief        获取当前播放时间
  177. *@param        fname : 文件指针
  178. *@param        wavx  : wavx播放控制器
  179. *@retval       无
  180. */
  181. voidwav_get_curtime(FIL *fx, __wavctrl *wavx)
  182. {
  183.     long long fpos;
  184.    wavx->totsec = wavx->datasize / (wavx->bitrate / 8); /* 歌曲总长度(单位:秒) */
  185.    fpos = fx->fptr-wavx->datastart;    /* 得到当前文件播放到的地方 */
  186.    wavx->cursec = fpos*wavx->totsec / wavx->datasize;   /* 当前播放到第多少秒了? */
  187. }
  188. /**
  189. *@brief        播放某个wav文件
  190. *@param        fname : 文件路径+文件名
  191. *@retval       KEY0_PRES,错误
  192. *                KEY1_PRES,打开文件失败
  193. *                其他,非WAV文件
  194. */
  195. uint8_t wav_play_song(char* fname)
  196. {
  197.     uint8_t key;
  198.     uint8_t t = 0;
  199.     uint8_t res;  
  200.     uint32_t fillnum;
  201.    
  202.    g_audiodev.file = (FIL*)mymalloc(SRAMIN, sizeof(FIL));
  203.    g_audiodev.i2sbuf1 = mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
  204.    g_audiodev.i2sbuf2 = mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
  205.    g_audiodev.tbuf = mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
  206.    
  207. if (g_audiodev.file && g_audiodev.i2sbuf1 && g_audiodev.i2sbuf2
  208.                         && g_audiodev.tbuf)
  209.     {
  210.        res =wav_decode_init(fname, &wavctrl);    /* 得到文件的信息 */
  211.       
  212.        if (res == 0)    /* 解析文件成功 */
  213.        {
  214.            if (wavctrl.bps == 16)
  215.            {
  216.                 es8388_i2s_cfg(0, 3);     /* 飞利浦标准,16位数据长度 */
  217.                 /* 飞利浦标准,主机发送,时钟低电平有效,16位扩展帧长度 */
  218.                 i2s_init(I2S_STANDARD_PHILIPS, I2S_MODE_MASTER_TX, I2S_CPOL_LOW,
  219.                            I2S_DATAFORMAT_16B_EXTENDED);
  220.            }
  221.            else if (wavctrl.bps == 24)
  222.            {
  223.                 es8388_i2s_cfg(0, 0);     /* 飞利浦标准,24位数据长度 */
  224.               /* 飞利浦标准,主机发送,时钟低电平有效,24位长度 */
  225.                 i2s_init(I2S_STANDARD_PHILIPS, I2S_MODE_MASTER_TX, I2S_CPOL_LOW,
  226.                            I2S_DATAFORMAT_24B);
  227.            }
  228.            
  229.            i2s_samplerate_set(wavctrl.samplerate);          /* 设置采样率 */
  230.            i2s_tx_dma_init(g_audiodev.i2sbuf1, g_audiodev.i2sbuf2,
  231.                                WAV_I2S_TX_DMA_BUFSIZE/ 2);     /* 配置TX DMA */
  232.             /* 回调函数指wav_i2s_dma_callback */
  233.            i2s_tx_callback = wav_i2s_dma_tx_callback;
  234.            audio_stop();
  235.            
  236.            res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);  /* 打开文件 */
  237.            
  238.            if (res == 0)
  239.            {
  240.                 f_lseek(g_audiodev.file, wavctrl.datastart);          /* 跳过文件头 */
  241.                
  242.                 fillnum = wav_buffill(g_audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,
  243.                                            wavctrl.bps);
  244.                 fillnum = wav_buffill(g_audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,
  245.                                            wavctrl.bps);
  246.                
  247.                 audio_start();
  248.                
  249.                 while (res == 0)
  250.                 {
  251.                     while(wavtransferend== 0);    /* 等待wav传输完成; */
  252.                     
  253.                     wavtransferend = 0;
  254.                     
  255.                     if (fillnum != WAV_I2S_TX_DMA_BUFSIZE)    /* 播放结束? */
  256.                     {
  257.                         res = KEY0_PRES;
  258.                         break;
  259.                     }
  260.                     
  261.                     if (wavwitchbuf)
  262.                     {
  263.                         fillnum = wav_buffill(g_audiodev.i2sbuf2,
  264.                                  WAV_I2S_TX_DMA_BUFSIZE, wavctrl.bps);  /* 填充buf2 */
  265.                     }
  266.                     else
  267.                     {
  268.                         fillnum = wav_buffill(g_audiodev.i2sbuf1,
  269.                                  WAV_I2S_TX_DMA_BUFSIZE, wavctrl.bps);  /* 填充buf1 */
  270.                     }
  271.                     
  272.                     while (1)
  273.                     {
  274.                        key = key_scan(0);
  275.                         if (key == WKUP_PRES)    /* 暂停 */
  276.                         {
  277.                             if (g_audiodev.status & 0x01)
  278.                             {
  279.                                 g_audiodev.status &= ~(1 << 0);
  280.                             }
  281.                             else
  282.                             {
  283.                                 g_audiodev.status |= 0x01;
  284.                             }
  285.                         }
  286.                         /* 下一曲/上一曲 */
  287.                         if (key == KEY2_PRES || key == KEY0_PRES)
  288.                         {
  289.                             res = key;
  290.                             break;
  291.                         }
  292.                         /* 得到总时间和当前播放的时间 */
  293.                         wav_get_curtime(g_audiodev.file, &wavctrl);
  294.                         audio_msg_show(wavctrl.totsec, wavctrl.cursec,
  295.                                           wavctrl.bitrate);
  296.                         t++;
  297.                         if (t == 20)
  298.                         {
  299.                             t = 0 ;
  300.                             LED0_TOGGLE();
  301.                         }
  302.                         
  303.                         if ((g_audiodev.status & 0x01) == 0)
  304.                         {
  305.                             delay_ms(10);
  306.                         }
  307.                         else
  308.                         {
  309.                             break;
  310.                         }
  311.                     }
  312.                 }
  313.                 audio_stop();
  314.            }
  315.            else
  316.            {
  317.                 res = 0xFF;
  318.            }
  319.        }
  320.        else
  321.        {
  322.            res = 0xFF;
  323.        }
  324.     }
  325.     else
  326.     {
  327.        res = 0xFF;
  328.     }
  329.    
  330.    myfree(SRAMIN, g_audiodev.tbuf);         /* 释放内存 */
  331.    myfree(SRAMIN, g_audiodev.i2sbuf1);     /* 释放内存 */
  332.    myfree(SRAMIN, g_audiodev.i2sbuf2);     /* 释放内存 */
  333.    myfree(SRAMIN, g_audiodev.file);         /* 释放内存 */
  334.    
  335.     return res;
  336. }
复制代码
以上代码中,wav_decode_init函数,用来对wav音频文件进行解析,得到wav的详细信息(音频采样率,位数,数据流起始位置等);wav_buffill函数,通过f_read读取数据,然后将数据填充到buf里面,注意:在读取24位音频数据的时候,读出数据需要经过转换后才填充到buf;wav_i2s_dma_tx_callbac函数则是DMA发送完成的回调函数(i2s_tx_callbackja函数指针指向该函数),这里,我们对数据进行了填0处理,而是采用2个变量:wavtransferend和wavwitchbuf,来告诉wav_paly_son函数是否传输完成,以及应该填充哪个数据buf(i2sbuf1或i2sbuf2)。

最后,wav_play_song函数,是播放WAV最终执行的函数,该函数解析完WAV文件后,设置ES8388和I2S的参数(采样率,位数等),并开启DMA,然后不断填充数据,实现WAV播放,该函数中还进行了按键检测,实现上下曲切换和暂停/播放等操作。

4. audioplay代码
这部分我们需要根据ES8388推荐的初始化顺序时行配置。我们需要借助SD卡和文件系统把我们需要播放的歌曲传给ES8388播放。我们在User目录下新建一个《APP》文件夹,同时在该目录下新建audioplay.c和audioplay.h并加入到工程。

首先判断音乐文件类型,符合条件的再把相应的文件数据发送给ES8388,我们在FATFS的扩展文件中已经实现了判断文件类型这个功能,在图片显示实验也演示了这部分代码的使用,我们把这个功能封装成了audio_get_tnum()函数,这部分参考我们光盘源码即可。接下来我们来分析一下audio play()和audio_play_song ()函数,实现播放歌曲的功能,代码如下:
  1. /**
  2. *@brief        播放音乐
  3. *@param        无
  4. *@retval       无
  5. */
  6. void audio_play(void)
  7. {
  8.     uint8_t res;
  9.     DIRwavdir;               /* 目录 */
  10.    FILINFO *wavfileinfo;   /* 文件信息 */
  11.     char *pname;              /* 带路径的文件名 */
  12.     uint16_t totwavnum;     /* 音乐文件总数 */
  13.     uint16_t curindex;       /*当前索引 */
  14.     uint8_t key;              /* 键值 */
  15.     uint32_t temp;
  16.     uint32_t *wavoffsettbl; /* 音乐offset索引表 */
  17.    es8388_adda_cfg(1, 0);  /* 开启DAC关闭ADC */
  18.    es8388_output_cfg(1, 1);/* DAC选择通道1输出 */
  19.     while (f_opendir(&wavdir, "0:/MUSIC"))  /* 打开音乐文件夹 */
  20.     {
  21.        text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
  22.        delay_ms(200);
  23.        lcd_fill(30, 190, 240, 206, WHITE); /* 清除显示 */
  24.        delay_ms(200);
  25.     }
  26.    totwavnum = audio_get_tnum("0:/MUSIC"); /* 得到总有效文件数 */
  27.     while (totwavnum == NULL)                 /* 音乐文件总数为0 */
  28.     {
  29.        text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
  30.        delay_ms(200);
  31.        lcd_fill(30, 190, 240, 146, WHITE); /* 清除显示 */
  32.        delay_ms(200);
  33.     }
  34.    
  35.    wavfileinfo = (FILINFO*)mymalloc(SRAMIN, sizeof(FILINFO)); /* 申请内存 */
  36.    pname = mymalloc(SRAMIN, FF_MAX_LFN * 2 + 1);    /* 为带路径的文件名分配内存 */
  37.     /* 申请4*totwavnum个字节的内存,用于存放音乐文件off block索引 */
  38.    wavoffsettbl = mymalloc(SRAMIN, 4 * totwavnum);
  39.     while (!wavfileinfo || !pname || !wavoffsettbl)              /* 内存分配出错 */
  40.     {
  41.        text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
  42.        delay_ms(200);
  43.        lcd_fill(30, 190, 240, 146, WHITE); /* 清除显示 */
  44.        delay_ms(200);
  45.     }
  46.    
  47.     /* 记录索引 */
  48.     res= f_opendir(&wavdir, "0:/MUSIC");   /* 打开目录 */
  49.     if (res == FR_OK)
  50.     {
  51.        curindex = 0;    /* 当前索引为0 */
  52.        while (1)         /*全部查询一遍 */
  53.        {
  54.            temp = wavdir.dptr;    /* 记录当前index */
  55.            res = f_readdir(&wavdir, wavfileinfo);   /* 读取目录下的一个文件 */
  56.            if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
  57.            {
  58.                 break;     /* 错误了/到末尾了,退出 */
  59.            }
  60.            res =exfuns_file_type(wavfileinfo->fname);
  61.            if ((res & 0xF0) == 0x40)   /* 取高四位,看看是不是音乐文件 */
  62.            {
  63.                 wavoffsettbl[curindex] = temp;    /* 记录索引 */
  64.                 curindex++;
  65.            }
  66.        }
  67.     }
  68.    
  69.    curindex = 0;    /* 从0开始显示 */
  70.     res= f_opendir(&wavdir, (const TCHAR*)"0:/MUSIC");    /* 打开目录 */
  71.     while (res == FR_OK)    /* 打开成功 */
  72.     {
  73.        dir_sdi(&wavdir, wavoffsettbl[curindex]);   /* 改变当前目录索引 */
  74.        res = f_readdir(&wavdir, wavfileinfo);       /* 读取目录下的一个文件 */
  75.        if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
  76.        {
  77.            break;           /* 错误了/到末尾了,退出 */
  78.        }
  79.       
  80.        strcpy((char*)pname, "0:/MUSIC/");           /* 复制路径(目录) */
  81.        strcat((char*)pname, (const char*)wavfileinfo->fname);/* 将文件名接在后面 */
  82.        lcd_fill(30, 190, lcddev.width - 1, 190 + 16, WHITE); /* 清除之前的显示 */
  83.        text_show_string(30, 190, lcddev.width - 60, 16,
  84.                            (char*)wavfileinfo->fname, 16, 0, BLUE); /* 显示歌曲名字 */
  85.        audio_index_show(curindex + 1, totwavnum);
  86.        key =audio_play_song(pname);    /* 播放这个音频文件 */
  87.        if (key == KEY2_PRES)            /* 上一曲 */
  88.        {
  89.            if (curindex)
  90.            {
  91.                 curindex--;
  92.            }
  93.            else
  94.            {
  95.                 curindex = totwavnum - 1;
  96.            }
  97.        }
  98.        else if (key == KEY0_PRES)  /* 下一曲 */
  99.        {
  100.            curindex++;
  101.            if (curindex >= totwavnum)
  102.            {
  103.                 curindex = 0;       /* 到末尾的时候,自动从头开始 */
  104.            }
  105.        }
  106.        else
  107.        {
  108.            break;  /* 产生了错误 */
  109.        }
  110.     }
  111.    myfree(SRAMIN, wavfileinfo);     /* 释放内存 */
  112.    myfree(SRAMIN, pname);           /* 释放内存 */
  113.    myfree(SRAMIN, wavoffsettbl);    /* 释放内存 */
  114. }
  115. /**
  116. *@brief        播放某个音频文件
  117. *@param        fname : 文件名
  118. *@retval       按键值
  119. *  @arg        KEY0_PRES , 下一曲.
  120. *  @arg        KEY2_PRES , 上一曲.
  121. *  @arg        其他 , 错误
  122. */
  123. uint8_t audio_play_song(char* fname)
  124. {
  125.     uint8_t res;  
  126.    
  127.     res=exfuns_file_type(fname);
  128.     switch (res)
  129.     {
  130.        case T_WAV:
  131.            res =wav_play_song(fname);
  132.            break;
  133.        default:           /* 其他文件,自动跳转到下一曲 */
  134.            printf("can'tplay:%s\r\n", fname);
  135.            res = KEY0_PRES;
  136.            break;
  137.     }
  138.     return res;
  139. }
复制代码
这里,audio_play函数在main函数里面被调用,该函数首先设置ES8388相关配置,然后查找SD卡里面的MUSIC文件夹,并统计该文件夹里面总共有多少音频文件(统计包括:WAV/MP3/APE/FLAC等),然后,该函数调用audio_play_song函数,按顺序播放这些音频文件。

在audio_play_song函数里面,通过判断文件类型,调用不同的解码函数,本章,只支持WAV文件,通过wav_play_song函数实现WAV解码。

4. main.c代码
解决了音乐播放的问题,main.c函数实现起来就比较简单了,我们可以按照流程图的设计思路进行编写即可:
  1. int main(void)
  2. {
  3.    HAL_Init();                             /* 初始化HAL库 */
  4.    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
  5.    delay_init(168);                       /* 延时初始化 */
  6.    usart_init(115200);                   /* 串口1初始化为115200 */
  7.    usmart_dev.init(84);                  /* 初始化USMART */
  8.    led_init();                             /* 初始化LED */
  9.    lcd_init();                             /* 初始化LCD */
  10.    key_init();                             /* 初始化按键 */
  11.    sram_init();                            /* SRAM初始化 */
  12.    norflash_init();                        /* 初始化NORFLASH */
  13.    my_mem_init(SRAMIN);                   /* 初始化内部SRAM内存池 */
  14.    my_mem_init(SRAMEX);                   /* 初始化外部SRAM内存池 */
  15.    
  16.     while (sd_init())                      /* 检测SD卡 */
  17.     {
  18.        lcd_show_string(30, 50, 200, 16, 16, "SD CardFailed!", RED);
  19.        delay_ms(200);
  20.        lcd_fill(30, 50, 200 + 30, 50 + 16, WHITE);
  21.        delay_ms(200);
  22.     }
  23.    exfuns_init();                /* 为fatfs相关变量申请内存 */
  24.    f_mount(fs[0], "0:", 1);    /* 挂载SD卡 */
  25.    f_mount(fs[1], "1:", 1);    /* 挂载FLASH */
  26.     while (fonts_init())         /* 检查字库 */
  27.     {
  28.        lcd_show_string(30, 50, 200, 16, 16, "FontError!", RED);
  29.        delay_ms(200);
  30.        lcd_fill(30, 50, 200 + 30, 50 + 16, WHITE);
  31.        delay_ms(200);
  32.     }
  33.    
  34.    es8388_init();                /* ES8388初始化 */
  35.    es8388_adda_cfg(1, 0);       /* 开启DAC关闭ADC */
  36.    es8388_output_cfg(1, 1);    /* DAC选择通道输出 */
  37.    es8388_hpvol_set(25);        /* 设置耳机音量 */
  38.    es8388_spkvol_set(30);       /* 设置喇叭音量 */
  39.    
  40.    text_show_string(30, 30, 200, 16, "正点原子STM32开发板", 16, 0, RED);
  41.    text_show_string(30, 50, 200, 16, "音乐播放器实验", 16, 0, RED);
  42.    text_show_string(30, 70, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
  43.    text_show_string(30, 90, 200, 16, "2021年11月16日", 16, 0, RED);
  44.    text_show_string(30, 110, 200, 16, "KEY0:NEXT  KEY2:PREV", 16, 0, RED);
  45.    text_show_string(30, 130, 200, 16, "KEY_UP:PAUSE/PLAY", 16, 0, RED);
  46.    
  47.     while (1)
  48.     {
  49.        audio_play();    /* 播放音乐 */
  50.     }
  51. }
复制代码
到这里本实验的代码基本就编写完成了,我们准备好音乐文件放到SD卡根目录下的《MUSIC》夹下测试本实验的代码,可以把ES8388的配置函数加到USMART下,这样就可以通过USMART来测试和调试ES8388了。

55.4 下载验证
在代码编译成功之后,我们下载代码到开发板上,程序先执行字库检测,然后当检测到SD卡根目录的MUSIC文件夹有音频文件(WAV格式音频)的时候,就开始自动播放歌曲了,如图55.4.1所示:
image027.png
图55.4.1音乐播放中

从上图可以看出,总共1首歌曲,当前正在播放第1首歌曲,歌曲名、播放时间、总时长、码率、音量等信息等也都有显示。此时DS0会随着音乐的播放而闪烁。

此时我们便可以听到开发板板载喇叭播放出来的音乐了,也可以在开发板的PHONE端子插入耳机来听歌。同时,我们可以通过按KEY0和KEY2来切换下一曲和上一曲,通过KEY_UP暂停和继续播放。

至此,我们就完成了一个简单的音乐播放器了,虽然只支持WAV文件,但是大家可以在此基础上,增加其他音频格式解码器(可参考综合实验),便可实现其他音频格式解码了。

正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-2-24 06:08

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表