《STM32H7R7开发指南 V1.1 》第五十九章 音乐播放器实验

第五十九章 音乐播放器实验


       正点原子STM32H7R7开发板拥有串行音频接口(SAI),支持I2S、LSB/MSB对其、PCM/DSP、TDM和AC’97等协议,且外扩了一颗高性能,低功耗,高性价比的CODEC芯片:ES8388,支持最高192K 24BIT的音频播放,并且支持录音(下一章介绍)。本章分为如下几个部分:

       59.1 WAV&ES8388&SAI简介

       59.2 硬件设计

       59.3 软件设计

       59.4 下载验证


        59.1 WAV&ES8388&SAI简介

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


       59.1.1 WAV简介

       WAV即WAVE文件,WAV是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),由于其扩展名为"*.wav"。它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW 等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16 位量化数字,因此在声音文件质量和CD相差无几!

       WAV一般采用线性PCM(脉冲编码调制)编码,本章,我们也主要讨论PCM的播放,因为这个最简单。

       WAV 文件是由若干个Chunk组成的。按照在文件中的出现位置包括:RIFF WAVE Chunk、Format Chunk、 Fact Chunk(可选)和Data Chunk。每个Chunk由块标识符、数据大小和数据三部分组成,如图 59.1.1.1 所示:


图59.1.1.1 Chunk组成结构


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


图59.1.1.2 PCM格式的wav文件构成


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

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

typedef __PACKED_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 __PACKED_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 */
}ChunkFMT;                     /* fmt块 */

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

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

       DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。

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

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

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


表59.1.1.1 WAVE文件数据采样格式


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


       59.1.2 ES8388简介

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

       ES8388的主要特性有:

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

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

       ●支持主机和从机模式

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

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

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

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

       ASDOUT:ADC数据输出

       DSDIN:DAC数据输入

       LRC:数据左/右对齐时钟

       SCLK:位时钟,用于同步

       ES8388可作为SAI主机,输出LRC和SLCK时钟,不过我们一般使用ES8388作为从机,接收LRC和SLCK。另外,ES8388的SAI接口支持4种不同的音频数据模式:左(MSB)对齐标准、右(LSB)对齐标准、飞利浦(SAI)标准、DSP/PCM。本章,我们用飞利浦标准来传输SAI数据。

       飞利浦(SAI)标准模式,数据在跟随LRC传输的BCLK的第二个上升沿时传输MSB,其他位一直到LSB按顺序传输。传输依赖于字长、BCLK频率和采样率,在每个采样的LSB和下一个采样的MSB之间都应该有未用的BCLK周期。飞利浦标准模式的SAI数据传输协议如图59.1.2.1所示:


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


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

       ES8388的框图如图59.1.2.2所示:


图59.1.2.2 ES8388框图


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

       本章,我们通过IIC接口(CE=0)连接ES8388,ES8388的IIC地址为:0X10。关于ES8388的IIC详细介绍,请看其数据手册第10页5.2节。

       这里我们简单介绍一下要正常使用ES8388来播放音乐,应该执行哪些配置。

       1,寄存器R0(00h),是芯片控制寄存器1,需要用到的位有:最高位SCPRese(bit7)用于控制ES8388的软复位,写0X80到该寄存器地址,即可实现软复位ES8388,复位后,再写0X00,ES8388恢复正常。VMIDSEL[1:0]位用于控制VMID(校正噪声用),我们一般设置为10,即用500KΩ校正。

       2,寄存器R1(01h),是芯片控制寄存器2,主要要设置PdnAna(bit3),该位设置为1,模拟部分掉电,相当于复位模拟部分;设置为0,模拟部分才会工作,才可以听到声音。

       3,寄存器R2(02h),是芯片电源管理控制寄存器,所有位都要用到:adc_DigPDN(bit7)和dac_DigPDN(bit6)分别用于控制ADC和DAC的DSM、DEM、滤波器和数字接口的复位,1复位,0正常;adc_stm_rst(bit5)和dac_stm_rst(bit4)分别用于控制ADC和DAC的状态机掉电,1掉电,0正常;ADCDLL_PDN(bit3)和DACDLL_PDN(bit2)分别用于控制ADC和DAC的DLL掉电,停止时钟,1掉电,0正常;adcVref_PDN(bit1)和dacVref_PDN(bit0)分别控制ADC和DAC的模拟参考电压掉电,1掉电,0正常;因此想要ADC和DAC都正常工作,R2寄存器必须全部设置为0,否则ADC或者DAC就会不能正常工作。 

       4,寄存器R3(03h),是ADC电源管理控制寄存器,需要用到的位有:PdnAINL(bit7)和PdnAINR(bit6)用于控制左右输入模拟通道的电源,1掉电,0正常;PdnADCL(bit5)和PdnADCR(bit4)用于控制左右通道ADC的电源,1掉电,0正常;pdnMICB(bit3)用于控制麦克风的偏置电源,1掉电,0正常;PdnADCBiasgen(bit2)用于控制偏置电源的产生,1掉电,0正常;这里6个位,我们全部设置为0,ADC部分就可以正常工作了。

       5,寄存器R4(04h),是DAC电源管理控制寄存器,需要用到的位有:PdnDACL(bit7)和PdnDACR(bit6)分别用于左右声道DAC的电源控制,1掉电;0正常;LOUT1(bit5)和ROUT1(bit4)分别用于控制通道1的左右声道输出是能,1使能,0禁止;LOUT2(bit3)和ROUT2(bit2)分别用于控制通道2的左右声道输出是能,1使能,0禁止;我们一般设置PdnDACL和PdnDACR为0,使能左右声道DAC,另外,两个输出通道则根据自己的需要设置。

       6,寄存器R8(08h),是主模式控制寄存器,需要用到的位有:MSC(bit7)用于控制接口模式,0从模式,1主模式;MCKDIV2(bit6)用于控制MCLK的2分频,0不分频,1二分频;BCLK_INV(bit5)用于控制BCLK的反相,0不反相;1,反相;一般设置这3个位都为0。

       7,寄存器R9(09h),是ADC控制寄存器1,所有位都要用到:MicAmpL(bit7:4)和MicAmpR(bit3:0),这两个分别用于控制MIC的左右通道增益,从0开始,3dB一个档,最大增益为24dB,我们一般设置MicAmpR/L[3:0]=1000,即24dB。

       8,寄存器R10(0Ah),是ADC控制寄存器2,需要用到的位有:LINSE(bit7:6)和RINSE(bit5:4)分别选择左右输入通道,0选择通道1,1选择通道2。

       9,寄存器R12(0Ch),是ADC控制寄存器4,全部位都要用到:DATSEL(bit7:6)用于选择数据格式,一般设置为01,左右边数据等于左右声道ADC数据;ADCLRP(bit5)在I2S模式下用于设置数据对其方式,一般设置为0,正常极性;ADCWL(bit4:2)用于选择数据长度,我们设置011,选择16位数据长度;ADCFORMAT(bit1:0)用于设置ADC数据格式,一般设置为00,选择I2S数据格式。

       10,寄存器R13(0Dh),是ADC控制寄存器5,全部位都要用到:ADCFsMode(bit7)用于设置Fs模式,0单速模式,1双倍速模式,一般设置为0;ADCFsRatio(bit4:0)用于设置ADC的MCLK和FS的比率,我们设置00010,即256倍关系。

       11,寄存器R16(10h)和R17(11h),这两个寄存器分别用于控制ADC左右声道的音量衰减,LADCVOL(bit7:0)和RADCVOL(bit7:0)分别控制左声道和右声道ADC的衰减,0.5dB每步,我们一般设置为0,即不衰减。

       12,寄存器R18(12h),是ADC控制寄存器10,全部位都要用到:ALCSEL(bit7:6)用于控制ALC,00表示ALC关闭,01表示ALC仅控制左声道,10表示ALC仅控制右声道,11表示ALC立体声控制;我们一般设置为11。

       13,寄存器R23(17h),是DAC控制寄存器1,需要用到的位有:DACLRSWAP(bit7)用于控制左右声道数据交换,0正常,1互换,一般设置为0;DACLRP(bit6) 在I2S模式下用于设置数据对其方式,一般设置为0,正常极性;DACWL(bit5:3)用于选择数据长度,我们设置011,选择16位数据长度;ADCFORMAT(bit1:0)用于设置DAC数据格式,一般设置为00,选择I2S数据格式。

       14,寄存器R24(18h),是DAC控制寄存器2,全部位都要用到:DACFsMode(bit7)用于设置Fs模式,0单速模式,1双倍速模式,一般设置为0;DACFsRatio(bit4:0)用于设置DAC的MCLK和FS的比率,我们设置00010,即256倍关系。

       15,寄存器R26(1Ah)和R27(1Bh),这两个寄存器分别用于控制DAC左右声道的音量衰减,LDACVOL(bit7:0)和RDACVOL(bit7:0)分别控制左声道和右声道DAC的衰减,0.5dB每步,0表示0dB衰减,192表示96dB衰减;通过这两个寄存器可以完成输出音量的调节。

       16,寄存器R29(1Dh),是DAC控制寄存器7,需要用到的位有:ZeroL(bit7)和ZeroR(bit6)分别控制左右声道的全0输出,类似静音,1输出0,0正常;一般设置为0。Mono(bit5)用于单声道控制,0立体声,1单声道;一般设置为0。SE(bit4:2)用于设置3D音效,0~7表示3D效果的强弱,0表示关闭。

       17,寄存器39(27h)和42(2Ah),分别控制DAC左右通道的混音器,LD2LO(bit7)和RD2RO(bit7)分别控制左右DAC的混音器开关,0关闭,1开启,需设置为1;LI2LO(bit6)和RI2RO(bit6)分别控制左右输入通道的混音器开关,0关闭,1开启,一般设置为1;LI2LOVOL(bit5:3)和RI2ROVOL(bit5:3)分别控制左右输入通道的增益,0~7表示-6 ~ -15dB的增益调节范围,默认设置为111,即-15dB。

       18,寄存器43(2Bh),是DAC控制寄存器21,这里我们只关心slrck(bit7)这个位,用于控制DACLRC和ADCLRC是否共用,我们设置为1,表示共用。 

       以上,就是我们使用ES8388时所需要用到的一些寄存器,按照以上所述,对各个寄存器进行相应的配置,即可使用ES8388正常播放音乐了。关于ES8388更详细的寄存器设置说明,我们这里就不再介绍了,请大家参考ES8388的数据手册自行研究。


       59.1.3 SAI简介

       STM32H7R7自带了一个串行音频接口(SAI),SAI具有灵活性高、配置多样的特点。可以支持:SAI标准、LSB或MSB对齐、PCM/DSP、TDM和AC 97等协议,适用于多声道或单声道应用

       SAI通过两个完全独立的音频子模块来实现这种灵活性与可配置性,每个音频子模块与多达4个引脚(SD、SCK、FS和MCLK)相连。如果将两个子模块声明为同步模块,则其中一些引脚可以共用,从而可释放一些引脚用作通用I/O。MCLK引脚是否用作输出引脚取决于实际应用和解码的要求以及音频模块是否配置为主模块。SAI可以配置为主模式或配置为从模式。音频子模块既可作为接收器,又可作为发送器;既可与另一模块同步,又可以不同步。

       STM32H7R7自带的SAI接口,其特点包括:

       ●具有两个独立的音频子模块,子模块既可作为接收器,也可作为发送器,并自带FIFO

       ●每个音频子模块集成多达8个字,每个字32位的FIFO

       ●两个音频子模块间可以是同步或异步模式

       ●两个音频子模块的主/从配置相互独立

       ●当两个音频子模块都配置为主模式时,每个子模块可设置互相独立的采样率

       ●数据大小可配置:8位、10位、16位、20位、24位或32位

       ●支持:I2S、LSB或MSB对齐、PCM/DSP、TDM和AC 97等音频协议

       ●高达16个大小可配置的Slot,可选择音频帧中的哪些Slot有效

       ●支持LSB或MSB数据传输

       ●支持DMA,有2个专业通道,用于处理对每个SAI音频子模块的专用集成FIFO的访问

       STM32H7R7的SAI框图如图59.1.3.1所示:


图59.1.3.1 SAI框图


       本章,我们将用SAI接口来驱动ES8388,而ES8388的接口是I2S接口的,所以,本章我们只介绍SAI支持I2S协议使用的方法,其他协议的使用介绍,请看《STM32H7Rx参考手册_V6(英文版).pdf》第56章。


       (1)SAI I2S信号线

       SAI作为I2S使用的时候,同I2S接口连接的信号线如表59.1.3.1所示:


表59.1.3.1 SAI同I2S接口连接关系表


       表中,A/B表示SAI内部的两个独立的音频子模块,可以独立的连接I2S,也可以共同连接同一个I2S(主从同步模式),主从同步模式,常用于全双工I2S通信(读/写同时进行),主从同步模式,还可以省略一些信号线(SCK/FS/MCLK等)。

       FS_A/B:连接I2S的LRC脚,用于切换左右声道的数据,它的频率等于音频信号采样率(fs)。

       SCK_A/B:连接I2S的BLCK脚,用作位时钟,是I2S主模式下的串行时钟输出以及从模式下的串行时钟输入。SCK_A/B频率= FS_A/B频率(fs)*slot个数*单个slot大小(slot后面介绍)。

       SD_A/B:连接I2S的DACDAT/ADCDAT脚,是数据输入/输出脚,用于发送或接收数据(单个音频子模块,只能做半双工通信。全双工需要2个音频子模块同时工作,使用主从同步模式)。

       MCLK_A/B:连接I2S的MCLK脚,是主时钟输出脚,固定输出频率为256*fs,fs即音频信号采样频率(fs)。


       (2)SAI slot简介

       slot是SAI音频帧中的基本元素,音频帧中slot的数目通过SAI_xSLOTR寄存器配置,每个音频帧的slot数,最大是16。在I2S模式下,SAI中slot的传输方式如图59.1.3.2所示:


图59.1.3.2 SAI I2S模式下slot传输示意图


       上图中,一个音频帧中,slot的个数为6个,每个半帧有3个slot,根据slot 数与音频帧的对齐与否分为两种情况,我们一般设计为slot数与音频帧对齐,也就是图59.1.3.2中下部分图所示的传输方式:一个半帧刚好是3个slot,每个slot可以传输一个声道的音频数据,这样,6个slot就可以传输6个声道的音频数据。一般音频文件都是立体声,所以只需要2个slot即可,每个半帧一个slot。STM32的SAI最多可以实现16声道数据传输(16个slot)。

       每个slot的大小是可以配置的,如图59.1.3.3所示:


图59.1.3.3 slot大小配置


       由图可知,数据大小(DS)可以和slot相等,也可以不相等(16bit/32bit),当数据大小小于slot的时候,可以通过SAI_xSLOTR寄存器的FBOFF位设置数据的偏移。各种设置的约束条件为:

FBOFF ≤(SLOTSZ-DS)

DS≤SLOTSZ

NBSLOT*SLOTSZ≤FRL

       其中,FBOFF为数据在slot里面的偏移量;SLOTSZ为单个slot的位数;DS为数据大小位数;NBSLOT为一帧中slot的个数;FRL为帧长度(位数)。

       在I2S模式下,我们配置slot大小为32位,每一帧slot个数为2个,偏移量为0,这样就可以支持16~32位的立体声音乐播放了。


       (3)SAI时钟发生器

       SAI每个音频子模块都有自己的时钟发生器,这样两个模块完全独立,可以同时工作,并互不干扰。当音频模块定义为主模块时,时钟发生器将产生位时钟(SCK)以及用于外部解码器的主时钟(MCLK),当音频模块定义为从模块时,时钟发生器将关闭(关SCK和MCLK)。SAI的时钟发生器架构如图59.1.3.4所示:


图59.1.3.4 SAI时钟发生器架构


       图中,NODIV可以用于控制是否使能MCLK输出。如果设置为1,则MCLK无输出,且SCK的时钟直接来自主分频器分频(由MCKDIV[5:0]控制分频系数)。如果设置为0,则MCLK有输出,且SCK的时钟来自位时钟分频器(FRL[7:0])的分频。我们一般设置NODIV=0,使能MCLK输出。

       SAI1的sai_x_ker_ck时钟来源由:RCC_CCIPR3寄存器的SAI1SEL[2:0]位选择,本章我们设置SAI1SEL[2:0]=010,选择SAI1的sai_x_ker_ck时钟来自pll3_p_ck。需要注意的是:LTDC的时钟来自pll3_r_ck,和pll3_p_ck共用一个倍频系数,不同音频采样率下,倍频系数可能不一样,因此可能会导致LTDC的频率变化,所以,我们在设置不同音频采样率的时候,需要根据当前倍频系数和LTDC的时钟频率,来调整PLL3的r分频系数。

       sai_x_ker_ck经过主时钟分频器(MCKDIV[5:0])分频,作为主时钟(MCLK)提供给ES8388,同时,经主分频器(MCKDIV)分频后,还会经由位时钟分频器(FRL[7:0])分频,作为位时钟(SCK)提供给ES8388。

       OSR用于设置主时钟的过采样率,当OSR设置为0时,F MCLK_x=256×F FS_x ;当OSR设置为1时,F MCLK_x=512×F FS_x ;我们一般设置OSR为0,选择256×fs。

       接下来,我们重点介绍当:NOMCK=0且OSR=0时,主时钟(F MCLK_x)、位时钟(F SCK_x)和帧同步时钟(F FS_x,也叫左右时钟/采样时钟)的计算关系:

       当MCKDIV[5:0]=000000时,F MCLKx = F sai_x_ker_ck

       当MCKDIV[5:0]!=000000时,F MCLK_x = F sai_x_ker_ck / MCKDIV[5:0]

       位时钟计算公式;F SCK_x =[ F MCLK_x × ( FRL[7:0] + 1 ) ] / 256 

       左右时钟计算公式:F FS_x = F MCLKx / 256 

       其中:

       256是MCLK和音频采样率之间的固定比率(当OSR=0时,MCLK恒等于fs*256);

       FRL[7:0]是音频帧中的位时钟-1(在I2S协议下,必须是奇数,+1后为偶数);

       因此,我们可以得到音频采样率(fs)的计算公式为:

       当MCKDIV[5:0]=000000时,fs = F sai_x_ker_ck / 256

       当MCKDIV[5:0] !=000000时,fs = F sai_x_ker_ck / ( MCKDIV[5:0] × 256 ) 

       其中:Fsai_x_ker_ck我们一般选择来自PLL3的P分频,即:pll3_p_ck,其计算公式为:

pll3_p_ck = ( HSE / PLL3DIVM ) × ( PLL3DIVN + 1) / ( PLL3DIVP + 1 )

       HSE我们是25Mhz,PLL3DIVM我们一般设置为25,这样结合以上公式,可得fs的计算公式如下:

       MCKDIV[5:0] !=000000时:

fs= 1000 × ( PLL3DIVN + 1) / ( PLL3DIVP + 1 ) / ( MCKDIV[5:0] × 256 )

       MCKDIV[5:0]=000000时:

fs= 1000 × ( PLL3DIVN + 1) / ( PLL3DIVP + 1 ) / 256

       其中:fs单位是:Khz;PLL3DIVN取值范围:3~511;PLL3DIVP取值范围:0~127;MCKDIV取值范围:0~63;

       根据以上约束条件,我们便可以根据fs来设置各个系数的值了,不过很多时候,并不能取得和fs一模一样的频率,只能近似等于fs,比如44.1Khz采样率,我们设置PLL3DIVN=428,PLL3DIVP =0,MCKDIV=38,得到fs=44.0995Khz,误差为:0.0011%。晶振频率决定了有时无法通过分频得到我们所要的fs,所以,某些fs如果要实现0误差,大家必须得选用外部时钟才可以。

       如果要通过程序去计算这些系数的值,是比较麻烦的,所以,我们事先计算好常用fs对应的系数值,建立一个表,这样用的时候只需要查表取值就可以了,大大简化了代码,常用fs对应叙述表如下:

const uint16_t SAI_PSC_TBL[][3]=
{
    { 800 , 13, 32},    /* 8Khz采样率 */
    { 1102, 338, 4},    /* 11.025Khz采样率 */
    { 1600, 8, 10},     /* 16Khz采样率 */
    { 2205, 10, 9},     /* 22.05Khz采样率 */
    { 3200, 10, 6},     /* 32Khz采样率 */
    { 4410, 34, 3},     /* 44.1Khz采样率 */
    { 4800, 61, 5},     /* 48Khz采样率 */
    { 8820, 9, 2},      /* 88.2Khz采样率 */
    { 9600, 10, 2},     /* 96Khz采样率 */
    { 17640, 18, 2},    /* 176.4Khz采样率  */
    { 19200, 49, 5},    /* 192Khz采样率 */
};

       有了上面的fs-系数对应表,我们可以很方便的完成SAI的时钟配置。


       (4)SAI相关寄存器

       接下来,我们看看本章需要用到的一些相关寄存器。

       首先是SAI配置寄存器:SAI_ACR1,该寄存器各位描述如图59.1.3.5所示:


图59.1.3.5 寄存器SAI_ACR1各位描述


       该寄存器的配置,需要在禁止SAI的状况下配置,接下来,看看本章我们需要用到的各位的描述:

       MODE[1:0]位:00,主发送器;01,主接收器;10,从发送器;11,从接收器。我们用来播放音乐,设置为00,就可以了。

       PRTCFG[1:0]位:00,自由协议(I2S/LSB/MSB/TDM/PCM/DSP等);10,AC  ’97协议。我们使用I2S协议,需要设置为00。

       DS[2:0]位:010~111,表示 8/10/16/20/24/32 位数据大小,我们使用的音频一般是 16/24 位,所以设置这三个位为:100(16 位)或 110(24 位)。

       LSBFIRST 位:控制数据传输时是MSB 还是LSB,I2S 为MSB,我们设置该位为 0。

       CKSTR 位:设置时钟选通边沿,我们设置为1,即数据在时钟的上升沿选通。

       SYNCEN[1:0]位:00,音频模块异步工作;01,音频模块与另外一个音频模块同步。我们要控制ES8388播放音乐,需要设置音频模工作在异步模式,即设置 SYNCEN[1:0]=00。

       MONO 位:用于设置单声道/立体声模式,我们设置为 0,工作在立体声模式。

       OUTDRIV 位:0,当 SAIEN 置 1 时,驱动音频模块输出;1,在该位设置为 1 后,立即驱动音频模块输出。这里我们设置为 1。

       SAIEN 位:0,禁止音频模块;1,使能音频模块;注意:必须在所有SAI 配置完成以后,才设置该位为 1。

       DMAEN 位:DMA 使能位,0,禁止DMA;1,使能 DMA;我们设置为1,使能 DMA。

       NODIV 位:0,使能主时钟和位时钟分频器;1,禁止主时钟和位时钟分频器;我们一般设置为0.MCKDIV[5:0]:主时钟分频器,当设置为 000000 时,表示 1 分频;其他情况,则分频值为:MCKDIV[5:0];我们需要根据音频采样率(fs)的不同来设置不同的值。

       OSR位:主时钟过采样设置位,设置为0时,MCLK=F FS × 256;设置为1时MCLK=F FS × 512;我们一般设置该位为0。

       第二个是 SAI 帧配置寄存器: SAI_AFRCR,该寄存器各位描述如图59.1.3.6 所示:


图59.1.3.6 寄存器SAI_A FRCR各位描述


       FRL[7:0]位:帧长度设置位,它等于音频帧中SCK的个数-1。FRL的最小值为8,最大值为256,且FRL+1应该为偶数,并且是2的指数倍关系。

       FSALL[6:0]位:帧同步有效电平长度,用于指定 FS 信号的有效电平长度,即高电平/低电平的宽度,应该等于帧长度的一半。计算方法为: FSALL=(FRL+1)/2-1。

       FSDEF 位:帧同步定义,0,FS 信号为起始帧信号;1,FS 信号为 SOF 信号+通道识别信号。使用 I2S 协议的时候,我们设置 FS 为 1。

       FSPOL 位:帧同步极性设置,0,FS 低电平有效(下降沿);1,FS 高电平有效(上升沿);我们设置为 FS 低电平有效,即 FSPOL 位为 0。

       FSOFF:帧同步偏移,0,在 slot0 的第一位上使能 FS;1,在 slot0 的第一位的前一位上使能 FS。使用I2S 协议时,需要设置FSOFF 位为1,以匹配 I2S 协议(见图 59.1.2.1)。

       第三个是 SAI slot 寄存器: SAI_ASLOTR,该寄存器各位描述如图 59.1.3.7 所示:


图59.1.3.7 寄存器SAI_ASLOTR各位描述


       FBOFF[4:0]位:设置第一个位的偏移量,用于设置 slot 中第一个数据传输位的位置,它表示一个偏移值。由于前面我们设置了 FSOFF 位,所以,我们设置 FBOFF=0 即可。

       SLOTSZ[1:0]位:设置 slot 大小, 00, slot 大小等于数据大小; 01, 16 位; 10, 32 位;我们设置 SLOTSZ 为 10( 32 位),以支持最高 32 位音频的播放。

       NBSLOT[3:0]位:设置音频帧中 slot 的个数(设置值+1)。比如我们用立体声,使用 2 个slot 就够了,所以设置 NBSLOT=1 即可。

       SLOTEN[15:0]位:设置 slot 使能,每个位表示 1 个 slot,最多是 16 个 slot。我们使用 2 个slot(即 slot0 和 slot1),所以,设置 SLOTEN 的最低 2 位为 1 即可。

       第四个是 PLLSAI 配置寄存器: RCC_PLLCKSELR,该寄存器各位描述如图 59.1.3.8 所示:


图59.1.3.8 寄存器RCC_PLLCKSELR各位描述


       该寄存器用于选择所有PLL的输入时钟和PLL1~3的预分频系数。对于PLL3来说,其预分频系数由:DIVM3[5:0]控制,取值范围为0~63。因为外部晶振是24MHz,为了方便计算,我们设置DIVM3[5:0]=6,可以得到4MHz的PLL3输入时钟。

       第五个是PLL3分频配置寄存器:RCC_PLL3DIVR1,该寄存器各位描述如图59.1.3.9所示:


图59.1.3.9 寄存器RCC_PLL3DIVR1各位描述


       该寄存器用于配置PLL3的DIVN3[8:0]、DIVP3[6:0]、DIVQ3[6:0]和DIVR3[6:0]四个系数。

       我们使用SAI1来驱动ES8388,且设置sai_1_ker_ck来自PLL3的P分频(pll3_p_ck),其频率计算公式为:

sai_1_ker_ck = pll3_p_ck = ( HSE / DIVM3 ) × ( DIVN3 + 1 ) / ( DIVP3 + 1)

       HSE为外部晶振频率,24Mhz;DIVM3,我们一般设置为6;DIVN3[8:0]用于设置PLL3的倍频系数,取值范围:3~511,表示4~512倍频;DIVP3[6:0]用于设置PLL3的P分频系数,取值范围为:0~127,表示1~128分频。DIVN3和DIVP3需要根据采样率(fs)的不同的值进行合适的设置。

       第六个是SAI数据寄存器:SAI_ADR,该寄存器各位描述如图59.1.3.10所示:


图59.1.3.10 寄存器SAI_ADR各位描述


       当我们需要向ES8388发送音频数据的时候,通过写这个寄存器,就可以实现。不过,我们采用DMA来传输,所以直接设置DMA的外设地址位SAI_ADR即可。

       此外,使用FIFO的时候,还要用到SAI_BCR2寄存器设置FIFO阈值和刷新,我们这里就不做多的介绍了,请大家参考《STM32H7Rx参考手册_V6(英文版).pdf》第56.6.11节。

       SAI的相关寄存器,就给大家介绍到这里。


        59.2 硬件设计


       1. 例程功能

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


       2. 硬件资源

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


       1)LED灯

              LED0 – PD14

              LED1 – PC0


       2)独立按键

              KEY0 – PE9

              KEY1 – PE8

              KEY_UP – PC13  (程序中的宏名:WK_UP)


       3)串口1 (PB14/PB15连接在板载USB转串口芯片CH340上面)


       4)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(包括MCU屏和RGB屏,都支持)


       5)SD卡,通过SDMMC连接


       6)NOR FLASH


       7)ES8388音频CODEC芯片,通过SAI驱动


       8)SAI音频接口


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


图59.2.1 ES8388与STM32H7R7原理图


       图中,PHONE接口可以用来插耳机,SPK+和SPK-连接了板载的喇叭(在开发板底部)。


        59.3 程序设计


       59.3.1 程序解析

       音乐文件我们要通过SD卡来传给单片机,那我们自然要用到文件系统。LCD、按键交互这些我们也需要实现,同样我们为了快速建立工程,复制之前的《实验43 FATFS实验》来修改成我们音乐播放器实验。

       由于播放功能涉及到多个外设的配合使用,用文件系统读音频文件,做播放控制等,所以我们把ES8388的硬件驱动放到BSP目录下,播放功能作为APP放到USER目录下。


       1. SAI驱动代码

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

       除去SAI的管脚,我们需要初始其它IO的模式,我们在头文件sai.h中定义SAI的引脚,方便如果IO变更之后作修改:

/* SAI1 引脚 定义 */
#define SAI1_CLK_GPIO_PORT             GPIOF
#define SAI1_CLK_GPIO_PIN              GPIO_PIN_7
#define SAI1_CLK_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)   /* PF口时钟使能 */
 
#define SAI1_SCK_GPIO_PORT             GPIOF
#define SAI1_SCK_GPIO_PIN              GPIO_PIN_8
#define SAI1_SCK_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)   /* PF口时钟使能 */
 
#define SAI1_FSB_GPIO_PORT             GPIOF
#define SAI1_FSB_GPIO_PIN              GPIO_PIN_9
#define SAI1_FSB_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)   /* PF口时钟使能 */
 
#define SAI1_SDA_GPIO_PORT             GPIOE
#define SAI1_SDA_GPIO_PIN              GPIO_PIN_6
#define SAI1_SDA_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */
 
#define SAI1_SDB_GPIO_PORT             GPIOF
#define SAI1_SDB_GPIO_PIN              GPIO_PIN_6
#define SAI1_SDB_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOF_CLK_ENABLE(); }while(0)   /* PF口时钟使能 */
 
/* SAI1相关定义 */
#define SAI1_SAI_CLK_ENABLE()          do{ __HAL_RCC_SAI1_CLK_ENABLE(); }while(0)    /* SAI1时钟使能 */
 
#define SAI1_TX_DMASx                  GPDMA1_Channel0
#define SAI1_TX_DMASx_IRQHandler       GPDMA1_Channel0_IRQHandler
#define SAI1_TX_DMASx_IRQ              GPDMA1_Channel0_IRQn
#define SAI1_TX_DMA_CLK_ENABLE()       do{ __HAL_RCC_GPDMA1_CLK_ENABLE(); }while(0)    /* SAIA TX DMA时钟使能 */

       接下来开始介绍sai.c,主要是SAI的初始化和DMA初始化,代码如下:

SAI_HandleTypeDef g_sai1_b_handle;                  /* SAI1 Block B句柄 */
DMA_HandleTypeDef g_sai1_tx_dma_handle = {0};       /* DMA发送句柄 */
DMA_NodeTypeDef g_sai1_tx_dma_node_struct[2] = {0}; /* DMA发送链表节点 */
DMA_QListTypeDef g_sai1_tx_dma_qlist_struct = {0};  /* DMA发送链表 */
 
static void sai1_tx_dma_transfer_complete_cb(DMA_HandleTypeDef *const dma_handle); /* DMA传输完成回调函数 */
 
/**
 * @brief       开启SAIB的DMA功能,HAL库没有提供此函数
 * @note        因此我们需要自己操作寄存器编写一个
 * @param       无
 * @retval      无
 */
void sai1_saib_dma_enable(void)
{
    uint32_t tempreg = 0;
    tempreg = SAI1_Block_B->CR1;            /* 先读出以前的设置 */
    tempreg |= 1 << 17;                     /* 使能DMA */
    SAI1_Block_B->CR1 = tempreg;            /* 写入CR1寄存器中 */
}
 
/**
 * @brief       SAI1 Block B初始化, I2S,飞利浦标准
 * @param       mode    : 00,主发送器;01,主接收器;10,从发送器;11,从接收器
 * @param       cpol    : 0,时钟下降沿选通;1,时钟上升沿选通
 * @param       datalen : 数据大小,0/1,未用到,2,8位;3,10位;4,16位;5,20位;
6,24位;7,32位.
 * @retval      无
 */
void sai1_saib_init(uint32_t mode, uint32_t cpol, uint32_t datalen)
{
    g_sai1_b_handle.Instance = SAI1_Block_B;             /* SAI1 Bock B */
    g_sai1_b_handle.Init.AudioMode = mode;               /* 设置SAI1工作模式 */
    g_sai1_b_handle.Init.Synchro = SAI_ASYNCHRONOUS;     /* 音频模块异步 */
g_sai1_b_handle.Init.OutputDrive = SAI_OUTPUTDRIVE_ENABLE;   
/* 立即驱动音频模块输出 */
g_sai1_b_handle.Init.NoDivider = SAI_MASTERDIVIDER_ENABLE;   
/* 使能主时钟分频器(MCKDIV) */
g_sai1_b_handle.Init.FIFOThreshold = SAI_FIFOTHRESHOLD_1QF;  
/* 设置FIFO阈值,1/4 FIFO */
    g_sai1_b_handle.Init.MonoStereoMode = SAI_STEREOMODE; /* 立体声模式 */
g_sai1_b_handle.Init.Protocol = SAI_FREE_PROTOCOL;           
/* 设置SAI1协议为:自由协议(支持I2S/LSB/MSB/TDM/PCM/DSP等协议) */
    g_sai1_b_handle.Init.DataSize = datalen;               /* 设置数据大小 */
    g_sai1_b_handle.Init.FirstBit = SAI_FIRSTBIT_MSB;      /* 数据MSB位优先 */
g_sai1_b_handle.Init.ClockStrobing = cpol;             
/* 数据在时钟的上升/下降沿选通 */
    
    /* 帧设置 */
g_sai1_b_handle.FrameInit.FrameLength = 64;                  
/* 设置帧长度为64,左通道32个SCK,右通道32个SCK. */
g_sai1_b_handle.FrameInit.ActiveFrameLength = 32;            
/* 设置帧同步有效电平长度,在I2S模式下=1/2帧长 */
g_sai1_b_handle.FrameInit.FSDefinition = SAI_FS_CHANNEL_IDENTIFICATION;/
* FS信号为SOF信号+通道识别信号 */
g_sai1_b_handle.FrameInit.FSPolarity = SAI_FS_ACTIVE_LOW;    
/* FS低电平有效(下降沿) */
g_sai1_b_handle.FrameInit.FSOffset = SAI_FS_BEFOREFIRSTBIT;  
/* 在slot0的第一位的前一位使能FS,以匹配飞利浦标准 */
 
    /* SLOT设置 */
g_sai1_b_handle.SlotInit.FirstBitOffset = 0;                 
/* slot偏移(FBOFF)为0 */
g_sai1_b_handle.SlotInit.SlotSize = SAI_SLOTSIZE_32B;        
/* slot大小为32位 */
    g_sai1_b_handle.SlotInit.SlotNumber = 2;            /* slot数为2个 */
g_sai1_b_handle.SlotInit.SlotActive = SAI_SLOTACTIVE_0 | 
SAI_SLOTACTIVE_1;                                   /* 使能slot0和slot1 */
    
    HAL_SAI_DeInit(&g_sai1_b_handle);                   /* 清除以前的配置 */
    HAL_SAI_Init(&g_sai1_b_handle);                     /* 初始化SAI */
    __HAL_SAI_ENABLE(&g_sai1_b_handle);                 /* 使能SAI */
}
 
/**
 * @brief       SAI底层驱动,引脚配置,时钟使能
 * @note        此函数会被HAL_SAI_Init()调用
 * @param       hsdram:SAI句柄
 * @retval      无
 */
void HAL_SAI_MspInit(SAI_HandleTypeDef *hsai)
{
    GPIO_InitTypeDef gpio_init_struct;
 
    SAI1_SAI_CLK_ENABLE();                     /* 使能SAI1时钟 */
    SAI1_CLK_GPIO_CLK_ENABLE();                /* CLK引脚时钟使能 */
    SAI1_SCK_GPIO_CLK_ENABLE();                /* SCK引脚时钟使能 */
    SAI1_FSB_GPIO_CLK_ENABLE();                /* FSB引脚时钟使能 */
    SAI1_SDA_GPIO_CLK_ENABLE();                /* SDA引脚时钟使能 */
    SAI1_SDB_GPIO_CLK_ENABLE();                /* SDB引脚时钟使能 */
    SAI1_TX_DMA_CLK_ENABLE();
    
    gpio_init_struct.Pin = SAI1_CLK_GPIO_PIN;  
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 推挽复用 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */
    gpio_init_struct.Alternate = GPIO_AF6_SAI1;             /* 复用为SAI */
    HAL_GPIO_Init(SAI1_CLK_GPIO_PORT, &gpio_init_struct);   /* 初始化CLK引脚 */
    
    gpio_init_struct.Pin = SAI1_SCK_GPIO_PIN;
    HAL_GPIO_Init(SAI1_SCK_GPIO_PORT,  &gpio_init_struct);  /* 初始化SCK引脚 */
    
    gpio_init_struct.Pin = SAI1_FSB_GPIO_PIN;
    HAL_GPIO_Init(SAI1_FSB_GPIO_PORT,  &gpio_init_struct);  /* 初始化FSB引脚 */
    
    gpio_init_struct.Pin = SAI1_SDA_GPIO_PIN;
    HAL_GPIO_Init(SAI1_SDA_GPIO_PORT,  &gpio_init_struct);  /* 初始化SDA引脚 */
    
    gpio_init_struct.Pin = SAI1_SDB_GPIO_PIN;
    HAL_GPIO_Init(SAI1_SDB_GPIO_PORT,  &gpio_init_struct);  /* 初始化SDB引脚 */
    
    HAL_NVIC_SetPriority(SAI1_TX_DMACx_IRQ, 0, 0);
    HAL_NVIC_EnableIRQ(SAI1_TX_DMACx_IRQ);
}
 
/* SAI Block B采样率设置
 * 采样率计算公式(以NOMCK=0,OSR=0为前提):
 * Fmclk = 256*Fs = sai_x_ker_ck / MCKDIV[5:0]
 * Fs = sai_x_ker_ck / (256 * MCKDIV[5:0])
 * Fsck = Fmclk * (FRL[7:0]+1) / 256 = Fs * (FRL[7:0] + 1)
 * 其中:
 * sai_x_ker_ck = (HSE / PLL2DIVM) * (PLL2DIVN + 1) / (PLL2DIVP + 1)
 * HSE:一般为25Mhz
 * PLL2DIVM     :1~63,表示1~63分频
 * PLL2DIVN     :3~511,表示4~512倍频
 * PLL2DIVP     :0~127,表示1~128分频
 * MCKDIV       :0~63,表示1~63分频,0也是1分频,推荐设置为偶数
 * SAI B分频系数表@PLL2DIVM=25,HSE=25Mhz,即vco输入频率为1Mhz
 * 表格式:
 * 采样率|(PLL2DIVN+1)|(PLL2DIVP+1)|MCKDIV
 */
const uint16_t SAI_PSC_TBL[][3]=
{
    { 800 , 13, 32},    /* 8Khz采样率 */
    { 1102, 338, 4},    /* 11.025Khz采样率 */
    { 1600, 8, 10},     /* 16Khz采样率 */
    { 2205, 10, 9},     /* 22.05Khz采样率 */
    { 3200, 10, 6},     /* 32Khz采样率 */
    { 4410, 34, 3},     /* 44.1Khz采样率 */
    { 4800, 61, 5},     /* 48Khz采样率 */
    { 8820, 9, 2},      /* 88.2Khz采样率 */
    { 9600, 10, 2},     /* 96Khz采样率 */
    { 17640, 18, 2},    /* 176.4Khz采样率  */
    { 19200, 49, 5},    /* 192Khz采样率 */
};
 
 
/**
 * @brief       设置SAIB的采样率(@MCKEN)
 * @param       samplerate:采样率, 单位:Hz
 * @retval      0,设置成功
 *              1,无法设置
 */
uint8_t sai1_samplerate_set(uint32_t samplerate)
{   
    uint8_t i = 0; 
    
    RCC_PeriphCLKInitTypeDef rcc_sai1_sture;  
 
for (i = 0; i < (sizeof(SAI_PSC_TBL) / sizeof(SAI_PSC_TBL[0])); i++)
/* 看看改采样率是否可以支持 */
    {
        if ((samplerate / 10) == SAI_PSC_TBL[i][0])
        {
            break;
        }
    }
 
    if (i == (sizeof(SAI_PSC_TBL) / sizeof(SAI_PSC_TBL[0])))
    {
        return 1;                       /* 搜遍了也找不到 */
    }
        
    /* 根据采样率,配置PLL3时钟 */  
    while((RCC->CR & (1 << 29)) != 0)
    {
        RCC->CR &= ~(1 << 28);                  /* 修改之前需先失能PPL3 */
    }       
    RCC->PLL3DIVR1= 0;                          /* 清空寄存器 */
    RCC->PLLCKSELR = 0;                         /* 清空寄存器 */
    RCC->PLLCKSELR = (25 << 20);                /* 设置M值 */
    RCC->PLLCKSELR |= 2;                        /* 选择HSE当时钟源 */
    RCC->PLL3DIVR1 |= SAI_PSC_TBL[i][1] - 1;    /* 配置N值 */
    RCC->PLL3DIVR1 |= (SAI_PSC_TBL[i][2] - 1) << 9; /* 配置P值 */
    RCC->PLL3DIVR1 |= (10 - 1) << 16;               /* 配置Q值 */
    RCC->CR |= (1 << 28);                           /* 使能PPL3 */
    while((RCC->CR & (1 << 29)) != 0);              /* 等待就绪 */
 
    rcc_sai1_sture.PeriphClockSelection = RCC_PERIPHCLK_SAI1; /* 外设时钟源选择 */ 
    rcc_sai1_sture.Sai1ClockSelection = RCC_SAI1CLKSOURCE_PLL3P;/* 设置PLL3P */
    HAL_RCCEx_PeriphCLKConfig(&rcc_sai1_sture);                 /* 设置时钟 */
 
    __HAL_SAI_DISABLE(&g_sai1_b_handle);                 /* 关闭SAI */
    g_sai1_b_handle.Init.AudioFrequency = samplerate;    /* 设置播放频率 */
    HAL_SAI_Init(&g_sai1_b_handle);                      /* 初始化SAI */
    sai1_saib_dma_enable();                              /* 开启SAI的DMA功能 */
    __HAL_SAI_ENABLE(&g_sai1_b_handle);                  /* 开启SAI */
 
    return 0;
}
 
/**
 * @brief       SAIA TX DMA配置
 * @note        设置为双缓冲模式,并开启DMA传输完成中断
 * @param       buf0:M0AR地址.
 * @param       buf1:M1AR地址.
 * @param       num:每次传输数据量
 * @param       width:位宽(存储器和外设,同时设置),0,8位;1,16位;2,32位;
 * @retval      0,设置成功
 *              1,无法设置
 */
void sai1_tx_dma_init(uint8_t *buf0,uint8_t *buf1,uint16_t num,uint8_t width)
{ 
    uint32_t memwidth = 0, perwidth = 0;      /* 外设和存储器位宽 */
    DMA_NodeConfTypeDef dma_node_conf_struct = {0};
 
    switch (width)
    {
        case 0:         /* 8位 */
            num = num;
            memwidth = DMA_SRC_DATAWIDTH_BYTE;
            perwidth = DMA_DEST_DATAWIDTH_BYTE;
            break;
 
        case 1:         /* 16位 */
            num *= 2;
            memwidth = DMA_SRC_DATAWIDTH_HALFWORD;
            perwidth = DMA_DEST_DATAWIDTH_HALFWORD;
            break;
 
        case 2:         /* 32位 */
            num *= 4;
            memwidth = DMA_SRC_DATAWIDTH_WORD;
            perwidth = DMA_DEST_DATAWIDTH_WORD;
            break;
    }
    
    /* 复位链表 */
    HAL_DMAEx_List_ResetQ(&g_sai1_tx_dma_qlist_struct);
    
    /* 构建链表节点 */
    dma_node_conf_struct.NodeType = DMA_GPDMA_LINEAR_NODE;
    dma_node_conf_struct.Init.Request = GPDMA1_REQUEST_SAI1_B;
    dma_node_conf_struct.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
    dma_node_conf_struct.Init.Direction = DMA_MEMORY_TO_PERIPH;
    dma_node_conf_struct.Init.SrcInc = DMA_SINC_INCREMENTED;
    dma_node_conf_struct.Init.DestInc = DMA_DINC_FIXED;
    dma_node_conf_struct.Init.SrcDataWidth = memwidth;
    dma_node_conf_struct.Init.DestDataWidth = perwidth;
    dma_node_conf_struct.Init.SrcBurstLength = 1;
    dma_node_conf_struct.Init.DestBurstLength = 1;
dma_node_conf_struct.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 |
DMA_DEST_ALLOCATED_PORT1;
dma_node_conf_struct.Init.TransferEventMode = 
DMA_TCEM_EACH_LL_ITEM_TRANSFER;
    dma_node_conf_struct.Init.Mode = DMA_NORMAL;
    dma_node_conf_struct.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
dma_node_conf_struct.DataHandlingConfig.DataAlignment = 
DMA_DATA_RIGHTALIGN_ZEROPADDED;
dma_node_conf_struct.TriggerConfig.TriggerPolarity = 
DMA_TRIG_POLARITY_MASKED;
    dma_node_conf_struct.DstAddress = (uint32_t)&SAI1_Block_B->DR;
    dma_node_conf_struct.DataSize = num;
    dma_node_conf_struct.SrcAddress = (uint32_t)buf0;
HAL_DMAEx_List_BuildNode(&dma_node_conf_struct, 
&g_sai1_tx_dma_node_struct[0]);
HAL_DMAEx_List_InsertNode_Tail(&g_sai1_tx_dma_qlist_struct, 
&g_sai1_tx_dma_node_struct[0]);
    dma_node_conf_struct.SrcAddress = (uint32_t)buf1;
HAL_DMAEx_List_BuildNode(&dma_node_conf_struct, 
&g_sai1_tx_dma_node_struct[1]);
HAL_DMAEx_List_InsertNode_Tail(&g_sai1_tx_dma_qlist_struct, 
&g_sai1_tx_dma_node_struct[1]);
    
    /* 循环链表 */
    HAL_DMAEx_List_SetCircularMode(&g_sai1_tx_dma_qlist_struct);
    
    /* 初始化DMA通道为链表模式 */
    g_sai1_tx_dma_handle.Instance = SAI1_TX_DMACx;
    g_sai1_tx_dma_handle.InitLinkedList.Priority = DMA_HIGH_PRIORITY;
    g_sai1_tx_dma_handle.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
g_sai1_tx_dma_handle.InitLinkedList.LinkAllocatedPort = 
DMA_LINK_ALLOCATED_PORT1;
g_sai1_tx_dma_handle.InitLinkedList.TransferEventMode = 
DMA_TCEM_EACH_LL_ITEM_TRANSFER;
g_sai1_tx_dma_handle.InitLinkedList.LinkedListMode = 
DMA_LINKEDLIST_CIRCULAR;
    HAL_DMAEx_List_DeInit(&g_sai1_tx_dma_handle);
    HAL_DMAEx_List_Init(&g_sai1_tx_dma_handle);
    
    /* 链接链表到DMA通道 */
    HAL_DMAEx_List_LinkQ(&g_sai1_tx_dma_handle, &g_sai1_tx_dma_qlist_struct);
    
    /* 链接到外设 */
    __HAL_LINKDMA(&g_sai1_b_handle, hdmatx, g_sai1_tx_dma_handle);
    
    /* 配置DMA通道的安全和特权属性 */
    HAL_DMA_ConfigChannelAttributes(&g_sai1_tx_dma_handle, DMA_CHANNEL_NPRIV);
    
    /* 注册DMA传输完成回调函数 */
HAL_DMA_RegisterCallback(&g_sai1_tx_dma_handle, HAL_DMA_XFER_CPLT_CB_ID, 
sai1_tx_dma_transfer_complete_cb);
    
    HAL_DMAEx_List_Start_IT(&g_sai1_tx_dma_handle);
}
 
/* SAI DMA回调函数指针 */
void (*sai1_tx_callback)(uint8_t node); /* TX回调函数 */
 
/**
 * @brief       GPDMA1_Channel0中断服务函数
 * @param       无
 * @retval      无
 */
void SAI1_TX_DMACx_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&g_sai1_tx_dma_handle);
}
 
/**
 * @brief   DMA传输完成回调函数
 * @param   dma_handle: DMA句柄指针
 * @retval  无
 */
static void sai1_tx_dma_transfer_complete_cb(DMA_HandleTypeDef *const dma_handle)
{
    uint32_t node_addr;
    uint8_t node;
    uint8_t i;
    
node_addr = (dma_handle->Instance->CLBAR & DMA_CLBAR_LBA) | 
(dma_handle->Instance->CLLR & DMA_CLLR_LA);
for (i=0; i<(sizeof(g_sai1_tx_dma_node_struct) / 
sizeof(g_sai1_tx_dma_node_struct[0])); i++)
    {
        if (node_addr == (uint32_t)&g_sai1_tx_dma_node_struct[i])
        {
            node = (i != 0) ? (i - 1) : ((sizeof(g_sai1_tx_dma_node_struct) 
/ sizeof(g_sai1_tx_dma_node_struct[0])) - 1);
            break;
        }
    }
    
    if (sai1_tx_callback != NULL)
    {
        sai1_tx_callback(node);
    }
}
 
/**
 * @brief       SAI开始播放
 * @param       无
 * @retval      无
 */
void sai1_play_start(void)
{
    g_sai1_b_handle.Instance->CR1 |= SAI_xCR1_DMAEN;
}
 
/**
 * @brief       关闭SAI播放
 * @param       无
 * @retval      无
 */
void sai1_play_stop(void)
{   
    g_sai1_b_handle.Instance->CR1 &= ~SAI_xCR1_DMAEN;
}

       函数sai1_saib_init完成SAI初始化,通过3个参数设置SAI的详细配置信息。函数sai1_samplerate_set则是用前面介绍的查表法,根据采样率来设置SAI的时钟。函数sai1_tx_dma_init用于设置SAI的DMA发送,使用双缓冲模式,发送数据给ES8388,并开启了发送完成中断。而函数SAI1_TX_DMACx_IRQHandler是DMA数据流发送完成中端的服务函数,该函数调用sai1_tx_callback函数(函数指针,使用前需指向特定函数)实现DMA数据填充。最后是sai1_play_start和sai1_play_stop函数,用于开启和关闭DMA中断。


       2. ES8388驱动代码

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

/**
 * @brief       ES8388初始化
 * @param       无
 * @retval      0,初始化正常
 *              其他,错误代码
 */
uint8_t es8388_init(void)
{
    iic_init();                     /* 初始化IIC接口 */
 
    es8388_write_reg(0, 0x80);      /* 软复位ES8388 */
    es8388_write_reg(0, 0x00);
    delay_ms(100);                  /* 等待复位 */
 
    es8388_write_reg(0x01, 0x58);
    es8388_write_reg(0x01, 0x50);
    es8388_write_reg(0x02, 0xF3);
    es8388_write_reg(0x02, 0xF0);
 
    es8388_write_reg(0x03, 0x09);   /* 麦克风偏置电源关闭 */
    es8388_write_reg(0x00, 0x06);   /* 使能参考 500K驱动使能 */
    es8388_write_reg(0x04, 0x00);   /* DAC电源管理,不打开任何通道 */
    es8388_write_reg(0x08, 0x00);   /* MCLK不分频 */
    es8388_write_reg(0x2B, 0x80);   /* DAC控制    DACLRC与ADCLRC相同 */
 
    es8388_write_reg(0x09, 0x88);   /* ADC L/R PGA增益配置为+24dB */
es8388_write_reg(0x0C, 0x4C);   
/* ADC 数据选择为left data = left ADC, right data=left ADC  音频数据为16bit */
    es8388_write_reg(0x0D, 0x02);   /* ADC配置 MCLK/采样率=256 */
    es8388_write_reg(0x10, 0x00);   /* ADC数字音量控制将信号衰减 L设置为最小!!! */
    es8388_write_reg(0x11, 0x00);   /* ADC数字音量控制将信号衰减 R设置为最小!!! */
 
    es8388_write_reg(0x17, 0x18);   /* DAC 音频数据为16bit */
    es8388_write_reg(0x18, 0x02);   /* DAC 配置 MCLK/采样率=256 */
    es8388_write_reg(0x1A, 0x00);   /* DAC数字音量控制将信号衰减 L设置为最小!!! */
    es8388_write_reg(0x1B, 0x00);   /* DAC数字音量控制将信号衰减 R设置为最小!!! */
    es8388_write_reg(0x27, 0xB8);   /* L混频器 */
    es8388_write_reg(0x2A, 0xB8);   /* R混频器 */
    delay_ms(100);
    
    return 0;
}
 
/**
 * @brief       ES8388写寄存器
 * @param       reg : 寄存器地址
 * @param       val : 要写入寄存器的值
 * @retval      0,成功
 *              其他,错误代码
 */
uint8_t es8388_write_reg(uint8_t reg, uint8_t val)
{
    iic_start();
    
    iic_send_byte((ES8388_ADDR << 1) | 0);  /* 发送器件地址+写命令 */
    if (iic_wait_ack())
    {
        return 1;                           /* 等待应答(成功?/失败?) */
    }
    
    iic_send_byte(reg);                     /* 写寄存器地址 */
    if (iic_wait_ack())
    {
        return 2;                           /* 等待应答(成功?/失败?) */
    }
    
    iic_send_byte(val & 0xFF);              /* 发送数据 */
    if (iic_wait_ack())
    {
        return 3;                           /* 等待应答(成功?/失败?) */
    }
    
    iic_stop();
    
    return 0;
}
 
/**
 * @brief       ES8388读寄存器
 * @param       reg : 寄存器地址
 * @retval      读取到的数据
 */
uint8_t es8388_read_reg(uint8_t reg)
{
    uint8_t temp = 0;
 
    iic_start();
    
    iic_send_byte((ES8388_ADDR << 1) | 0);  /* 发送器件地址+写命令 */
    if (iic_wait_ack())
    {
        return 1;                           /* 等待应答(成功?/失败?) */
    }
    
    iic_send_byte(reg);                     /* 写寄存器地址 */
    if (iic_wait_ack())
    {
        return 1;                           /* 等待应答(成功?/失败?) */
    }
    
    iic_start();
    iic_send_byte((ES8388_ADDR << 1) | 1);  /* 发送器件地址+读命令 */
    if (iic_wait_ack())
    {
        return 1;                           /* 等待应答(成功?/失败?) */
    }
    
    temp = iic_read_byte(0);
    
    iic_stop();
 
    return temp;
}
 
/**
 * @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_sai_cfg(uint8_t fmt, uint8_t len)
{
    fmt &= 0x03;
    len &= 0x07;                                    /* 限定范围 */
    es8388_write_reg(23, (fmt << 1) | (len << 3));  /* R23,ES8388工作模式设置 */
}
 
/**
 * @brief       设置耳机音量
 * @param       voluem : 音量大小(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       mingain : 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配置SAI工作模式,最后设置音量才可以接收SAI音频数据,实现音乐播放。


       3. wavplay代码

       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*)mymalloc(SRAMIN, sizeof(FIL));
    buf = mymalloc(SRAMIN, 512);
    
    if (ftemp && buf)                                   /* 内存申请成功 */
    {
        res = f_open(ftemp, (TCHAR*)fname, FA_READ);    /* 打开文件 */
        
        if (res == FR_OK)
        {
            f_read(ftemp, buf, 512, &br);               /* 读取512字节在数据 */
            riff = (ChunkRIFF *)buf;                    /* 获取RIFF块 */
            if (riff->Format == 0x45564157)             /* 是WAV文件 */
            {
                fmt = (ChunkFMT *)(buf + 12);           /* 获取FMT块 */
                fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);
 /* 读取FACT块 */                    
                
                if (fact->ChunkID == 0x74636166 || fact->ChunkID == 0x5453494C)
                {
                    wavx->datastart = 12 + 8 + fmt->ChunkSize + 8 + 
fact->ChunkSize;    /* 具有fact/LIST块的时候(未测试) */
                }
                else
                {
                    wavx->datastart = 12 + 8 + fmt->ChunkSize;
                }
                
                data = (ChunkDATA *)(buf + wavx->datastart);  /* 读取DATA块 */
                
                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;     /* 位数,16/24/32位 */
                    
                    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:%d\r\n", wavx->samplerate);
                    printf("wavx->bitrate:%d\r\n", wavx->bitrate);
                    printf("wavx->blockalign:%d\r\n", wavx->blockalign);
                    printf("wavx->bps:%d\r\n", wavx->bps);
                    printf("wavx->datasize:%d\r\n", wavx->datasize);
                    printf("wavx->datastart:%d\r\n", wavx->datastart);  
                }
                else
                {
                    res = 3;    /* data区域未找到. */
                }
            }
            else
            {
                res = 2;        /* 非wav文件 */
            }
        }
        else
        {
            res = 1;            /* 打开文件错误 */
        }
    }
    
    f_close(ftemp);
    
    myfree(SRAMIN, ftemp);      /* 释放内存 */
    myfree(SRAMIN, buf); 
    
    return 0;
}
 
/**
 * @brief       填充buf
 * @param       buf  : 填充区
 * @param       size : 填充数据量
 * @param       bits : 位数(16/24)
 * @retval      读取到的数据长度
 */
uint32_t wav_buffill(uint8_t *buf, uint16_t size, uint8_t bits)
{
    uint16_t readlen = 0;
    uint32_t bread;
    uint16_t i;
    uint32_t *p, *pbuf;
    
    if (bits == 24)                             /* 24bit音频,需要处理一下 */
    {
        readlen = (size / 4) * 3;               /* 此次要读取的字节数 */
        f_read(g_audiodev.file, g_audiodev.tbuf, readlen, (UINT*)&bread);   
/* 读取数据 */
        pbuf = (uint32_t *)buf;
        
        for (i = 0; i < size / 4; i++)
        {
            p = (uint32_t *)(g_audiodev.tbuf + i * 3);
            pbuf[i] = p[0];
        }
        
        bread = (bread * 4) / 3;              /* 填充后的大小. */
    }
    else
    {
        f_read(g_audiodev.file, buf, size, (UINT*)&bread);  
/* 16bit音频,直接读取数据 */
        if (bread < size)                    /* 不够数据了,补充0 */
        {
            for (i = bread; i < size - bread; i++)
            {
                buf[i] = 0;
            }
        }
    }
    
    return bread;
}
 
/**
 * @brief       WAV播放时,SAI DMA传输回调函数
 * @param       无
 * @retval      无
 */
void wav_sai_dma_tx_callback(void) 
{   
    uint16_t i;
 
    if (node != 0)
    {
        wavwitchbuf = 0;
 
        if ((g_audiodev.status & 0X01) == 0)
        {
            for (i = 0; i < WAV_SAI_TX_DMA_BUFSIZE; i++) /* 暂停 */
            {
                g_audiodev.saibuf1[i] = 0;               /* 填充0 */
            }
        }
    }
    else 
    {
        wavwitchbuf = 1;
 
        if ((g_audiodev.status & 0X01) == 0)
        {
            for (i = 0; i < WAV_SAI_TX_DMA_BUFSIZE; i++) /* 暂停 */
            {
                g_audiodev.saibuf2[i] = 0;               /* 填充0 */
            }
        }
    }
    wavtransferend = 1;
} 
/**
 * @brief       获取当前播放时间
 * @param       fname : 文件指针
 * @param       wavx  : wavx播放控制器
 * @retval      无
 */
void wav_get_curtime(FIL *fx, __wavctrl *wavx)
{
    long long fpos;
 
    wavx->totsec = wavx->datasize / (wavx->bitrate / 8);/* 歌曲总长度(单位:秒) */
    fpos = fx->fptr-wavx->datastart;               /* 得到当前文件播放到的地方 */
    wavx->cursec = fpos * wavx->totsec / wavx->datasize;/* 当前播放到第多少秒了? */
}
 
/**
 * @brief       播放某个wav文件
 * @param       fname : 文件路径+文件名
 * @retval      KEY0_PRES,错误
 *              KEY1_PRES,打开文件失败
 *              其他,非WAV文件
 */
uint8_t wav_play_song(uint8_t *fname)
{
    uint8_t key;
    uint8_t t = 0; 
    uint8_t res;  
    uint32_t fillnum; 
    
    g_audiodev.file = (FIL*)mymalloc(SRAMIN, sizeof(FIL));
    g_audiodev.saibuf1 = mymalloc(SRAMIN, WAV_SAI_TX_DMA_BUFSIZE);
    g_audiodev.saibuf2 = mymalloc(SRAMIN, WAV_SAI_TX_DMA_BUFSIZE);
    g_audiodev.tbuf = mymalloc(SRAMIN, WAV_SAI_TX_DMA_BUFSIZE);
    
if (g_audiodev.file && g_audiodev.saibuf1 && g_audiodev.saibuf2 && 
g_audiodev.tbuf)
    { 
        res = wav_decode_init(fname, &wavctrl);         /* 得到文件的信息 */
        
        if (res == 0)                                   /* 解析文件成功 */
        {
            if (wavctrl.bps == 16)
            {
                es8388_sai_cfg(0, 3);    /* 飞利浦标准,16位数据长度 */                                 
                sai1_saib_init(SAI_MODEMASTER_TX, SAI_CLOCKSTROBING_RISINGEDGE,
 SAI_DATASIZE_16);
                sai1_tx_dma_init(g_audiodev.saibuf1, g_audiodev.saibuf2,
 WAV_SAI_TX_DMA_BUFSIZE / 2, 1); /* 配置TX DMA,16位 */
            }
            else if (wavctrl.bps == 24)
            {
                es8388_sai_cfg(0, 0);     /* 飞利浦标准,24位数据长度 */                                                               
                sai1_saia_init(SAI_MODEMASTER_TX, SAI_CLOCKSTROBING_RISINGEDGE, 
SAI_DATASIZE_24);
                sai1_tx_dma_init(g_audiodev.saibuf1, g_audiodev.saibuf2,
 WAV_SAI_TX_DMA_BUFSIZE / 4, 2); /* 配置TX DMA */
             }
 
            sai1_samplerate_set(wavctrl.samplerate);     /* 设置采样率 */
            sai1_tx_callback = wav_sai_dma_tx_callback;  
/* 回调函数指wav_sai_dma_callback */
            audio_stop();
            
            res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ);/* 打开文件 */
            
            if (res == 0)
            {
                f_lseek(g_audiodev.file, wavctrl.datastart); /* 跳过文件头 */
                fillnum = wav_buffill(g_audiodev.saibuf1,
 WAV_SAI_TX_DMA_BUFSIZE, wavctrl.bps);
                fillnum = wav_buffill(g_audiodev.saibuf2, 
WAV_SAI_TX_DMA_BUFSIZE, wavctrl.bps);
                audio_start();  
                
                while (res == 0)
                { 
                    while(wavtransferend == 0);         /* 等待wav传输完成; */
                    wavtransferend = 0;
                    
                    if (fillnum != WAV_SAI_TX_DMA_BUFSIZE)/* 播放结束? */
                    {
                        res = KEY0_PRES;
                        break;
                    }
                    
                    if (wavwitchbuf)
                    {
                        fillnum = wav_buffill(g_audiodev.saibuf2, 
WAV_SAI_TX_DMA_BUFSIZE, wavctrl.bps);   /* 填充buf2 */
                    }
                    else
                    {
                        fillnum = wav_buffill(g_audiodev.saibuf1, 
WAV_SAI_TX_DMA_BUFSIZE, wavctrl.bps);   /* 填充buf1 */
                    }
                    
                    while (1)
                    {
                        key = key_scan(0);
                        if (key == WKUP_PRES)      /* 暂停 */
                        {
                            if (g_audiodev.status & 0x01)
                            {
                                g_audiodev.status &= ~(1 << 0);
                            }
                            else 
                            {
                                g_audiodev.status |= 0x01;
                            }
                        }
                        
                        if (key == KEY1_PRES || key == KEY0_PRES)   
/* 下一曲/上一曲 */
                        {
                            res = key;
                            break;
                        }
                        
                        wav_get_curtime(g_audiodev.file, &wavctrl); 
/* 得到总时间和当前播放的时间 */
                        audio_msg_show(wavctrl.totsec, wavctrl.cursec, 
wavctrl.bitrate);
                        
                        t++;
                        if (t == 20)
                        {
                            t = 0 ;
                            LED0_TOGGLE();
                        }
                        
                        if ((g_audiodev.status & 0x01) == 0)
                        {
                            delay_ms(10);
                        }
                        else
                        {
                            break;
                        }
                    }
                }
                audio_stop();
            }
            else
            {
                res = 0xFF;
            }
        }
        else
        {
            res = 0xFF;
        }
    }
    else
    {
        res = 0xFF;
    }
    
    myfree(SRAMIN, g_audiodev.tbuf);      /* 释放内存 */
    myfree(SRAMIN, g_audiodev.saibuf1);   /* 释放内存 */
    myfree(SRAMIN, g_audiodev.saibuf2);   /* 释放内存 */
    myfree(SRAMIN, g_audiodev.file);      /* 释放内存 */
    
    return res;
}

       以上代码中,wav_decode_init函数,用来对wav音频文件进行解析,得到wav的详细信息(音频采样率,位数,数据流起始位置等);wav_buffill函数,通过f_read读取数据,然后将数据填充到buf里面,注意:在读取24位音频数据的时候,读出数据需要经过转换后才填充到buf;wav_sai_dma_tx_callback函数则是DMA发送完成的回调函数(sai_tx_callbackja函数指针指向该函数),这里,我们对数据进行了填0处理,而是采用2个变量:wavtransferend和wavwitchbuf,来告诉wav_paly_song函数是否传输完成,以及应该填充哪个数据buf(saibuf1或saibuf2)。

       最后,wav_play_song函数,是播放WAV最终执行的函数,该函数解析完WAV文件后,设置ES8388和SAI的参数(采样率,位数等),并开启DMA,然后不断填充数据,实现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;
    DIR wavdir;                             /* 目录 */
    FILINFO *wavfileinfo;                   /* 文件信息 */
    uint8_t *pname;                         /* 带路径的文件名 */
    uint16_t totwavnum;                     /* 音乐文件总数 */
    uint16_t curindex;                      /* 当前索引 */
    uint8_t key;                            /* 键值 */
    uint32_t temp;
    uint32_t *wavoffsettbl;                 /* 音乐offset索引表 */
 
    es8388_adda_cfg(1, 0);                  /* 开启DAC关闭ADC */
    es8388_output_cfg(1, 1);                /* DAC选择通道1输出 */
    es8388_hpvol_set(20);                   /* 设置耳机音量 */
    es8388_spkvol_set(10);                  /* 设置喇叭音量 */
 
    while (f_opendir(&wavdir, "0:/MUSIC"))  /* 打开音乐文件夹 */
    {
        text_show_string(30, 190, 240, 16, "MUSIC文件夹错误!", 16, 0, BLUE);
        delay_ms(200);
        lcd_fill(30, 190, 240, 206, WHITE); /* 清除显示 */
        delay_ms(200);
    }
 
    totwavnum = audio_get_tnum("0:/MUSIC"); /* 得到总有效文件数 */
    while (totwavnum == NULL)               /* 音乐文件总数为0 */
    {
        text_show_string(30, 190, 240, 16, "没有音乐文件!", 16, 0, BLUE);
        delay_ms(200);
        lcd_fill(30, 190, 240, 146, WHITE); /* 清除显示 */
        delay_ms(200);
    }
    
    wavfileinfo = (FILINFO*)mymalloc(SRAMIN, sizeof(FILINFO));  /* 申请内存 */
pname = mymalloc(SRAMIN, FF_MAX_LFN * 2 + 1);               
/* 为带路径的文件名分配内存 */
wavoffsettbl = mymalloc(SRAMIN, 4 * totwavnum);             
/* 申请4*totwavnum个字节的内存,用于存放音乐文件off block索引 */
    while (!wavfileinfo || !pname || !wavoffsettbl)         /* 内存分配出错 */
    {
        text_show_string(30, 190, 240, 16, "内存分配失败!", 16, 0, BLUE);
        delay_ms(200);
        lcd_fill(30, 190, 240, 146, WHITE);             /* 清除显示 */
        delay_ms(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)                               /* 打开成功 */
    {
        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, lcddev.width - 1, 190 + 16, WHITE); /* 清除之前的显示 */
        text_show_string(30, 190, lcddev.width - 60, 16, (char *)wavfileinfo-
>fname, 16, 0, BLUE);                          /* 显示歌曲名字 */
        audio_index_show(curindex + 1, totwavnum);
 
        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;                               /* 产生了错误 */
        }
    }
 
    myfree(SRAMIN, wavfileinfo);                 /* 释放内存 */
    myfree(SRAMIN, pname);                       /* 释放内存 */
    myfree(SRAMIN, wavoffsettbl);                /* 释放内存 */
}
 
/**
 * @brief       播放某个音频文件
 * @param       fname : 文件名
 * @retval      按键值
 *   @arg       KEY0_PRES , 下一曲.
 *   @arg       KEY2_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;
 
        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解码。


       4. main.c代码

       解决了音乐播放的问题,main.c函数实现起来就比较简单了,我们可以按照流程图的设计思路进行编写即可:

int main(void)
{
    sys_mpu_config();                   /* 配置MPU */
    sys_cache_enable();                 /* 使能Cache */
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(300, 6, 2);    /* 配置时钟,600MHz */
    delay_init(600);                    /* 初始化延时 */
    usart_init(115200);                 /* 初始化串口 */
    led_init();                         /* 初始化LED */
    key_init();                         /* 初始化按键 */
    hyperram_init();                    /* 初始化HyperRAM */
    lcd_init();                         /* 初始化LCD */
    my_mem_init(SRAMIN);                /* 初始化AXI-SRAM1~4内存池 */
    my_mem_init(SRAMEX);                /* 初始化XSPI2 HyperRAM内存池 */
    my_mem_init(SRAM12);                /* 初始化AHB-SRAM1~2内存池 */
    my_mem_init(SRAMDTCM);              /* 初始化DTCM内存池 */
    my_mem_init(SRAMITCM);              /* 初始化ITCM内存池 */
    exfuns_init();                      /* 为exfuns申请内存 */
    f_mount(fs[0], "0:", 1);            /* 挂载SD卡 */
    f_mount(fs[1], "1:", 1);            /* 挂载NOR Flash */
    f_mount(fs[2], "2:", 1);            /* 挂载NAND Flash */
    
    /* 初始化SD卡 */
    while (sd_init() != 0)
    {
        lcd_show_string(30, 30, 200, 16, 16, "SD Card Error!", RED);
        delay_ms(500);
        lcd_show_string(30, 30, 200, 16, 16, "Please Check! ", RED);
        delay_ms(500);
    }
    lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
    
    /* 检查字库 */
    while (fonts_init() != 0)
    {
        lcd_show_string(30, 30, 200, 16, 16, "Font Error!  ", RED);
        delay_ms(500);
        lcd_show_string(30, 30, 200, 16, 16, "Please Check!", RED);
        delay_ms(500);
    }
    lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
    
    text_show_string(30, 50, 200, 16, "正点原子STM32开发板",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", 16, 0, RED);
    text_show_string(30, 130, 200, 16, "KEY1: Prev", 16, 0, RED);
    text_show_string(30, 150, 200, 16, "KWY_UP: Play/Pause", 16, 0, RED);
    
    es8388_init();                      /* ES8388初始化 */
    es8388_adda_cfg(1, 0);              /* 开启DAC关闭ADC */
    es8388_output_cfg(1, 1);            /* DAC选择通道输出 */
    es8388_hpvol_set(25);               /* 设置耳机音量 */
    es8388_spkvol_set(25);              /* 设置喇叭音量 */
    
    while (1)
    {
        audio_play();
    }
}

       到这里本实验的代码基本就编写完成了,我们准备好音乐文件放到SD卡根目录下的《MUSIC》夹下测试本实验的代码。 


        59.4 下载验证

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


图59.4.1音乐播放中


       从上图可以看出,总共1首歌曲,当前正在播放第1首歌曲,歌曲名、播放时间、总时长、码率、音量等信息等也都有显示。此时DS0会随着音乐的播放而闪烁。

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

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


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