本帖最后由 正点原子运营 于 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
正点原子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的主要特性
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数据传输协议如下图所示:
图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)支持多种音频标准:
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模块的结构如下图所示。
图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,也就是上图讲解的三个时钟源其中一个,通过以下公式计算:
其中,𝑁是一个整数分频值,范围为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频率的计算公式如下:
其中,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 文件结构
图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.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文件,但是大家可以在此基础上,增加其他音频格式解码器,便可实现其他音频格式解码了。 |