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

41.1 WAV&ES8388&SAI 简介

        本章涉及知识比较多,包括: WAV、 ES8388 和 SAI 等三个知识点。下面分别介绍学习。

41.1.1 WAV 简介

        WAV是一种常见的数字音频文件格式,由微软和IBM联合开发,是Windows环境下标准的音频文件格式之一,文件扩展名为“.wav”。WAV格式的核心特点主要体现在以下几个方面:

特性

描述

​高保真音质​

由于通常采用​​无损的PCM(脉冲编码调制)编码​,WAV文件能够真实记录自然声波形,基本无数据压缩,还原的波形曲线十分逼真,音质与CD相差无几。

​灵活的参数支持​

支持多种​​采样频率​​(如11.025kHz、22.05kHz、44.1kHz、48kHz)、​​量化位数​​(8位、16位、24位)和​​声道数​​(单声道、立体声)。

​广泛的兼容性​

被Windows平台及其应用程序广泛支持,并在多种操作系统中得到良好支持。

表41.1.1.1 WAV格式的核心特点

        WAV文件遵循RIFF(资源互换文件格式)结构,这使得它层次清晰,易于解析。WAV 一般采用线性 PCM(脉冲编码调制)编码,本章也主要讨论 PCM 的播放,因为这个最简单。
        WAV 文件是由若干个 Chunk 组成的。按照在文件中的出现位置包括: RIFF WAVE Chunk、
Format Chunk、 Fact Chunk(可选)和 Data Chunk。其中RIFF Chunk、FMT Chunk​​、Data Chunk块三种块是必不可少的文件中第一个 Chunk 是RIFF Chunk,然后是 FMT Chunk,最后是 Data Chunk。对于其他的 Chunk,顺序没有严格的限制。三个Chunk​​说明如下:

  • RIFF Chunk​​:文件头,标识这是一个RIFF格式文件,并包含文件大小信息,同时指明格式类型为“WAVE”;
  • ​​fmt Chunk​​:​​格式说明块​​,至关重要。它定义了音频数据的格式参数,包括编码格式(如PCM)、声道数、采样频率、量化位数等;
  • ​​data Chunk:​​实际音频数据块​​,存储着原始的PCM样本或经过特定算法压缩后的音频数据。对于多声道音频,样本数据通常是交替存储的

        每个 Chunk 由块标识符、数据大小和数据三部分组成,如下表所示:

偏移量

字段名

大小(字节)

描述

0

​​Chunk ID​​

4

用4个ASCII字符标识块的类型,如 RIFFfmtdata

4

​​Chunk Size​​

4

​一个32位的无符号整数,表示 数据字段(Data Field)的大小​​。

8

​​Chunk Data​​

Chunk Size

块的实际数据内容。

表41.1.1.2 Chunk 组成结构

        使用 WAVE 文件的应用程序必须具有读取以上三种 chunk 信息的能力,如果程序想要复制WAVE 文件,必须拷贝文件中所有的 chunk。本章,我们主要讨论 PCM,只包含 3 个 Chunk,看一下它的文件构成,如下图所示:

图 41.1.1.1 PCM 格式的 wav 文件构成

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

        接下来通过实际的WAV文件,分别分析下每个chunk:

(1)首部数据:

        对于整个WAV文件而言,其首部结构,也即整个RIFF数据块的头部结构如下。

名称 偏移地址 字节数 端序 内容
ID 0x00 4Byte 大端 'RIFF' (0x52494646)
Size 0x04 4Byte 小端 文件大小
Type 0x08 4Byte 大端 'WAVE'(0x57415645)

图 41.1.1.3 Chunk 组成结构

        实际看下本次实验 周笔畅-最美的期待.WAV 歌曲,先右键看下属性,如下图所示,可以看到大小为40,486,446 字节:

图 41.1.1.2 样例歌曲属性大小

        在UltraEdit软件中打开歌曲 周笔畅-最美的期待.WAV,样例音频文件,打开内容如下:

图 41.1.1.3 样例歌曲打开后信息

        黄框标出来三部分分别为:

  • ID:四个字节,52 49 46 46 对应的ASCII字符为RIFF;
  • Size:四个字节,代表文件大小(小端模式),即从低位向高位读入,该部分的实际值应为 26 C6 69  02 的反向顺序,即 02 69 C6 26 换成10进制为40486438字节,与实际文件大小40486446差了8个字节,原因为Chunk Size字段不包括 Chunk ID和 Chunk Size这前8个字节本身的大小,加上后相等;
  • Type:57 41 56 45对应的ASCII字符为WAVE。

(2) FMT Chunk

        WAV文件的其中一个重要的子块就是Format数据块,包含WAV音频文件的一些详细属性,具体结构如下。

名称 偏移地址 字节数 端序 内容 取值说明与示例
ID 0x00 4Byte 大端 'fmt' (0x666D7420)

固定值​​:字符串 "fmt "(十六进制 0x666D7420,​​末尾有

一个空格​​)。

标识接下来的子块是​​格式说明块​​,描述了音频数据的编

码格式。

Size 0x04 4Byte 小端 16

对于PCM音频,固定为 16​ (十六进制 0x10000000)

表示紧随其后的fmt子块的数据部分大小为16字节。

对于非PCM编码,此值可能更大。

AudioFormat 0x08 2Byte 小端 音频格式

编码格式代码​​。

1表示​​PCM​​(脉冲编码调制,即未压缩的原始音频)。

这是最常用的格式。其他值(如6)代表压缩格式。​

NumChannels 0x0A 2Byte 小端 声道数

声道数量​​。​

1​ 表示单声道(Mono),​2​ 表示立体声(Stereo)。

SampleRate 0x0C 4Byte 小端 采样率 采样率​​。一个32位无符号整数(小端序)。
表示每秒对声音波形采样的次数,单位为Hz。
​常见值​​:44100(CD音质), 48000(专业音频), 220508000(电话音质)。
ByteRate 0x10 4Byte 小端 每秒数据字节数 数据传输速率​​。一个32位无符号整数(小端序)。
表示每秒产生的音频数据字节数。
​计算公式​​:SampleRate * NumChannels * BitsPerSample / 8
BlockAlign 0x14 2Byte 小端 数据块对齐 数据块对齐​​。一个16位无符号整数。
表示一个采样点(包含所有声道)的数据所占的字节数。
​计算公式​​:NumChannels * BitsPerSample / 8
它是处理数据时的一个基本单位。
BitsPerSample 0x16 2Byte 小端 采样位数 位深度​​(或称采样位数)。一个16位无符号整数。
表示每个采样点用多少位二进制数来记录。
​常见值​​:16(CD音质), 8(低品质), 24(高解析度)。位深度越高,动态范围和保真度越好。

表 41.1.1.4 FMT Chunk结构字段说明

图 41.1.1.4 样例歌曲打开后FMT Chunk相关信息

        对照上图,相关数据值如下(注意小端计算):

名称 偏移地址 字节数 端序 内容 含义
ID 0x00 4Byte 大端 'fmt' (0x666D7420) 66 6D 74 20 fmt,注意t后面有个空格(20)
Size 0x04 4Byte 小端 16 00 00 00 12 值为18
AudioFormat 0x08 2Byte 小端 音频格式 00 01 值为1,1​​表示​​PCM压缩格式
NumChannels 0x0A 2Byte 小端 声道数 00 02 值为2,表示立体声(Stereo)
SampleRate 0x0C 4Byte 小端 采样率 00 00 BB 80 值为48,000,48000(专业音频)
ByteRate 0x10 4Byte 小端 每秒数据字节数 00 02 EE 00

值为192,000,与上面公式计算结果

相同

BlockAlign 0x14 2Byte 小端 数据块对齐 00 04 值为4,与上面公式计算结果相同
BitsPerSample 0x16 2Byte 小端 采样位数 00 10 值为16,16(CD音质)

表 41.1.1.5 FMT Chunk结构字段取值

        特别注意:Size值为18而不是16,查了相关资料​说是扩展数据字段​​:一些音频软件或硬件在生成WAV 文件时,在标准 16 字节的 fmt数据后附加一个 ​​2 字节的扩展字段​​(例如,用于存储“有效位深度”或保留字段),这使总大小变为 18 字节。该文件分析应该是保留字段,值为00 00。

(3)Data Chunk

        Data数据块是存放实际音频数据的数据块,具体结构如下。

名称 偏移地址 字节数 端序 内容
ID 0x00 4Byte 大端 'data' (0x64617461)
Size 0x04 4Byte 小端 N
Data 0x08 NByte 小端 音频数据

表 41.1.1.6 DATA Chunk结构字段

图 41.1.1.5 样例歌曲打开后Data Chunk相关信息

        主要看下Size大小,该部分的实际值应为 00 C6 69  02 的反向顺序,即 02 69 C6 00 换成10进制为40,486,400字节。后面即为实际的音频数据。

        根据 Format Chunk 中的声道数以及采样 bit 数, wav 数据的 bit 位置可以分成如表 41.1.1.1 所示的几种形式:

表 41.1.1.7 WAVE 文件数据采样格式

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

41.1.2 ES8388 简介

        ES8388 是一款高性能、低功耗的立体声音频编解码器(Codec),由 ​​Everest Semiconductor(苏州顺芯电子)​​ 推出。它集成了​​立体声模数转换器(ADC)​​ 和​​立体声数模转换器(DAC)​​,并内置麦克风放大器、耳机放大器以及数字音效处理等功能,非常适合便携式和嵌入式音频应用。核心参数如下表所示:

特性类别

具体参数

​​ADC(录音)​​

24位分辨率,采样率 8kHz 至 96kHz,动态范围 95dB,信噪比 95dB 

​​DAC(播放)​​

24位分辨率,采样率 8kHz 至 96kHz,动态范围 96dB,信噪比 96dB 

​​耳机放大器​​

输出功率 40mW,具备防爆音(Pop-Noise Free)功能 

​​电源电压​​

1.8V 至 3.3V 宽电压操作 

​​功耗​​

仅播放时约 7mW,播放兼录音时约 16mW 

​​控制接口​​

I²C 或 SPI 

​​音频数据接口​​

支持 I2S、左对齐、DSP/PCM 模式,可工作于主模式或从模式 

​​封装​​

QFN-28(4mm x 4mm) 

表41.1.2.1 ES8388核心参数

        核心功能与接口:
        ES8388 的功能可以理解为一条清晰的音频信号通路:

  • 录音通路(ADC)​​模拟音频信号(如来自麦克风)经过芯片内部的​​麦克风放大器​​和​​模拟混音器​​后,由 ​​ADC​​ 转换为高质量的数字信号。该通路支持​​自动电平控制(ALC)​​ 和​​噪声门​​功能,能有效提升录音质量;
  • 播放通路(DAC)​​:来自处理器(如 MCU、SoC)的数字音频信号,经由 ​​DAC​​ 转换为模拟信号,再通过​​耳机放大器​​直接驱动耳机或输出到线路。通路集成了​​立体声增强​​、​​低音和高音调节​​等数字音效;
  • 关键接口​​:芯片通过 ​​I²C 或 SPI​​ 接口接受外部控制,用于配置音量、音效、电源模式等所有参数。音频数据则通过 ​​I2S​​ 等串行接口与主处理器进行传输。

        ES8388 的控制通过 I2S 接口(即数字音频接口)同MCU 进行音频数据传输(支持音频接收和发送),通过两线(CE=0/1,即 IIC 接口)或三线(CE 脚产生一个下降沿,即 SPI 接口)接口进行配置。 ES8388 的 SAI 接口,由 4 个引脚组成:
        ASDOUT:ADC 数据输出;
        DSDIN:DAC 数据输入;
        LRC:数据左/右对齐时钟;
        SCLK:位时钟,用于同步。
        ES8388 可作为 SAI 主机,输出 LRC 和 SLCK 时钟,不过一般使用 ES8388 作为从机,接收 LRC 和 SLCK。另外, ES8388 的 SAI 接口支持 4 种不同的音频数据模式:左( MSB)对齐标准、右( LSB)对齐标准、飞利浦(SAI)标准、 DSP/PCM。本章,我们用飞利浦标准来传输 SAI 数据。
        飞利浦(SAI)标准模式,数据在跟随 LRC 传输的 BCLK 的第二个上升沿时传输 MSB,其他位一直到 LSB 按顺序传输。传输依赖于字长、 BCLK 频率和采样率,在每个采样的 LSB 和下一个采样的 MSB 之间都应该有未用的 BCLK 周期。飞利浦标准模式的 SAI 数据传输协议如图41.1.2.1 所示:

图 41.1.2.1 飞利浦标准模式 SAI 数据传输图

       图中, fs即音频信号的采样率,比如 44.1Khz,因此可以知道,LRC的频率就是音频信号的采样率。另外, ES8388 还需要一个 MCLK,本章我们采用 DNESP32S3 为其提供 MCLK 时钟,MCLK 的频率必须等于 256fs,也就是音频采样率的 256 倍。注意:256fs不是一个可以随意更改参数,而是由 ES8388 芯片硬件架构决定的​​固定比例​​。在驱动代码中必须严格遵守这个倍数关系来配置 MCLK,具体原因可以搜索,不再说明。
        这里需要注意:​​SAI 是物理接口的统称,指的是 ES8388 芯片上与 MCU 连接的那​​一组物理引脚​​,用于传输数字音频数据。而 I2S 是运行在 SAI 接口上的一种具体协议。​
        ES8388 的框图如图 41.1.2.2 所示:

图 41.1.2.2 ES8388 框图

        从上图可以看出, ES8388 内部有很多的模拟开关,用来选择通道,同时还有一些运放调节器,用来设置增益和音量。
        本章,通过IIC接口(CE=0)连接ES8388, ES8388的IIC地址为:0X10。关于ES8388的 IIC 详细介绍,请看其数据手册第10页5.2 节。
        这里简单介绍要正常使用 ES8388 来播放音乐,应该配置哪些寄存器,具体如下表:

寄存器地址

寄存器名称

位域

推荐设置

功能说明

00h

芯片控制寄存器1

SCPReset(bit7)

0x80复位, 0x00正常

软复位控制:写0x80复位ES8388,然后写0x00恢复正常操作。

VMIDSEL

10

控制VMID(校正噪声),设置为10(500kΩ)。

01h

芯片控制寄存器2

PdnAna(bit3)

0

模拟部分电源控制:0表示模拟部分工作(可听到声音),1表示掉电。

02h

电源管理控制寄存器

所有位

0x00

控制ADC和DAC的数字部分、状态机、DLL和参考电压:全部设为0使能所有功能。

03h

ADC电源管理控制

所有位

0x00

控制ADC模拟部分电源:全部设为0使能左右输入通道、ADC、麦克风偏置和偏置生成。

04h

DAC电源管理控制

PdnDACL(bit7), PdnDACR(bit6)

0

DAC左右声道电源控制:0使能。

LOUT1(bit5), ROUT1(bit4)

根据需要

通道1输出使能:1使能,0禁止。

LOUT2(bit3), ROUT2(bit2)

根据需要

通道2输出使能:1使能,0禁止。

08h

主模式控制寄存器

MSC(bit7)

0

接口模式:0表示从模式,1表示主模式。设置为0(从模式)。

MCKDIV2(bit6)

0

MCLK分频:0不分频,1二分频。设置为0。

BCLK_INV(bit5)

0

BCLK反相:0不反相,1反相。设置为0。

09h

ADC控制寄存器1

MicAmpL(bit7:4), MicAmpR(bit3:0)

1000 (24dB)

麦克风增益控制:设置左右通道增益为24dB。

0Ah

ADC控制寄存器2

LINSE(bit7:6), RINSE(bit5:4)

根据需要

输入通道选择:0选择通道1,1选择通道2。

0Ch

ADC控制寄存器4

DATSEL(bit7:6)

01

数据格式选择:01表示左右边数据等于左右声道ADC数据。

ADCLRP(bit5)

0

数据对齐方式:0正常极性(I2S模式)。

ADCWL(bit4:2)

011

数据长度:011表示16位。

ADCFORMAT(bit1:0)

00

ADC数据格式:00表示I2S格式。

0Dh

ADC控制寄存器5

ADCFsMode(bit7)

0

Fs模式:0单速模式,1双倍速模式。设置为0。

ADCFsRatio(bit4:0)

00010

MCLK与FS比率:00010表示256倍关系。

10h

ADC左声道音量

LADCVOL(bit7:0)

0

左声道ADC音量衰减:0表示0dB衰减(不衰减)。

11h

ADC右声道音量

RADCVOL(bit7:0)

0

右声道ADC音量衰减:0表示0dB衰减(不衰减)。

12h

ADC控制寄存器10

ALCSEL(bit7:6)

11

ALC(自动电平控制):11表示立体声控制。

17h

DAC控制寄存器1

DACLRSWAP(bit7)

0

左右声道数据交换:0正常,1互换。设置为0。

DACLRP(bit6)

0

数据对齐方式:0正常极性(I2S模式)。

DACWL(bit5:3)

011

数据长度:011表示16位。

DACFORMAT(bit1:0)

00

DAC数据格式:00表示I2S格式。

18h

DAC控制寄存器2

DACFsMode(bit7)

0

Fs模式:0单速模式,1双倍速模式。设置为0。

DACFsRatio(bit4:0)

00010

MCLK与FS比率:00010表示256倍关系。

1Ah

DAC左声道音量

LDACVOL(bit7:0)

0-192

左声道DAC音量衰减:0表示0dB衰减,192表示96dB衰减(可调节音量)。

1Bh

DAC右声道音量

RDACVOL(bit7:0)

0-192

右声道DAC音量衰减:0表示0dB衰减,192表示96dB衰减(可调节音量)。

1Dh

DAC控制寄存器7

ZeroL(bit7), ZeroR(bit6)

0

静音控制:0正常,1输出0(静音)。设置为0。

Mono(bit5)

0

单声道控制:0立体声,1单声道。设置为0。

SE(bit4:2)

0

3D音效控制:0关闭3D效果。

27h

DAC左通道混音器

LD2LO(bit7)

1

左DAC混音器开关:1开启。

LI2LO(bit6)

1

左输入通道混音器开关:1开启。

LI2LOVOL(bit5:3)

111

左输入通道增益:111表示-15dB。

2Ah

DAC右通道混音器

RD2RO(bit7)

1

右DAC混音器开关:1开启。

RI2RO(bit6)

1

右输入通道混音器开关:1开启。

RI2ROVOL(bit5:3)

111

右输入通道增益:111表示-15dB。

2Bh

DAC控制寄存器21

slrck(bit7)

1

DACLRC和ADCLRC共用控制:1表示共用。

表 41.1.2.1 ES8388常用寄存器配置

        寄存器设置说明:
        寄存器配置顺序建议:先配置电源管理寄存器(如00h、01h、02h、03h、04h),然后设置时钟和数据格式(如08h、0Ch、0Dh、17h、18h),最后调整音量和效果(如09h、10h、11h、1Ah、1Bh、1Dh、27h、2Ah、2Bh);
        所有寄存器通过I2C或SPI接口写入,确保使用正确的设备地址(通常I2C地址为0x10)。
        以上列出了ES8388常用的寄存器,更详细的寄存器设置说明,参考 ES8388 的数据手册研究。

41.1.3 I²S 控制器介绍

        I²S(​​Inter-IC Sound​​)是一种专为​​数字音频设备之间传输高质量音频数据​​而设计的​​同步串行通信协议​​。它最初由飞利浦半导体(现恩智浦 NXP)提出,现已成为音频领域的工业标准。
        I²S 控制器的核心功能非常简单直接:​​在发送端(如应用处理器)和接收端(如音频编解码器 CODEC、DAC 芯片)之间,提供一条精准、高效的数字通道,以传输代表声音的脉冲编码调制(PCM)数据流。​​
        不负责​​模拟信号的转换(那是 CODEC 或 DAC 芯片的工作),也​​不负责​​复杂的音频处理(如均衡器、混响等)。它的任务就是 ​​“搬运”数字音频数据​​,并确保数据同步。
        一个标准的 I²S 总线由 ​​3 条基本信号线​​和 ​​1 条可选信号线​​组成:

信号线名称

方向

功能描述

​​SD (Serial Data)​​

单向

​串行数据线​​。传输实际的音频数据(PCM 样本),从发送器到接收器。

​​BCLK (Bit Clock)​​

由主设备产生

​位时钟​​。每个时钟周期同步传输 1 位数据。频率 = 采样率*位数*通道数

​​LRCK (Word Select)​​

由主设备产生

​字选择(或称帧同步)线​​。
• ​​低电平​​:表示正在传输​​左声道​​数据。
• ​​高电平​​:表示正在传输​​右声道​​数据。
其频率就是​​音频采样率​​(如 44.1 kHz)。

​​MCLK (Master Clock)​​

由主设备产生

​主时钟​​(可选)。为接收端(如 CODEC 芯片)提供系统参考时钟,通常是采样率的 ​​256 或 384 倍​​(如 44.1kHz × 256 = 11.2896 MHz)。

表 41.1.3.1 接口信号定义说明

        配置一个 I²S 控制器时,需要关注以下几个核心参数,它们共同决定了音频数据的传输格式:

参数

选项

说明

​​操作模式​​

​​主模式 (Master)​​

I²S 控制器​​产生​​ BCLK 和 LRCK。大多数 MCU 驱动音频编解码器时处于此模式。

​​从模式 (Slave)​​

I²S 控制器​​接收​​外部的 BCLK 和 LRCK。

​​数据格式​​

​​标准 I²S​​

最常用的格式。LRCK 变化后的​​第二个 BCLK 周期​​开始传输数据。

​​左对齐​​

LRCK 变化后​​第一个 BCLK 周期​​就开始传输数据。

​​右对齐​​

数据样本紧靠在下一个 LRCK 变化之前传输。

​​数据位宽​​

​​16位 / 24位 / 32位​​

每个音频样本的位数。必须与音频源和接收端设置一致。

​​采样率​​

​​8kHz, 16kHz, 44.1kHz, 48kHz, 96kHz...​​

每秒采集或播放的音频样本数。由 BCLK 和 LRCK 的频率体现。

表 41.1.3.2 关键特性与配置参数说明

        I²S 有如下功能:

  • 主机模式: I²Sn 作为主机, BCK/WS 向外部输出,向从机发送或从其接收数据;
  • 从机模式: I²Sn 作为从机, BCK/WS 从外部输入,从主机接收或向其发送数据;
  • 全双工: 主机与从机之间的发送线和接收线各自独立,发送数据和接收数据同时进行;
  • 半双工: 主机和从机只能有一方先发送数据,另一方接收数据。发送数据和接收数据不能同时进行;
  • TDM RX 模式: 利用时分复用方式接收脉冲编码调制(PCM)数据,并将其通过 DMA 存入储存器的模式。信号线包括 BCK、 WS 和 DATA。可以接收最多 16 个通道的数据。通过用户配置,可支持 TDM Philips 格式、 TDM MSB 对齐格式、 TDM PCM 格式等;
  • PDM RX 模式: 接收脉冲密度调制(PDM)数据,并将其通过 DMA 存入储存器的模式。信号线包括 WS 和 DATA。通过用户配置,可支持 PDM 标准格式等;
  • TDM TX 模式: 通过 DMA 从储存器中取得脉冲编码调制(PCM)数据,并利用时分复用方式将其发送的模式。信号线包括 BCK、 WS 和 DATA,可以发送最多 16 个通道的数据。通过用户配置,可支持 TDM Philips 格式、 TDM MSB 对齐格式、 TDM PCM 格式等;
  • PDM TX 模式: 通过 DMA 从储存器中取得脉冲密度调制(PDM)数据,并将其发送的模式。信号线包括 WS 和 DATA。通过用户配置,可支持 PDM 标准格式等;
  • PCMtoPDM TX 模式(仅对 I²S0 有效): 通过 DMA 从储存器中取得脉冲编码调制(PCM)数据,将其转换为脉冲密度调制(PDM)数据,并将其发送的主机模式。信号线包括 WS 和DATA。通过用户配置,可支持 PDM 标准格式等;
  • PDMtoPCM RX 模式(仅对 I²S0 有效): 接收脉冲密度调制(PDM)数据,将其转换为脉冲编码调制(PCM)数据,并将其通过 DMA 存入储存器的主机模式或从机模式。信号线包括 WS 和DATA。通过用户配置,可支持 PDM 标准格式等。

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

41.2 硬件设计
41.2.1 例程功能

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

41.2.2 硬件资源

        本实验,大家需要准备 1 个 SD 卡(在里面新建一个 MUSIC 文件夹,并存放一些歌曲在MUSIC 文件夹下)和一个耳机(非必备),分别插入 SD 卡接口和耳机接口,然后下载本实验通过耳机或板载喇叭来听歌。实验用到的硬件资源如下:
        1. LED 灯
                LED -IO0
        2.独立按键
                KEY0(XL9555) - IO1_7
                KEY1(XL9555) - IO1_6
                KEY2(XL9555) - IO1_5
                KEY3(XL9555) - IO1_4
        3. XL9555
                IIC_SDA-IO41
                IIC_SCL-IO42
        4. SPILCD
                CS-IO21
                SCK-IO12
                SDA-IO11
                DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
                PWR- IO1_3(XL9555)
                RST- IO1_2(XL9555)
        5. SD
                CS-IO2
                SCK-IO12
                MOSI-IO11
                MISO-IO13
        6. ES8388 音频 CODEC 芯片( IIC 端口 0)
                IIC_SDA-IO41
                IIC_SCL-IO42
                I2S_BCK_IO-IO46
                I2S_WS_IO-IO9
                I2S_DO_IO-IO10
                I2S_DI_IO-IO14
                IS2_MCLK_IO-IO3

41.2.3 原理图

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

图 41.2.3.1 ES838 原理图

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

41.3 程序设计
41.3.1 程序流程图

        本实验的程序流程图:

图 41.3.1.1 音频播放实验程序流程图

41.3.2 I2S 函数解析

        ESP-IDF 提供了一套 API 来配置 I2S。要使用此功能,需要导入必要的头文件:

#include "driver/i2s.h"
#include "driver/i2s_std.h"
#include "driver/i2s_pdm.h"

        接下来,介绍一些常用的 ESP32-S3 中的 I2S 函数,这些函数的描述及其作用如下:
(1)设置 I2S 引脚
        该函数用给定的配置,来配置 I2S 总线,该函数原型如下所示:

esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin);

        该函数的形参描述如下表所示:

参数 描述
i2c_num I2S端口号,有 I2S_NUM_0、 I2S_NUM_1两个端口可供配
i2c_conf 指向 I2S 配置的指针

表 41.3.2.1 i2s_set_pin()函数形参描述

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

返回值 描述
ESP_OK 返回: 0,配置成功
ESP_ERR_INVALID_ARG 参数错误
ESP_FAIL IO 配置错误

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

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

结构体 成员变量 可选参数
i2s_pin_config_t mck_io_num; MCK 引脚, 用作输出
bck_io_num; BCK 引脚, 从机输入, 主机输入
ws_io_num; WS 引脚, 从机输入, 主机输入
data_out_num; 数据引脚,用作输出
data_in_num; 数据引脚,用作输入

表 41.3.2.3 i2s_pin_config_t 结构体参数值描述

(2)安装 I2S 驱动
        该函数安装 I2S 驱动,该函数原型如下所示:

esp_err_t i2s_driver_install(i2s_port_t i2s_num,
                             const i2s_config_t *i2s_config,
                             int queue_size,
                             void *i2s_queue);

        该函数的形参描述如下表所示:

参数 描述
i2s_num I2S端口号,有 I2S_NUM_0、 I2S_NUM_1两个端口可供配
i2s_config I2S 配置
queue_size 事件队列大小/深度。
i2s_queue 事件队列句柄

表 41.3.2.4 i2s_driver_install()函数形参描述

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

返回值 描述
ESP_OK 成功
ESP_ERR_INVALID_ARG 参数错误
ESP_ERR_NO_MEM 内存不足
ESP_ERR_INVALID_STATE 当前 I2S 端口被占用

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

(3)处理缓冲区

        该函数将 TX DMA 缓冲区的内容归零,该函数原型如下所示:

esp_err_t i2s_zero_dma_buffer(i2s_port_t i2s_num);

        该函数的形参描述如下表所示:

参数 描述
i2s_num I2S端口号,有 I2S_NUM_0、 I2S_NUM_1两个端口可供配置

表41.3.2.6 i2s_zero_dma_buffer ()函数形参描述

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

返回值 描述
ESP_OK 成功
ESP_ERR_INVALID_ARG 参数错误

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

41.3.3 音频播放驱动解析

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

        音乐文件要通过SD卡来传给单片机,因此要用到文件系统。 LCD、按键交互这些也需要实现。
        由于播放功能涉及到多个外设的配合使用,用文件系统读音频文件,做播放控制等,所以把 ES8388 的硬件驱动放到 components\BSP 目录下,播放功能作为 APP 放到 main 目录下。
        这里分析核心代码, I²S 的驱动主要包括两个文件: i2s.c 和 i2s.h。
        除去 I²S 的管脚,需要初始其它 IO 的模式,在头文件 sai.h 中定义 SAI 的引脚,方便如果 IO 变更之后作修改:

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

        接下来分析 i2s.c,主要是 I²S 的初始化代码如下:

i2s_chan_handle_t tx_handle = NULL;     /* I2S发送通道句柄 */
i2s_chan_handle_t rx_handle = NULL;     /* I2S接收通道句柄 */
i2s_std_config_t my_std_cfg;            /* 标准模式配置结构体 */

/*
 * @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通道 */

    i2s_std_config_t std_cfg = {    /* 标准通信模式配置 */
        .clk_cfg  = {               /* 时钟配置 可用I2S_STD_CLK_DEFAULT_CONFIG(I2S_SAMPLE_RATE)宏函数辅助配置 */
            .sample_rate_hz = I2S_SAMPLE_RATE,              /* I2S采样率 */
            .clk_src        = I2S_CLK_SRC_DEFAULT,          /* I2S时钟源 */
            .mclk_multiple  = I2S_MCLK_MULTIPLE,            /* I2S主时钟MCLK相对于采样率的倍数(默认256) */
        },

        .slot_cfg = {               /* 声道配置,可用I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)宏函数辅助配置(支持16位宽采样数据) */
            .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,                         /* 位移位(Philips模式下配置) */
            .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;

    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));    /* 初始化TX通道 */
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));    /* 初始化RX通道 */
    ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));                     /* 启用TX通道 */
    ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));                     /* 启用RX通道 */

    return ESP_OK;
}

/**
 * @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));
}

/**
 * @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));
}

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

/**
 * @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));
}

/**
 * @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;
}

/**
 * @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_init()完成初始化 I²S,该初始化不需要像 I²C 以及 IO 扩展芯片那样设置传参,通过配置相关的结构体并安装 I²S 的驱动和配置 I²S 引脚以及将 TX DMA 缓冲区的内容归零。函数 sai1_samplerate_set 则是用前面介绍的查表法,根据采样率来设置 SAI 的时钟。函数i2s_trx_start()用于启动 I²S驱动,在调用了 i2s_driver_install()之后不需要调用这个函数(它是自动启动的),但是在调用了 i2s_stop()之后调用该函数是必要的。而函数 i2s_trx_stop()用于停止 I²S驱动,在调用 i2s_driver_uninstall()之前不需要调用 i2s_stop()。i2s_set_samplerate_bits_sample()用于设置 I2S RX 和 TX 的时钟和位宽度。函数 i2s_tx_write()用于将数据写入 I2S DMA 传输缓冲区。函数 i2s_rx_read()从 I2S DMA 接收缓冲区读取数据。以上是对 I²S 驱动文件下部分函数的功能概述,具体内容请参照该驱动文件。

(2)ES8388 驱动
        ES8388 主要用来将音频信号转换为数字信号或将数字信号转换为音频信号,接下来,开始介绍 ES8388 的几个函数,首先看下面几个函数实现:

  • es8388_adda_cfg
void es8388_adda_cfg(uint8_t dacen, uint8_t adcen)

函数通过操作ES8388的寄存器2(地址0x02) 来实现对DAC和ADC模块的开关控制。这个寄存器的相关控制位如下:

寄存器位 (Bit) 功能描述 控制逻辑 (代码中)
0 (PDNDAC) DAC模块断电控制 0=DAC上电工作, 1=DAC断电
1 (PDNADC) ADC模块断电控制 0=ADC上电工作, 1=ADC断电
2 (PDNDACBUF) DAC模拟输出缓冲器断电控制 0=缓冲器上电, 1=缓冲器断电
3 (PDNADCBUF) ADC模拟输入缓冲器断电控制 0=缓冲器上电, 1=缓冲器断电

:代码中的控制逻辑是“反相”的。当你想开启DAC(dacen=1)时,实际写入寄存器对应位的是0(上电);反之,关闭(dacen=0)时写入的是1(断电)。ADC部分同理。

  • es8388_input_cfg
void es8388_input_cfg(uint8_t in)

函数通过设置 寄存器0x0A(ADC控制寄存器) 的高4位,来选择ADC左通道(LINPUT1)的信号来源。其核心逻辑是参数 in 的值,决定了使用哪一组物理输入引脚

参数 in 计算 (5*in)<<4 寄存器 0x0A 值 功能描述 对应开发板物理接口
0 (0) << 4 = 0x00 高4位: 0000 左通道(L)选择 INPUT1 麦克风 (MIC1/MIC2)
1 (5) << 4 = 0x50 高4位: 0101 左通道(L)选择 INPUT2 线路输入 (LINEIN1/LINEIN2)

:此函数仅配置了左通道(L)的输入源。在初始化es8388_init()代码中,ADC被设置为“左声道数据来自左ADC,右声道数据也来自左ADC”(寄存器0x0C写入0x4C),即单声道(Mono)录音模式。因此,右通道(R)的输入选择在此配置下未被使用。

  • es8388_output_cfg
void es8388_output_cfg(uint8_t o1en, uint8_t o2en)

函数的作用是 开启或关闭ES8388芯片的模拟音频输出通道。这直接决定了经过DAC转换后的音频信号,可以从芯片的哪些物理引脚输出。

函数参数 (o1en, o2en) 写入寄存器 (0x04) 的值 控制的输出通道 (物理引脚) 典型用途
(1, 0) 0x30 (0011 0000) 开启通道1 (LOUT1/ROUT1),关闭通道2 驱动耳机
(0, 1) 0x0C (0000 1100) 开启通道2 (LOUT2/ROUT2),关闭通道1 线路输出 (Line Out)
(1, 1) 0x3C (0011 1100) 同时开启通道1和通道2 耳机和线路同时输出
(0, 0) 0x00 (0000 0000) 关闭所有模拟输出 录音、待机等无需播放的场景
  • es8388_hpvol_set
void es8388_hpvol_set(uint8_t volume)

函数用于设置ES8388芯片的耳机输出音量。它的实现很直接:通过向芯片的两个特定寄存器写入相同的音量值,来分别控制左右声道的输出衰减。
实现原理:
参数范围限制:芯片规定耳机音量值范围为0-33,所以函数首先检查并限制参数。
核心操作:分别向 寄存器0x2E(左声道) 和 寄存器0x2F(右声道) 写入目标音量值 volume。
音量控制本质:写入的值并非“增益”,而是数字衰减器(Digital Attenuator)的衰减系数。这意味着:

  • 值越小,衰减越小,听到的音量越大。
  • 值越大,衰减越大,听到的音量越小。
音量值 (volume) 对应衰减量 听觉效果 备注
0 0 dB (无衰减) 最大音量 这是理论最大值,实际输出受限于前端数字音量和芯片模拟输出能力。
1 -1.5 dB 音量很大 通常每步衰减约1.5dB。
12 -18 dB 中等音量 一个常用的、舒适的聆听电平。
33 -51 dB 几乎无声 可视为静音。

:上表中“约-1.5dB/步”是根据芯片总衰减范围-51dB除以33步估算的典型值,实际芯片的衰减曲线可能非线性。

  • es8388_spkvol_set

函数用于设置ES8388芯片的喇叭(或线路)输出通道音量。它的工作方式与之前讨论的耳机音量设置函数非常相似,同样是通过向寄存器写入一个代表衰减量的值来控制音量。

实现原理:
参数限幅:音量值有效范围是0-33,所以函数先做边界检查。
核心操作:分别向 寄存器0x30(左声道) 和 寄存器0x31(右声道) 写入目标音量值 volume。
音量本质:和耳机音量一样,写入的数值并非增益,而是数字衰减器的衰减系数。这意味着:

  • 值越小(如0),衰减越小,最终输出的模拟信号越强。
  • 值越大(如33),衰减越大,最终输出的模拟信号越弱,直至静音。

该函数和 es8388_hpvol_set 在形式和逻辑上几乎完全相同,唯一的区别在于它们控制的物理输出路径不同

对比项 es8388_hpvol_set (耳机) es8388_spkvol_set (喇叭/线路)
控制的寄存器 0x2E (左), 0x2F (右) 0x30 (左), 0x31 (右)
影响的输出通道 通道1 (LOUT1/ROUT1),通常连接耳机放大器 通道2 (LOUT2/ROUT2),通常连接喇叭或线路输出
典型用途 控制插入3.5mm接口的耳机音量 控制板载喇叭线路输出(Line Out)的音量
在例程中的使用 音乐播放时设置耳机音量 录音时关闭喇叭(设为0)避免啸叫

注:LOUT2/ROUT2 这个输出通道可以被用作线路输出(Line Out)连接到外部功放,也可以直接驱动一个小功率的喇叭,具体取决于硬件设计。在原子的开发板上,它通常用于驱动板载喇叭。

整体代码如下:

const char* es8388_tag = "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, &reg_addr, 1, &reg_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));
    ESP_ERROR_CHECK(i2c_master_bus_wait_all_done(bus_handle,1000));

    ret_val |= es8388_write_reg(0, 0x80);       /* 软复位ES8388 */
    ret_val |= es8388_write_reg(0, 0x00);
    vTaskDelay(pdMS_TO_TICKS(200));             /* 等待复位 */

    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 */
    ret_val |= es8388_write_reg(0x0C, 0x4C);    /* ADC数据选择为left data = left ADC, right data = left ADC  音频数据为16bit */
    ret_val |= es8388_write_reg(0x0D, 0x02);    /* ADC配置 MCLK/采样率=256 */
    ret_val |= es8388_write_reg(0x10, 0x00);    /* ADC数字音量控制将信号衰减 L  设置为最小!!! */
    ret_val |= es8388_write_reg(0x11, 0x00);    /* ADC数字音量控制将信号衰减 R  设置为最小!!! */

    ret_val |= es8388_write_reg(0x17, 0x18);    /* DAC音频数据为16bit */
    ret_val |= es8388_write_reg(0x18, 0x02);    /* DAC配置 MCLK/采样率=256 */
    ret_val |= es8388_write_reg(0x1A, 0x00);    /* DAC数字音量控制将信号衰减 L  设置为最小!!! */
    ret_val |= es8388_write_reg(0x1B, 0x00);    /* DAC数字音量控制将信号衰减 R  设置为最小!!! */
    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)
    {
        ESP_LOGI(es8388_tag, "ES8388 fail");
        return 1;
    }
    else
    {
        ESP_LOGI(es8388_tag, "ES8388 success");
        vTaskDelay(pdMS_TO_TICKS(100));
        return 0;
    }

    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_sai_cfg 配置 ES8388 工作模式,最后设置音量才可以接收 I²S 音频数据,实现音乐播放。

(3)wavplay 驱动
        wavpaly 主要用于 wav 格式的音频文件解码,接下来看看 wavplay.c 里面的几个函数,代码
如下:

/* MUSIC 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define MUSIC_PRIO      4                   /* 任务优先级 */
#define MUSIC_STK_SIZE  5*1024              /* 任务堆栈大小 */
TaskHandle_t            MUSICTask_Handler;  /* 任务句柄 */
void music(void *pvParameters);             /* 任务函数 */

static portMUX_TYPE my_spinlock = portMUX_INITIALIZER_UNLOCKED;

/******************************************************************************************************/

__wavctrl wavctrl;                          /* WAV音频文件解码参数结构体 */
UINT bytes_write = 0;                       /* 写一次I2S大小 */
volatile long long int i2s_table_size = 0;  /* 积累每次发送音频数据总大小 */
esp_err_t i2s_play_end = ESP_FAIL;          /* 播放结束标志位 */
esp_err_t i2s_play_next_prev = ESP_FAIL;    /* 下一首或者上一首标志位 */
FSIZE_t file_read_pos = 0;                  /* 记录当前WAV读取位置 */

/**
 * @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);      /* 打开WAV音频文件 */

            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;
}

        代码中, wav_decode_init 函数,用来对 wav 音频文件进行解析,得到 wav 的详细信息(音频采样率,位数,数据流起始位置等); wav_play_song 函数,是播放 WAV 最终执行的函数,该函数解析完 WAV 文件后,设置 ES8388 和 I²S 的参数(采样率,位数等),然后不断填充数
据,实现 WAV 播放,该函数中还进行了按键检测,实现上下曲切换和暂停/播放等操作。

(4)audioplay 驱动
        这部分需要根据 ES8388 推荐的初始化顺序时行配置。需要借助 SD 卡和文件系统需要播放的歌曲传给 ES8388播放。在 User目录下新建一个 APP 文件夹,同时在该目录下新建 audioplay.c 和 audioplay.h 并加入到工程。
        首先判断音乐文件类型,符合条件的再把相应的文件数据发送给 ES8388,在 FATFS的扩展文件中已经实现了判断文件类型这个功能,在图片显示实验也演示了这部分代码的使用,把这个功能封装成了 audio_get_tnum()函数。接下来分析一下 audio play()和 audio_play_song ()函数,实现播放歌曲的功能,代码如下:

/**
 * @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);
        spilcd_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);
        spilcd_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);
        spilcd_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);
        spilcd_fill(30, 190, spilcddev.width, spilcddev.height, WHITE);
        audio_index_show(curindex + 1, totwavnum);
        text_show_string(30, 190, 300, 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;
}
41.3.4 CMakeLists.txt 文件

        打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirs
            LED
            KEY
            MYIIC
            XL9555
            MYSPI
            SPILCD
            SPI_SD
            ES8388
            MYI2S)

set(include_dirs
            LED
            KEY
            MYIIC
            XL9555
            MYSPI
            SPILCD
            SPI_SD
            ES8388
            MYI2S)

set(requires
            driver
            esp_lcd
            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)

        打开本实验 main 文件下的 CMakeLists.txt 文件,其内容如下所示:

idf_component_register(
    SRC_DIRS 
        "."
        "APP"
        "APP/AUDIO"
    INCLUDE_DIRS 
        "."
        "APP"
        "APP/AUDIO"
    )
41.3.5 实验应用代码

        打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。

/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
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());
    }

    led_init();                 /* LED初始化 */
    my_spi_init();              /* SPI初始化 */
    key_init();                 /* KEY初始化 */
    myiic_init();               /* MYIIC初始化 */
    xl9555_init();              /* XL9555初始化 */
    spilcd_init();              /* SPILCD初始化 */

    while (es8388_init())       /* ES8388初始化 */
    {
        spilcd_show_string(30, 110, 200, 16, 16, "ES8388 Error", RED);
        vTaskDelay(pdMS_TO_TICKS(200));
        spilcd_fill(30, 110, 239, 126, WHITE);
        vTaskDelay(pdMS_TO_TICKS(200));
    }

    xl9555_pin_write(SPK_EN_IO, 0);     /* 打开喇叭 */

    while (sd_spi_init())       /* 检测不到SD卡 */
    {
        spilcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
        vTaskDelay(pdMS_TO_TICKS(500));
        spilcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
    ret = exfuns_init();    /* 为fatfs相关变量申请内存 */

    while (fonts_init())    /* 检查字库 */
    {
        spilcd_clear(WHITE);
        spilcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
        
        key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);  /* 更新字库 */
        
        while (key)         /* 更新失败 */
        {
            spilcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
            vTaskDelay(pdMS_TO_TICKS(200));
            spilcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
            vTaskDelay(pdMS_TO_TICKS(200));
        }

        spilcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
        vTaskDelay(pdMS_TO_TICKS(1000));
        spilcd_clear(WHITE);   
    }

    text_show_string(30, 50,  200, 16, "正点原子ESP32-S3开发板",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();       /* 播放音乐 */
    }
}

        注意:实验前要准备好音乐文件放到 SD 卡根目录下的 MUSIC 文件夹下测试本实验的代码。

41.4 下载验证

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

图 41.4.1 音乐播放中

        也可以在开发板的 PHONE 端子插入耳机听歌。同时,可以通过按KEY0和KEY1来切换下一曲和上一曲,通过KEY2暂停和继续播放。
        特别注意:可能下载后显示屏上显示乱码,原因是源代码文件以 UTF-8 编码保存,字符串字面值(如 "正点原子ESP32-S3开发板")将被编译器解释为 UTF-8 字节序列。UTF-8 编码的汉字通常由三个字节组成,而 GBK 编码的汉字由两个字节组成,这导致显示函数错误解析汉字字节,从而从字库中获取错误的点阵数据,出现乱码。解决方法:首先检查源代码文件的编码方式(如用编辑器查看或使用 file命令),如果文件是 UTF-8 编码,尝试转换为 GBK 编码并重新编译。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐