搜索
bottom↓
回复: 0

《DNESP32S3使用指南-IDF版_V1.6》第四十一章 音乐播放器实验

[复制链接]

出0入234汤圆

发表于 2024-7-31 10:36:13 | 显示全部楼层 |阅读模式
2.jpg
1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729
1.png
3.png

第四十一章 音乐播放器实验


       正点原子DNESP32S3开发板拥有串行音频接口(SAI),支持SAI、LSB/MSB对齐、PCM/DSP、TDM和AC’97等协议,且外扩了一颗HIFI级CODEC芯片:ES8388,支持最高192K 24BIT的音频播放,并且支持录音(下一章介绍)本章,我们将利用DNESP32S3开发板实现一个简单的音乐播放器(仅支持WAV播放)。
       本章分为如下几个小节:
       41.1 WAV&ES8388&SAI简介
       41.2 硬件设计
       41.3 程序设计
       41.4 下载验证

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

       41.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由块标识符、数据大小和数据三部分组成,如图 41.1.1 所示:

第四十一章 音乐播放器实验804.png
图41.1.1 Chunk组成结构

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

第四十一章 音乐播放器实验1047.png
图41.1.2 PCM格式的wav文件构成

       可以看到,不同的Chunk有不同的长度,编码文件时,按照Chunk的字节和位序排列好之后写入文件头,加上wav的后缀,就可以生成一个能被正确解析的wav文件了,对于PCM结构,我们只需要把获取到的音频数据填充到Data Chunk中即可。我们将利用ES8388实现16位,8Khz采样率的单声道WAV录音(PCM格式)。
       首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:
typedef __PACKED_STRUCT
  1. {
  2.     uint32_t ChunkID;              /* chunk id;这里固定为"RIFF",即0X46464952 */
  3.     uint32_t ChunkSize;          /* 集合大小;文件总大小-8 */
  4.     uint32_t Format;               /* 格式;WAVE,即0X45564157 */
  5. }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. }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位置可以分成如表41.1.1.1所示的几种形式:

8.png
表41.1.1.1 WAVE文件数据采样格式

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

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

       ●SAI接口,支持最高192K,24bit音频播放

       ●DAC信噪比96dB;ADC信噪比95dB

       ●支持主机和从机模式

       ●支持立体声差分输入/麦克风输入

       ●支持左右声道音量独立调节

       ●支持40mW耳机输出,无爆音

       ES8388的控制通过I2S接口(即数字音频接口)同MCU进行音频数据传输(支持音频接收和发送),通过两线(CE=0/1,即IIC接口)或三线(CE脚产生一个下降沿,即SPI接口)接口进行配置。ES8388的SAI接口,由4个引脚组成:

       ASDOUT:ADC数据输出

       DSDIN:DAC数据输入

       LRC:数据左/右对齐时钟

       SCLK:位时钟,用于同步

       ES8388可作为SAI主机,输出LRC和SLCK时钟,不过我们一般使用ES8388作为从机,接收LRC和SLCK。另外,ES8388的SAI接口支持4种不同的音频数据模式:左(MSB)对齐标准、右(LSB)对齐标准、飞利浦(SAI)标准、DSP/PCM。本章,我们用飞利浦标准来传输SAI数据。
       飞利浦(SAI)标准模式,数据在跟随LRC传输的BCLK的第二个上升沿时传输MSB,其他位一直到LSB按顺序传输。传输依赖于字长、BCLK频率和采样率,在每个采样的LSB和下一个采样的MSB之间都应该有未用的BCLK周期。飞利浦标准模式的SAI数据传输协议如图41.1.2.1所示:

第四十一章 音乐播放器实验4313.png
图41.1.2.1 飞利浦标准模式SAI数据传输图

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

第四十一章 音乐播放器实验4490.png
图41.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的数据手册自行研究。

       41.1.3 I²S控制器介绍
       I²S(Inter-IC Sound,集成电路内置音频总线)是一种同步串行通信协议,通常用于两个数字音频设备之间传输音频数据。DNESP32S3内置两个I²S接口(I²S0和I²S1),为多媒体应用,尤其是为数字音频应用提供了灵活的数据通信接口。
       I²S标准总线定义了三种信号:串行时钟信号BCK、字选择信号WS和串行数据信号SD。一个基本的I²S数据总线有一个主机和一个从机。主机和从机的角色在通信过程中保持不变。DNESP32S3的I²S模块包含独立的发送单元和接收单元,能够保证优良的通信性能。
       I²S有如下功能:

       主机模式:I²Sn作为主机,BCK/WS向外部输出,向从机发送或从其接收数据。

       从机模式:I²Sn作为从机,BCK/WS从外部输入,从主机接收或向其发送数据。

       全双工:主机与从机之间的发送线和接收线各自独立,发送数据和接收数据同时进行。

       半双工:主机和从机只能有一方先发送数据,另一方接收数据。发送数据和接收数据不能同时进行。

       TDM RX模式:利用时分复用方式接收脉冲编码调制(PCM)数据,并将其通过DMA存入储存器的模式。信号线包括

       BCK、WS和DATA。可以接收最多16个通道的数据。通过用户配置,可支持TDM Philips格式、TDM MSB对齐格式、TDM PCM格式等。

       PDM RX模式:接收脉冲密度调制(PDM)数据,并将其通过DMA存入储存器的模式。信号线包括WS和DATA。通过用户配置,可支持PDM标准格式等。

       TDM TX模式:通过DMA从储存器中取得脉冲编码调制(PCM)数据,并利用时分复用方式将其发送的模式。信号线包括BCK、WS和DATA,可以发送最多16个通道的数据。通过用户配置,可支持TDM Philips格式、TDM MSB对齐格式、TDM PCM格式等。

       PDM TX模式:通过DMA从储存器中取得脉冲密度调制(PDM)数据,并将其发送的模式。信号线包括WS和DATA。通过用户配置,可支持PDM标准格式等。

       PCMtoPDM TX模式(仅对I²S0有效):通过DMA从储存器中取得脉冲编码调制(PCM)数据,将其转换为脉冲密度调制(PDM)数据,并将其发送的主机模式。信号线包括WS和DATA。通过用户配置,可支持PDM标准格式等。

       PDMtoPCM RX模式(仅对I²S0有效):接收脉冲密度调制(PDM)数据,将其转换为脉冲编码调制(PCM)数据,并将其通过DMA存入储存器的主机模式或从机模式。信号线包括WS和DATA。通过用户配置,可支持PDM标准格式等。

       更详细的内容请大家参考《ESP32-S3技术参考手册.pdf》第28章。

       41.2 硬件设计

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

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

       1. LED灯
              LED -IO0

       2.独立按键
              KEY0(XL9555) - IO1_7
              KEY1(XL9555) - IO1_6
              KEY2(XL9555) - IO1_5
              KEY3(XL9555) - IO1_4

       3. XL9555
              IIC_SDA-IO41
              IIC_SCL-IO42

       4. SPILCD
              CS-IO21
              SCK-IO12
              SDA-IO11
              DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)
              PWR- IO1_3(XL9555)
              RST- IO1_2(XL9555)

       5. SD
              CS-IO2
              SCK-IO12
              MOSI-IO11
              MISO-IO13

       6. ES8388音频CODEC芯片(IIC端口0)
              IIC_SDA-IO41
              IIC_SCL-IO42
              I2S_BCK_IO-IO46
              I2S_WS_IO-IO9
              I2S_DO_IO-IO10
              I2S_DI_IO-IO14
              IS2_MCLK_IO-IO3

       41.2.3 原理图
       DNESP32S3开发板板载了ES8388解码芯片的驱动电路,原理图如图41.1.1所示:

第四十一章 音乐播放器实验9763.png
图41.2.3.1 ES838原理图

       图中,PHONE接口可以用来插耳机,并连接了板载的喇叭SPEAKER(开发板正上方)。

       41.3 程序设计

       41.3.1 程序流程图
       程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:

第四十一章 音乐播放器实验9939.png
图41.3.1.1 音频播放实验程序流程图

       41.3.2 I2S函数解析
       ESP-IDF提供了一套API来配置I2S。要使用此功能,需要导入必要的头文件:
  1. #include "driver/i2s.h"
  2. #include "driver/i2s_std.h"
  3. #include "driver/i2s_pdm.h"
复制代码
       接下来,作者将介绍一些常用的ESP32-S3中的I2S函数,这些函数的描述及其作用如下:

       1,设置I2S引脚
       该函数用给定的配置,来配置I2S总线,该函数原型如下所示:
  1. esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin);
复制代码
       该函数的形参描述如下表所示:

1.png
表41.3.2.1 i2s_set_pin()函数形参描述

       该函数的返回值描述,如下表所示:

2.png
表41.3.2.2 函数i2s_set_pin ()返回值描述

       该函数使用i2s_pin_config_t类型的结构体变量传入,该结构体的定义如下所示:

3.png
表41.3.2.3 i2s_pin_config_t结构体参数值描述

       完成上述结构体参数配置之后,可以将结构传递给 i2s_set_pin () 函数,用以实例化IIC并返回IIC句柄。

       2,安装I2S驱动
       该函数安装I2S驱动,该函数原型如下所示:
  1. esp_err_t i2s_driver_install(i2s_port_t i2s_num,
  2.                              const i2s_config_t *i2s_config,
  3.                              int queue_size,
  4.                              void *i2s_queue);
复制代码
       该函数的形参描述如下表所示:

4.png
表41.3.2.4 i2s_driver_install()函数形参描述

       该函数的返回值描述,如下表所示:

5.png
表41.3.2.5 函数i2s_driver_install ()返回值描述

       3,处理缓冲区
       该函数将TX DMA缓冲区的内容归零,该函数原型如下所示:
  1. esp_err_t i2s_zero_dma_buffer(i2s_port_t i2s_num);
复制代码
       该函数的形参描述如下表所示:

6.png
表41.3.2.6 i2s_zero_dma_buffer ()函数形参描述

       该函数的返回值描述,如下表所示:

7.png
表41.3.2.7 函数i2s_zero_dma_buffer ()返回值描述

       41.3.3 音频播放驱动解析
       在IDF版的30_music例程中,作者在30_music \components\BSP路径下新增了一个I2S文件夹和一个ES8388文件夹,分别用于存放i2s.c、i2s.h和es8388.c以及es8388.h这四个文件。其中,i2s.h和es8388.h文件负责声明I2S以及ES8388相关的函数和变量,而i2s.c和es8388.c文件则实现了I2S以及ES8388的驱动代码。下面,我们将详细解析这四个文件的实现内容。

       1,i2s驱动
       音乐文件我们要通过SD卡来传给单片机,那我们自然要用到文件系统。LCD、按键交互这些我们也需要实现。
       由于播放功能涉及到多个外设的配合使用,用文件系统读音频文件,做播放控制等,所以我们把ES8388的硬件驱动放到components\BSP目录下,播放功能作为APP放到main目录下。
       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,I²S的驱动主要包括两个文件:i2s.c和i2s.h。
       除去I²S的管脚,我们需要初始其它IO的模式,我们在头文件sai.h中定义SAI的引脚,方便如果IO变更之后作修改:
  1. #define I2S_NUM            (I2S_NUM_0)                /* I2S端口 */
  2. #define I2S_BCK_IO    (GPIO_NUM_46)            /* 设置串行时钟引脚,ES8388_SCLK */
  3. #define I2S_WS_IO     (GPIO_NUM_9)                        /* 设置左右声道的时钟引脚,ES8388_LRCK */
  4. #define I2S_DO_IO     (GPIO_NUM_10)                /* ES8388_SDOUT */
  5. #define I2S_DI_IO          (GPIO_NUM_14)                   /* ES8388_SDIN */
  6. #define IS2_MCLK_IO          (GPIO_NUM_3)                     /* ES8388_MCLK */
  7. #define SAMPLE_RATE          (44100)                                /* 采样率 */]
复制代码
       接下来开始介绍i2s.c,主要是I²S的初始化代码如下:
  1. /* I2S默认配置 */
  2. #define I2S_CONFIG_DEFAULT() { \
  3.     .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX),            \
  4.     .sample_rate = SAMPLE_RATE,                                                             \
  5.     .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,                                   \
  6.     .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,                                   \
  7.     .communication_format = I2S_COMM_FORMAT_STAND_I2S,                              \
  8.     .intr_alloc_flags = 0,                                                                  \
  9.     .dma_buf_count = 8,                                                                     \
  10.     .dma_buf_len = 256,                                                                     \
  11.     .use_apll = false                                                                       \
  12. }

  13. /**
  14. * @brief       初始化I2S
  15. * @param       无
  16. * @retval      ESP_OK:初始化成功;其他:失败
  17. */
  18. esp_err_t i2s_init(void)
  19. {
  20.     esp_err_t ret_val = ESP_OK;

  21.     i2s_pin_config_t pin_config = {
  22.         .bck_io_num = I2S_BCK_IO,
  23.         .ws_io_num = I2S_WS_IO,
  24.         .data_out_num = I2S_DO_IO,
  25.         .data_in_num = I2S_DI_IO,
  26.         .mck_io_num = IS2_MCLK_IO,
  27.     };
  28.    
  29.     i2s_config_t i2s_config = I2S_CONFIG_DEFAULT();
  30.     i2s_config.sample_rate = SAMPLE_RATE;
  31.     i2s_config.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT;
  32.     i2s_config.use_apll = true;
  33.     ret_val |= i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  34.     ret_val |= i2s_set_pin(I2S_NUM, &pin_config);
  35.     ret_val |= i2s_zero_dma_buffer(I2S_NUM);
  36.     return ret_val;
  37. }

  38. /**
  39. * @brief       I2S TRX启动
  40. * @param       无
  41. * @retval      无
  42. */
  43. void i2s_trx_start(void)
  44. {
  45.     i2s_start(I2S_NUM);
  46. }

  47. /**
  48. * @brief       I2S TRX停止
  49. * @param       无
  50. * @retval      无
  51. */
  52. void i2s_trx_stop(void)
  53. {
  54.     i2s_stop(I2S_NUM);
  55. }

  56. /**
  57. * @brief       I2S卸载
  58. * @param       无
  59. * @retval      无
  60. */
  61. void i2s_deinit(void)
  62. {
  63.     i2s_driver_uninstall(I2S_NUM);
  64. }

  65. /**
  66. * @brief       设置采样率
  67. * @param       sampleRate  : 采样率
  68. * @param       bits_sample :位宽
  69. * @retval      无
  70. */
  71. void i2s_set_samplerate_bits_sample(int samplerate,int bits_sample)
  72. {
  73.     i2s_set_clk(I2S_NUM,samplerate,bits_sample,I2S_CHANNEL_STEREO);
  74. }

  75. /**
  76. * @brief       I2S传输数据
  77. * @param       buffer: 数据存储区的首地址
  78. * @param       frame_size: 数据大小
  79. * @retval      无
  80. */
  81. size_t i2s_tx_write(uint8_t *buffer, uint32_t frame_size)
  82. {
  83.     size_t bytes_written;
  84.     i2s_write(I2S_NUM, buffer, frame_size, &bytes_written, 100);
  85.     return bytes_written;
  86. }

  87. /**
  88. * @brief       I2S读取数据
  89. * @param       buffer: 读取数据存储区的首地址
  90. * @param       frame_size: 读取数据大小
  91. * @retval      无
  92. */
  93. size_t i2s_rx_read(uint8_t *buffer, uint32_t frame_size)
  94. {
  95.     size_t bytes_written;
  96.     i2s_read(I2S_NUM, buffer, frame_size, &bytes_written, 1000);
  97.     return bytes_written;
  98. }
复制代码
       函数i2s_init()完成初始化I²S,该初始化不需要像I²C以及IO扩展芯片那样设置传参,通过配置相关的结构体并安装I²S的驱动和配置I²S引脚以及将TX DMA缓冲区的内容归零。函数sai1_samplerate_set则是用前面介绍的查表法,根据采样率来设置SAI的时钟。函数i2s_trx_start()用于启动I²S驱动,在调用了i2s_driver_install()之后不需要调用这个函数(它是自动启动的),但是在调用了i2s_stop()之后调用该函数是必要的。而函数i2s_trx_stop()用于停止I²S驱动,在调用i2s_driver_uninstall()之前不需要调用i2s_stop()。i2s_set_samplerate_bits_sample()用于设置I2S RX和TX的时钟和位宽度。函数i2s_tx_write()用于将数据写入I2S DMA传输缓冲区。函数i2s_rx_read()从I2S DMA接收缓冲区读取数据。以上是对I²S驱动文件下部分函数的功能概述,具体内容请参照该驱动文件。

       2,ES8388驱动
       ES8388主要用来将音频信号转换为数字信号或将数字信号转换为音频信号,接下来,我们开始介绍ES8388的几个函数,代码如下:
  1. /**
  2. * @brief       ES8388初始化
  3. * @param       无
  4. * @retval      0,初始化正常
  5. *               其他,错误代码
  6. */
  7. uint8_t es8388_init(i2c_obj_t self)
  8. {
  9.     esp_err_t ret_val = ESP_OK;

  10.     if (self.init_flag == ESP_FAIL)
  11.     {
  12.         /* 初始化IIC */
  13.         iic_init(I2C_NUM_0);
  14.     }

  15.     es8388_i2c_master = self;

  16.     /* 软复位ES8388 */
  17.     ret_val |= es8388_write_reg(0, 0x80);
  18.     ret_val |= es8388_write_reg(0, 0x00);
  19.     /* 等待复位 */
  20.     vTaskDelay(100);

  21.     ret_val |= es8388_write_reg(0x01, 0x58);
  22.     ret_val |= es8388_write_reg(0x01, 0x50);
  23.     ret_val |= es8388_write_reg(0x02, 0xF3);
  24.     ret_val |= es8388_write_reg(0x02, 0xF0);

  25.     /* 麦克风偏置电源关闭 */
  26.     ret_val |= es8388_write_reg(0x03, 0x09);
  27.     /* 使能参考 500K驱动使能 */
  28.     ret_val |= es8388_write_reg(0x00, 0x06);
  29.     /* DAC电源管理,不打开任何通道 */
  30.     ret_val |= es8388_write_reg(0x04, 0x00);
  31.     /* MCLK不分频 */
  32.     ret_val |= es8388_write_reg(0x08, 0x00);
  33.     /* DAC控制 DACLRC与ADCLRC相同 */
  34.     ret_val |= es8388_write_reg(0x2B, 0x80);

  35.     /* ADC L/R PGA增益配置为+24dB */
  36.     ret_val |= es8388_write_reg(0x09, 0x88);
  37.     /* ADC数据选择为left data = left ADC, right data=left ADC音频数据为16bit */
  38.     ret_val |= es8388_write_reg(0x0C, 0x4C);
  39.     /* ADC配置 MCLK/采样率=256 */
  40.     ret_val |= es8388_write_reg(0x0D, 0x02);
  41.     /* ADC数字音量控制将信号衰减 L  设置为最小!!! */
  42.     ret_val |= es8388_write_reg(0x10, 0x00);
  43.     /* ADC数字音量控制将信号衰减 R  设置为最小!!! */
  44.     ret_val |= es8388_write_reg(0x11, 0x00);
  45.     /* DAC 音频数据为16bit */
  46.     ret_val |= es8388_write_reg(0x17, 0x18);
  47.     /* DAC 配置 MCLK/采样率=256 */
  48.     ret_val |= es8388_write_reg(0x18, 0x02);
  49.     /* DAC数字音量控制将信号衰减 L  设置为最小!!! */
  50.     ret_val |= es8388_write_reg(0x1A, 0x00);
  51.     /* DAC数字音量控制将信号衰减 R  设置为最小!!! */
  52.     ret_val |= es8388_write_reg(0x1B, 0x00);
  53.     /* L混频器 */
  54.     ret_val |= es8388_write_reg(0x27, 0xB8);
  55.     /* R混频器 */
  56.     ret_val |= es8388_write_reg(0x2A, 0xB8);
  57.     vTaskDelay(100);

  58.     if (ret_val != ESP_OK)
  59.     {
  60.         while(1)
  61.         {
  62.             printf("ES8388初始化失败!!!\r\n");
  63.             vTaskDelay(500);
  64.         }
  65.     }
  66.     else
  67.     {
  68.         printf("ES8388初始化成功!!!\r\n");
  69.     }
  70.    
  71.     return 0;
  72. }

  73. /**
  74. * @brief       IIC写入函数
  75. * @param       slave_addr:ES8388地址
  76. * @param       reg_add:寄存器地址
  77. * @param       data:写入的数据
  78. * @retval      无
  79. */
  80. esp_err_t es8388_write_reg(uint8_t reg_addr, uint8_t data)
  81. {
  82.     i2c_buf_t buf[2] = {
  83.         {.len = 1, .buf = ®_addr},
  84.         {.len = 1, .buf = &data},
  85.     };

  86.     i2c_transfer(&es8388_i2c_master, ES8388_ADDR >> 1, 2, buf, I2C_FLAG_STOP);

  87.     return ESP_OK;
  88. }

  89. /**
  90. * @brief       读取数据
  91. * @param       reg_add:寄存器地址
  92. * @param       p_data:读取的数据
  93. * @retval      无
  94. */
  95. esp_err_t es8388_read_reg(uint8_t reg_addr, uint8_t *pdata)
  96. {
  97.     i2c_buf_t buf[2] = {
  98.         {.len = 1, .buf = ®_addr},
  99.         {.len = 1, .buf = pdata},
  100.     };
  101.     i2c_transfer(&es8388_i2c_master, ES8388_ADDR >> 1, 2, buf, I2C_FLAG_WRITE|
  102.                                                                                     I2C_FLAG_READ |
  103.                                                                                     I2C_FLAG_STOP);
  104.     return ESP_OK;
  105. }

  106. /**
  107. * @brief       设置ES8388工作模式
  108. * @param       fmt : 工作模式
  109. * @arg         0, 飞利浦标准I2S;
  110. * @arg         1, MSB(左对齐);
  111. * @arg         2, LSB(右对齐);
  112. * @arg         3, PCM/DSP
  113. * @param       len : 数据长度
  114. * @arg         0, 24bit
  115. * @arg         1, 20bit
  116. * @arg         2, 18bit
  117. * @arg         3, 16bit
  118. * @arg         4, 32bit
  119. * @retval      无
  120. */
  121. void es8388_sai_cfg(uint8_t fmt, uint8_t len)
  122. {
  123.     fmt &= 0x03;
  124.     len &= 0x07;    /* 限定范围 */
  125.     es8388_write_reg(23, (fmt << 1) | (len << 3));  /* R23,ES8388工作模式设置 */
  126. }

  127. /**
  128. * @brief       设置耳机音量
  129. * @param       volume : 音量大小(0 ~ 33)
  130. * @retval      无
  131. */
  132. void es8388_hpvol_set(uint8_t volume)
  133. {
  134.     if (volume > 33)
  135.     {
  136.         volume = 33;
  137.     }
  138.    
  139.     es8388_write_reg(0x2E, volume);
  140.     es8388_write_reg(0x2F, volume);
  141. }

  142. /**
  143. * @brief       设置喇叭音量
  144. * @param       volume : 音量大小(0 ~ 33)
  145. * @retval      无
  146. */
  147. void es8388_spkvol_set(uint8_t volume)
  148. {
  149.     if (volume > 33)
  150.     {
  151.         volume = 33;
  152.     }
  153.    
  154.     es8388_write_reg(0x30, volume);
  155.     es8388_write_reg(0x31, volume);
  156. }

  157. /**
  158. * @brief       设置3D环绕声
  159. * @param       depth : 0 ~ 7(3D强度,0关闭,7最强)
  160. * @retval      无
  161. */
  162. void es8388_3d_set(uint8_t depth)
  163. {
  164.     depth &= 0x7;       /* 限定范围 */
  165.     es8388_write_reg(0x1D, depth << 2);    /* R7,3D环绕设置 */
  166. }

  167. /**
  168. * @brief       ES8388 DAC/ADC配置
  169. * @param       dacen : dac使能(1)/关闭(0)
  170. * @param       adcen : adc使能(1)/关闭(0)
  171. * @retval      无
  172. */
  173. void es8388_adda_cfg(uint8_t dacen, uint8_t adcen)
  174. {
  175.     uint8_t tempreg = 0;
  176.    
  177.     tempreg |= ((!dacen) << 0);
  178.     tempreg |= ((!adcen) << 1);
  179.     tempreg |= ((!dacen) << 2);
  180.     tempreg |= ((!adcen) << 3);
  181.     es8388_write_reg(0x02, tempreg);
  182. }

  183. /**
  184. * @brief       ES8388 DAC输出通道配置
  185. * @param       o1en : 通道1使能(1)/禁止(0)
  186. * @param       o2en : 通道2使能(1)/禁止(0)
  187. * @retval      无
  188. */
  189. void es8388_output_cfg(uint8_t o1en, uint8_t o2en)
  190. {
  191.     uint8_t tempreg = 0;
  192.     tempreg |= o1en * (3 << 4);
  193.     tempreg |= o2en * (3 << 2);
  194.     es8388_write_reg(0x04, tempreg);
  195. }

  196. /**
  197. * @brief       ES8388 MIC增益设置(MIC PGA增益)
  198. * @param       gain : 0~8, 对应0~24dB  3dB/Step
  199. * @retval      无
  200. */
  201. void es8388_mic_gain(uint8_t gain)
  202. {
  203.     gain &= 0x0F;
  204.     gain |= gain << 4;
  205.     es8388_write_reg(0x09, gain);    /* R9,左右通道PGA增益设置 */
  206. }

  207. /**
  208. * @brief       ES8388 ALC设置
  209. * @param       sel
  210. * @arg         0,关闭ALC
  211. * @arg         1,右通道ALC
  212. * @arg         2,左通道ALC
  213. * @arg         3,立体声ALC
  214. * @param       maxgain : 0~7,对应-6.5~+35.5dB
  215. * @param       minigain: 0~7,对应-12~+30dB 6dB/STEP
  216. * @retval      无
  217. */
  218. void es8388_alc_ctrl(uint8_t sel, uint8_t maxgain, uint8_t mingain)
  219. {
  220.     uint8_t tempreg = 0;
  221.     tempreg = sel << 6;
  222.     tempreg |= (maxgain & 0x07) << 3;
  223.     tempreg |= mingain & 0x07;
  224.     es8388_write_reg(0x12, tempreg);    /* R18,ALC设置 */
  225. }

  226. /**
  227. * @brief       ES8388 ADC输出通道配置
  228. * @param       in : 输入通道
  229. * @arg         0, 通道1输入
  230. * @arg         1, 通道2输入
  231. * @retval      无
  232. */
  233. void es8388_input_cfg(uint8_t in)
  234. {
  235.     es8388_write_reg(0x0A, (5 * in) << 4);    /* ADC1 输入通道选择L/R  INPUT1 */
  236. }
复制代码
       以上代码中,es8388_init函数用于初始化es8388,这里只是通用配置(ADC&DAC),初始化完成后,并不能正常播放音乐,还需要通过es8388_adda_cfg函数使能DAC,然后通过设置es8388_output_cfg选择DAC输出,通过es8388_sai_cfg配置ES8388工作模式,最后设置音量才可以接收I²S音频数据,实现音乐播放。

       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(uint8_t *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(sizeof(FIL));
  22.     buf = mymalloc(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, (UINT *)&br);         /* 读取512字节在数据 */
  31.             riff = (ChunkRIFF *)buf;                                    /* 获取RIFF块 */
  32.             
  33. if (riff->Format == 0x45564157)                                     /* 是WAV文件 */
  34.             {
  35.                 fmt = (ChunkFMT *)(buf + 12);                            /* 获取FMT块 */
  36.                 /*读取FACT块*/
  37.                                 fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);  
  38.                
  39.                 if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
  40.                 {
  41.                     /* 具有fact/LIST块的时候(未测试)*/
  42.                                         wavx->datastart = 12 + 8 + fmt->ChunkSize+8+fact->ChunkSize;
  43.                 }
  44.                 else
  45.                 {
  46.                     wavx->datastart = 12 + 8 + fmt->ChunkSize;
  47.                 }
  48.                
  49.                 data = (ChunkDATA *)(buf + wavx->datastart);        /* 读取DATA块 */
  50.                
  51.                 if (data->ChunkID == 0x61746164)                   /* 解析成功! */
  52.                 {
  53.                     wavx->audioformat = fmt->AudioFormat;          /* 音频格式 */
  54.                     wavx->nchannels = fmt->NumOfChannels;          /* 通道数 */
  55.                     wavx->samplerate = fmt->SampleRate;            /* 采样率 */
  56.                     wavx->bitrate = fmt->ByteRate * 8;             /* 得到位速 */
  57.                     wavx->blockalign = fmt->BlockAlign;            /* 块对齐 */
  58.                     wavx->bps = fmt->BitsPerSample;/* 位数,16/24/32位 */
  59.                     
  60.                     wavx->datasize = data->ChunkSize;              /* 数据块大小 */
  61.                     wavx->datastart = wavx->datastart + 8;/* 数据流开始的地方. */
  62.                     
  63.                     printf("wavx->audioformat:%d\r\n", wavx->audioformat);
  64.                     printf("wavx->nchannels:%d\r\n", wavx->nchannels);
  65.                     printf("wavx->samplerate:%d\r\n", wavx->samplerate);
  66.                     printf("wavx->bitrate:%d\r\n", wavx->bitrate);
  67.                     printf("wavx->blockalign:%d\r\n", wavx->blockalign);
  68.                     printf("wavx->bps:%d\r\n", wavx->bps);
  69.                     printf("wavx->datasize:%d\r\n", wavx->datasize);
  70.                     printf("wavx->datastart:%d\r\n", wavx->datastart);  
  71.                 }
  72.                 else
  73.                 {
  74.                     res = 3;         /* data区域未找到. */
  75.                 }
  76.             }
  77.             else
  78.             {
  79.                 res = 2;       /* 非wav文件 */
  80.             }
  81.         }
  82.         else
  83.         {
  84.             res = 1;                 /* 打开文件错误 */
  85.         }
  86.     }
  87.    
  88.     f_close(ftemp);
  89.     free(ftemp);                            /* 释放内存 */
  90.     free(buf);
  91.    
  92.     return 0;
  93. }

  94. /**
  95. * @brief       获取当前播放时间
  96. * @param       fname : 文件指针
  97. * @param       wavx  : wavx播放控制器
  98. * @retval      无
  99. */
  100. void wav_get_curtime(FIL *fx, __wavctrl *wavx)
  101. {
  102.     long long fpos;
  103.     wavx->totsec = wavx->datasize / (wavx->bitrate / 8); /* 歌曲总长度(单位:秒) */
  104.     fpos = fx->fptr-wavx->datastart;                 /* 得到当前文件播放到的地方 */
  105.     wavx->cursec = fpos*wavx->totsec / wavx->datasize;  /* 当前播放到第多少秒了? */
  106. }

  107. /**
  108. * @brief       播放某个wav文件
  109. * @param       fname : 文件路径+文件名
  110. * @retval      KEY0_PRES,错误
  111. *              KEY1_PRES,打开文件失败
  112. *              其他,非WAV文件
  113. */
  114. uint8_t wav_play_song(uint8_t *fname)
  115. {
  116.     uint8_t key = 0;
  117.     uint8_t t = 0;
  118.     uint8_t res;
  119.     i2s_play_end = ESP_FAIL;
  120.     i2s_play_next_prev = ESP_FAIL;
  121.     g_audiodev.file = (FIL*)malloc(sizeof(FIL));
  122.     g_audiodev.tbuf = malloc(WAV_TX_BUFSIZE);
  123.    
  124.     if (g_audiodev.file || g_audiodev.tbuf)
  125.     {
  126.         /* 得到文件的信息 */
  127.         res = wav_decode_init(fname, &wavctrl);

  128.         /* 解析文件成功 */
  129.         if (res == 0)
  130.         {
  131.             if (wavctrl.bps == 16)
  132.             {
  133.                 /* 飞利浦标准,16位数据长度 */
  134.                 es8388_sai_cfg(0, 3);
  135.                 i2s_set_samplerate_bits_sample(wavctrl.samplerate,
  136.                                                                                           I2S_BITS_PER_SAMPLE_16BIT);
  137.             }
  138.             else if (wavctrl.bps == 24)
  139.             {
  140.                 /* 飞利浦标准,24位数据长度 */
  141.                 es8388_sai_cfg(0, 0);
  142.                 i2s_set_samplerate_bits_sample(wavctrl.samplerate,
  143.                                                                                           I2S_BITS_PER_SAMPLE_24BIT);
  144.              }

  145.             audio_stop();

  146.             if (MUSICTask_Handler == NULL)
  147.             {
  148.                 taskENTER_CRITICAL(&my_spinlock);
  149.                 /* 创建任务1,任务函数 */
  150.                 xTaskCreatePinnedToCore((TaskFunction_t )music,
  151.                
  152.                                         /* 任务名称 */
  153.                                         (const char*    )"music",
  154.                                        
  155.                                         /* 任务堆栈大小 */
  156.                                         (uint16_t       )MUSIC_STK_SIZE,
  157.                                        
  158.                                         /* 传入给任务函数的参数 */
  159.                                         (void*          )NULL,
  160.                                        
  161.                                         /* 任务优先级 */
  162.                                         (UBaseType_t    )MUSIC_PRIO,
  163.                                        
  164.                                         /* 任务句柄 */
  165.                                         (TaskHandle_t*  )&MUSICTask_Handler,
  166.                                        
  167.                                         /* 该任务哪个内核运行 */
  168.                                         (BaseType_t     ) 0);
  169.                                        
  170.                 taskEXIT_CRITICAL(&my_spinlock);
  171.             }

  172.             /* 打开文件 */
  173.             res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);

  174.             if (res == 0)
  175.             {
  176.                 /* 开始音频播放 */
  177.                 audio_start();
  178.                 vTaskDelay(100);
  179.                 audio_start();
  180.                 vTaskDelay(100);

  181.                 while (res == 0)
  182.                 {
  183.                     while (1)
  184.                     {
  185.                         if (i2s_play_end == ESP_OK)
  186.                         {
  187.                             res = KEY0_PRES;
  188.                             break;
  189.                         }

  190.                         key = xl9555_key_scan(0);
  191.                         
  192.                         /* 暂停 */
  193.                         if (key == KEY3_PRES)
  194.                         {
  195.                             if ((g_audiodev.status & 0x0F) == 0x03)
  196.                             {
  197.                                 audio_stop();
  198.                                 vTaskDelay(100);
  199.                             }
  200.                             else if ((g_audiodev.status & 0x0F) == 0x00)
  201.                             {
  202.                                 audio_start();
  203.                                 vTaskDelay(100);
  204.                             }
  205.                         }
  206.                         
  207.                         /* 下一曲/上一曲 */
  208.                         if (key == KEY2_PRES || key == KEY0_PRES)
  209.                         {
  210.                             i2s_play_next_prev = ESP_OK;
  211.                             vTaskDelay(100);
  212.                             res = KEY0_PRES;
  213.                             break;
  214.                         }
  215.                         
  216.                         /* 暂停不刷新时间 */
  217.                         if ((g_audiodev.status & 0x0F) == 0x03)
  218.                         {
  219.                             /* 得到总时间和当前播放的时间 */
  220.                             wav_get_curtime(g_audiodev.file, &wavctrl);
  221.                             audio_msg_show(wavctrl.totsec,
  222.                                                                             wavctrl.cursec,
  223.                                                                             wavctrl.bitrate);
  224.                         }
  225.                         
  226.                         t++;
  227.                         if (t == 20)
  228.                         {
  229.                             t = 0 ;
  230.                             LED_TOGGLE();
  231.                         }
  232.                         
  233.                         if ((g_audiodev.status & 0x01) == 0)
  234.                         {
  235.                             vTaskDelay(10);
  236.                         }
  237.                         else
  238.                         {
  239.                             break;
  240.                         }
  241.                     }

  242.                     /* 退出切换歌曲 */
  243.                     if (key == KEY2_PRES || key == KEY0_PRES)
  244.                     {
  245.                         res = key;
  246.                         break;
  247.                     }
  248.                 }
  249.                 audio_stop();
  250.             }
  251.             else
  252.             {
  253.                 res = 0xFF;
  254.             }
  255.         }
  256.         else
  257.         {
  258.             res = 0xFF;
  259.         }
  260.     }
  261.     else
  262.     {
  263.         res = 0xFF;
  264.     }
  265.    
  266.     /* 释放内存 */
  267.     free(g_audiodev.tbuf);
  268.    
  269.     /* 释放内存 */
  270.     free(g_audiodev.file);
  271.    
  272.     return res;
  273. }
复制代码
       以上代码中,wav_decode_init函数,用来对wav音频文件进行解析,得到wav的详细信息(音频采样率,位数,数据流起始位置等);wav_play_song函数,是播放WAV最终执行的函数,该函数解析完WAV文件后,设置ES8388和I²S的参数(采样率,位数等),然后不断填充数据,实现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.    
  10.     /* 目录 */
  11.     FF_DIR wavdir;
  12.    
  13.     /* 文件信息 */
  14.     FILINFO *wavfileinfo;
  15.    
  16.     /* 带路径的文件名 */
  17.     uint8_t *pname;
  18.    
  19.     /* 音乐文件总数 */
  20.     uint16_t totwavnum;
  21.    
  22.     /* 当前索引 */
  23.     uint16_t curindex;
  24.    
  25.     /* 键值 */
  26.     uint8_t key;
  27.     uint32_t temp;
  28.    
  29.     /* 音乐offset索引表 */
  30.     uint32_t *wavoffsettbl;

  31.     /* 开启DAC关闭ADC */
  32.     es8388_adda_cfg(1, 0);
  33.    
  34.     /* DAC选择通道1输出 */
  35.     es8388_output_cfg(1, 1);

  36.     /* 打开音乐文件夹 */
  37.     while (f_opendir(&wavdir, "0:/MUSIC"))
  38.     {
  39.         text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
  40.         vTaskDelay(200);
  41.         
  42.         /* 清除显示 */
  43.         lcd_fill(30, 190, 240, 206, WHITE);
  44.         vTaskDelay(200);
  45.     }

  46.     /* 得到总有效文件数 */
  47.     totwavnum = audio_get_tnum((uint8_t *)"0:/MUSIC");
  48.    
  49.     /* 音乐文件总数为0 */
  50.     while (totwavnum == NULL)
  51.     {
  52.         text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
  53.         vTaskDelay(200);
  54.         
  55.         /* 清除显示 */
  56.         lcd_fill(30, 190, 240, 146, WHITE);
  57.         vTaskDelay(200);
  58.     }
  59.    
  60.     /* 申请内存 */
  61.     wavfileinfo = (FILINFO*)malloc(sizeof(FILINFO));
  62.    
  63.     /* 为带路径的文件名分配内存 */
  64.     pname = malloc(255 * 2 + 1);
  65.    
  66.     /* 申请4*totwavnum个字节的内存,用于存放音乐文件off block索引 */
  67.     wavoffsettbl = malloc(4 * totwavnum);
  68.    
  69.     /* 内存分配出错 */
  70.     while (!wavfileinfo || !pname || !wavoffsettbl)
  71.     {
  72.         text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
  73.         vTaskDelay(200);
  74.         
  75.         /* 清除显示 */
  76.         lcd_fill(30, 190, 240, 146, WHITE);
  77.         vTaskDelay(200);
  78.     }
  79.    
  80.     /* 记录索引,打开目录 */
  81.     res = f_opendir(&wavdir, "0:/MUSIC");
  82.    
  83.     if (res == FR_OK)
  84.     {
  85.         /* 当前索引为0 */
  86.         curindex = 0;
  87.         
  88.         /* 全部查询一遍 */
  89.         while (1)
  90.         {
  91.             /* 记录当前index */
  92.             temp = wavdir.dptr;

  93.             /* 读取目录下的一个文件 */
  94.             res = f_readdir(&wavdir, wavfileinfo);
  95.             
  96.             if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
  97.             {
  98.                 break;  /* 错误了/到末尾了,退出 */
  99.             }

  100.             res = exfuns_file_type(wavfileinfo->fname);
  101.             
  102.             /* 取高四位,看看是不是音乐文件 */
  103.             if ((res & 0xF0) == 0x40)
  104.             {
  105.                 /* 记录索引 */
  106.                 wavoffsettbl[curindex] = temp;
  107.                 curindex++;
  108.             }
  109.         }
  110.     }
  111.    
  112.     /* 从0开始显示 */
  113.     curindex = 0;
  114.    
  115.     /* 打开目录 */
  116.     res = f_opendir(&wavdir, (const TCHAR*)"0:/MUSIC");
  117.    
  118.     /* 打开成功 */
  119.     while (res == FR_OK)
  120.     {
  121.         /* 改变当前目录索引 */
  122.         dir_sdi(&wavdir, wavoffsettbl[curindex]);
  123.         
  124.         /* 读取目录下的一个文件 */
  125.         res = f_readdir(&wavdir, wavfileinfo);
  126.         
  127.         if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
  128.         {
  129.             /* 错误了/到末尾了,退出 */
  130.             break;
  131.         }
  132.         
  133.         /* 复制路径(目录) */
  134.         strcpy((char *)pname, "0:/MUSIC/");
  135.         
  136.         /* 将文件名接在后面 */
  137.         strcat((char *)pname, (const char *)wavfileinfo->fname);
  138.         
  139.         /* 清除之前的显示 */
  140.         lcd_fill(30, 190, lcd_self.width - 1, 190 + 16, WHITE);
  141.         audio_index_show(curindex + 1, totwavnum);
  142.         
  143.         /* 显示歌曲名字 */
  144.         text_show_string(30, 190, lcd_self.width - 60, 16,
  145.                                             (char *)wavfileinfo->fname, 16, 0, BLUE);
  146.         
  147.         /* 播放这个音频文件 */
  148.         key = audio_play_song(pname);
  149.         
  150.         /* 上一曲 */
  151.         if (key == KEY2_PRES)
  152.         {
  153.             if (curindex)
  154.             {
  155.                 curindex--;
  156.             }
  157.             else
  158.             {
  159.                 curindex = totwavnum - 1;
  160.             }
  161.         }
  162.         
  163.         /* 下一曲 */
  164.         else if (key == KEY0_PRES)
  165.         {
  166.             curindex++;

  167.             if (curindex >= totwavnum)
  168.             {
  169.                 /* 到末尾的时候,自动从头开始 */
  170.                 curindex = 0;
  171.             }
  172.         }
  173.         else
  174.         {
  175.             break;  /* 产生了错误 */
  176.         }
  177.     }

  178.     /* 释放内存 */
  179.     free(wavfileinfo);
  180.    
  181.     /* 释放内存 */
  182.     free(pname);
  183.    
  184.     /* 释放内存 */
  185.     free(wavoffsettbl);
  186. }

  187. /**
  188. * @brief       播放某个音频文件
  189. * @param       fname : 文件名
  190. * @retval      按键值
  191. * @arg         KEY0_PRES , 下一曲.
  192. * @arg         KEY2_PRES , 上一曲.
  193. * @arg         其他 , 错误
  194. */
  195. uint8_t audio_play_song(uint8_t *fname)
  196. {
  197.     uint8_t res;  
  198.    
  199.     res = exfuns_file_type((char *)fname);

  200.     switch (res)
  201.     {
  202.         case T_WAV:
  203.             res = wav_play_song(fname);
  204.             break;
  205.         case T_MP3:
  206.             /* 自行实现 */
  207.             break;

  208.         default:            /* 其他文件,自动跳转到下一曲 */
  209.             printf("can't play:%s\r\n", fname);
  210.             res = KEY0_PRES;
  211.             break;
  212.     }
  213.     return res;
  214. }
复制代码
       这里,audio_play函数在main函数里面被调用,该函数首先设置ES8388相关配置,然后查找SD卡里面的MUSIC文件夹,并统计该文件夹里面总共有多少音频文件(统计包括:WAV/MP3/APE/FLAC等),然后,该函数调用audio_play_song函数,按顺序播放这些音频文件。
       在audio_play_song函数里面,通过判断文件类型,调用不同的解码函数,本章,支持WAV文件,通过wav_play_song函数实现WAV解码。

       41.3.4 CMakeLists.txt文件
       打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:
  1. set(src_dirs
  2.             IIC
  3.             LCD
  4.             LED
  5.             SDIO
  6.             SPI
  7.             XL9555
  8.             ES8388
  9.             I2S)

  10. set(include_dirs
  11.             IIC
  12.             LCD
  13.             LED
  14.             SDIO
  15.             SPI
  16.             XL9555
  17.             ES8388
  18.             I2S)

  19. set(requires
  20.             driver
  21.             fatfs)

  22. idf_component_register(SRC_DIRS ${src_dirs}
  23. INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})

  24. component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
复制代码
       上述的红色I2C、ES8388驱动需要由开发者自行添加,以确保音频播放驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了音频播放驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
       打开本实验main文件下的CMakeLists.txt文件,其内容如下所示:
  1. idf_component_register(
  2.     SRC_DIRS
  3.         "."
  4.         "APP"
  5.     INCLUDE_DIRS
  6.         "."
  7.         "APP")
复制代码
       上述的红色APP驱动需要由开发者自行添加,在此便不做赘述了。

       41.3.5 实验应用代码
       打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
  1. i2c_obj_t i2c0_master;

  2. /**
  3. * @brief       程序入口
  4. * @param       无
  5. * @retval      无
  6. */
  7. void app_main(void)
  8. {
  9.     esp_err_t ret;
  10.     uint8_t key = 0;

  11.     /* 初始化NVS */
  12.     ret = nvs_flash_init();

  13. if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
  14. ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
  15.     {
  16.         ESP_ERROR_CHECK(nvs_flash_erase());
  17.         ret = nvs_flash_init();
  18.     }

  19.     /* 初始化LED */
  20.     led_init();
  21.    
  22.     /* 初始化IIC0 */
  23.     i2c0_master = iic_init(I2C_NUM_0);
  24.    
  25.     /* 初始化SPI */
  26.     spi2_init();
  27.    
  28.     /* 初始化IO扩展芯片 */  
  29.     xl9555_init(i2c0_master);
  30.    
  31.     /* 初始化LCD */
  32.     lcd_init();
  33.    
  34.     /* ES8388初始化 */
  35.     es8388_init(i2c0_master);
  36.    
  37.     /* 开启DAC关闭ADC */
  38.     es8388_adda_cfg(1, 0);
  39.     es8388_input_cfg(0);
  40.    
  41.     /* DAC选择通道输出 */
  42.     es8388_output_cfg(1, 1);
  43.    
  44.     /* 设置耳机音量 */
  45.     es8388_hpvol_set(20);
  46.    
  47.     /* 设置喇叭音量 */
  48.     es8388_spkvol_set(20);
  49.    
  50.     /* 打开喇叭 */
  51.     xl9555_pin_write(SPK_EN_IO,0);
  52.    
  53.     /* I2S初始化 */
  54.     i2s_init();
  55.    
  56.     /* 检测不到SD卡 */
  57.     while (sd_spi_init())
  58.     {
  59.         lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
  60.         vTaskDelay(500);
  61.         lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
  62.         vTaskDelay(500);
  63.     }
  64.    
  65.     /* 检查字库 */
  66.     while (fonts_init())
  67.     {
  68.         /* 清屏 */
  69.         lcd_clear(WHITE);
  70.         lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
  71.         
  72.         /* 更新字库 */
  73.         key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);

  74.         /* 更新失败 */
  75.         while (key)
  76.         {
  77.             lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
  78.             vTaskDelay(200);
  79.             lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
  80.             vTaskDelay(200);
  81.         }

  82.         lcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
  83.         vTaskDelay(1500);
  84.         
  85.         /* 清屏 */
  86.         lcd_clear(WHITE);
  87.     }

  88.     /* 为fatfs相关变量申请内存 */
  89.     ret = exfuns_init();
  90.    
  91.     /* 实验信息显示延时 */
  92.     vTaskDelay(500);
  93.     text_show_string(30, 50, 200, 16, "正点原子ESP32开发板",16,0, RED);
  94.     text_show_string(30, 70, 200, 16, "音乐播放器 实验", 16, 0, RED);
  95.     text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
  96.     text_show_string(30, 110, 200, 16, "KEY0:NEXT   KEY2:PREV", 16, 0, RED);
  97.     text_show_string(30, 130, 200, 16, "KEY3:PAUSE/PLAY", 16, 0, RED);

  98.     while (1)
  99.     {
  100.         /* 播放音乐 */
  101.         audio_play();
  102.     }
  103. }
复制代码
       到这里本实验的代码基本就编写完成了,我们准备好音乐文件放到SD卡根目录下的《MUSIC》夹下测试本实验的代码。

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

第四十一章 音乐播放器实验40420.png
图41.4.1音乐播放中

       从上图可以看出,总共1首歌曲,当前正在播放第1首歌曲,歌曲名、播放时间、总时长、码率等也都有显示。此时LED会随着音乐的播放而闪烁。
       此时我们便可以听到开发板板载喇叭播放出来的音乐了,也可以在开发板的PHONE端子插入耳机来听歌。同时,我们可以通过按KEY0和KEY2来切换下一曲和上一曲,通过KEY_UP暂停和继续播放。

阿莫论坛20周年了!感谢大家的支持与爱护!!

曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片。注意:要连续压缩2次才能满足要求!!】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-8-25 06:17

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

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