《ESP32-P4开发指南—V1.0》第四十三章 音乐播放器实验

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


       正点原子DNESP32P4开发板配备了三个HP系统的全双工I2S接口和一个LP系统的双工低功耗I2S接口,此外,还外接了一颗HIFI级音频编解码芯片——ES8388,支持最高192KHz 24位的音频播放。该开发板不仅支持高质量的音频播放,还具备音频录制功能(将在下一章详细介绍)。本章将带领读者利用DNESP32P4开发板实现一个简单的音乐播放器,目前仅支持WAV格式的播放。

       本章分为如下几个小节:

       43.1 WAV&ES8388&I2S简介

       43.2 硬件设计

       43.3 程序设计

       43.4 下载验证


        43.1 WAV&ES8388&I2S简介

       本节将介绍本章的几个关键知识点,包括WAV格式、ES8388音频编解码器和I2S接口。下面逐一介绍这三个重要概念。


       43.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 Chunk、Format Chunk、 Fact Chunk(可选)和Data Chunk。每个Chunk由块标识符、数据大小和数据三部分组成,如下图所示:


图43.1.1.1 Chunk组成结构


       一个基本的WAVE文件包含三种必备的Chunk:RIFF Chunk、FMT Chunk 和 Data Chunk。如下图所示。


图43.1.1.2 PCM格式的wav文件构成


       上图展示了WAV文件的基本必备Chunk,除了这些必备的Chunk,还有其他可选的Chunk。值得注意的是,虽然这些其他Chunk的顺序并没有严格的限制,但其中一个特别需要关注的可选Chunk 是Fact Chunk。Fact Chunk通常用于非PCM编码格式的WAV 文件,提供音频的额外信息。接下来,笔者将使用十六进制编辑器打开WAV文件,逐一分析各个Chunk的内容和作用,帮助读者更深入地理解WAV文件的结构和数据存储方式。


       1,RIFF Chunk,共12字节

       RIFF Chunk 是WAV文件的根Chunk,它定义了文件的基本类型。RIFF是“Resource Interchange File Format”的缩写,用于标识这是一个基于RIFF格式的文件。下图为十六进制编辑器解析RIFF Chunk的数据。


图43.1.1.3 RIFF Chunk数据


       在上图中,RIFF Chunk 占据了文件的前 12 字节。下面表格展示了这 12 字节的字段内容及其说明。


表43.1.1.1 RIFF chunk字段描述


       从上表可以得知,所解析的WAV文件总大小为37199220 + 8,即37199228字节。该文件通过Chunk ID字段标识为“RIFF”格式,并通过Format字段标注为WAV文件(“WAVE”)。


       2,Format Chunk,共24~26字节

       Format Chunk(通常标识为 “fmt ”)描述了音频数据的格式,包括采样率、比特深度、声道数等。下图为十六进制编辑器解析Format Chunk的数据。


图43.1.1.4 Format Chunk数据


       在上图中,Format Chunk位于RIFF Chunk的后方,占据24字节。下表展示了这24字节的字段内容及其说明。


表43.1.1.2 Format chunk字段描述


       从上表中可以看到,该WAV文件采用PCM音频格式,声道数为2(即立体声),采样率为44100Hz,且每个样本的位数为16位。这些信息表明该文件存储的是标准的CD质量音频数据。值得注意的是,有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息,具体大小请看Format Size字段。


       3,Fact Chunk可选块,共12字节

       Fact Chunk(0X74636166)是一个可选的Chunk,通常用于非PCM编码格式的WAV文件(例如压缩音频格式)。它提供额外的元数据,例如压缩音频格式的附加信息或其他特定的音频数据。如果WAV文件使用的是PCM编码格式,则通常不需要Fact Chunk。在本章节中,解析的WAV文件未包含Fact Chunk,因此我们在这里不再对其进行详细讲解。


       4,Data Chunk,共N字节

       Data Chunk包含实际的音频样本数据,是音频播放的核心数据内容。下图为十六进制编辑器解析Data Chunk的数据。


图43.1.1.5 Data Chunk数据


       在上图中,Data Chunk位于Format Chunk的后方(若有Fact Chunk,则Data Chunk位于Fact Chunk的后方),占据N字节。下表展示了这N字节的字段内容及其说明。


表43.1.1.2 Data Chunk字段描述


       从上表中可以看到,WAV文件的数据大小为37198936字节。要获取音频数据,我们需要偏移到Data Chunk块的起始位置,这个位置可以通过偏移到文件的0x80地址来访问。此时,我们可以开始读取WAV音频数据并将其通过I2S接口进行发送。

       下面是笔者以数据结构体的形式对上述各个Chunk进行描述。每个结构体代表一个Chunk,包含该Chunk的字段信息及其数据类型,帮助读者更清晰地理解各个Chunk的结构。

       首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:

typedef struct
{
    uint32_t ChunkID;           /* chunk id;这里固定为"RIFF",即0X46464952 */
    uint32_t ChunkSize;         /* 集合大小;文件总大小-8 */
    uint32_t Format;            /* 格式;WAVE,即0X45564157 */
} ChunkRIFF;                    /* RIFF块 */

       接着,我们看看Format块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:

typedef struct
{
    uint32_t ChunkID;           /* chunk id;这里固定为"fmt ",即0X20746D66 */
    uint32_t ChunkSize;         /* 子集合大小(不包括ID和Size);这里为:20 */
    uint16_t AudioFormat;       /* 音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM */
    uint16_t NumOfChannels;     /* 通道数量;1,表示单声道;2,表示双声道 */
    uint32_t SampleRate;        /* 采样率;0X1F40,表示8Khz */
    uint32_t ByteRate;          /* 字节速率 */
    uint16_t BlockAlign;        /* 块对齐(字节) */
    uint16_t BitsPerSample;     /* 单个采样数据大小;4位ADPCM,设置为4 */
//    uint16_t ByteExtraData;   /* 附加的数据字节;2个; 线性PCM,没有这个参数 */
} ChunkFMT;                     /* fmt块 */

       然后,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:

typedef struct 
{
    uint32_t ChunkID;           /* chunk id;这里固定为"fact",即0X74636166 */
    uint32_t ChunkSize;         /* 子集合大小(不包括ID和Size);这里为:4 */
    uint32_t NumOfSamples;      /* 采样的数量 */
} ChunkFACT;                    /* fact块 */

       最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”作为该Chunk的标示,然后是数据的大小。数据块的Chunk结构如下:

typedef struct
{
    uint32_t ChunkID;           /* chunk id;这里固定为"data",即0X5453494C */
    uint32_t ChunkSize;         /* 子集合大小(不包括ID和Size) */
} ChunkDATA;                    /* data块 */

       ChunkSize后紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如下表所示的几种形式:


表43.1.1.3 WAVE文件数据采样格式


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


       43.1.2 ES8388简介

       ES8388音频编解码器是上海顺芯推出的一款高性能、低功耗、性价比高的音频编解码器,具备丰富的功能,如2个ADC通道、2个DAC通道、麦克风放大器、耳机放大器、数字音效处理、模拟混合与增益控制等。


       1,ES8388的主要特性

       l I2S接口:支持最高96kHz采样率和24bit音频播放。

       l DAC信噪比:96dB,ADC信噪比:95dB。

       l 工作模式:支持主机模式和从机模式。

       l 支持立体声差分输入和麦克风输入。

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

       l 支持40mW耳机输出,确保没有爆音现象。


       2,音频数据传输与控制

       I2S接口:ES8388通过I2S接口(即数字音频接口)与MCU进行音频数据传输,支持音频接收和发送。

       配置接口:通过两线接口(CE=0/1,即I2C接口)或三线接口(CE脚产生一个下降沿,即SPI接口)进行配置。


       3,ES8388的I2S接口

       I2S接口由4个引脚组成,分别为:

       l ASDOUT:ADC数据输出。

       l DSDIN:DAC数据输入。

       l LRC:数据左/右对齐时钟。

       l SCLK:位时钟,用于同步音频数据的传输。

       l MCLK:提供从机ES8388参考时钟。


       4,主机/从机模式

       ES8388可作为I2S接口的主机,输出LRC和SCLK时钟。然而,在大多数应用中,ES8388作为从机接收LRC和SCLK时钟信号。


       5,音频数据模式

       ES8388的I2S接口支持4种不同的音频数据模式:

       l 左(MSB)对齐标准。

       l 右(LSB)对齐标准。

       l 飞利浦(I2S)标准。

       l DSP/PCM模式。


       6,飞利浦(I2S)标准模式

       在飞利浦(I2S)标准模式下,数据通过跟随LRC的BCLK的第二个上升沿传输MSB(最高有效位),之后的数据按顺序依次传输,直到LSB(最低有效位)。这种传输方式的具体协议依赖于字长、BCLK频率和采样率。每个采样的LSB与下一个采样的MSB之间应有一个未用的BCLK周期,用于同步和时序的调整。飞利浦标准模式的I2S数据传输协议如下图所示:


图43.1.2.1 飞利浦标准模式I2S数据传输图


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

       ES8388的框图如下图所示:


图43.1.2.2 ES8388框图


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

       本章,我们通过IIC接口(CE=0)连接ES8388,ES8388的IIC地址为:0X10。关于ES8388的IIC详细介绍,请看《ES8388-DS》数据手册第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的数据手册自行研究。


       43.1.3 I2S简介

       I2S(Inter-IC Sound)是一种用于数字音频设备之间传输音频数据的串行通信协议,广泛应用于音频数据的传输,如在DSP(数字信号处理器)、DAC(数模转换器)、ADC(模数转换器)及其他音频处理单元之间的数据交换。ESP32-P4的I2S模块集成了多功能的音频传输接口,支持多种音频数据格式和采样率,适用于音频录制、回放、音频处理等应用。

       ESP32-P4的HP系统集成了三个I2S接口:I2S0、I2S1和I2S2和LP系统的一个低功耗I2S接口。这些接口为多媒体应用中的数字数据流传输提供了灵活的通信方式,尤其适用于数字音频应用。I2S协议常用于数字音频设备之间的数据传输,使ESP32-P4能够高效支持各种音频相关的应用。


       1,I2标准总线有三个主要信号:

       1)位时钟(BCK):位时钟用于同步设备之间的数据传输速率,定义了I2S总线上传输数据位的速度。

       2)字选择(WS):该信号也称为通道选择信号,用于区分传输的数据是左声道还是右声道,从而实现立体声音频传输。

       3)串行数据(SD):此线路以串行位流的形式传输实际的音频数据。

       4)主时钟线(MCLK):该信号线可选,具体取决于从机,用于向 I2S 从机提供参考时钟。

       在基本的I2S总线配置中,包含一个主设备和一个从设备。主设备生成BCK和WS信号,控制通信的时序,从设备则遵循主设备的时序。这种主从关系在整个通信过程中保持不变ESP32-P4的I2S模块设计有独立的发送(TX)和接收(RX)单元,支持高性能的同时数据传输和接收。


       2,ESP32-P4的I2S模块特性

       1)主模式和从模式:支持I2S设备作为主设备或从设备进行通信。

       2)全双工和半双工通信:支持数据的双向传输,可以在同一时间发送和接收数据。

       3)独立的 TX和RX单元:可以独立工作或同时工作,提高数据处理的灵活性。

       4)支持多种音频标准:

       l TDM Philips标准

       l TDM MSB对齐标准

       l TDM PCM标准

       l PDM 标准

       5)支持多种TX/RX模式:

       l TDM TX模式:支持最多16个通道

       l TDM RX模式:支持最多16个通道

       l PDM TX模式:原始 PDM 数据传输和PCM 转 PDM 数据格式转换(仅支持 I2S0),最多支持 2 个通道

       6)PDM RX模式

       l 原始PDM数据接收

       l PDM转PCM数据格式转换(仅支持 I2S0),最多支持 8 个通道

       7)可配置的APLL时钟:频率最高可达240 MHz。

       8)可配置的高精度采样时钟:支持多种采样频率。

       9)8/16/24/32位数据宽度:支持多种数据宽度,以满足不同的数据精度需求。

       10)TX模式下的同步计数器:确保数据传输的同步性。

       11)ETM特性:扩展了数据的传输能力。

       12)直接内存访问(仅支持GDMA-AHB):提供高效的数据传输。

       13)标准I2S接口中断:便于实时数据处理。


       43.1.3.1 I2S硬件架构与工作原理

       ESP32-P4 I2Sn模块的结构如下图所示。


图43.1.3.1.1 ESP32-P4 I2Sn框架图


       上图展示了ESP32-P4 I2S模块(I2Sn)的架构,包含多个关键部件,以支持音频数据的传输和接收。具体描述如下:


       1,时钟生成器(Clock Generator)

       用于生成I2S模块的时钟信号,支持XTAL(晶振)、APLL(音频锁相环)以及外部MCLK输入,灵活调整时钟源,以满足不同的采样率和传输速率需求。


       2,GDMA支持

       I2S模块可以通过GDMA控制器直接访问内部存储器,以实现高效的数据处理,详细内容可参考《ESP32-P4技术参考手册》的GDMA控制器章节。


       3,压缩/解压缩单元

       支持A-law和μ-law音频压缩/解压功能,适用于语音数据压缩需求,从而有效地降低传输带宽要求,提高数据传输效率。


       4,发送和接收FIFO缓冲区

       用于生成I2S模块的时钟信号,支持XTAL(晶振)、APLL(锁相环)以及外部MCLK输入,灵活调整时钟源,以满足不同的采样率和传输速率需求。


       5,发送(TX)和接收(RX)单元

       发送控制单元(TX Unit):负责将音频数据从TX FIFO缓冲区传输到外部设备。它支持传统TDM(时分复用)格式和PDM(脉冲密度调制)格式,还内置了一个PCM(脉冲编码调制)到PDM的转换器,以实现多种音频编码的兼容性。

       接收控制单元(RX Unit):从外部设备接收音频数据并存储到RX FIFO缓冲区。与TX单元相似,RX单元也支持TDM和PDM格式,并配有PDM到PCM的转换器,能够处理不同编码格式的音频信号。


       6,GPIO矩阵(I/O Sync)

       确保I2S的输入和输出信号同步。在主模式下,该单元会生成时钟信号(如BCK、WS),在从模式下则接收外部时钟信号以进行同步。这种设计使I2S模块能够作为主设备或从设备运行,灵活适应不同的应用场景。I2S信号可以通过高性能GPIO矩阵(HP GPIO Matrix)映射到芯片的特定引脚,相关内容请参考《ESP32-P4技术参考手册》的GPIO矩阵和IO MUX章节

       通过这些模块的协同工作,ESP32-P4的I2S接口可以在高效处理和传输音频数据的同时,支持多种数据格式(如PCM、PDM),满足语音、音乐等多种应用场景的需求。

       下面,笔者将重点讲解时钟生成器(Clock Generator)如何为TX/RX模块提供所需的时钟。具体内容请参见下图。


图43.1.1.2 I2Sn时钟生成器


       上图详细描述了ESP32-P4的I2S时钟生成模块的工作原理。以下是该模块的主要总结:

       1)时钟源选择

       I2S模块的TX(发送)和RX(接收)时钟可以从多个时钟源中选择,以确保与系统的时序需求匹配。时钟源包括系统的40 MHz晶体时钟(XTAL_CLK)、音频锁相环(APLL_CLK)时钟以及外部输入的MCLK信号(I2Sn_MCLK_in)。其中,XTAL_CLK适用于大多数标准时序要求,APLL_CLK则提供了可调频率的优势,适合高保真音频应用,而I2Sn_MCLK_in则用于需要外部同步的场景。通过配置 HP_SYS_CLKRST_I2Sn_TX/RX_CLK_SRC_SEL 和 HP_SYS_CLKRST_I2S0_TX/RX_CLK_EN 寄存器,用户可以灵活地选择TX和RX的时钟源,确保数据传输的精确性和稳定性。

       2)分频器配置

       I2S模块的TX和RX主时钟(分别为I2Sn_TX_CLK和I2Sn_RX_CLK)是通过所选时钟源经过分频器生成的。主时钟频率fI2Sn_CLK_S,也就是上图讲解的三个时钟源其中一个,通过以下公式计算:



       其中,N是一个整数分频值,范围为2到256,用户可以通过配置 HP_SYS_CLKRST_I2Sn_TX/RX_DIV_N 寄存器来设置。参数a和b则用于配置分数分频器(fractional divider),通过精细调节这些值,可以实现更高精度的频率设置,从而满足特定应用的时钟要求。

       3)串行时钟(BCK)生成

       I2S模块的TX和RX串行时钟(分别为 I2SnO_BCK_out 和 I2SnI_BCK_out)是通过主时钟 I2Sn_TX_CLK 和 I2Sn_RX_CLK 进一步分频生成的。发送BCK频率的计算公式如下:



       其中,MO是一个整数值,通过I2Sn_TX_BCK_DIV_NUM配置,且需确保MO≠1。接收BCK频率的计算方式类似,MI为I2Sn_RX_BCK_DIV_NUM寄存器数值+1,同样它不允许为1。这些分频设置使得可以精确调整串行时钟频率,以适应不同的音频数据传输需求。

       4)I2Sn_MCLK输出

       在I2S主模式下,I2Sn模块可以输出主时钟(I2Sn_MCLK_out)作为外设的时钟源,用于同步外围设备的操作。

       通过上述配置和分频设计,ESP32-P4的I2S时钟生成器可以灵活地选择时钟源并生成适合的时钟信号,以适应不同的音频采样率和传输速率要求。这些模块的协同工作增强了ESP32-P4在音频处理方面的灵活性和兼容性。


       43.1.3.2 I2S驱动文件调用结构

       在ESP32-P4的开发中,I2S(Inter-IC Sound)接口驱动提供了多种通信模式,包括标准模式、PDM模式和TDM模式,以适应不同的音频应用需求。I2S驱动的文件结构和调用关系经过精心设计,以支持多种应用场景,提供了新旧两套API供用户选择使用。

       为了兼容已有的I2S驱动应用,同时提供更灵活和高效的音频处理能力,ESP-IDF框架中实现了原有的I2S驱动和新驱动。用户可以基于需求选择适合的API接口,但需要注意的是,原有驱动与新驱动无法共存。在开发过程中,用户应根据项目需求选择合适的头文件,选择适合的通信模式,并依赖正确的头文件结构来确保应用的稳定性和兼容性。I2S驱动文件结构分为公共头文件、私有头文件和源文件三部分。公共头文件主要向应用程序提供API接口,私有头文件则主要用于驱动内部调用。整个结构的模块化设计使得代码维护和扩展更为方便,帮助开发者更快地集成I2S功能。下图为I2S 文件结构


图43.1.3.2.1 I2S 文件结构


       上图展示了I2S文件结构和各文件的依赖关系。以下是对文件结构的说明:


       1,顶层应用接口文件:

       i2s.h:提供原有I2S API,供使用旧驱动的应用程序调用。旧版API不支持新特性,但在一些老应用中依然可用。

       i2s_std.h:提供标准通信模式的API,用于新驱动的标准模式。该文件包含配置标准I2S通信的函数。

       i2s_pdm.h:提供PDM通信模式的API,供新驱动的PDM模式使用。PDM(脉冲密度调制)模式主要用于麦克风输入等音频处理场景。

       i2s_tdm.h:提供TDM通信模式的API,供新驱动的TDM模式使用。TDM(时分多路复用)模式用于多通道音频通信。


       2,公共类型文件:

       i2s_types_legacy.h:定义旧驱动中使用的类型和结构体,专为与旧I2S API兼容而设计。

       i2s_types.h:定义I2S模块中的公共类型,包括在各个I2S模式下都可能使用的基础类型。


       3,公共功能文件

       i2s_common.h:提供所有通信模式通用的API接口,包括初始化、配置、启动等公共操作。这样可以避免在每个模式文件中重复实现公共操作。

       i2s_common.c:实现了i2s_common.h中声明的通用API逻辑,将公共操作的功能具体实现,以便不同模式复用。


       4,私有头文件

       i2s_private.h:包含仅在驱动内部使用的私有函数和定义,避免了与公共接口的混淆。该文件中的内容不直接对用户开放,只在驱动的内部实现中使用。


       5,HAL层文件

       i2s_hal.h:定义了硬件抽象层(HAL)的接口,屏蔽了底层硬件细节,为上层提供更为抽象的I2S操作。

       i2s_hal.c:实现了HAL层的具体操作逻辑,与底层硬件直接交互。

       i2s_ll.h:提供低层(LL)接口,包含底层寄存器操作函数,以更直接的方式访问I2S硬件。


       6,旧版I2S驱动文件

       i2s_legacy.c:包含旧I2S驱动的具体实现逻辑,适用于兼容原有I2S API的应用。

       注意:原有驱动与新驱动不可共存。应用应选择包含i2s.h来使用原有驱动,或包含i2s_std.h、i2s_pdm.h、i2s_tdm.h来使用新驱动。未来,原有驱动可能会被移除。


        43.2 硬件设计


       43.2.1 程序功能

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

       注意:在接入RGBLCD屏时,我们发现MCU与ES8388之间的通信可能会出现丢包现象。经过半个月的调试,最终通过将RGBLCD的数据线与I2C通信线分开布线,极大的减少了丢包现象。然而,偶尔仍会出现丢包现象。为了解决这一问题,我们在ES8388的写函数中添加了do-while语句,确保ES8388能收到应答后再继续执行操作。若读者希望进一步减少丢包现象,可以尝试调整RGBLCD的电源电压。具体方法是在rgblcd.c文件中,将LDO4的输出电压调整为2200mV(即2V)。在DNESP32P4开发板中,ESP32-P4芯片的LDO4用于控制VDDPST1电源域的电压,而VDDPST1管脚则用于与RGBLCD进行通信。通过调节LDO4的输出电压,可以更好地减少与ES8388通信时的丢包问题。需要注意的是,此问题仅出现在接入RGBLCD屏时,若接入MIPI显示屏则不会出现丢包现象。


       43.2.2 硬件资源

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


       1)LED灯

              LED 0  - IO51


       2)ES8388音频CODEC芯片,通过I2S驱动


       3)I2S音频接口

              I2S_BCK_IO  - IO47

              I2S_WS_IO  - IO48

              I2S_DO_IO  - IO49

              I2S_DI_IO  - IO50

              I2S_MCK_IO  - IO46


       4)XL9555

              IIC_INT  - IO36

              IIC_SDA  - IO33

              IIC_SCL  - IO32

              EXIO_8  - KEY0

              EXIO_9  - KEY1

              EXIO_10  - KEY2


       5)RGBLCD/MIPILCD(引脚太多,不罗列出来)


       6) SPIFFS


       7)SD卡

              CMD    -  IO44

              CLK  -  IO43

              D0        -  IO39

              D1        -  IO40

              D2       -  IO41

              D3        -  IO42


       43.2.3 原理图

       ES8388器件相关原理图,如下图所示。


图43.2.3.1 ES8388硬件原理图


       DNESP32P4开发板板载的ES8388解码芯片的驱动电路原理图如上图所示。图中,PHONE接口可以用来插耳机,SPK_IN连接了板载的喇叭,IIC接口和24C02等芯片共用。


        43.3 程序设计


       43.3.1 I2S的IDF驱动

       MYI2S外设驱动位于ESP-IDF的components\BSP\目录。该目录中的include文件夹存放MYI2S相关的头文件,声明了MYI2S函数和结构体等;而i2s_common.c文件是实现I2S操作函数。要使用MYI2S功能,必须先导入以下头文件。

#include "driver/i2s_std.h"

       接下来,作者将介绍一些常用的MYI2S函数,这些函数的描述及其作用如下:


       1,分配新的I2S通道i2s_new_channel

       该函数用于分配新的I2S通道,其函数原型如下:

esp_err_t i2s_new_channel(const i2s_chan_config_t *chan_cfg, 
i2s_chan_handle_t *ret_tx_handle, 
i2s_chan_handle_t *ret_rx_handle);

       函数形参:


表43.3.1.1 i2s_new_channel函数形参描述


       返回值:

       ESP_OK表示成功分配新通道。

       ESP_ERR_NOT_SUPPORTED表示当前芯片不支持该通信模式。

       ESP_ERR_INVALID_ARG表示参数无效,例如NULL指针。

       ESP_ERR_NOT_FOUND表示未找到可用的I2S通道。

       chan_cfg为指向I2S通道配置结构体的指针。接下来,笔者将详细介绍i2s_chan_config_t结构体中的各个成员变量,如下代码所示:

typedef struct {
    i2s_port_t id;         /* I2S端口ID */
    i2s_role_t role;       /* I2S角色,I2S_ROLE_MASTER或I2S_ROLE_SLAVE */
 
    /* DMA配置 */
    uint32_t dma_desc_num;   /* I2S DMA缓冲区的数量,也即DMA描述符的数量 */
    uint32_t dma_frame_num;  /* 每个DMA缓冲区的帧数。每帧表示一次采样数据,*/
    union {
        bool auto_clear;          /* `auto_clear_after_cb`的别名 */
        bool auto_clear_after_cb;  /* 设置在`on_sent`回调之后自动清除DMA TX缓冲区,
如果没有数据发送,I2S会自动发送零。
这样用户可以直接在回调中分配数据到DMA缓冲区,
且在退出回调后数据不会被清除。*/
    };
    bool auto_clear_before_cb; /* 设置在`on_sent`回调之前自动清除DMA TX缓冲区,
                                  如果没有数据发送,I2S会自动发送零。
                                  这样用户可以在回调中访问刚刚发送完的数据。*/
bool allow_pd;    /* 允许I2S控制器进入低功耗模式。启用此标志后,
驱动会在进入睡眠模式前备份I2S寄存器,
                         并在退出时恢复这些寄存器。这样可以节省功耗,但会消耗更多的RAM。*/
int intr_priority;   /* I2S中断优先级,范围为[0, 7],如果设置为0,
驱动会尝试分配一个相对较低的中断优先级(1,2,3) */
} i2s_chan_config_t;

       i2s_chan_config_t结构体用于传递I2S的通道配置参数,以便在调用i2s_new_channel时进行初始化和设置。通过这个结构体,开发者可以灵活配置I2S的主从机模式,以满足不同的需求。


       2,初始化I2S通道为标准模式i2s_channel_init_std_mode

       该函数用于初始化I2S通道为标准模式,其函数原型如下:

esp_err_t i2s_channel_init_std_mode(i2s_chan_handle_t handle, 
const i2s_std_config_t *std_cfg);

       函数形参:


表43.3.1.2 i2s_channel_init_std_mode函数形参描述


       返回值:

       ESP_OK表示初始化成功。

       ESP_ERR_NO_MEM表示没有足够的内存存储通道信息。

       ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。

       ESP_ERR_INVALID_STATE表示此通道未注册。

       std_cfg为指向I2S标准模式结构体的指针。接下来,笔者将详细  介绍i2s_std_config_t结构体中的各个成员变量,如下代码所示:

/**
 * @brief I2S标准模式声道配置
 */
typedef struct {
    /* 一般字段 */
    i2s_data_bit_width_t data_bit_width; /* I2S采样数据位宽 */
    i2s_slot_bit_width_t slot_bit_width; /* I2S声道位宽(每个声道的总位数) */
    i2s_slot_mode_t slot_mode;           /* 设置单声道或立体声模式, */
 
    /* 特殊字段 */
    i2s_std_slot_mask_t slot_mask; /* 选择左声道、右声道或两者的声道 */
    uint32_t ws_width;       /* WS信号宽度(即WS信号为高电平的BCLK时钟周期数) */
    bool ws_pol;              /* WS信号极性,设置为true时,表示先为高电平 */
    bool bit_shift;           /* 在Philips模式下启用位移 */
#if SOC_I2S_HW_VERSION_1     /* 对于ESP32/ESP32-S2 */
    bool msb_right;           /* 设置在FIFO中将右声道数据放置在MSB */
#else
    bool left_align;          /* 设置启用左对齐 */
    bool big_endian;          /* 设置启用大端字节序 */
    bool bit_order_lsb;       /* 设置启用LSB优先 */
#endif
} i2s_std_slot_config_t;
 
/**
 * @brief I2S标准模式时钟配置
 */
typedef struct {
    /* 一般字段 */
    uint32_t sample_rate_hz;     /* I2S采样率 */
    i2s_clock_src_t clk_src;    /* 选择时钟源 */
#if SOC_I2S_HW_VERSION_2
    uint32_t ext_clk_freq_hz;   /* 外部时钟源频率(单位Hz)*/
#endif
    i2s_mclk_multiple_t mclk_multiple; /* MCLK与采样率的倍数 */
} i2s_std_clk_config_t;
 
/**
 * @brief I2S标准模式GPIO引脚配置
 */
typedef struct {
    gpio_num_t mclk;             /* MCK引脚,默认输出 */
    gpio_num_t bclk;               /* BCK引脚,主机角色时为输出,Slave角色时为输入 */
    gpio_num_t ws;                 /* WS引脚,主机角色时为输出,Slave角色时为输入 */
    gpio_num_t dout;               /* 数据输出引脚 */
    gpio_num_t din;                /* 数据输入引脚 */
    struct {
        uint32_t mclk_inv: 1;     /* 设置为1时,反转MCLK输入/输出 */
        uint32_t bclk_inv: 1;     /* 设置为1时,反转BCLK输入/输出 */
        uint32_t ws_inv: 1;       /* 设置为1时,反转WS输入/输出 */
    } invert_flags;                /* GPIO引脚反转标志 */
} i2s_std_gpio_config_t;
 
/**
 * @brief I2S标准模式主要配置,包括时钟、声道和GPIO配置
 */
typedef struct {
    i2s_std_clk_config_t clk_cfg;     /* 标准模式时钟配置 */
    i2s_std_slot_config_t slot_cfg;    /* 标准模式声道配置 */
    i2s_std_gpio_config_t gpio_cfg;    /* 标准模式GPIO配置 */
} i2s_std_config_t;

       这些结构体在I2S配置中起着至关重要的作用,可以帮助用户定制I2S的数据传输、时钟源选择和GPIO引脚设置等。


       3,启用I2S TX/RX通道i2s_channel_enable

       该函数用于启用I2S TX/RX通道,其函数原型如下:

esp_err_t i2s_channel_enable(i2s_chan_handle_t handle);

       函数形参:


表43.3.1.3 i2s_channel_enable函数形参描述


       返回值:

       ESP_OK表示启动成功。

       ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。

       ESP_ERR_INVALID_STATE表示该通道尚未初始化或已启动。


       4,禁用I2S TX/RX通道i2s_channel_disable

       该函数用于禁用I2S TX/RX通道,其函数原型如下:

esp_err_t i2s_channel_disable(i2s_chan_handle_t handle);

       函数形参:


表43.3.1.4 i2s_channel_disable函数形参描述


       返回值:

       ESP_OK表示禁用成功。

       ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。

       ESP_ERR_INVALID_STATE表示该通道尚未初始化或已启动。


       5,删除I2S通道i2s_del_channel

       该函数用于删除I2S通道,其函数原型如下:

esp_err_t i2s_del_channel(i2s_chan_handle_t handle);

       函数形参:


表43.3.1.5 i2s_del_channel函数形参描述


       返回值:

       ESP_OK表示删除成功。

       ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。


       6,重新配置I2S时钟以适应标准模式i2s_channel_reconfig_std_clock

       该函数用于重新配置I2S时钟以适应标准模式,其函数原型如下:

esp_err_t i2s_channel_reconfig_std_clock(i2s_chan_handle_t handle, 
const i2s_std_clk_config_t *clk_cfg);

       函数形参:


表43.3.1.6 i2s_channel_reconfig_std_clock函数形参描述


       返回值:

       ESP_OK表示时钟设置成功。

       ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。

       ESP_ERR_INVALID_STATE表示通道未初始化或未停止。


       7,重新配置I2S声道以适应标准模式i2s_channel_reconfig_std_slot

       该函数用于重新配置I2S声道以适应标准模式,其函数原型如下:

esp_err_t i2s_channel_reconfig_std_slot(i2s_chan_handle_t handle, 
const i2s_std_slot_config_t *slot_cfg);

       函数形参:


表43.3.1.7 i2s_channel_reconfig_std_slot函数形参描述


       返回值:

       ESP_OK表示声道设置成功。

       ESP_ERR_NO_MEM表示没有足够内存分配DMA缓冲区。

       ESP_ERR_INVALID_ARG表示参数无效或通道不是标准模式。

       ESP_ERR_INVALID_STATE表示通道未初始化或未处于停止状态。


       8,I2S写数据i2s_channel_write

       该函数用于I2S写数据,其函数原型如下:

esp_err_t i2s_channel_write(i2s_chan_handle_t handle, const void *src, 
size_t size, size_t *bytes_written, 
uint32_t timeout_ms);

       函数形参:


表43.3.1.8 i2s_channel_write函数形参描述


       返回值:

       ESP_OK表示写入成功。

       ESP_ERR_TIMEOUT表示写入超时。

       ESP_ERR_INVALID_ARG表示NULL指针或该句柄不是TX句柄。

       ESP_ERR_INVALID_STATE表示写入数据失败。


       9,I2S读数据i2s_channel_read

       该函数用于I2S读数据,其函数原型如下:

esp_err_t i2s_channel_read(i2s_chan_handle_t handle, const void *dest, 
size_t size, size_t *bytes_read, 
uint32_t timeout_ms);

       函数形参:


表43.3.1.9 i2s_channel_read函数形参描述


       返回值:

       ESP_OK表示读取成功。

       ESP_ERR_TIMEOUT表示写入超时。

       ESP_ERR_INVALID_ARG表示NULL指针或该句柄不是RX句柄。

       ESP_ERR_INVALID_STATE表示写入数据失败。


       43.3.2 程序流程图


图43.3.2.1 音乐播放器实验程序流程图


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


       43.3.3 程序解析

       在34_music例程中,作者在34_music\components\BSP路径下新建MYI2S和ES8388文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。


       1,I2S驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MYI2S驱动源码包括两个文件:myi2s.c和myi2s.h。

       下面先解析myi2s.h的程序。对I2S所需的管脚及采样率做了定义。

#define I2S_NUM                 (I2S_NUM_0)                 /* I2S port */
#define I2S_BCK_IO              (GPIO_NUM_47)               /* ES8388_SCLK */
#define I2S_WS_IO               (GPIO_NUM_48)               /* ES8388_LRCK */
#define I2S_DO_IO               (GPIO_NUM_49)               /* ES8388_SDIN */
#define I2S_DI_IO               (GPIO_NUM_50)               /* ES8388_SDOUT */
#define I2S_MCK_IO              (GPIO_NUM_46)               /* ES8388_MCLK */
#define I2S_RECV_BUF_SIZE       (2400)                      /* 接收大小 */
#define I2S_SAMPLE_RATE         (44100)                     /* 采样率 */
#define I2S_MCLK_MULTIPLE       (384) /* 如果不使用24位数据宽度,256应该足够了 */

       下面我们再解析myi2s.c的程序,该文件定义了myi2s_init、i2s_trx_start、i2s_trx_stop等相关I2S操作函数,这些函数实现代码如下所示:


       1,myi2s_init函数

/*
 * @brief     初始化I2S
 * @param     无
 * @retval   ESP_OK:初始化成功;其他:失败
 */
esp_err_t myi2s_init(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM,
                             I2S_ROLE_MASTER); /* 默认的通道配置(I2S0,主机) */
    chan_cfg.auto_clear = true;  /* 自动清除DMA缓冲区遗留的数据 */ 
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));    
 
    i2s_std_config_t std_cfg = {          /* 标准通信模式配置 */
        .clk_cfg  = {                     /* 时钟配置 */
            .sample_rate_hz = I2S_SAMPLE_RATE,             /* I2S采样率 */
            .clk_src        = I2S_CLK_SRC_DEFAULT,         /* I2S时钟源 */
            .mclk_multiple  = I2S_MCLK_MULTIPLE,           /* I2S主时钟MCLK */
        },
        .slot_cfg = {                     /* 声道配置 */
            .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,  /* 声道支持16位宽 */
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,    /* 通道位宽 */
            .slot_mode      = I2S_SLOT_MODE_STEREO,       /* 立体声 */
            .slot_mask      = I2S_STD_SLOT_BOTH,        /* 启用通道 */
            .ws_width       = I2S_DATA_BIT_WIDTH_16BIT,  /* WS信号位宽 */
            .ws_pol         = false,                     /* WS信号极性 */
            .bit_shift      = true,                     /* 位移位 */
            .left_align     = true,                       /* 左对齐 */
            .big_endian     = false,                     /* 小端模式 */
            .bit_order_lsb  = false                      /* MSB */
        }, 
        
        .gpio_cfg = {                     /* 引脚配置 */
            .mclk = I2S_MCK_IO,           /* 主时钟线 */
            .bclk = I2S_BCK_IO,           /* 位时钟线 */
            .ws   = I2S_WS_IO,            /* 字(声道)选择线 */
            .dout = I2S_DO_IO,            /* 串行数据输出线 */
            .din  = I2S_DI_IO,            /* 串行数据输入线 */
            .invert_flags = {             /* 引脚翻转(不反相) */
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv   = false,
            },
        },
    };
    my_std_cfg = std_cfg;
/* 初始化TX通道 */
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));    
/* 初始化RX通道 */
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));    
/* 启用TX通道 */
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));                     
/* 启用RX通道 */
    ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));                     
    return ESP_OK;
}

       上述函数 myi2s_init用于初始化ESP32-P4的I2S外设,并配置其为标准通信模式。首先,函数设置了I2S通道的默认配置,包括通道角色(主机模式)和DMA数据自动清除功能。接着,函数定义了标准模式下的时钟、数据通道和GPIO配置,确保I2S信号的时钟、数据位宽、信号极性等符合应用要求。然后,通过调用i2s_new_channel创建新的TX和RX通道句柄,并分别初始化为标准模式。最后,函数启用TX和RX通道,启动数据传输。该函数通过ESP_ERROR_CHECK确保每个步骤成功执行,若有任何错误会终止执行并返回错误信息。


       2,i2s_trx_start函数

/**
 * @brief      I2S TRX启动
 * @param      无
 * @retval     无
 */
void i2s_trx_start(void)
{
    ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
    ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}

       上述函数i2s_trx_start用于启动I2S的TX与RX通道。


       3,i2s_trx_stop函数

/**
 * @brief     I2S TRX停止
 * @param      无
 * @retval     无
 */
void i2s_trx_stop(void)
{
    ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
    ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
}

       上述函数i2s_trx_stop用于禁用I2S的TX与RX通道。


       3,i2s_deinit函数

/**
 * @brief      I2S卸载
 * @param      无
 * @retval   无
 */
void i2s_deinit(void)
{
    ESP_ERROR_CHECK(i2s_del_channel(tx_handle));
    ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
}

       上述函数用于卸载I2S设备。


       4,i2s_set_samplerate_bits_sample函数

/**
 * @brief      设置采样率和位宽
 * @param      sampleRate  :采样率
 * @param      bits_sample :位宽
 * @retval     无
 */
void i2s_set_samplerate_bits_sample(int samplerate, int bits_sample)
{
    i2s_trx_stop();
    /* 如果需要更新声道或时钟配置,需要在更新前先禁用通道 */
    my_std_cfg.slot_cfg.ws_width = bits_sample;        /* 位宽 */
ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(tx_handle,
                                             &my_std_cfg.slot_cfg));
    my_std_cfg.clk_cfg.sample_rate_hz = samplerate;    /* 设置采样率 */
ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(tx_handle, 
&my_std_cfg.clk_cfg));
}

       上述函数i2s_set_samplerate_bits_sample用于重新配置通道位宽和采样率,值得注意的是,必须先禁用Tx/Rx通道,方能重新配置这些参数。


       5,i2s_tx_write函数

/**
 * @brief     I2S传输数据
 * @param    buffer: 数据存储区的首地址
 * @param     frame_size: 数据大小
 * @retval    发送的数据长度
 */
size_t i2s_tx_write(uint8_t *buffer, uint32_t frame_size)
{
    size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_write(tx_handle, buffer, frame_size,
                                 &bytes_written, 1000));
    return bytes_written;
}

       上述函数i2s_tx_write将用于I2S音频数据传输至es8388当中,并返回实际发送的大小。


       6,i2s_rx_read函数

/**
 * @brief    I2S读取数据
 * @param      buffer: 读取数据存储区的首地址
 * @param      frame_size: 读取数据大小
 * @retval     接收的数据长度
 */
size_t i2s_rx_read(uint8_t *buffer, uint32_t frame_size)
{
    size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_read(rx_handle, buffer, frame_size, 
&bytes_written, 1000));
    return bytes_written;
}

       上述函数i2s_rx_read将用于读取es8388设备的音频数据,并返回实际接收的数据大小。


       2,ES8388驱动代码

       ES8388主要用来将音频信号转换为数字信号或将数字信号转换为音频信号,接下来,我们开始介绍ES8388的几个函数,代码如下:

i2c_master_dev_handle_t es8388_handle = NULL;
 
/**
 * @brief       ES8388写寄存器
 * @param       reg_addr:寄存器地址
 * @param       data:写入的数据
 * @retval      无
 */
esp_err_t es8388_write_reg(uint8_t reg_addr, uint8_t data)
{
    esp_err_t ret;
    uint8_t *buf = malloc(2);
    if (buf == NULL)
    {
        ESP_LOGE(es8388_tag, "%s memory failed", __func__);
        return ESP_ERR_NO_MEM;      /* 分配内存失败 */
    }
 
    buf[0] = reg_addr;              
    buf[1] = data;                  /* 拷贝数据至存储区当中 */
 
    do 
    {
        i2c_master_bus_wait_all_done(bus_handle, 1000);
        ret = i2c_master_transmit(es8388_handle, buf, 2, 1000);   
    } while (ret != ESP_OK);
 
    free(buf);                      /* 发送完成释放内存 */
 
    return ret;
}
 
 
/**
 * @brief     ES8388读寄存器
 * @param     reg_add:寄存器地址
 * @param      p_data:读取的数据
 * @retval     无
 */
esp_err_t es8388_read_reg(uint8_t reg_addr, uint8_t *pdata)
{
    uint8_t reg_data = 0;
    i2c_master_transmit_receive(es8388_handle, ®_addr, 1, ®_data, 1, -1);
    return reg_data;
}
 
/**
 * @brief     ES8388初始化
 * @param     无
 * @retval      0,初始化正常
 *              其他,错误代码
 */
uint8_t es8388_init(void)
{
    uint8_t ret_val = 0;
 
    /* 未调用myiic_init初始化IIC */
    if (bus_handle == NULL)
    {
        ESP_ERROR_CHECK(myiic_init());
    }
 
    i2c_device_config_t es8388_i2c_dev_conf = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,  /* 从机地址长度 */
        .scl_speed_hz    = IIC_SPEED_CLK,       /* 传输速率 */
        .device_address  = ES8388_ADDR,         /* 从机7位的地址 */
    };
    /* I2C总线上添加ES8388设备 */
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &es8388_i2c_dev_conf, 
&es8388_handle));
 
    ret_val |= es8388_write_reg(0, 0x80);       /* 软复位ES8388 */
    ret_val |= es8388_write_reg(0, 0x00);
    vTaskDelay(pdMS_TO_TICKS(100));             /* 等待复位 */
 
    ret_val |= es8388_write_reg(0x01, 0x58);
    ret_val |= es8388_write_reg(0x01, 0x50);
    ret_val |= es8388_write_reg(0x02, 0xF3);
    ret_val |= es8388_write_reg(0x02, 0xF0);
 
    ret_val |= es8388_write_reg(0x03, 0x09);    /* 麦克风偏置电源关闭 */
    ret_val |= es8388_write_reg(0x00, 0x06);    /* 使能参考 500K驱动使能 */
    ret_val |= es8388_write_reg(0x04, 0x00);    /* DAC电源管理,不打开任何通道 */
    ret_val |= es8388_write_reg(0x08, 0x00);    /* MCLK不分频 */
    ret_val |= es8388_write_reg(0x2B, 0x80);    /* DAC控制 DACLRC与ADCLRC相同 */
 
ret_val |= es8388_write_reg(0x09, 0x88);    /* ADC L/R PGA增益配置为+24dB */
/* ADC数据选择为left data = left ADC, right data=left ADC  音频数据为16bit */
    ret_val |= es8388_write_reg(0x0C, 0x4C);    
ret_val |= es8388_write_reg(0x0D, 0x02);    /* ADC配置 MCLK/采样率=256 */
/* ADC数字音量控制将信号衰减 L  设置为最小!!! */
    ret_val |= es8388_write_reg(0x10, 0x00);    
/* ADC数字音量控制将信号衰减 R  设置为最小!!! */
ret_val |= es8388_write_reg(0x11, 0x00);    
 
    ret_val |= es8388_write_reg(0x17, 0x18);    /* DAC音频数据为16bit */
ret_val |= es8388_write_reg(0x18, 0x02);    /* DAC配置 MCLK/采样率=256 */
/* DAC数字音量控制将信号衰减 L  设置为最小!!! */
ret_val |= es8388_write_reg(0x1A, 0x00);    
/* DAC数字音量控制将信号衰减 R  设置为最小!!! */
    ret_val |= es8388_write_reg(0x1B, 0x00);    
    ret_val |= es8388_write_reg(0x27, 0xB8);    /* L混频器 */
    ret_val |= es8388_write_reg(0x2A, 0xB8);    /* R混频器 */
    vTaskDelay(pdMS_TO_TICKS(100));
 
    if (ret_val != ESP_OK)
    {
        while(1)
        {
            ESP_LOGI(es8388_tag, "ES8388 fail");
            vTaskDelay(pdMS_TO_TICKS(500));
        }
    }
    else
    {
        ESP_LOGI(es8388_tag, "ES8388 success");
    }
 
    es8388_adda_cfg(0, 0);      /* 开启DAC关闭ADC */
    es8388_input_cfg(0);        /* 关闭录音输入 */
    es8388_output_cfg(0, 0);    /* DAC选择通道输出 */
    es8388_hpvol_set(0);        /* 设置耳机音量 */
    es8388_spkvol_set(0);       /* 设置喇叭音量 */
    
    return 0;
}
 
/**
 * @brief      ES8388反初始化
 * @param      无
 * @retval    0,初始化正常
 *             其他,错误代码
 */
esp_err_t es8388_deinit(void)
{
    return es8388_write_reg(0x02, 0xFF);    /* 复位和暂停ES8388 */
}
 
/**
 * @brief      设置ES8388工作模式
 * @param       fmt : 工作模式
 *    @arg     0, 飞利浦标准I2S;
 *    @arg     1, MSB(左对齐);
 *    @arg      2, LSB(右对齐);
 *    @arg     3, PCM/DSP
 * @param      len : 数据长度
 *    @arg     0, 24bit
 *    @arg     1, 20bit
 *    @arg     2, 18bit
 *    @arg     3, 16bit
 *    @arg      4, 32bit
 * @retval     无
 */
void es8388_i2s_cfg(uint8_t fmt, uint8_t len)
{
    fmt &= 0x03;
    len &= 0x07;    /* 限定范围 */
    es8388_write_reg(23, (fmt << 1) | (len << 3));  /* R23,ES8388工作模式设置 */
}
 
/**
 * @brief      设置耳机音量
 * @param     volume : 音量大小(0 ~ 33)
 * @retval     无
 */
void es8388_hpvol_set(uint8_t volume)
{
    if (volume > 33)
    {
        volume = 33;
    }
 
    es8388_write_reg(0x2E, volume);
    es8388_write_reg(0x2F, volume);
}
 
/**
 * @brief       设置喇叭音量
 * @param    volume : 音量大小(0 ~ 33)
 * @retval     无
 */
void es8388_spkvol_set(uint8_t volume)
{
    if (volume > 33)
    {
        volume = 33;
    }
 
    es8388_write_reg(0x30, volume);
    es8388_write_reg(0x31, volume);
}
 
/**
 * @brief       设置3D环绕声
 * @param      depth : 0 ~ 7(3D强度,0关闭,7最强)
 * @retval     无
 */
void es8388_3d_set(uint8_t depth)
{
    depth &= 0x7;             /* 限定范围 */
    es8388_write_reg(0x1D, depth << 2);     /* R7,3D环绕设置 */
}
 
/**
 * @brief      ES8388 DAC/ADC配置
 * @param      dacen : dac使能(1) / 关闭(0)
 * @param      adcen : adc使能(1) / 关闭(0)
 * @retval     无
 */
void es8388_adda_cfg(uint8_t dacen, uint8_t adcen)
{
    uint8_t tempreg = 0;
 
    tempreg |= !dacen << 0;
    tempreg |= !adcen << 1;
    tempreg |= !dacen << 2;
    tempreg |= !adcen << 3;
    es8388_write_reg(0x02, tempreg);
}
 
/**
 * @brief       ES8388 DAC输出通道配置
 * @param      o1en : 通道1使能(1)/禁止(0)
 * @param       o2en : 通道2使能(1)/禁止(0)
 * @retval     无
 */
void es8388_output_cfg(uint8_t o1en, uint8_t o2en)
{
    uint8_t tempreg = 0;
    tempreg |= o1en * (3 << 4);
    tempreg |= o2en * (3 << 2);
    es8388_write_reg(0x04, tempreg);
}
 
/**
 * @brief      ES8388 MIC增益设置(MIC PGA增益)
 * @param      gain : 0~8, 对应0~24dB  3dB/Step
 * @retval     无
 */
void es8388_mic_gain(uint8_t gain)
{
    gain &= 0x0F;
    gain |= gain << 4;
    es8388_write_reg(0x09, gain);       /* R9,左右通道PGA增益设置 */
}
 
/**
 * @brief      ES8388 ALC设置
 * @param      sel
 *   @arg      0,关闭ALC
 *   @arg      1,右通道ALC
 *   @arg       2,左通道ALC
 *   @arg       3,立体声ALC
 * @param       maxgain : 0~7,对应-6.5~+35.5dB
 * @param       minigain: 0~7,对应-12~+30dB 6dB/STEP
 * @retval     无
 */
void es8388_alc_ctrl(uint8_t sel, uint8_t maxgain, uint8_t mingain)
{
    uint8_t tempreg = 0;
    tempreg = sel << 6;
    tempreg |= (maxgain & 0x07) << 3;
    tempreg |= mingain & 0x07;
    es8388_write_reg(0x12, tempreg);     /* R18,ALC设置 */
}
 
/**
 * @brief       ES8388 ADC输出通道配置
 * @param       in : 输入通道
 *    @arg     0, 通道1输入
 *    @arg    1, 通道2输入
 * @retval     无
 */
void es8388_input_cfg(uint8_t in)
{
    es8388_write_reg(0x0A, (5 * in) << 4);   /* ADC1 输入通道选择L/R INPUT1 */
}

       以上代码中,es8388_init函数用于初始化es8388,这里只是通用配置(ADC&DAC),初始化完成后,并不能正常播放音乐,还需要通过es8388_adda_cfg函数使能DAC,然后通过设置es8388_output_cfg选择DAC输出,通过es8388_i2s_cfg配置I2S工作模式,最后设置音量才可以接收I2S音频数据,实现音乐播放。


       3,wavplay代码

       该文件用于wav格式的音频文件解码,接下来看看wavplay.c里面的几个函数,代码如下:

/**
 * @brief      WAV解析初始化
 * @param      fname : 文件路径+文件名
 * @param      wavx  : 信息存放结构体指针
 * @retval     0,打开文件成功
 *             1,打开文件失败
 *            2,非WAV文件
 *             3,DATA区域未找到
 */
uint8_t wav_decode_init(uint8_t *fname, __wavctrl *wavx)
{
    FIL *ftemp;
    uint8_t *buf; 
    uint32_t br = 0;
    uint8_t res = 0;
 
    ChunkRIFF *riff;
    ChunkFMT *fmt;
    ChunkFACT *fact;
    ChunkDATA *data;
    
    ftemp = (FIL*)malloc(sizeof(FIL));
    buf = malloc(512);
    
    if (ftemp && buf)
    {
        res = f_open(ftemp, (TCHAR*)fname, FA_READ); /* 打开文件 */
        
        if (res == FR_OK)
        {
            f_read(ftemp, buf, 512, (UINT *)&br);   /* 读取512字节在数据 */
            riff = (ChunkRIFF *)buf;
            
            if (riff->Format == 0x45564157)           /* 是WAV文件 */
            {
                fmt = (ChunkFMT *)(buf + 12);
                fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);
                
                if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
                {
                    wavx->datastart=12+8 + fmt->ChunkSize + 8 + fact->ChunkSize;
                }
                else
                {
                    wavx->datastart = 12 + 8 + fmt->ChunkSize;
                }
                
                data = (ChunkDATA *)(buf + wavx->datastart);
                
                if (data->ChunkID == 0x61746164)                /* 解析成功 */
                {
                    wavx->audioformat = fmt->AudioFormat;       /* 音频格式 */
                    wavx->nchannels = fmt->NumOfChannels;       /* 通道数 */
                    wavx->samplerate = fmt->SampleRate;         /* 采样率 */
                    wavx->bitrate = fmt->ByteRate * 8;
                    wavx->blockalign = fmt->BlockAlign;
                    wavx->bps = fmt->BitsPerSample;
                    
                    wavx->datasize = data->ChunkSize;
                    wavx->datastart = wavx->datastart + 8;
                     
                    printf("wavx->audioformat:%d\r\n", wavx->audioformat);
                    printf("wavx->nchannels:%d\r\n", wavx->nchannels);
                    printf("wavx->samplerate:%ld\r\n", wavx->samplerate);
                    printf("wavx->bitrate:%ld\r\n", wavx->bitrate);
                    printf("wavx->blockalign:%d\r\n", wavx->blockalign);
                    printf("wavx->bps:%d\r\n", wavx->bps);
                    printf("wavx->datasize:%ld\r\n", wavx->datasize);
                    printf("wavx->datastart:%ld\r\n", wavx->datastart);  
                }
                else
                {
                    res = 3;
                }
            }
            else
            {
                res = 2;
            }
        }
        else
        {
            res = 1;
        }
    }
    
    f_close(ftemp);
    free(ftemp);
    free(buf); 
    
    return 0;
}
 
/**
 * @brief      获取当前播放时间
 * @param      fx    : 文件指针
 * @param      wavx  : wavx播放控制器
 * @retval     无
 */
void wav_get_curtime(FIL *fx, __wavctrl *wavx)
{
    long long fpos = 0;
 
    wavx->totsec = wavx->datasize / (wavx->bitrate / 8); /* 歌曲总长度(单位:秒) */
    fpos = fx->fptr-wavx->datastart;             /* 得到当前文件播放到的地方 */
    wavx->cursec = fpos * wavx->totsec / wavx->datasize;/* 当前播放到第多少秒了? */
}
 
/**
 * @brief     music任务
 * @param      pvParameters : 传入参数(未用到)
 * @retval     无
 */
void music(void *pvParameters)
{
    pvParameters = pvParameters;
 
    /* ES8388初始化配置,有效降低启动时发出沙沙声 */
    es8388_adda_cfg(1,0);                           /* 打开DAC,关闭ADC */
    es8388_input_cfg(0);                            /* 录音关闭 */
    es8388_output_cfg(1,1);                         /* 喇叭通道和耳机通道打开 */
    es8388_hpvol_set(20);                           /* 设置喇叭 */
    es8388_spkvol_set(20);                          /* 设置耳机 */
    xl9555_pin_write(SPK_EN_IO,0);                  /* 打开喇叭 */
    vTaskDelay(pdMS_TO_TICKS(20));
    i2s_tx_write(g_audiodev.tbuf, WAV_TX_BUFSIZE);  /* 先发送一段无声音的数据 */
 
    while(1)
    {
        if ((g_audiodev.status & 0x0F) == 0x03)     /* 打开了音频 */
        {
            for(uint16_t readTimes = 0; readTimes < (wavctrl.datasize
/ WAV_TX_BUFSIZE); readTimes++)
            {
                if ((g_audiodev.status & 0x0F) == 0x00)     /* 暂停播放 */
                {
                    file_read_pos = f_tell(g_audiodev.file);  /* 记录暂停位置 */
 
                    while(1)
                    {
                        if ((g_audiodev.status & 0x0F) == 0x03) /* 重新打开了 */
                        {
                            break;
                        }
 
                        vTaskDelay(pdMS_TO_TICKS(5));           /* 死等 */
                    }
 
                    f_lseek(g_audiodev.file,file_read_pos);
                }
 
                /* 判断是否播放完成 */
                if (i2s_table_size >= wavctrl.datasize
|| i2s_play_next_prev== ESP_OK)
                {
                    audio_stop();                 /* 先停止播放 */
                    i2s_deinit();                 /* 卸载I2S */
                    i2s_table_size = 0;           /* 总大小清零 */
                    i2s_play_end = ESP_OK;        /* 已播放完成标志位 */
                    vTaskDelete(NULL);            /* 删除当前任务 */
                    vTaskDelay(pdMS_TO_TICKS(5)); /* 适当延时(为了删除这个任务) */
                    break;                        /* 防止延时5ms未能删除音频任务 */
                }
 
                f_read(g_audiodev.file,g_audiodev.tbuf, WAV_TX_BUFSIZE, 
(UINT*)&bytes_write);
                i2s_table_size = i2s_table_size + i2s_tx_write(g_audiodev.tbuf, 
WAV_TX_BUFSIZE);
            }
        }
 
        vTaskDelay(pdMS_TO_TICKS(1));
    }
 
    vTaskDelete(NULL);
}
 
/**
 * @brief      播放某个wav文件
 * @param      fname : 文件路径+文件名
 * @retval     KEY0_PRES : 下一首
 *             KEY1_PRES : 上一首
 *            KEY2_PRES : 停止/启动
 *             其他,非WAV文件
 */
uint8_t wav_play_song(uint8_t *fname)
{
    uint8_t key = 0;
    uint8_t res = 0;
    
    i2s_play_end = ESP_FAIL;
    i2s_play_next_prev = ESP_FAIL;
    g_audiodev.file = (FIL*)heap_caps_malloc(sizeof(FIL),MALLOC_CAP_DMA);
    g_audiodev.tbuf = heap_caps_malloc(WAV_TX_BUFSIZE, MALLOC_CAP_DMA);       
 
    myi2s_init();                                   /* I2S初始化 */
    vTaskDelay(pdMS_TO_TICKS(50));                  /* 适当延时 */
 
    if (g_audiodev.file || g_audiodev.tbuf)
    {
        memset(g_audiodev.file,0,sizeof(FIL));      /* 文件指针清零 */
        memset(g_audiodev.tbuf,0,WAV_TX_BUFSIZE);   /* buf清零 */
        memset(&wavctrl,0,sizeof(__wavctrl));       /* 对WAV结构体相关参数清零 */
        res = wav_decode_init(fname, &wavctrl);     /* 对wav音频文件解码 */
 
        if (res == 0)                               /* 解码成功 */
        {
            if (wavctrl.bps == 16)  /* 根据解码文件重新配置采样率和位宽 */
            {
                i2s_set_samplerate_bits_sample(wavctrl.samplerate,
I2S_BITS_PER_SAMPLE_16BIT);
            }
            else if (wavctrl.bps == 24)
            {
                i2s_set_samplerate_bits_sample(wavctrl.samplerate,
I2S_BITS_PER_SAMPLE_24BIT);
            }
 
            res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);      
 
            if (res == FR_OK)
            {
                audio_start();  /* 开启I2S */
                /* 打开成功后,才创建音频任务 */
                if (MUSICTask_Handler == NULL && res == FR_OK)
                {
                    taskENTER_CRITICAL(&my_spinlock);
                    xTaskCreate(music, "music",4096,&MUSICTask_Handler,5, NULL);
                    taskEXIT_CRITICAL(&my_spinlock);
                }
 
                while (res == FR_OK)
                { 
                    while (1)
                    {
                        /* 播放结束,下一首 */
                        if (i2s_play_end == ESP_OK)
                        {
                            res = KEY0_PRES;
                            break;
                        }
 
                        key = xl9555_key_scan(0);
 
                        switch (key)
                        {
                            /* 下一首/上一首 */
                            case KEY0_PRES:
                            case KEY1_PRES:
                                i2s_play_next_prev = ESP_OK;
                                break;
                            /* 暂停/开启 */
                            case KEY2_PRES:
                                if ((g_audiodev.status & 0x0F) == 0x03)
                                {
                                    audio_stop();
                                }
                                else if ((g_audiodev.status & 0x0F) == 0x00)
                                {
                                    audio_start();
                                }
                                break;
                        }
 
                        if ((g_audiodev.status & 0x0F) == 0x03)                 
                        {
                            wav_get_curtime(g_audiodev.file, &wavctrl);         
                            audio_msg_show(wavctrl.totsec, wavctrl.cursec, 
wavctrl.bitrate);
                        }
 
                        vTaskDelay(pdMS_TO_TICKS(10));
                    }
 
                    if (key == KEY1_PRES || key == KEY0_PRES)                   
                    {
                        res = key;
                        break;
                    }
                }
            }
            else
            {
                res = 0xFF;
            }
        }
        else
        {
            res = 0xFF;
        }
    }
    else
    {
        res = 0xFF;
    }
 
    heap_caps_free(g_audiodev.file);
    heap_caps_free(g_audiodev.tbuf);
    g_audiodev.tbuf = NULL;
    g_audiodev.file = NULL;
    MUSICTask_Handler = NULL;
    return res;
}

       这段代码实现了基于ESP32-P4平台的WAV音频文件播放功能。核心包括wav_decode_init、wav_get_curtime、music和wav_play_song函数,分别负责WAV文件的解析、播放时间的获取、音频数据的传输以及整体音频播放的控制。

       wav_decode_init函数解析WAV文件头,提取音频格式、采样率、位宽等信息,并计算数据的起始位置。music函数则用于在FreeRTOS环境下运行音频播放任务,控制音频数据的读取与传输,支持暂停、播放、切换歌曲等功能。wav_play_song函数是音频播放的主控制逻辑,负责初始化I2S播放器,处理用户输入并响应控制命令,如切换歌曲或暂停播放。通过这些函数,系统能够实现高效的 WAV 音频文件播放,并根据用户输入进行实时控制。


       4,audioplay代码

       该文件主要实现了对 SD 卡中的文件进行扫描,检查是否包含音频文件,并识别这些文件是否为有效的音频文件,如下代码。

/**
 * @brief      播放音乐
 * @param      无
 * @retval     无
 */
void audio_play(void)
{
    uint8_t res;
    FF_DIR wavdir;
    FILINFO *wavfileinfo;
    uint8_t *pname;
    uint16_t totwavnum;
    uint16_t curindex;
    uint8_t key;
    uint32_t temp;
    uint32_t *wavoffsettbl;
 
    while (f_opendir(&wavdir, "0:/MUSIC"))
    {
        text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
        vTaskDelay(200);
        lcd_fill(30, 190, 240, 206, WHITE);
        vTaskDelay(200);
    }
 
    totwavnum = audio_get_tnum((uint8_t *)"0:/MUSIC");  /* 得到总有效文件数 */
    
    while (totwavnum == 0)
    {
        text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
        vTaskDelay(200);
        lcd_fill(30, 190, 240, 146, WHITE);
        vTaskDelay(200);
    }
    
    wavfileinfo = (FILINFO*)malloc(sizeof(FILINFO));
    pname = malloc(255 * 2 + 1);
    wavoffsettbl = malloc(4 * totwavnum);
    
    while (!wavfileinfo || !pname || !wavoffsettbl)
    {
        text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
        vTaskDelay(200);
        lcd_fill(30, 190, 240, 146, WHITE);
        vTaskDelay(200);
    }
    
    res = f_opendir(&wavdir, "0:/MUSIC");
    
    if (res == FR_OK)
    {
        curindex = 0;                              /* 当前索引为0 */
        
        while (1)
        {
            temp = wavdir.dptr;                  /* 记录当前index */
 
            res = f_readdir(&wavdir, wavfileinfo);  /* 读取目录下的一个文件 */
            
            if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
            {
                break;                              /* 错误了/到末尾了,退出 */
            }
 
            res = exfuns_file_type(wavfileinfo->fname);
            
            if ((res & 0xF0) == 0x40)
            {
                wavoffsettbl[curindex] = temp;          /* 记录索引 */
                curindex++;
            }
        }
    }
    
    curindex = 0;                                        /* 从0开始显示 */
    res = f_opendir(&wavdir, (const TCHAR*)"0:/MUSIC");
 
    while (res == FR_OK)                                  /* 打开目录 */
    {
        atk_dir_sdi(&wavdir, wavoffsettbl[curindex]);    /* 改变当前目录索引 */
 
        res = f_readdir(&wavdir, wavfileinfo);           /* 读取文件 */
        
        if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
        {
            break;
        }
        
        strcpy((char *)pname, "0:/MUSIC/");
        strcat((char *)pname, (const char *)wavfileinfo->fname);
        lcd_fill(30, 190, mipidev.pwidth - 1, 190 + 16, WHITE);
        audio_index_show(curindex + 1, totwavnum);
        text_show_string(30, 190, mipidev.pwidth - 60, 16, 
(char *)wavfileinfo->fname, 16, 0, BLUE);
        key = audio_play_song(pname);
 
        if (key == KEY1_PRES)       /* 上一首 */
        {
            if (curindex)
            {
                curindex--;
            }
            else
            {
                curindex = totwavnum - 1;
            }
        }
        else if (key == KEY0_PRES)  /* 下一首 */
        {
            curindex++;
 
            if (curindex >= totwavnum)
            {
                curindex = 0;
            }
        }
        else
        {
            break;
        }
    }
 
    free(wavfileinfo);
    free(pname);
    free(wavoffsettbl);
}
 
/**
 * @brief      播放某个音频文件
 * @param     fname : 文件名
 * @retval     按键值
 *   @arg      KEY0_PRES , 下一曲.
 *   @arg      KEY1_PRES , 上一曲.
 *   @arg      其他 , 错误
 */
uint8_t audio_play_song(uint8_t *fname)
{
    uint8_t res;  
    
    res = exfuns_file_type((char *)fname); 
 
    switch (res)
    {
        case T_WAV:
            res = wav_play_song(fname);
            break;
        case T_MP3:
            /* 自行实现 */
            break;
 
        default:            /* 其他文件,自动跳转到下一曲 */
            printf("can't play:%s\r\n", fname);
            res = KEY0_PRES;
            break;
    }
    return res;
}

       这里,audio_play函数在main函数里面被调用,该函数首先设置ES8388相关配置,然后查找SD卡里面的MUSIC文件夹,并统计该文件夹里面总共有多少音频文件(统计包括:WAV/MP3/APE/FLAC等),然后,该函数调用audio_play_song函数,按顺序播放这些音频文件。

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


       5,CMakeLists.txt文件

       本例程的功能实现主要依靠MYI2S驱动。要在main函数中,成功调用MYI2S文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:

set(src_dirs
             LED
             KEY
             MYIIC
             XL9555
             LCD
             ES8388
             MYI2S
             SDMMC)
 
set(include_dirs
             LED
             KEY
             MYIIC
             XL9555
             LCD
             ES8388
             MYI2S
             SDMMC)
 
set(requires
             driver
             esp_lcd
             esp_common
             fatfs)
 
idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs}
                       REQUIRES ${requires})
 
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)


       6,main.c驱动代码

       在main.c里面编写如下代码。

void app_main(void)
{
    esp_err_t ret;
    uint8_t key = 0;
 
    ret = nvs_flash_init();        /* 初始化NVS */
 
    if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ESP_ERROR_CHECK(nvs_flash_init());
    }
 
    key_init();            /* 初始化KEY */
    myiic_init();         /* IIC0初始化 */
    xl9555_init();        /* XL9555初始化 */
    lcd_init();           /* LCD屏初始化 */
 
    es8388_init();                       /* ES8388初始化 */
    xl9555_pin_write(SPK_EN_IO, 1);      /* 打开喇叭 */
 
    while (sdmmc_init())        /* 检测不到SD卡 */
    {
        lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
        vTaskDelay(pdMS_TO_TICKS(200));
        lcd_fill(30, 110, 239, 126, WHITE);
        vTaskDelay(pdMS_TO_TICKS(200));
    }
 
    ret = exfuns_init();        /* 为fatfs相关变量申请内存 */
 
    while (fonts_init())        /* 检查字库 */
    {
        lcd_clear(WHITE);
        lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", RED);
        
        key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);  /* 更新字库 */
        
        while (key)             /* 更新失败 */
        {
            lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
            vTaskDelay(pdMS_TO_TICKS(200));
            lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
            vTaskDelay(pdMS_TO_TICKS(200));
        }
 
        lcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
        vTaskDelay(pdMS_TO_TICKS(1000));
        lcd_clear(WHITE);   
    }
 
    text_show_string(30, 50,  200, 16, "正点原子ESP32-P4开发板",16, 0, RED);
    text_show_string(30, 70,  200, 16, "音乐播放器 实验", 16, 0, RED);
    text_show_string(30, 90,  200, 16, "正点原子@ALIENTEK", 16, 0, RED);
    text_show_string(30, 110, 200, 16, "KEY0:NEXT  KEY1:PREV", 16, 0, RED);
    text_show_string(30, 130, 200, 16, "KEY2:PAUSE/PLAY", 16, 0, RED);
 
    vTaskDelay(pdMS_TO_TICKS(1000));
 
    while (1)
    {
        audio_play();       /* 播放音乐 */
    }
}

       该函数就相对简单了,在初始化各个外设后,通过 audio_play 函数,开始音频播放,到这里本实验的代码基本就编写完成了。


        43.4 下载验证

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


图43.1.1 音乐播放中


       从上图可以看出,总共10首歌曲,当前正在播放第2首歌曲,歌曲名、播放时间、总时长、码率等信息等也都有显示。LED0闪烁表现程序正在运行。

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

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


请使用浏览器的分享功能分享到微信等