OpenEdv-开源电子网

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

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

[复制链接]

1256

主题

1270

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5395
金钱
5395
注册时间
2019-5-8
在线时间
1394 小时
发表于 1 小时前 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2026-2-7 10:18 编辑

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

1)实验平台:正点原子DNESP32P4开发板

2)章节摘自【正点原子】ESP32-P4开发指南— V1.0

3)购买链接:https://detail.tmall.com/item.htm?id=873309579825

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

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

6)正点原子DNESP32S3开发板技术交流群:132780729


2.jpg

3.png

正点原子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由块标识符、数据大小和数据三部分组成,如下图所示:

第四十三章 音乐播放器实验850.png
图43.1.1.1 Chunk组成结构

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

第四十三章 音乐播放器实验938.png
图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的数据。


第四十三章 音乐播放器实验1328.png
图43.1.1.3 RIFF Chunk数据

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

1.png
表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的数据。


第四十三章 音乐播放器实验1793.png
图43.1.1.4 Format Chunk数据

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

2.png
表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的数据。


第四十三章 音乐播放器实验2717.png
图43.1.1.5 Data Chunk数据

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

3.png
表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结构如下:
  1. typedef struct
  2. {
  3.     uint32_t ChunkID;           /* chunk id;这里固定为"RIFF",即0X46464952 */
  4.     uint32_t ChunkSize;         /* 集合大小;文件总大小-8 */
  5.     uint32_t Format;            /* 格式;WAVE,即0X45564157 */
  6. } ChunkRIFF;                    /* RIFF块 */
复制代码
接着,我们看看Format块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:
  1. typedef struct
  2. {
  3.     uint32_t ChunkID;           /* chunk id;这里固定为"fmt ",即0X20746D66 */
  4.     uint32_t ChunkSize;         /* 子集合大小(不包括ID和Size);这里为:20 */
  5.     uint16_t AudioFormat;       /* 音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM */
  6.     uint16_t NumOfChannels;     /* 通道数量;1,表示单声道;2,表示双声道 */
  7.     uint32_t SampleRate;        /* 采样率;0X1F40,表示8Khz */
  8.     uint32_t ByteRate;          /* 字节速率 */
  9.     uint16_t BlockAlign;        /* 块对齐(字节) */
  10.     uint16_t BitsPerSample;     /* 单个采样数据大小;4位ADPCM,设置为4 */
  11. //    uint16_t ByteExtraData;   /* 附加的数据字节;2个; 线性PCM,没有这个参数 */
  12. } ChunkFMT;                     /* fmt块 */
复制代码
然后,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:
  1. typedef struct
  2. {
  3.     uint32_t ChunkID;           /* chunk id;这里固定为"fact",即0X74636166 */
  4.     uint32_t ChunkSize;         /* 子集合大小(不包括ID和Size);这里为:4 */
  5.     uint32_t NumOfSamples;      /* 采样的数量 */
  6. } ChunkFACT;                    /* fact块 */
复制代码
最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”作为该Chunk的标示,然后是数据的大小。数据块的Chunk结构如下:
  1. typedef 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位置可以分成如下表所示的几种形式:

4.png
表43.1.1.3 WAVE文件数据采样格式

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

43.1.2 ES8388简介
ES8388音频编解码器是上海顺芯推出的一款高性能、低功耗、性价比高的音频编解码器,具备丰富的功能,如2个ADC通道、2个DAC通道、麦克风放大器、耳机放大器、数字音效处理、模拟混合与增益控制等。
1,ES8388的主要特性
I2S接口:支持最高96kHz采样率和24bit音频播放。
DAC信噪比:96dB,ADC信噪比:95dB。
工作模式:支持主机模式和从机模式。
支持立体声差分输入和麦克风输入。
支持左右声道音量独立调节。
支持40mW耳机输出,确保没有爆音现象。
2,音频数据传输与控制
I2S接口:ES8388通过I2S接口(即数字音频接口)与MCU进行音频数据传输,支持音频接收和发送。
配置接口:通过两线接口(CE=0/1,即I2C接口)或三线接口(CE脚产生一个下降沿,即SPI接口)进行配置。
3,ES8388的I2S接口
I2S接口由4个引脚组成,分别为:
ASDOUT:ADC数据输出。
DSDIN:DAC数据输入。
LRC:数据左/右对齐时钟。
SCLK:位时钟,用于同步音频数据的传输。
MCLK:提供从机ES8388参考时钟。
4,主机/从机模式
ES8388可作为I2S接口的主机,输出LRC和SCLK时钟。然而,在大多数应用中,ES8388作为从机接收LRC和SCLK时钟信号。
5,音频数据模式
ES8388的I2S接口支持4种不同的音频数据模式:
左(MSB)对齐标准。
右(LSB)对齐标准。
飞利浦(I2S)标准。
DSP/PCM模式。
6,飞利浦(I2S)标准模式
在飞利浦(I2S)标准模式下,数据通过跟随LRC的BCLK的第二个上升沿传输MSB(最高有效位),之后的数据按顺序依次传输,直到LSB(最低有效位)。这种传输方式的具体协议依赖于字长、BCLK频率和采样率。每个采样的LSB与下一个采样的MSB之间应有一个未用的BCLK周期,用于同步和时序的调整。飞利浦标准模式的I2S数据传输协议如下图所示:

第四十三章 音乐播放器实验6363.png
图43.1.2.1 飞利浦标准模式I2S数据传输图

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


第四十三章 音乐播放器实验6532.png
图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)支持多种音频标准:
TDM Philips标准
TDM MSB对齐标准
TDM PCM标准
PDM 标准
5)支持多种TX/RX模式:
TDM TX模式:支持最多16个通道
TDM RX模式:支持最多16个通道
PDM TX模式:原始 PDM 数据传输和PCM 转 PDM 数据格式转换(仅支持 I2S0),最多支持 2 个通道
6)PDM RX模式
原始PDM数据接收
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模块的结构如下图所示。

第四十三章 音乐播放器实验11125.png
图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模块提供所需的时钟。具体内容请参见下图。


第四十三章 音乐播放器实验12129.png
图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,也就是上图讲解的三个时钟源其中一个,通过以下公式计算:


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

其中,𝑁是一个整数分频值,范围为2到256,用户可以通过配置 HP_SYS_CLKRST_I2Sn_TX/RX_DIV_N 寄存器来设置。参数𝑎和𝑏则用于配置分数分频器(fractional divider),通过精细调节这些值,可以实现更高精度的频率设置,从而满足特定应用的时钟要求。
3)串行时钟(BCK)生成
I2S模块的TX和RX串行时钟(分别为 I2SnO_BCK_out 和 I2SnI_BCK_out)是通过主时钟 I2Sn_TX_CLK 和 I2Sn_RX_CLK 进一步分频生成的。发送BCK频率的计算公式如下:


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

其中,MO是一个整数值,通过I2Sn_TX_BCK_DIV_NUM配置,且需确保𝑀𝑂≠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 文件结构


第四十三章 音乐播放器实验13650.png
图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器件相关原理图,如下图所示。

第四十三章 音乐播放器实验15793.png
图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功能,必须先导入以下头文件。
  1. #include "driver/i2s_std.h"
复制代码
接下来,作者将介绍一些常用的MYI2S函数,这些函数的描述及其作用如下:
1,分配新的I2S通道i2s_new_channel
该函数用于分配新的I2S通道,其函数原型如下:
  1. esp_err_t i2s_new_channel(const i2s_chan_config_t *chan_cfg,
  2. i2s_chan_handle_t *ret_tx_handle,
  3. i2s_chan_handle_t *ret_rx_handle);
复制代码
函数形参:

5.png
表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结构体中的各个成员变量,如下代码所示:
  1. typedef struct {
  2.     i2s_port_t id;                       /* I2S端口ID */
  3.     i2s_role_t role;                     /* I2S角色,I2S_ROLE_MASTER或I2S_ROLE_SLAVE */

  4.     /* DMA配置 */
  5.     uint32_t dma_desc_num;                 /* I2S DMA缓冲区的数量,也即DMA描述符的数量 */
  6.     uint32_t dma_frame_num;                /* 每个DMA缓冲区的帧数。每帧表示一次采样数据,*/
  7.     union {
  8.         bool auto_clear;                 /* `auto_clear_after_cb`的别名 */
  9.         bool auto_clear_after_cb;         /* 设置在`on_sent`回调之后自动清除DMA TX缓冲区,
  10. 如果没有数据发送,I2S会自动发送零。
  11. 这样用户可以直接在回调中分配数据到DMA缓冲区,
  12. 且在退出回调后数据不会被清除。*/
  13.     };
  14.     bool auto_clear_before_cb; /* 设置在`on_sent`回调之前自动清除DMA TX缓冲区,
  15.                                   如果没有数据发送,I2S会自动发送零。
  16.                                   这样用户可以在回调中访问刚刚发送完的数据。*/
  17. bool allow_pd;    /* 允许I2S控制器进入低功耗模式。启用此标志后,
  18. 驱动会在进入睡眠模式前备份I2S寄存器,
  19.                          并在退出时恢复这些寄存器。这样可以节省功耗,但会消耗更多的RAM。*/
  20. int intr_priority;   /* I2S中断优先级,范围为[0, 7],如果设置为0,
  21. 驱动会尝试分配一个相对较低的中断优先级(1,2,3) */
  22. } i2s_chan_config_t;
复制代码
i2s_chan_config_t结构体用于传递I2S的通道配置参数,以便在调用i2s_new_channel时进行初始化和设置。通过这个结构体,开发者可以灵活配置I2S的主从机模式,以满足不同的需求。
2,初始化I2S通道为标准模式i2s_channel_init_std_mode
该函数用于初始化I2S通道为标准模式,其函数原型如下:
  1. esp_err_t i2s_channel_init_std_mode(i2s_chan_handle_t handle,
  2. const i2s_std_config_t *std_cfg);
复制代码
函数形参:

6.png
表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结构体中的各个成员变量,如下代码所示:
  1. /**
  2. * @brief I2S标准模式声道配置
  3. */
  4. typedef struct {
  5.     /* 一般字段 */
  6.     i2s_data_bit_width_t data_bit_width;        /* I2S采样数据位宽 */
  7.     i2s_slot_bit_width_t slot_bit_width;        /* I2S声道位宽(每个声道的总位数) */
  8.     i2s_slot_mode_t slot_mode;                  /* 设置单声道或立体声模式, */

  9.     /* 特殊字段 */
  10.     i2s_std_slot_mask_t slot_mask;        /* 选择左声道、右声道或两者的声道 */
  11.     uint32_t ws_width;              /* WS信号宽度(即WS信号为高电平的BCLK时钟周期数) */
  12.     bool ws_pol;                     /* WS信号极性,设置为true时,表示先为高电平 */
  13.     bool bit_shift;                  /* 在Philips模式下启用位移 */
  14. #if SOC_I2S_HW_VERSION_1            /* 对于ESP32/ESP32-S2 */
  15.     bool msb_right;                  /* 设置在FIFO中将右声道数据放置在MSB */
  16. #else
  17.     bool left_align;                 /* 设置启用左对齐 */
  18.     bool big_endian;                 /* 设置启用大端字节序 */
  19.     bool bit_order_lsb;              /* 设置启用LSB优先 */
  20. #endif
  21. } i2s_std_slot_config_t;

  22. /**
  23. * @brief I2S标准模式时钟配置
  24. */
  25. typedef struct {
  26.     /* 一般字段 */
  27.     uint32_t sample_rate_hz;                          /* I2S采样率 */
  28.     i2s_clock_src_t clk_src;                         /* 选择时钟源 */
  29. #if SOC_I2S_HW_VERSION_2
  30.     uint32_t ext_clk_freq_hz;                        /* 外部时钟源频率(单位Hz)*/
  31. #endif
  32.     i2s_mclk_multiple_t mclk_multiple;        /* MCLK与采样率的倍数 */
  33. } i2s_std_clk_config_t;

  34. /**
  35. * @brief I2S标准模式GPIO引脚配置
  36. */
  37. typedef struct {
  38.     gpio_num_t mclk;                           /* MCK引脚,默认输出 */
  39.     gpio_num_t bclk;               /* BCK引脚,主机角色时为输出,Slave角色时为输入 */
  40.     gpio_num_t ws;                 /* WS引脚,主机角色时为输出,Slave角色时为输入 */
  41.     gpio_num_t dout;               /* 数据输出引脚 */
  42.     gpio_num_t din;                /* 数据输入引脚 */
  43.     struct {
  44.         uint32_t mclk_inv: 1;            /* 设置为1时,反转MCLK输入/输出 */
  45.         uint32_t bclk_inv: 1;            /* 设置为1时,反转BCLK输入/输出 */
  46.         uint32_t ws_inv: 1;              /* 设置为1时,反转WS输入/输出 */
  47.     } invert_flags;                /* GPIO引脚反转标志 */
  48. } i2s_std_gpio_config_t;

  49. /**
  50. * @brief I2S标准模式主要配置,包括时钟、声道和GPIO配置
  51. */
  52. typedef struct {
  53.     i2s_std_clk_config_t clk_cfg;            /* 标准模式时钟配置 */
  54.     i2s_std_slot_config_t slot_cfg;           /* 标准模式声道配置 */
  55.     i2s_std_gpio_config_t gpio_cfg;           /* 标准模式GPIO配置 */
  56. } i2s_std_config_t;
复制代码
这些结构体在I2S配置中起着至关重要的作用,可以帮助用户定制I2S的数据传输、时钟源选择和GPIO引脚设置等。
3,启用I2S TX/RX通道i2s_channel_enable
该函数用于启用I2S TX/RX通道,其函数原型如下:
  1. esp_err_t i2s_channel_enable(i2s_chan_handle_t handle);
复制代码
函数形参:

7.png
表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通道,其函数原型如下:
  1. esp_err_t i2s_channel_disable(i2s_chan_handle_t handle);
复制代码
函数形参:

8.png
表43.3.1.4 i2s_channel_disable函数形参描述

返回值:
ESP_OK表示禁用成功。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
ESP_ERR_INVALID_STATE表示该通道尚未初始化或已启动。
5,删除I2S通道i2s_del_channel
该函数用于删除I2S通道,其函数原型如下:
  1. esp_err_t i2s_del_channel(i2s_chan_handle_t handle);
复制代码
函数形参:

9.png
表43.3.1.5 i2s_del_channel函数形参描述

返回值:
ESP_OK表示删除成功。
ESP_ERR_INVALID_ARG表示NULL指针或无效的配置。
6,重新配置I2S时钟以适应标准模式i2s_channel_reconfig_std_clock
该函数用于重新配置I2S时钟以适应标准模式,其函数原型如下:
  1. esp_err_t i2s_channel_reconfig_std_clock(i2s_chan_handle_t handle,
  2. const i2s_std_clk_config_t *clk_cfg);
复制代码
函数形参:

10.png
表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声道以适应标准模式,其函数原型如下:
  1. esp_err_t i2s_channel_reconfig_std_slot(i2s_chan_handle_t handle,
  2. const i2s_std_slot_config_t *slot_cfg);
复制代码
函数形参:

11.png
表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写数据,其函数原型如下:
  1. esp_err_t i2s_channel_write(i2s_chan_handle_t handle, const void *src,
  2. size_t size, size_t *bytes_written,
  3. uint32_t timeout_ms);
复制代码
函数形参:

12.png
表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 程序流程图

第四十三章 音乐播放器实验22521.png
图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所需的管脚及采样率做了定义。
  1. #define I2S_NUM                 (I2S_NUM_0)                 /* I2S port */
  2. #define I2S_BCK_IO              (GPIO_NUM_47)               /* ES8388_SCLK */
  3. #define I2S_WS_IO               (GPIO_NUM_48)               /* ES8388_LRCK */
  4. #define I2S_DO_IO               (GPIO_NUM_49)               /* ES8388_SDIN */
  5. #define I2S_DI_IO               (GPIO_NUM_50)               /* ES8388_SDOUT */
  6. #define I2S_MCK_IO              (GPIO_NUM_46)               /* ES8388_MCLK */
  7. #define I2S_RECV_BUF_SIZE       (2400)                      /* 接收大小 */
  8. #define I2S_SAMPLE_RATE         (44100)                     /* 采样率 */
  9. #define I2S_MCLK_MULTIPLE       (384)        /* 如果不使用24位数据宽度,256应该足够了 */
复制代码
下面我们再解析myi2s.c的程序,该文件定义了myi2s_init、i2s_trx_start、i2s_trx_stop等相关I2S操作函数,这些函数实现代码如下所示:
1,myi2s_init函数
  1. /*
  2. * @brief            初始化I2S
  3. * @param            无
  4. * @retval                 ESP_OK:初始化成功;其他:失败
  5. */
  6. esp_err_t myi2s_init(void)
  7. {
  8. i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM,
  9.                              I2S_ROLE_MASTER);        /* 默认的通道配置(I2S0,主机) */
  10.     chan_cfg.auto_clear = true;         /* 自动清除DMA缓冲区遗留的数据 */
  11.     ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));   

  12.     i2s_std_config_t std_cfg = {                                                    /* 标准通信模式配置 */
  13.         .clk_cfg  = {                                                               /* 时钟配置 */
  14.             .sample_rate_hz = I2S_SAMPLE_RATE,                    /* I2S采样率 */
  15.             .clk_src        = I2S_CLK_SRC_DEFAULT,                /* I2S时钟源 */
  16.             .mclk_multiple  = I2S_MCLK_MULTIPLE,                  /* I2S主时钟MCLK */
  17.         },
  18.         .slot_cfg = {                                                               /* 声道配置 */
  19.             .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,         /* 声道支持16位宽 */
  20.             .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,           /* 通道位宽 */
  21.             .slot_mode      = I2S_SLOT_MODE_STEREO,              /* 立体声 */
  22.             .slot_mask      = I2S_STD_SLOT_BOTH,               /* 启用通道 */
  23.             .ws_width       = I2S_DATA_BIT_WIDTH_16BIT,         /* WS信号位宽 */
  24.             .ws_pol         = false,                            /* WS信号极性 */
  25.             .bit_shift      = true,                            /* 位移位 */
  26.             .left_align     = true,                              /* 左对齐 */
  27.             .big_endian     = false,                            /* 小端模式 */
  28.             .bit_order_lsb  = false                             /* MSB */
  29.         },
  30.         
  31.         .gpio_cfg = {                                                               /* 引脚配置 */
  32.             .mclk = I2S_MCK_IO,                                                     /* 主时钟线 */
  33.             .bclk = I2S_BCK_IO,                                                     /* 位时钟线 */
  34.             .ws   = I2S_WS_IO,                                                      /* 字(声道)选择线 */
  35.             .dout = I2S_DO_IO,                                                      /* 串行数据输出线 */
  36.             .din  = I2S_DI_IO,                                                      /* 串行数据输入线 */
  37.             .invert_flags = {                                                       /* 引脚翻转(不反相) */
  38.                 .mclk_inv = false,
  39.                 .bclk_inv = false,
  40.                 .ws_inv   = false,
  41.             },
  42.         },
  43.     };
  44.     my_std_cfg = std_cfg;
  45. /* 初始化TX通道 */
  46. ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));   
  47. /* 初始化RX通道 */
  48. ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));   
  49. /* 启用TX通道 */
  50. ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));                     
  51. /* 启用RX通道 */
  52.     ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));                     
  53.     return ESP_OK;
  54. }
复制代码
上述函数 myi2s_init用于初始化ESP32-P4的I2S外设,并配置其为标准通信模式。首先,函数设置了I2S通道的默认配置,包括通道角色(主机模式)和DMA数据自动清除功能。接着,函数定义了标准模式下的时钟、数据通道和GPIO配置,确保I2S信号的时钟、数据位宽、信号极性等符合应用要求。然后,通过调用i2s_new_channel创建新的TX和RX通道句柄,并分别初始化为标准模式。最后,函数启用TX和RX通道,启动数据传输。该函数通过ESP_ERROR_CHECK确保每个步骤成功执行,若有任何错误会终止执行并返回错误信息。
2,i2s_trx_start函数
  1. /**
  2. * @brief             I2S TRX启动
  3. * @param             无
  4. * @retval            无
  5. */
  6. void i2s_trx_start(void)
  7. {
  8.     ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
  9.     ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
  10. }
复制代码
上述函数i2s_trx_start用于启动I2S的TX与RX通道。
3,i2s_trx_stop函数
  1. /**
  2. * @brief            I2S TRX停止
  3. * @param             无
  4. * @retval            无
  5. */
  6. void i2s_trx_stop(void)
  7. {
  8.     ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));
  9.     ESP_ERROR_CHECK(i2s_channel_disable(rx_handle));
  10. }
复制代码
上述函数i2s_trx_stop用于禁用I2S的TX与RX通道。
3,i2s_deinit函数
  1. /**
  2. * @brief             I2S卸载
  3. * @param             无
  4. * @retval          无
  5. */
  6. void i2s_deinit(void)
  7. {
  8.     ESP_ERROR_CHECK(i2s_del_channel(tx_handle));
  9.     ESP_ERROR_CHECK(i2s_del_channel(rx_handle));
  10. }
复制代码
上述函数用于卸载I2S设备。
4,i2s_set_samplerate_bits_sample函数
  1. /**
  2. * @brief             设置采样率和位宽
  3. * @param             sampleRate  :采样率
  4. * @param             bits_sample :位宽
  5. * @retval            无
  6. */
  7. void i2s_set_samplerate_bits_sample(int samplerate, int bits_sample)
  8. {
  9.     i2s_trx_stop();
  10.     /* 如果需要更新声道或时钟配置,需要在更新前先禁用通道 */
  11.     my_std_cfg.slot_cfg.ws_width = bits_sample;        /* 位宽 */
  12. ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(tx_handle,
  13.                                              &my_std_cfg.slot_cfg));
  14.     my_std_cfg.clk_cfg.sample_rate_hz = samplerate;    /* 设置采样率 */
  15. ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(tx_handle,
  16. &my_std_cfg.clk_cfg));
  17. }
复制代码
上述函数i2s_set_samplerate_bits_sample用于重新配置通道位宽和采样率,值得注意的是,必须先禁用Tx/Rx通道,方能重新配置这些参数。
5,i2s_tx_write函数
  1. /**
  2. * @brief            I2S传输数据
  3. * @param           buffer: 数据存储区的首地址
  4. * @param            frame_size: 数据大小
  5. * @retval           发送的数据长度
  6. */
  7. size_t i2s_tx_write(uint8_t *buffer, uint32_t frame_size)
  8. {
  9.     size_t bytes_written;
  10. ESP_ERROR_CHECK(i2s_channel_write(tx_handle, buffer, frame_size,
  11.                                  &bytes_written, 1000));
  12.     return bytes_written;
  13. }
复制代码
上述函数i2s_tx_write将用于I2S音频数据传输至es8388当中,并返回实际发送的大小。
6,i2s_rx_read函数
  1. /**
  2. * @brief                  I2S读取数据
  3. * @param             buffer: 读取数据存储区的首地址
  4. * @param             frame_size: 读取数据大小
  5. * @retval            接收的数据长度
  6. */
  7. size_t i2s_rx_read(uint8_t *buffer, uint32_t frame_size)
  8. {
  9.     size_t bytes_written;
  10. ESP_ERROR_CHECK(i2s_channel_read(rx_handle, buffer, frame_size,
  11. &bytes_written, 1000));
  12.     return bytes_written;
  13. }
复制代码
上述函数i2s_rx_read将用于读取es8388设备的音频数据,并返回实际接收的数据大小。

2,ES8388驱动代码
ES8388主要用来将音频信号转换为数字信号或将数字信号转换为音频信号,接下来,我们开始介绍ES8388的几个函数,代码如下:
  1. i2c_master_dev_handle_t es8388_handle = NULL;

  2. /**
  3. * @brief       ES8388写寄存器
  4. * @param       reg_addr:寄存器地址
  5. * @param       data:写入的数据
  6. * @retval      无
  7. */
  8. esp_err_t es8388_write_reg(uint8_t reg_addr, uint8_t data)
  9. {
  10.     esp_err_t ret;
  11.     uint8_t *buf = malloc(2);
  12.     if (buf == NULL)
  13.     {
  14.         ESP_LOGE(es8388_tag, "%s memory failed", __func__);
  15.         return ESP_ERR_NO_MEM;      /* 分配内存失败 */
  16.     }

  17.     buf[0] = reg_addr;              
  18.     buf[1] = data;                  /* 拷贝数据至存储区当中 */

  19.     do
  20.     {
  21.         i2c_master_bus_wait_all_done(bus_handle, 1000);
  22.         ret = i2c_master_transmit(es8388_handle, buf, 2, 1000);   
  23.     } while (ret != ESP_OK);

  24.     free(buf);                      /* 发送完成释放内存 */

  25.     return ret;
  26. }


  27. /**
  28. * @brief            ES8388读寄存器
  29. * @param            reg_add:寄存器地址
  30. * @param             p_data:读取的数据
  31. * @retval            无
  32. */
  33. esp_err_t es8388_read_reg(uint8_t reg_addr, uint8_t *pdata)
  34. {
  35.     uint8_t reg_data = 0;
  36.     i2c_master_transmit_receive(es8388_handle, &reg_addr, 1, &reg_data, 1, -1);
  37.     return reg_data;
  38. }

  39. /**
  40. * @brief            ES8388初始化
  41. * @param            无
  42. * @retval             0,初始化正常
  43. *                     其他,错误代码
  44. */
  45. uint8_t es8388_init(void)
  46. {
  47.     uint8_t ret_val = 0;

  48.     /* 未调用myiic_init初始化IIC */
  49.     if (bus_handle == NULL)
  50.     {
  51.         ESP_ERROR_CHECK(myiic_init());
  52.     }

  53.     i2c_device_config_t es8388_i2c_dev_conf = {
  54.         .dev_addr_length = I2C_ADDR_BIT_LEN_7,  /* 从机地址长度 */
  55.         .scl_speed_hz    = IIC_SPEED_CLK,       /* 传输速率 */
  56.         .device_address  = ES8388_ADDR,         /* 从机7位的地址 */
  57.     };
  58.     /* I2C总线上添加ES8388设备 */
  59. ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &es8388_i2c_dev_conf,
  60. &es8388_handle));

  61.     ret_val |= es8388_write_reg(0, 0x80);       /* 软复位ES8388 */
  62.     ret_val |= es8388_write_reg(0, 0x00);
  63.     vTaskDelay(pdMS_TO_TICKS(100));             /* 等待复位 */

  64.     ret_val |= es8388_write_reg(0x01, 0x58);
  65.     ret_val |= es8388_write_reg(0x01, 0x50);
  66.     ret_val |= es8388_write_reg(0x02, 0xF3);
  67.     ret_val |= es8388_write_reg(0x02, 0xF0);

  68.     ret_val |= es8388_write_reg(0x03, 0x09);    /* 麦克风偏置电源关闭 */
  69.     ret_val |= es8388_write_reg(0x00, 0x06);    /* 使能参考 500K驱动使能 */
  70.     ret_val |= es8388_write_reg(0x04, 0x00);    /* DAC电源管理,不打开任何通道 */
  71.     ret_val |= es8388_write_reg(0x08, 0x00);    /* MCLK不分频 */
  72.     ret_val |= es8388_write_reg(0x2B, 0x80);    /* DAC控制 DACLRC与ADCLRC相同 */

  73. ret_val |= es8388_write_reg(0x09, 0x88);    /* ADC L/R PGA增益配置为+24dB */
  74. /* ADC数据选择为left data = left ADC, right data=left ADC  音频数据为16bit */
  75.     ret_val |= es8388_write_reg(0x0C, 0x4C);   
  76. ret_val |= es8388_write_reg(0x0D, 0x02);    /* ADC配置 MCLK/采样率=256 */
  77. /* ADC数字音量控制将信号衰减 L  设置为最小!!! */
  78.     ret_val |= es8388_write_reg(0x10, 0x00);   
  79. /* ADC数字音量控制将信号衰减 R  设置为最小!!! */
  80. ret_val |= es8388_write_reg(0x11, 0x00);   

  81.     ret_val |= es8388_write_reg(0x17, 0x18);    /* DAC音频数据为16bit */
  82. ret_val |= es8388_write_reg(0x18, 0x02);    /* DAC配置 MCLK/采样率=256 */
  83. /* DAC数字音量控制将信号衰减 L  设置为最小!!! */
  84. ret_val |= es8388_write_reg(0x1A, 0x00);   
  85. /* DAC数字音量控制将信号衰减 R  设置为最小!!! */
  86.     ret_val |= es8388_write_reg(0x1B, 0x00);   
  87.     ret_val |= es8388_write_reg(0x27, 0xB8);    /* L混频器 */
  88.     ret_val |= es8388_write_reg(0x2A, 0xB8);    /* R混频器 */
  89.     vTaskDelay(pdMS_TO_TICKS(100));

  90.     if (ret_val != ESP_OK)
  91.     {
  92.         while(1)
  93.         {
  94.             ESP_LOGI(es8388_tag, "ES8388 fail");
  95.             vTaskDelay(pdMS_TO_TICKS(500));
  96.         }
  97.     }
  98.     else
  99.     {
  100.         ESP_LOGI(es8388_tag, "ES8388 success");
  101.     }

  102.     es8388_adda_cfg(0, 0);      /* 开启DAC关闭ADC */
  103.     es8388_input_cfg(0);        /* 关闭录音输入 */
  104.     es8388_output_cfg(0, 0);    /* DAC选择通道输出 */
  105.     es8388_hpvol_set(0);        /* 设置耳机音量 */
  106.     es8388_spkvol_set(0);       /* 设置喇叭音量 */
  107.    
  108.     return 0;
  109. }

  110. /**
  111. * @brief             ES8388反初始化
  112. * @param             无
  113. * @retval           0,初始化正常
  114. *                    其他,错误代码
  115. */
  116. esp_err_t es8388_deinit(void)
  117. {
  118.     return es8388_write_reg(0x02, 0xFF);    /* 复位和暂停ES8388 */
  119. }

  120. /**
  121. * @brief             设置ES8388工作模式
  122. * @param              fmt : 工作模式
  123. *    @arg            0, 飞利浦标准I2S;
  124. *    @arg            1, MSB(左对齐);
  125. *    @arg             2, LSB(右对齐);
  126. *    @arg            3, PCM/DSP
  127. * @param             len : 数据长度
  128. *    @arg            0, 24bit
  129. *    @arg            1, 20bit
  130. *    @arg            2, 18bit
  131. *    @arg            3, 16bit
  132. *    @arg             4, 32bit
  133. * @retval            无
  134. */
  135. void es8388_i2s_cfg(uint8_t fmt, uint8_t len)
  136. {
  137.     fmt &= 0x03;
  138.     len &= 0x07;    /* 限定范围 */
  139.     es8388_write_reg(23, (fmt << 1) | (len << 3));  /* R23,ES8388工作模式设置 */
  140. }

  141. /**
  142. * @brief             设置耳机音量
  143. * @param            volume : 音量大小(0 ~ 33)
  144. * @retval            无
  145. */
  146. void es8388_hpvol_set(uint8_t volume)
  147. {
  148.     if (volume > 33)
  149.     {
  150.         volume = 33;
  151.     }

  152.     es8388_write_reg(0x2E, volume);
  153.     es8388_write_reg(0x2F, volume);
  154. }

  155. /**
  156. * @brief              设置喇叭音量
  157. * @param           volume : 音量大小(0 ~ 33)
  158. * @retval            无
  159. */
  160. void es8388_spkvol_set(uint8_t volume)
  161. {
  162.     if (volume > 33)
  163.     {
  164.         volume = 33;
  165.     }

  166.     es8388_write_reg(0x30, volume);
  167.     es8388_write_reg(0x31, volume);
  168. }

  169. /**
  170. * @brief              设置3D环绕声
  171. * @param             depth : 0 ~ 7(3D强度,0关闭,7最强)
  172. * @retval            无
  173. */
  174. void es8388_3d_set(uint8_t depth)
  175. {
  176.     depth &= 0x7;                                                       /* 限定范围 */
  177.     es8388_write_reg(0x1D, depth << 2);            /* R7,3D环绕设置 */
  178. }

  179. /**
  180. * @brief             ES8388 DAC/ADC配置
  181. * @param             dacen : dac使能(1) / 关闭(0)
  182. * @param             adcen : adc使能(1) / 关闭(0)
  183. * @retval            无
  184. */
  185. void es8388_adda_cfg(uint8_t dacen, uint8_t adcen)
  186. {
  187.     uint8_t tempreg = 0;

  188.     tempreg |= !dacen << 0;
  189.     tempreg |= !adcen << 1;
  190.     tempreg |= !dacen << 2;
  191.     tempreg |= !adcen << 3;
  192.     es8388_write_reg(0x02, tempreg);
  193. }

  194. /**
  195. * @brief              ES8388 DAC输出通道配置
  196. * @param             o1en : 通道1使能(1)/禁止(0)
  197. * @param              o2en : 通道2使能(1)/禁止(0)
  198. * @retval            无
  199. */
  200. void es8388_output_cfg(uint8_t o1en, uint8_t o2en)
  201. {
  202.     uint8_t tempreg = 0;
  203.     tempreg |= o1en * (3 << 4);
  204.     tempreg |= o2en * (3 << 2);
  205.     es8388_write_reg(0x04, tempreg);
  206. }

  207. /**
  208. * @brief             ES8388 MIC增益设置(MIC PGA增益)
  209. * @param             gain : 0~8, 对应0~24dB  3dB/Step
  210. * @retval            无
  211. */
  212. void es8388_mic_gain(uint8_t gain)
  213. {
  214.     gain &= 0x0F;
  215.     gain |= gain << 4;
  216.     es8388_write_reg(0x09, gain);       /* R9,左右通道PGA增益设置 */
  217. }

  218. /**
  219. * @brief             ES8388 ALC设置
  220. * @param             sel
  221. *   @arg             0,关闭ALC
  222. *   @arg             1,右通道ALC
  223. *   @arg              2,左通道ALC
  224. *   @arg              3,立体声ALC
  225. * @param              maxgain : 0~7,对应-6.5~+35.5dB
  226. * @param              minigain: 0~7,对应-12~+30dB 6dB/STEP
  227. * @retval            无
  228. */
  229. void es8388_alc_ctrl(uint8_t sel, uint8_t maxgain, uint8_t mingain)
  230. {
  231.     uint8_t tempreg = 0;
  232.     tempreg = sel << 6;
  233.     tempreg |= (maxgain & 0x07) << 3;
  234.     tempreg |= mingain & 0x07;
  235.     es8388_write_reg(0x12, tempreg);     /* R18,ALC设置 */
  236. }

  237. /**
  238. * @brief              ES8388 ADC输出通道配置
  239. * @param              in : 输入通道
  240. *    @arg            0, 通道1输入
  241. *    @arg           1, 通道2输入
  242. * @retval            无
  243. */
  244. void es8388_input_cfg(uint8_t in)
  245. {
  246.     es8388_write_reg(0x0A, (5 * in) << 4);   /* ADC1 输入通道选择L/R INPUT1 */
  247. }
复制代码
以上代码中,es8388_init函数用于初始化es8388,这里只是通用配置(ADC&DAC),初始化完成后,并不能正常播放音乐,还需要通过es8388_adda_cfg函数使能DAC,然后通过设置es8388_output_cfg选择DAC输出,通过es8388_i2s_cfg配置I2S工作模式,最后设置音量才可以接收I2S音频数据,实现音乐播放。

3,wavplay代码
该文件用于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*)malloc(sizeof(FIL));
  22.     buf = malloc(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;
  32.             
  33.             if (riff->Format == 0x45564157)                  /* 是WAV文件 */
  34.             {
  35.                 fmt = (ChunkFMT *)(buf + 12);
  36.                 fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);
  37.                
  38.                 if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
  39.                 {
  40.                     wavx->datastart=12+8 + fmt->ChunkSize + 8 + fact->ChunkSize;
  41.                 }
  42.                 else
  43.                 {
  44.                     wavx->datastart = 12 + 8 + fmt->ChunkSize;
  45.                 }
  46.                
  47.                 data = (ChunkDATA *)(buf + wavx->datastart);
  48.                
  49.                 if (data->ChunkID == 0x61746164)                /* 解析成功 */
  50.                 {
  51.                     wavx->audioformat = fmt->AudioFormat;       /* 音频格式 */
  52.                     wavx->nchannels = fmt->NumOfChannels;       /* 通道数 */
  53.                     wavx->samplerate = fmt->SampleRate;         /* 采样率 */
  54.                     wavx->bitrate = fmt->ByteRate * 8;
  55.                     wavx->blockalign = fmt->BlockAlign;
  56.                     wavx->bps = fmt->BitsPerSample;
  57.                     
  58.                     wavx->datasize = data->ChunkSize;
  59.                     wavx->datastart = wavx->datastart + 8;
  60.                      
  61.                     printf("wavx->audioformat:%d\r\n", wavx->audioformat);
  62.                     printf("wavx->nchannels:%d\r\n", wavx->nchannels);
  63.                     printf("wavx->samplerate:%ld\r\n", wavx->samplerate);
  64.                     printf("wavx->bitrate:%ld\r\n", wavx->bitrate);
  65.                     printf("wavx->blockalign:%d\r\n", wavx->blockalign);
  66.                     printf("wavx->bps:%d\r\n", wavx->bps);
  67.                     printf("wavx->datasize:%ld\r\n", wavx->datasize);
  68.                     printf("wavx->datastart:%ld\r\n", wavx->datastart);  
  69.                 }
  70.                 else
  71.                 {
  72.                     res = 3;
  73.                 }
  74.             }
  75.             else
  76.             {
  77.                 res = 2;
  78.             }
  79.         }
  80.         else
  81.         {
  82.             res = 1;
  83.         }
  84.     }
  85.    
  86.     f_close(ftemp);
  87.     free(ftemp);
  88.     free(buf);
  89.    
  90.     return 0;
  91. }

  92. /**
  93. * @brief             获取当前播放时间
  94. * @param             fx    : 文件指针
  95. * @param             wavx  : wavx播放控制器
  96. * @retval            无
  97. */
  98. void wav_get_curtime(FIL *fx, __wavctrl *wavx)
  99. {
  100.     long long fpos = 0;

  101.     wavx->totsec = wavx->datasize / (wavx->bitrate / 8);        /* 歌曲总长度(单位:秒) */
  102.     fpos = fx->fptr-wavx->datastart;                    /* 得到当前文件播放到的地方 */
  103.     wavx->cursec = fpos * wavx->totsec / wavx->datasize;/* 当前播放到第多少秒了? */
  104. }

  105. /**
  106. * @brief            music任务
  107. * @param             pvParameters : 传入参数(未用到)
  108. * @retval            无
  109. */
  110. void music(void *pvParameters)
  111. {
  112.     pvParameters = pvParameters;

  113.     /* ES8388初始化配置,有效降低启动时发出沙沙声 */
  114.     es8388_adda_cfg(1,0);                           /* 打开DAC,关闭ADC */
  115.     es8388_input_cfg(0);                            /* 录音关闭 */
  116.     es8388_output_cfg(1,1);                         /* 喇叭通道和耳机通道打开 */
  117.     es8388_hpvol_set(20);                           /* 设置喇叭 */
  118.     es8388_spkvol_set(20);                          /* 设置耳机 */
  119.     xl9555_pin_write(SPK_EN_IO,0);                  /* 打开喇叭 */
  120.     vTaskDelay(pdMS_TO_TICKS(20));
  121.     i2s_tx_write(g_audiodev.tbuf, WAV_TX_BUFSIZE);  /* 先发送一段无声音的数据 */

  122.     while(1)
  123.     {
  124.         if ((g_audiodev.status & 0x0F) == 0x03)     /* 打开了音频 */
  125.         {
  126.             for(uint16_t readTimes = 0; readTimes < (wavctrl.datasize
  127. / WAV_TX_BUFSIZE); readTimes++)
  128.             {
  129.                 if ((g_audiodev.status & 0x0F) == 0x00)            /* 暂停播放 */
  130.                 {
  131.                     file_read_pos = f_tell(g_audiodev.file);         /* 记录暂停位置 */

  132.                     while(1)
  133.                     {
  134.                         if ((g_audiodev.status & 0x0F) == 0x03)        /* 重新打开了 */
  135.                         {
  136.                             break;
  137.                         }

  138.                         vTaskDelay(pdMS_TO_TICKS(5));                         /* 死等 */
  139.                     }

  140.                     f_lseek(g_audiodev.file,file_read_pos);
  141.                 }

  142.                 /* 判断是否播放完成 */
  143.                 if (i2s_table_size >= wavctrl.datasize
  144. || i2s_play_next_prev== ESP_OK)
  145.                 {
  146.                     audio_stop();                 /* 先停止播放 */
  147.                     i2s_deinit();                 /* 卸载I2S */
  148.                     i2s_table_size = 0;           /* 总大小清零 */
  149.                     i2s_play_end = ESP_OK;        /* 已播放完成标志位 */
  150.                     vTaskDelete(NULL);            /* 删除当前任务 */
  151.                     vTaskDelay(pdMS_TO_TICKS(5)); /* 适当延时(为了删除这个任务) */
  152.                     break;                        /* 防止延时5ms未能删除音频任务 */
  153.                 }

  154.                 f_read(g_audiodev.file,g_audiodev.tbuf, WAV_TX_BUFSIZE,
  155. (UINT*)&bytes_write);
  156.                 i2s_table_size = i2s_table_size + i2s_tx_write(g_audiodev.tbuf,
  157. WAV_TX_BUFSIZE);
  158.             }
  159.         }

  160.         vTaskDelay(pdMS_TO_TICKS(1));
  161.     }

  162.     vTaskDelete(NULL);
  163. }

  164. /**
  165. * @brief             播放某个wav文件
  166. * @param             fname : 文件路径+文件名
  167. * @retval            KEY0_PRES : 下一首
  168. *                    KEY1_PRES : 上一首
  169. *                   KEY2_PRES : 停止/启动
  170. *                    其他,非WAV文件
  171. */
  172. uint8_t wav_play_song(uint8_t *fname)
  173. {
  174.     uint8_t key = 0;
  175.     uint8_t res = 0;
  176.    
  177.     i2s_play_end = ESP_FAIL;
  178.     i2s_play_next_prev = ESP_FAIL;
  179.     g_audiodev.file = (FIL*)heap_caps_malloc(sizeof(FIL),MALLOC_CAP_DMA);
  180.     g_audiodev.tbuf = heap_caps_malloc(WAV_TX_BUFSIZE, MALLOC_CAP_DMA);      

  181.     myi2s_init();                                   /* I2S初始化 */
  182.     vTaskDelay(pdMS_TO_TICKS(50));                  /* 适当延时 */

  183.     if (g_audiodev.file || g_audiodev.tbuf)
  184.     {
  185.         memset(g_audiodev.file,0,sizeof(FIL));      /* 文件指针清零 */
  186.         memset(g_audiodev.tbuf,0,WAV_TX_BUFSIZE);   /* buf清零 */
  187.         memset(&wavctrl,0,sizeof(__wavctrl));       /* 对WAV结构体相关参数清零 */
  188.         res = wav_decode_init(fname, &wavctrl);     /* 对wav音频文件解码 */

  189.         if (res == 0)                               /* 解码成功 */
  190.         {
  191.             if (wavctrl.bps == 16)                /* 根据解码文件重新配置采样率和位宽 */
  192.             {
  193.                 i2s_set_samplerate_bits_sample(wavctrl.samplerate,
  194. I2S_BITS_PER_SAMPLE_16BIT);
  195.             }
  196.             else if (wavctrl.bps == 24)
  197.             {
  198.                 i2s_set_samplerate_bits_sample(wavctrl.samplerate,
  199. I2S_BITS_PER_SAMPLE_24BIT);
  200.             }

  201.             res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);      

  202.             if (res == FR_OK)
  203.             {
  204.                 audio_start();  /* 开启I2S */
  205.                 /* 打开成功后,才创建音频任务 */
  206.                 if (MUSICTask_Handler == NULL && res == FR_OK)
  207.                 {
  208.                     taskENTER_CRITICAL(&my_spinlock);
  209.                     xTaskCreate(music, "music",4096,&MUSICTask_Handler,5, NULL);
  210.                     taskEXIT_CRITICAL(&my_spinlock);
  211.                 }

  212.                 while (res == FR_OK)
  213.                 {
  214.                     while (1)
  215.                     {
  216.                         /* 播放结束,下一首 */
  217.                         if (i2s_play_end == ESP_OK)
  218.                         {
  219.                             res = KEY0_PRES;
  220.                             break;
  221.                         }

  222.                         key = xl9555_key_scan(0);

  223.                         switch (key)
  224.                         {
  225.                             /* 下一首/上一首 */
  226.                             case KEY0_PRES:
  227.                             case KEY1_PRES:
  228.                                 i2s_play_next_prev = ESP_OK;
  229.                                 break;
  230.                             /* 暂停/开启 */
  231.                             case KEY2_PRES:
  232.                                 if ((g_audiodev.status & 0x0F) == 0x03)
  233.                                 {
  234.                                     audio_stop();
  235.                                 }
  236.                                 else if ((g_audiodev.status & 0x0F) == 0x00)
  237.                                 {
  238.                                     audio_start();
  239.                                 }
  240.                                 break;
  241.                         }

  242.                         if ((g_audiodev.status & 0x0F) == 0x03)                 
  243.                         {
  244.                             wav_get_curtime(g_audiodev.file, &wavctrl);         
  245.                             audio_msg_show(wavctrl.totsec, wavctrl.cursec,
  246. wavctrl.bitrate);
  247.                         }

  248.                         vTaskDelay(pdMS_TO_TICKS(10));
  249.                     }

  250.                     if (key == KEY1_PRES || key == KEY0_PRES)                  
  251.                     {
  252.                         res = key;
  253.                         break;
  254.                     }
  255.                 }
  256.             }
  257.             else
  258.             {
  259.                 res = 0xFF;
  260.             }
  261.         }
  262.         else
  263.         {
  264.             res = 0xFF;
  265.         }
  266.     }
  267.     else
  268.     {
  269.         res = 0xFF;
  270.     }

  271.     heap_caps_free(g_audiodev.file);
  272.     heap_caps_free(g_audiodev.tbuf);
  273.     g_audiodev.tbuf = NULL;
  274.     g_audiodev.file = NULL;
  275.     MUSICTask_Handler = NULL;
  276.     return res;
  277. }
复制代码
这段代码实现了基于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 卡中的文件进行扫描,检查是否包含音频文件,并识别这些文件是否为有效的音频文件,如下代码。
  1. /**
  2. * @brief             播放音乐
  3. * @param             无
  4. * @retval            无
  5. */
  6. void audio_play(void)
  7. {
  8.     uint8_t res;
  9.     FF_DIR wavdir;
  10.     FILINFO *wavfileinfo;
  11.     uint8_t *pname;
  12.     uint16_t totwavnum;
  13.     uint16_t curindex;
  14.     uint8_t key;
  15.     uint32_t temp;
  16.     uint32_t *wavoffsettbl;

  17.     while (f_opendir(&wavdir, "0:/MUSIC"))
  18.     {
  19.         text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
  20.         vTaskDelay(200);
  21.         lcd_fill(30, 190, 240, 206, WHITE);
  22.         vTaskDelay(200);
  23.     }

  24.     totwavnum = audio_get_tnum((uint8_t *)"0:/MUSIC");         /* 得到总有效文件数 */
  25.    
  26.     while (totwavnum == 0)
  27.     {
  28.         text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
  29.         vTaskDelay(200);
  30.         lcd_fill(30, 190, 240, 146, WHITE);
  31.         vTaskDelay(200);
  32.     }
  33.    
  34.     wavfileinfo = (FILINFO*)malloc(sizeof(FILINFO));
  35.     pname = malloc(255 * 2 + 1);
  36.     wavoffsettbl = malloc(4 * totwavnum);
  37.    
  38.     while (!wavfileinfo || !pname || !wavoffsettbl)
  39.     {
  40.         text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
  41.         vTaskDelay(200);
  42.         lcd_fill(30, 190, 240, 146, WHITE);
  43.         vTaskDelay(200);
  44.     }
  45.    
  46.     res = f_opendir(&wavdir, "0:/MUSIC");
  47.    
  48.     if (res == FR_OK)
  49.     {
  50.         curindex = 0;                                            /* 当前索引为0 */
  51.         
  52.         while (1)
  53.         {
  54.             temp = wavdir.dptr;                                       /* 记录当前index */

  55.             res = f_readdir(&wavdir, wavfileinfo);         /* 读取目录下的一个文件 */
  56.             
  57.             if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
  58.             {
  59.                 break;                                            /* 错误了/到末尾了,退出 */
  60.             }

  61.             res = exfuns_file_type(wavfileinfo->fname);
  62.             
  63.             if ((res & 0xF0) == 0x40)
  64.             {
  65.                 wavoffsettbl[curindex] = temp;                 /* 记录索引 */
  66.                 curindex++;
  67.             }
  68.         }
  69.     }
  70.    
  71.     curindex = 0;                                               /* 从0开始显示 */
  72.     res = f_opendir(&wavdir, (const TCHAR*)"0:/MUSIC");

  73.     while (res == FR_OK)                                         /* 打开目录 */
  74.     {
  75.         atk_dir_sdi(&wavdir, wavoffsettbl[curindex]);           /* 改变当前目录索引 */

  76.         res = f_readdir(&wavdir, wavfileinfo);                  /* 读取文件 */
  77.         
  78.         if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
  79.         {
  80.             break;
  81.         }
  82.         
  83.         strcpy((char *)pname, "0:/MUSIC/");
  84.         strcat((char *)pname, (const char *)wavfileinfo->fname);
  85.         lcd_fill(30, 190, mipidev.pwidth - 1, 190 + 16, WHITE);
  86.         audio_index_show(curindex + 1, totwavnum);
  87.         text_show_string(30, 190, mipidev.pwidth - 60, 16,
  88. (char *)wavfileinfo->fname, 16, 0, BLUE);
  89.         key = audio_play_song(pname);

  90.         if (key == KEY1_PRES)       /* 上一首 */
  91.         {
  92.             if (curindex)
  93.             {
  94.                 curindex--;
  95.             }
  96.             else
  97.             {
  98.                 curindex = totwavnum - 1;
  99.             }
  100.         }
  101.         else if (key == KEY0_PRES)  /* 下一首 */
  102.         {
  103.             curindex++;

  104.             if (curindex >= totwavnum)
  105.             {
  106.                 curindex = 0;
  107.             }
  108.         }
  109.         else
  110.         {
  111.             break;
  112.         }
  113.     }

  114.     free(wavfileinfo);
  115.     free(pname);
  116.     free(wavoffsettbl);
  117. }

  118. /**
  119. * @brief             播放某个音频文件
  120. * @param            fname : 文件名
  121. * @retval            按键值
  122. *   @arg             KEY0_PRES , 下一曲.
  123. *   @arg             KEY1_PRES , 上一曲.
  124. *   @arg             其他 , 错误
  125. */
  126. uint8_t audio_play_song(uint8_t *fname)
  127. {
  128.     uint8_t res;  
  129.    
  130.     res = exfuns_file_type((char *)fname);

  131.     switch (res)
  132.     {
  133.         case T_WAV:
  134.             res = wav_play_song(fname);
  135.             break;
  136.         case T_MP3:
  137.             /* 自行实现 */
  138.             break;

  139.         default:            /* 其他文件,自动跳转到下一曲 */
  140.             printf("can't play:%s\r\n", fname);
  141.             res = KEY0_PRES;
  142.             break;
  143.     }
  144.     return res;
  145. }
复制代码
这里,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文件,修改如下:
  1. set(src_dirs
  2.                     LED
  3.                     KEY
  4.                     MYIIC
  5.                     XL9555
  6.                     LCD
  7.                     ES8388
  8.                     MYI2S
  9.                     SDMMC)

  10. set(include_dirs
  11.                     LED
  12.                     KEY
  13.                     MYIIC
  14.                     XL9555
  15.                     LCD
  16.                     ES8388
  17.                     MYI2S
  18.                     SDMMC)

  19. set(requires
  20.                     driver
  21.                     esp_lcd
  22.                     esp_common
  23.                     fatfs)

  24. idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs}
  25.                        REQUIRES ${requires})

  26. component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
复制代码

6,main.c驱动代码
在main.c里面编写如下代码。
  1. void app_main(void)
  2. {
  3.     esp_err_t ret;
  4.     uint8_t key = 0;

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

  6.     if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
  7.     {
  8.         ESP_ERROR_CHECK(nvs_flash_erase());
  9.         ESP_ERROR_CHECK(nvs_flash_init());
  10.     }

  11.     key_init();                                                             /* 初始化KEY */
  12.     myiic_init();                                                   /* IIC0初始化 */
  13.     xl9555_init();                                                  /* XL9555初始化 */
  14.     lcd_init();                                                     /* LCD屏初始化 */

  15.     es8388_init();                              /* ES8388初始化 */
  16.     xl9555_pin_write(SPK_EN_IO, 1);             /* 打开喇叭 */

  17.     while (sdmmc_init())                                    /* 检测不到SD卡 */
  18.     {
  19.         lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
  20.         vTaskDelay(pdMS_TO_TICKS(200));
  21.         lcd_fill(30, 110, 239, 126, WHITE);
  22.         vTaskDelay(pdMS_TO_TICKS(200));
  23.     }

  24.     ret = exfuns_init();                                    /* 为fatfs相关变量申请内存 */

  25.     while (fonts_init())                                    /* 检查字库 */
  26.     {
  27.         lcd_clear(WHITE);
  28.         lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", RED);
  29.         
  30.         key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);  /* 更新字库 */
  31.         
  32.         while (key)                                         /* 更新失败 */
  33.         {
  34.             lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
  35.             vTaskDelay(pdMS_TO_TICKS(200));
  36.             lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
  37.             vTaskDelay(pdMS_TO_TICKS(200));
  38.         }

  39.         lcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
  40.         vTaskDelay(pdMS_TO_TICKS(1000));
  41.         lcd_clear(WHITE);   
  42.     }

  43.     text_show_string(30, 50,  200, 16, "正点原子ESP32-P4开发板",16, 0, RED);
  44.     text_show_string(30, 70,  200, 16, "音乐播放器 实验", 16, 0, RED);
  45.     text_show_string(30, 90,  200, 16, "正点原子@ALIENTEK", 16, 0, RED);
  46.     text_show_string(30, 110, 200, 16, "KEY0:NEXT  KEY1:PREV", 16, 0, RED);
  47.     text_show_string(30, 130, 200, 16, "KEY2:PAUSE/PLAY", 16, 0, RED);

  48.     vTaskDelay(pdMS_TO_TICKS(1000));

  49.     while (1)
  50.     {
  51.         audio_play();       /* 播放音乐 */
  52.     }
  53. }
复制代码
该函数就相对简单了,在初始化各个外设后,通过 audio_play 函数,开始音频播放,到这里本实验的代码基本就编写完成了。

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

第四十三章 音乐播放器实验52814.png
图43.1.1 音乐播放中

从上图可以看出,总共10首歌曲,当前正在播放第2首歌曲,歌曲名、播放时间、总时长、码率等信息等也都有显示。LED0闪烁表现程序正在运行。
此时我们便可以听到开发板板载喇叭播放出来的音乐了,也可以在开发板的PHONE端子插入耳机来听歌。同时,我们可以通过按KEY0和KEY1来切换下一曲和上一曲,通过KEY2暂停和继续播放。
至此,我们就完成了一个简单的音乐播放器了,虽然只支持WAV文件,但是大家可以在此基础上,增加其他音频格式解码器,便可实现其他音频格式解码了。
回复

使用道具 举报

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

本版积分规则


关闭

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

正点原子公众号

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

GMT+8, 2026-2-7 11:53

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

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