正点原子探索者原理图_正点原子【STM32-F407探索者】第四十八章 音乐播放器实验...

1)资料下载:点击资料即可下载

2)对正点原子Linux感兴趣的同学可以加群讨论:935446741

3)关注正点原子公众号,获取最新资料更新

173e7449362367d87ea95f2728e9c661.png

ALIENTEK 探索者 STM32F4 开发板拥有全双工 I2S,且外扩了一颗 HIFI 级 CODEC 芯片: WM8978G,支持最高 192K 24BIT 的音频播放,并且支持录音(下一章介绍)。本章,我们将 利用探索者 STM32F4 开发板实现一个简单的音乐播放器(仅支持 WAV 播放)。本章分为如下 几个部:

48.1 WAV&WM8978&I2S 简介

48.2 硬件设计

48.3 软件设计

48.4 下载验证

48.1 WAV&WM8978&I2S 简介

本章新知识点比较多,包括:WAV、WM8978 和 I2S 等三个知识点。下面我们将分别向大

家介绍。

48.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 由块标识符、数据大小和数据三部分组成,如图 48.1.1.1 所示:

ed69483a5d4c29579ca6c55a4c101497.png
图 48.1.1.1 Chunk 结构示意图

其中块标识符由 4 个 ASCII 码构成,数据大小则标出紧跟其后的数据的长度(单位为字节),

注意这个长度不包含块标识符和数据大小的长度,即不包含最前面的 8 个字节。所以实际 Chunk

的大小为数据大小加 8。

首先,我们来看看 RIFF 块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟 wav

文件大小(该大小是 wav 文件的总大小-8),然后数据段为“WAVE”,表示是 wav 文件。RIFF

块的 Chunk 结构如下:

//RIFF 块

typedef __packed struct

{

u32 ChunkID;

//chunk id;这里固定为"RIFF",即 0X46464952

u32 ChunkSize ;

//集合大小;文件总大小-8

u32 Format;

//格式;WAVE,即 0X45564157

}ChunkRIFF ;

接着,我们看看 Format 块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!),

一般情况下,该段的大小为 16 个字节,但是有些软件生成的 wav 格式,该部分可能有 18 个字

节,含有 2 个字节的附加信息。Format 块的 Chunk 结构如下:

//fmt 块

typedef __packed struct

{

u32 ChunkID;

//chunk id;这里固定为"fmt ",即 0X20746D66

u32 ChunkSize ;

//子集合大小(不包括 ID 和 Size);这里为:20.

u16 AudioFormat; //音频格式;0X10,表示线性 PCM;0X11 表示 IMA ADPCM

u16 NumOfChannels; //通道数量;1,表示单声道;2,表示双声道;

u32 SampleRate;

//采样率;0X1F40,表示 8Khz

u32 ByteRate;

//字节速率;

u16 BlockAlign;

//块对齐(字节);

u16 BitsPerSample;

//单个采样数据大小;4 位 ADPCM,设置为 4

}ChunkFMT;

接下来,我们再看看 Fact 块(Fact Chunk),该块为可选块,以“fact”作为标示,不是

每个 WAV 文件都有,在非 PCM 格式的文件中,一般会在 Format 结构后面加入一个 Fact 块,

该块 Chunk 结构如下:

//fact 块

typedef __packed struct

{

u32 ChunkID;

//chunk id;这里固定为"fact",即 0X74636166;

u32 ChunkSize ;

//子集合大小(不包括 ID 和 Size);这里为:4.

u32 DataFactSize;

//数据转换为 PCM 格式后的大小

}ChunkFACT;

DataFactSize 是这个 Chunk 中最重要的数据,如果这是某种压缩格式的声音文件,那么从

这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的

是 PCM 格式,所以不存在这个块。

最后,我们来看看数据块(Data Chunk),该块是真正保存 wav 数据的地方,以“data”

'作为该 Chunk 的标示,然后是数据的大小。数据块的 Chunk 结构如下:

//data 块

typedef __packed struct

{

u32 ChunkID;

//chunk id;这里固定为"data",即 0X61746164

u32 ChunkSize ;

//子集合大小(不包括 ID 和 Size);文件大小-60.

}ChunkDATA;

ChunkSize 后紧接着就是 wav 数据。根据 Format Chunk 中的声道数以及采样 bit 数,wav

数据的 bit 位置可以分成如表 48.1.1.1 所示的几种形式:

d7c98db4676fe8dccbad576328ad967f.png

cd7c1740641126d39ff313e29fb14915.png
表 48.1.1.1 WAVE 文件数据采样格式

本章,我们播放的音频支持:16 位和 24 位,立体声,所以每个取样为 4/6 个字节,低字

节在前,高字节在后。在得到这些 wav 数据以后,通过 I2S 丢给 WM8978,就可以欣赏音乐了。

48.1.2 WM8978 简介

WM8978 是欧胜(Wolfson)推出的一款全功能音频处理器。它带有一个 HI-FI 级数字信号

处理内核,支持增强 3D 硬件环绕音效,以及 5 频段的硬件均衡器,可以有效改善音质;并有

一个可编程的陷波滤波器,用以去除屏幕开、切换等噪音。

WM8978 同样集成了对麦克风的支持,以及用于一个强悍的扬声器功放,可提供高达

900mW 的高质量音响效果扬声器功率。

一个数字回放限制器可防止扬声器声音过载。WM8978 进一步提升了耳机放大器输出功率,

在推动 16 欧姆耳机的时候,每声道最大输出功率高达 40 毫瓦!可以连接市面上绝大多数适合

随身听的高端 HI-FI 耳机。

WM8988 的主要特性有:

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

●DAC 信噪比 98dB;ADC 信噪比 90dB

●支持无电容耳机驱动(提供 40mW@16Ω的输出能力)

●支持扬声器输出(提供 0.9W@8Ω的驱动能力)

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

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

●支持 3D 效果,支持 5 路 EQ 调节

WM8978 的控制通过 I2S 接口(即数字音频接口)同 MCU 进行音频数据传输(支持音频接收

和发送),通过两线(MODE=0,即 IIC 接口)或三线(MODE=1)接口进行配置。WM8978 的 I2S

接口,由 4 个引脚组成:

1,ADCDAT:ADC 数据输出

2,DACDAT:DAC 数据输入

3,LRC:数据左/右对齐时钟

4,BCLK:位时钟,用于同步

WM8978 可作为 I2S 主机,输出 LRC 和 BLCK 时钟,不过我们一般使用 WM8978 作为从机,接

收 LRC 和 BLCK。另外,WM8978 的 I2S 接口支持 5 中不同的音频数据模式:左(MSB)对齐标准、

右(LSB)对齐标准、飞利浦(I2S)标准、DSP 模式 A 和 DSP 模式 B。本章,我们用飞利浦标准

来传输 I2S 数据。

飞利浦(I2S)标准模式,数据在跟随 LRC 传输的 BCLK 的第二个上升沿时传输 MSB,其他

位一直到 LSB 按顺序传输。传输依赖于字长、BCLK 频率和采样率,在每个采样的 LSB 和下一个

采样的 MSB 之间都应该有未用的 BCLK 周期。飞利浦标准模式的 I2S 数据传输协议如图 48.1.2.1 所示:

7430c9417fcee7bf4cb5dfc0f9b41324.png
图 48.1.2.1 飞利浦标准模式 I2S 数据传输图

图中,fs 即音频信号的采样率,比如 44.1Khz,因此可以知道,LRC 的频率就是音频信号

的采样率。另外,WM8978 还需要一个 MCLK,本章我们采用 STM32F4 为其提供 MCLK 时钟,MCLK

的频率必须等于 256fs,也就是音频采样率的 256 倍。

WM8978 的框图如图 48.1.2.2 所示:

9bbce37e7539e300d678f6349e5a0571.png
图 48.1.2.2 WM8978 框图

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

本章,我们通过 IIC 接口(MODE=0)连接 WM8978,不过 WM8978 的 IIC 接口比较特殊:

1,只支持写,不支持读数据;2,寄存器长度为 7 位,数据长度为 9 位。3,寄存器字节的最低

位用于传输数据的最高位(也就是 9 位数据的最高位,7 位寄存器的最低位)。WM8978 的 IIC

地址固定为:0X1A。关于 WM8978 的 IIC 详细介绍,请看其数据手册第 77 页。

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

1,寄存器 R0(00h),该寄存器用于控制 WM8978 的软复位,写任意值到该寄存器地址,

即可实现软复位 WM8978。

2,寄存器 R1(01h),该寄存器主要要设置 BIASEN(bit3),该位设置为 1,模拟部分

的放大器才会工作,才可以听到声音。

3,寄存器 R2(02h),该寄存器要设置 ROUT1EN(bit8),LOUT1EN(bit7)和 SLEEP(bit6)

等三个位,ROUT1EN 和 LOUT1EN,设置为 1,使能耳机输出,SLEEP 设置为 0,进入正

常工作模式。

4,寄存器 R3(03h),该寄存器要设置 LOUT2EN(bit6),ROUT2EN(bit5),RMIXER(bit3),

LMIXER(bit2),DACENR(bit1)和 DACENL(bit0)等 6 个位。LOUT2EN 和 ROUT2EN,设置

为 1,使能喇叭输出;LMIXER 和 RMIXER 设置为 1,使能左右声道混合器;DACENL 和

DACENR 则是使能左右声道的 DAC 了,必须设置为 1。

5,寄存器 R4(04h),该寄存器要设置 WL(bit6:5)和 FMT(bit4:3)等 4 个位。WL(bit6:5)用

于设置字长(即设置音频数据有效位数),00 表示 16 位音频,10 表示 24 位音频;FMT(bit4:3)

用于设置 I2S 音频数据格式(模式),我们一般设置为 10,表示 I2S 格式,即飞利浦模式。

6,寄存器 R6(06h),该寄存器我们直接全部设置为 0 即可,设置 MCLK 和 BCLK 都来

自外部,即由 STM32F4 提供。

7,寄存器 R10(0Ah),该寄存器我们要设置 SOFTMUTE(bit6)和 DACOSR128(bit3)等两

个位,SOFTMUTE 设置为 0,关闭软件静音;DACOSR128 设置为 1,DAC 得到最好的 SNR。

8,寄存器 R43(2Bh),该寄存器我们只需要设置 INVROUT2 为 1 即可,反转 ROUT2 输

出,更好的驱动喇叭。

9,寄存器 R49(31h),该寄存器我们要设置 SPKBOOST(bit2)和 TSDEN(bit1)这两个位。

SPKBOOST 用于设置喇叭的增益,我们默认设置为 0 就好了(gain=-1),如想获得更大的

声音,设置为 1(gain=+1.5)即可;TSDEN 用于设置过热保护,设置为 1(开启)即可。

10,寄存器 R50(32h)和 R51(33h),这两个寄存器设置类似,一个用于设置左声道(R50),

另外一个用于设置右声道(R51)。我们只需要设置这两个寄存器的最低位为 1 即可,将

左右声道的 DAC 输出接入左右声道混合器里面,才能在耳机/喇叭听到音乐。

11,寄存器 R52(34h)和 R53(35h),这两个寄存器用于设置耳机音量,同样一个用于

设置左声道(R52),另外一个用于设置右声道(R53)。这两个寄存器的最高位(HPVU)

用于设置是否更新左右声道的音量,最低 6 位用于设置左右声道的音量,我们可以先设置

好两个寄存器的音量值,最后设置其中一个寄存器最高位为 1,即可更新音量设置。

12,寄存器 R54(36h)和 R55(37h),这两个寄存器用于设置喇叭音量,同 R52,R53

设置一模一样,这里就不细说了。

以上,就是我们用 WM8978 播放音乐时的设置,按照以上所述,对各个寄存器进行相应的

配置,即可使用 WM8978 正常播放音乐了。还有其他一些 3D 设置,EQ 设置等,我们这里就 不再介绍了,大家参考 WM8978 的数据手册自行研究下即可。

48.1.3 I2S 简介

I2S(Inter IC Sound)总线, 又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间

的音频数据传输而制定的一种总线标准,该总线专责于音频设备之间的数据传输,广泛应用于

各种多媒体系统。它采用了沿独立的导线传输时钟与数据信号的设计,通过将数据和时钟信号

分离,避免了因时差诱发的失真,为用户节省了购买抵抗音频抖动的专业设备的费用。

STM32F4 自带了 2 个全双工 I2S 接口,其特点包括:

●支持全双工/半双工通信

●主持主/从模式设置

●8 位可编程线性预分频器,可实现精确的音频采样频率(8~192Khz)

●支持 16 位/24 位/32 位数据格式

●数据包帧固定为 16 位(仅 16 位数据帧)或 32 位(可容纳 16/24/32 位数据帧)

●可编程时钟极性

●支持 MSB 对齐(左对齐)、LSB 对齐(右对齐)、飞利浦标准和 PCM 标准等 I2S 协议

●支持 DMA 数据传输(16 位宽)

●数据方向固定位 MSB 在前

●支持主时钟输出(固定为 256*fs,fs 即音频采样率)

STM32F4 的 I2S 框图如图 48.1.3.1 所示:

921449c95b19378fc08d78d5c17d8757.png
图 48.1.3.1 I2S 框图

STM32F4 的 I2S 是与 SPI 部分共用的,通过设置 SPI_I2SCFGR 寄存器的 I2SMOD 位即可

开启 I2S 功能,I2S 接口使用了几乎与 SPI 相同的引脚、标志和中断。

I2S 用到的信号有:

1,SD:串行数据(映射到 MOSI 引脚),用于发送或接收两个时分复用的数据通道上的

数据(仅半双工模式)。

2,WS:字选择(映射到 NSS 引脚),即帧时钟,用于切换左右声道的数据。WS 频率等

于音频信号采样率(fs)。

3,CK:串行时钟(映射到 SCK 引脚),即位时钟,是主模式下的串行时钟输出以及从模

式下的串行时钟输入。CK 频率=WS 频率(fs)*2*16(16 位宽),如果是 32 位宽,则是:CK

频率=WS 频率(fs)*2*32(32 位宽)

4,I2S2ext_SD 和 I2S3ext_SD:用于控制 I2S 全双工模式的附加引脚(映射到 MISO 引脚)。

5,MCK:即主时钟输出,当 I2S 配置为主模式(并且 SPI_I2SPR 寄存器中的 MCKOE 位

置 1)时,使用此时钟,该时钟输出频率 256×fs,fs 即音频信号采样频率(fs)。

为支持 I2S 全双工模式,除了 I2S2 和 I2S3,还可以使用两个额外的 I2S,它们称为扩展 I2S

(I2S2_ext、I2S3_ext),如图 48.1.3.2:

5736c9c1aaa6bdd0e7dabc647ad963ea.png
图 48.1.3.2 I2S 全双工框图

因此,第一个 I2S 全双工接口基于 I2S2 和 I2S2_ext,第二个基于 I2S3 和 I2S3_ext。注意:

I2S2_ext 和 I2S3_ext 仅用于全双工模式。

I2Sx 可以在主模式下工作。因此:

1,只有 I2Sx 可在半双工模式下输出 SCK 和 WS

2,只有 I2Sx 可在全双工模式下向 I2S2_ext 和 I2S3_ext 提供 SCK 和 WS。

扩展 I2S (I2Sx_ext)只能用于全双工模式。I2Sx_ext 始终在从模式下工作。I2Sx 和 I2Sx_ext

均可用于发送和接收。

STM32F4 的 I2S 支持 4 种数据和帧格式组合,分别是:1,将 16 位数据封装在 16 位帧中;

2,将 16 位数据封装在 32 位帧中;3,将 24 位数据封装在 32 位帧中;4,将 32 位数据封装在

32 位帧中。

将 16 位数据封装在 32 位帧中时,前 16 位(MSB)为有效位,16 位 LSB 被强制清零,无需

任何软件操作或 DMA 请求(只需一个读/写操作)。如果应用程序首选 DMA,则 24 位和 32

位数据帧需要对 SPI_DR 执行两次 CPU 读取或写入操作,或者需要两次 DMA 操作。24 位的数

据帧,硬件会将 8 位非有效位扩展到带有 0 位的 32 位。

对于所有数据格式和通信标准而言,始终会先发送最高有效位(MSB 优先)。

STM32F4 的 I2S 支持:MSB 对齐(左对齐)标准、LSB 对齐(右对齐)标准、飞利浦标准和

PCM 标准等 4 种音频标准,本章我们用飞利浦标准,仅针对该标准进行介绍,其他的请大家参

考《STM32F4xx 中文参考手册》第 27.4 节。

I2S 飞利浦标准,使用 WS 信号来指示当前正在发送的数据所属的通道。该信号从当前通道

数据的第一个位(MSB)之前的一个时钟开始有效。发送方在时钟信号(CK)的下降沿改变数据,接

收方在上升沿读取数据。WS 信号也在 CK 的下降沿变化。这和我们 48.1.2 节介绍的是一样的。

本章我们使用 16 位/24 位数据格式,16 位时采用扩展帧格式(即将 16 位数据封装在 32 位

帧中),以 24 位帧为例,I2S 波形(飞利浦标准)如图 48.1.3.3 所示:

68701624640187539bce1c110f436e7b.png
图 48.1.3.3 I2S 飞利浦标准 24 位帧格式波形

这个图和图 48.1.2.1 是一样的时序,在 24 位模式下数据传输,需要对 SPI_DR 执行两次读

取或写入操作。比如我们要发送 0X8EAA33 这个数据,就要分两次写入 SPI_DR,第一次写入:

0X8EAA,第二次写入 0X33xx(xx 可以为任意数值),这样就把 0X8EAA33 发送出去了。

顺便说一下 SD 卡读取到的 24 位 WAV 数据流,是低字节在前,高字节在后的,比如,我们

读到一个声道的数据(24bit),存储在 buf[3]里面,那么要通过 SPI_DR 发送这个 24 位数据,

过程如下:

SPI_DR=((u16)buf[2]<<8)+buf[1];

SPI_DR=(u16)buf[0]<<8;

这样,第一次发送高 16 为数据,第二次发送低 8 位数据,完成一次 24bit 数据的发送。

接下来,我们介绍下 STM32F4 的 I2S 时钟发生器,其架构如图 48.1.3.4 所示:

图中 I2SxCLK 可以来自 PLLI2S 输出(通过 R 系数分频)或者来自外部时钟(I2S_CKIN

引脚),一般我们使用前者作为 I2SxCLK 输入时钟。

一般我们需要根据音频采样率(fs,即 CK 的频率)来计算各个分频器的值,常用的音频

采样率有:22.05Khz、44.1Khz、48Khz、96Khz、196Khz 等。

根据是否使能 MCK 输出,fs 频率的计算公式有 2 种情况。不过,本章只考虑 MCK 输出

使能时的情况,当 MCK 输出使能时,fs 频率计算公式如下:

fs=I2SxCLK/[256*(2*I2SDIV+ODD)

其中:I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR。HSE 我们是 8Mhz,而 pllm 在系统时钟

初始化就确定了,是 8,这样结合以上 2 式,可得计算公式如下:

fs= (1000*PLLI2SN/PLLI2SR )/[256*(2*I2SDIV+ODD)]

fs 单位是:Khz。其中:PLL2SN 取值范围:192~432;PLLI2SR 取值范围:2~7;I2SDIV

取值范围:2~255;ODD 取值范围:0/1。根据以上约束条件,我们便可以根据 fs 来设置各个系

数的值了,不过很多时候,并不能取得和 fs 一模一样的频率,只能近似等于 fs,比如 44.1Khz

采样率,我们设置 PLL2SN=271,PLL2SR=2,I2SDIV=6,ODD=0,得到 fs=44.108073Khz,误

差为:0.0183%。晶振频率决定了有时无法通过分频得到我们所要的 fs,所以,某些 fs 如果要

实现 0 误差,大家必须得选用外部时钟才可以。

如果要通过程序去计算这些系数的值,是比较麻烦的,所以,我们事先计算好常用 fs 对应

的系数值,建立一个表,这样,用的时候,只需要查表取值就可以了,大大简化了代码,常用

fs 对应系数表如下

edc4c082fbbd178f08a8b8d1d5f94983.png
图 48.1.3.4 I2S 时钟发生器架构

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

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

首先,是 SPI_I2S 配置寄存器:SPI_I2SCFGR,该寄存器各位描述如图 48.1.3.5 所示:

2285d1cb2b49142784ad95e523672e28.png
图 48.1.3.5 寄存器 SPI_I2SCFGR 各位描述

I2SMOD 位,设置为 1,选择 I2S 模式,注意,必须在 I2S/SPI 禁止的时候,设置该位。

I2SE 位,设置为 1,使能 I2S 外设,该位必须在 I2SMOD 位设置之后再设置。

I2SCFG[1:0]位, 这两个位用于配置 I2S 模式,设置为 10,选择主模式(发送)。

I2SSTD[1:0]位,这两个位用于选择 I2S 标准,设置为 00,选择飞利浦模式。

CKPOL 位,用于设置空闲时时钟电平,设置为 0,空闲时时钟低电平。

DATLEN[1:0]位,用于设置数据长度,00,表示 16 位数据;01 表示 24 位数据。

CHLEN 位,用于设置通道长度,即帧长度,0,表示 16 位;1,表示 32 位。

第二个是 SPI_I2S 预分配器寄存器:SPI_I2SPR,该寄存器各位描述如图 48.1.3.6 所示:

023773db71cec3b02f35b778de4edf40.png
图 48.1.3.6 寄存器 SPI_ I2SPR 各位描述

本章我们设置 MCKOE 为 1,开启 MCK 输出,ODD 和 I2SDIV 则根据不同的 fs,查表进

行设置。

第三个是 PLLI2S 配置寄存器:RCC_PLLI2SCFGR,该寄存器各位描述如图 48.1.3.7 所示:

该寄存器用于配置 PLLI2SR 和 PLLI2SN 两个系数,PLLI2SR 的取值范围是:2~7,PLLI2SN

的取值范围是:192~432。同样,这两个也是根据 fs 的值来设置的。

此外,还要用到 SPI_CR2 寄存器的 bit1 位,设置 I2S TX DMA 数据传输,SPI_DR 寄存器

用于传输数据,本章用 DMA 来传输,所以直接设置 DMA 的外设地址位 SPI_DR 即可。

最后,我们看看要通过 STM32F4 的 I2S,驱动 WM8978 播放音乐的简要步骤。这里需要

说明一下,I2S 相关的库函数申明和定义在 stm32f4xx_hal_i2s.c/ stm32f4xx_hal_i2s_ex.c 以及头

文件 stm32f4xx_hal_i2s.h/ stm32f4xx_hal_i2s_ex.h 中。具体步骤如下:

1)初始化 WM8978

这个过程就是在 48.1.2 节最后那十几个寄存器的配置,包括软复位、DAC 设置、输出设置

和音量设置等。在我们实验工程中是在文件 wm8978.c 中,大家可以打开实验工程参考。

2)初始化 I2S

此过程主要设置 SPI_I2SCFGR 寄存器,设置 I2S 模式、I2S 标准、时钟空闲电平和数据帧

长等,最后开启 I2S TX DMA,使能 I2S 外设。在库函数中初始化 I2S 调用的函数为:

HAL_StatusTypeDef HAL_I2S_Init(I2S_HandleTypeDef *hi2s)

我们主要讲解结构体 I2S_HandleTypeDef 各个成员变量的含义。结构体 I2S_HandleTypeDef

的定义为:

typedef struct __I2S_HandleTypeDef

{

SPI_TypeDef

*Instance;

I2S_InitTypeDef

Init;

uint16_t

*pTxBuffPtr;

__IO uint16_t

TxXferSize;

__IO uint16_t

TxXferCount;

uint16_t

*pRxBuffPtr;

__IO uint16_t

RxXferSize;

__IO uint16_t

RxXferCount;

void (*IrqHandlerISR)(struct __I2S_HandleTypeDef *hi2s);

DMA_HandleTypeDef *hdmatx;

DMA_HandleTypeDef *hdmarx;

__IO HAL_LockTypeDef Lock;

__IO HAL_I2S_StateTypeDef

State;

__IO uint32_t

ErrorCode;

} I2S_HandleTypeDef;

3)解析 WAV 文件,获取音频信号采样率和位数并设置 I2S 时钟分频器

这里,要先解析 WAV 文件,取得音频信号的采样率(fs)和位数(16 位或 32 位),根据

这两个参数,来设置 I2S 的时钟分频,这里我们用前面介绍的查表法来设置即可。这是我们单

独写了一个设置频率的函数为 I2S2_SampleRate_Set,我们后面程序章节会讲解。

4)设置 DMA

I2S 播放音频的时候,一般都是通过 DMA 来传输数据的,所以必须配置 DMA,本章我们

用 I2S2,其 TX 是使用的 DMA1 数据流 4 的通道 0 来传输的。并且,STM32F4 的 DMA 具有双

缓冲机制,这样可以提高效率,大大方便了我们的数据传输,本章将 DMA1 数据流 4 设置为:

双缓冲循环模式,外设和存储器都是 16 位宽,并开启 DMA 传输完成中断(方便填充数据)。

DMA 具体配置过程请参考我们光盘工程代码,前面 DMA 实验我们已经讲解过 DMA 相关配置

过程。

5)编写 DMA 传输完成中断服务函数

为了方便填充音频数据,我们使用 DMA 传输完成中断,每当一个缓冲数据发送完后,硬

件自动切换为下一个缓冲,同时进入中断服务函数,填充数据到发送完的这个缓冲。过程如图

48.1.3.8 所示:

1888898842fe50f2686823f661ba3d4c.png
图 48.1.3.7 寄存器 RCC_ PLLI2SCFGR 各位描述

47f7216610008e76a06520d1efc87039.png

6)开启 DMA 传输,填充数据

最后,我们就只需要开启 DMA 传输,然后及时填充 WAV 数据到 DMA 的两个缓存区即

可。此时,就可以在 WM8978 的耳机和喇叭通道听到所播放音乐了。操作方法为:

__HAL_DMA_ENABLE(&I2S2_TXDMA_Handler);//开启 DMA TX 传输

48.2 硬件设计

本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,

则开始循环播放 SD 卡 MUSIC 文件夹里面的歌曲(必须在 SD 卡根目录建立一个 MUSIC 文件

夹,并存放歌曲(仅支持 wav 格式)在里面),在 TFTLCD 上显示歌曲名字、播放时间、歌

曲总时间、歌曲总数目、当前歌曲的编号等信息。KEY0 用于选择下一曲,KEY2 用于选择上

一曲,KEY_UP 用来控制暂停/继续播放。DS0 还是用于指示程序运行状态。

本实验用到的资源如下:

1) 指示灯 DS0

2) 三个按键(KEY_UP/KEY0/KEY1)

3) 串口

4) TFTLCD 模块

5) SD 卡

6) SPI FLASH

7) WM8978

8) I2S2

这些硬件我们都已经介绍过了,不过 WM8978 和 STM32F4 的连接,还没有介绍,连接如

图 48.2.1 所示:

图中,PHONE 接口,可以用来插耳机,P1 接口,可以外接喇叭(1W@8Ω,需自备)。硬

件上,IIC 接口和 24C02,MPU6050 等共用,另外 I2S_MCLK 和 DCMI_D0 共用,所以 I2S 和

DCMI 不可以同时使用。

本实验,大家需要准备 1 个 SD 卡(在里面新建一个 MUSIC 文件夹,并存放一些 wav 歌

曲在 MUSIC 文件夹下)和一个耳机(或喇叭),分别插入 SD 卡接口和耳机接口(喇叭接 P1

接口),然后下载本实验就可以通过耳机来听歌了。

48.3 软件设计

打开本章实验工程目录可以看到,我们在工程根目录文件夹下新建 APP 和 AUDIOCODEC

两个文件夹。在 APP 文件夹里面新建了 audioplay.c 和 audioplay.h 两个文件。在 AUDIOCODEC

文件夹里面新建了 wav 文件夹,然后在其中新建了 wavplay.c 和 wavplay.h 两个文件。同时,我

们把相关的源文件引入工程相应分组,同时将 APP 和 wav 文件夹加入头文件包含路径。

然后,我们在 HARDWARE 文件夹下新建了 WM8978 和 I2S 两个文件夹,在 WM8978 文

件夹里面新建了 wm8978.c 和 wm8978.h 两个文件,在 I2S 文件夹里面新建了 i2s.c 和 i2s.h 两个

文件。

最后将 wm8978.c 和 i2s.c 添加到工程 HARDWARE 组下。同时相应的头文件加入到 PATH

中。

本章代码比较多,我们就不全部贴出来给大家介绍了,这里仅挑一些重点函数给大家介绍

下。首先是 i2s.c 里面,重点函数代码如下:

I2S_HandleTypeDef I2S2_Handler;

//I2S2 句柄

DMA_HandleTypeDef I2S2_TXDMA_Handler;

//I2S2 发送 DMA 句柄

void I2S2_Init(u32 I2S_Standard,u32 I2S_Mode,u32 I2S_Clock_Polarity,u32 I2S_DataFormat)

{

I2S2_Handler.Instance=SPI2;

I2S2_Handler.Init.Mode=I2S_Mode;

//IIS 模式

I2S2_Handler.Init.Standard=I2S_Standard;

//IIS 标准

I2S2_Handler.Init.DataFormat=I2S_DataFormat;

//IIS 数据长度

I2S2_Handler.Init.MCLKOutput=I2S_MCLKOUTPUT_ENABLE; //主时钟输出使能

I2S2_Handler.Init.AudioFreq=I2S_AUDIOFREQ_DEFAULT;

//IIS 频率设置

I2S2_Handler.Init.CPOL=I2S_Clock_Polarity;

//空闲状态时钟电平

I2S2_Handler.Init.ClockSource=I2S_CLOCK_PLL;

//IIS 时钟源为 PLL

HAL_I2S_Init(&I2S2_Handler);

SPI2->CR2|=1<<1;

//SPI2 TX DMA 请求使能.

__HAL_I2S_ENABLE(&I2S2_Handler);

//使能 I2S2

}

//I2S 底层驱动,时钟使能,引脚配置,DMA 配置

//此函数会被 HAL_I2S_Init()调用

//hi2s:I2S 句柄

void HAL_I2S_MspInit(I2S_HandleTypeDef *hi2s)

{

GPIO_InitTypeDef GPIO_Initure;

__HAL_RCC_SPI2_CLK_ENABLE();

//使能 SPI2/I2S2 时钟

__HAL_RCC_GPIOB_CLK_ENABLE();

//使能 GPIOB 时钟

__HAL_RCC_GPIOC_CLK_ENABLE();

//使能 GPIOC 时钟

bfecf441cca3a7bef91e1f3fcd162e7c.png
图 48.2.1 WM8978 与 STM32F4 连接原理图

//初始化 PB12/13

GPIO_Initure.Pin=GPIO_PIN_12|GPIO_PIN_13;

GPIO_Initure.Mode=GPIO_MODE_AF_PP;

//推挽复用

GPIO_Initure.Pull=GPIO_PULLUP; //上拉

GPIO_Initure.Speed=GPIO_SPEED_HIGH;

//高速

GPIO_Initure.Alternate=GPIO_AF5_SPI2;

//复用为 SPI/I2S

HAL_GPIO_Init(GPIOB,&GPIO_Initure);

//初始化

//初始化 PC2/PC3/PC6

GPIO_Initure.Pin=GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_6;

HAL_GPIO_Init(GPIOC,&GPIO_Initure);

//初始化

}

//采样率计算公式:Fs=I2SxCLK/[256*(2*I2SDIV+ODD)]

//I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR

//一般 HSE=8Mhz

//pllm:在 Sys_Clock_Set 设置的时候确定,一般是 8

//PLLI2SN:一般是 192~432

//PLLI2SR:2~7

//I2SDIV:2~255

//ODD:0/1

//I2S 分频系数表@pllm=8,HSE=8Mhz,即 vco 输入频率为 1Mhz

//表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD

const u16 I2S_PSC_TBL[][5]=

{

……//省略部分代码,见 48.1.3 节介绍

};

//开启 I2S 的 DMA 功能,HAL 库没有提供此函数

//因此我们需要自己操作寄存器编写一个

void I2S_DMA_Enable(void)

{

u32 tempreg=0;

tempreg=SPI2->CR2; //先读出以前的设置

tempreg|=1<<1;

//使能 DMA

SPI2->CR2=tempreg;

//写入 CR1 寄存器中

}

//设置 SAIA 的采样率(@MCKEN)

//samplerate:采样率,单位:Hz

//返回值:0,设置成功;1,无法设置.

u8 I2S2_SampleRate_Set(u32 samplerate)

{

u8 i=0;

u32 tempreg=0;

RCC_PeriphCLKInitTypeDef RCCI2S2_ClkInitSture;

for(i=0;i<(sizeof(I2S_PSC_TBL)/10);i++)//看看改采样率是否可以支持

if((samplerate/10)==I2S_PSC_TBL[i][0])break;

}

if(i==(sizeof(I2S_PSC_TBL)/10))return 1;//搜遍了也找不到

RCCI2S2_ClkInitSture.PeriphClockSelection=RCC_PERIPHCLK_I2S;//外设时钟源选择

RCCI2S2_ClkInitSture.PLLI2S.PLLI2SN=(u32)I2S_PSC_TBL[i][1]; //设置 PLLI2SN

RCCI2S2_ClkInitSture.PLLI2S.PLLI2SR=(u32)I2S_PSC_TBL[i][2]; //设置 PLLI2SR

HAL_RCCEx_PeriphCLKConfig(&RCCI2S2_ClkInitSture); //设置时钟

RCC->CR|=1<<26;

//开启 I2S 时钟

while((RCC->CR&1<<27)==0);

//等待 I2S 时钟开启成功.

tempreg=I2S_PSC_TBL[i][3]<<0;

//设置 I2SDIV

tempreg|=I2S_PSC_TBL[i][4]<<8;

//设置 ODD 位

tempreg|=1<<9;

//使能 MCKOE 位,输出 MCK

SPI2->I2SPR=tempreg;

//设置 I2SPR 寄存器

return 0;

}

//I2S2 TX DMA 配置

//设置为双缓冲模式,并开启 DMA 传输完成中断

//buf0:M0AR 地址.

//buf1:M1AR 地址.

//num:每次传输数据量

void I2S2_TX_DMA_Init(u8* buf0,u8 *buf1,u16 num)

{

__HAL_RCC_DMA1_CLK_ENABLE(); //使能 DMA1 时钟

__HAL_LINKDMA(&I2S2_Handler,hdmatx,I2S2_TXDMA_Handler);

//将 DMA 与 I2S 联系起来

I2S2_TXDMA_Handler.Instance=DMA1_Stream4; //DMA1 数据流 4

I2S2_TXDMA_Handler.Init.Channel=DMA_CHANNEL_0; //通道 0

I2S2_TXDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH;

//存储器到外设模式

I2S2_TXDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE;

//外设非增量模式

I2S2_TXDMA_Handler.Init.MemInc=DMA_MINC_ENABLE;

//存储器增量模式

I2S2_TXDMA_Handler.Init.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD;

//外设数据长度:16 位

I2S2_TXDMA_Handler.Init.MemDataAlignment=DMA_MDATAALIGN_HALFWORD;

//存储器数据长度:16 位

I2S2_TXDMA_Handler.Init.Mode=DMA_CIRCULAR; //使用循环模式

I2S2_TXDMA_Handler.Init.Priority=DMA_PRIORITY_HIGH; //高优先级

I2S2_TXDMA_Handler.Init.FIFOMode=DMA_FIFOMODE_DISABLE;//不使用 FIFO

I2S2_TXDMA_Handler.Init.MemBurst=DMA_MBURST_SINGLE;

//存储器单次突发传输

I2S2_TXDMA_Handler.Init.PeriphBurst=DMA_PBURST_SINGLE; //外设突发单次传输

HAL_DMA_DeInit(&I2S2_TXDMA_Handler); //先清除以前的设置

HAL_DMA_Init(&I2S2_TXDMA_Handler); //初始化 DMA

HAL_DMAEx_MultiBufferStart(&I2S2_TXDMA_Handler,(u32)buf0,

(u32)&SPI2->DR,(u32)buf1,num);//开启双缓冲

__HAL_DMA_DISABLE(&I2S2_TXDMA_Handler); //先关闭 DMA

delay_us(10); //10us 延时,防止-O2 优化出问题

__HAL_DMA_ENABLE_IT(&I2S2_TXDMA_Handler,DMA_IT_TC);

//开启传输完成中断

__HAL_DMA_CLEAR_FLAG(&I2S2_TXDMA_Handler,DMA_FLAG_TCIF0_4);

//清除 DMA 传输完成中断标志位

HAL_NVIC_SetPriority(DMA1_Stream4_IRQn,0,0); //DMA 中断优先级

HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn);

}

//I2S DMA 回调函数指针

void (*i2s_tx_callback)(void);

//TX 回调函数

//DMA1_Stream4 中断服务函数

void DMA1_Stream4_IRQHandler(void)

{

if(__HAL_DMA_GET_FLAG(&I2S2_TXDMA_Handler,DMA_FLAG_TCIF0_4)

!=RESET) //DMA 传输完成

{

__HAL_DMA_CLEAR_FLAG(&I2S2_TXDMA_Handler,DMA_FLAG_TCIF0_4);

//清除 DMA 传输完成中断标志位

i2s_tx_callback(); //执行回调函数,读取数据等操作在这里面处理

}

}

//I2S 开始播放

void I2S_Play_Start(void)

{

__HAL_DMA_ENABLE(&I2S2_TXDMA_Handler);//开启 DMA TX 传输

}

//关闭 I2S 播放

void I2S_Play_Stop(void)

{

__HAL_DMA_DISABLE(&I2S2_TXDMA_Handler);//结束播放;

}

其中,I2S2_Init 完成 I2S2 的初始化,通过 4 个参数设置 I2S2 的详细配置信息。另外一个

函数:I2S2_SampleRate_Set,则是用前面介绍的查表法,根据音频采样率来设置 I2S 的时钟部

分。函数 I2S2_TX_DMA_Init,用于设置 I2S2 的 DMA 发送,使用双缓冲循环模式,发送数据

给 WM8978,并开启了发送完成中断。而 DMA1_Stream4_IRQHandler 函数,则是 DMA1 数据

流 4 发送完成中断的服务函数,该函数调用 i2s_tx_callback 函数(函数指针,使用前需指向特

定函数)实现 DMA 数据填充。在 i2s.c 里面,还有 2 个函数:I2S_Play_Start 和 I2S_Play_Stop,

用于开启和关闭 DMA 传输,这里我们没贴出来了,请大家参考光盘本例程源码。

再来看 wm8978.c 里面的几个函数,代码如下:

//WM8978 初始化

//返回值:0,初始化正常

// 其他,错误代码

u8 WM8978_Init(void)

{

u8 res;

IIC_Init();//初始化 IIC 接口

res=WM8978_Write_Reg(0,0);

//软复位 WM8978

if(res)return 1;

//发送指令失败,WM8978 异常

//以下为通用设置

WM8978_Write_Reg(1,0X1B); //R1,MICEN 设置为 1(MIC 使能),BIASEN 设置为 1

//(模拟器工作),VMIDSEL[1:0]设置为:11(5K)

WM8978_Write_Reg(2,0X1B0); //R2,ROUT1,LOUT1 输出使能(耳机可以工作)

//,BOOSTENR,BOOSTENL 使能

WM8978_Write_Reg(3,0X6C); //R3,LOUT2,ROUT2,喇叭输出,RMIX,LMIX 使能

WM8978_Write_Reg(6,0);

//R6,MCLK 由外部提供

WM8978_Write_Reg(43,1<<4); //R43,INVROUT2 反向,驱动喇叭

WM8978_Write_Reg(47,1<<8); //R47 设置,PGABOOSTL,左通道 MIC 获得 20 倍增益

WM8978_Write_Reg(48,1<<8); //R48 设置,PGABOOSTR,右通道 MIC 获得 20 倍增益

WM8978_Write_Reg(49,1<<1); //R49,TSDEN,开启过热保护

WM8978_Write_Reg(10,1<<3); //R10,SOFTMUTE 关闭,128x 采样,最佳 SNR

WM8978_Write_Reg(14,1<<3); //R14,ADC 128x 采样率

return 0;

}

//WM8978 DAC/ADC 配置

//adcen:adc 使能(1)/关闭(0)

//dacen:dac 使能(1)/关闭(0)

void WM8978_ADDA_Cfg(u8 dacen,u8 adcen)

{

u16 regval;

regval=WM8978_Read_Reg(3); //读取 R3

if(dacen)regval|=3<<0;

//R3 最低 2 个位设置为 1,开启 DACR&DACL

else regval&=~(3<<0);

//R3 最低 2 个位清零,关闭 DACR&DACL.

WM8978_Write_Reg(3,regval); //设置 R3

regval=WM8978_Read_Reg(2); //读取 R2

if(adcen)regval|=3<<0;

//R2 最低 2 个位设置为 1,开启 ADCR&ADCL

else regval&=~(3<<0);

//R2 最低 2 个位清零,关闭 ADCR&ADCL.

WM8978_Write_Reg(2,regval); //设置 R2

}

//WM8978 输出配置

//dacen:DAC 输出(放音)开启(1)/关闭(0)

//bpsen:Bypass 输出(录音,包括 MIC,LINE IN,AUX 等)开启(1)/关闭(0)

void WM8978_Output_Cfg(u8 dacen,u8 bpsen)

{

u16 regval=0;

if(dacen)regval|=1<<0; //DAC 输出使能

if(bpsen)

{

regval|=1<<1;

//BYPASS 使能

regval|=5<<2;

//0dB 增益

}

WM8978_Write_Reg(50,regval);//R50 设置

WM8978_Write_Reg(51,regval);//R51 设置

}

//设置 I2S 工作模式

//fmt:0,LSB(右对齐);1,MSB(左对齐);2,飞利浦标准 I2S;3,PCM/DSP;

//len:0,16 位;1,20 位;2,24 位;3,32 位;

void WM8978_I2S_Cfg(u8 fmt,u8 len)

{

fmt&=0X03;

len&=0X03;//限定范围

WM8978_Write_Reg(4,(fmt<<3)|(len<<5)); //R4,WM8978 工作模式设置

}

以上代码 WM8978_Init 用于初始化 WM8978,这里只是通用配置(ADC&DAC),初始化

之后,并不能正常播放音乐,还需要通过 WM8978_ADDA_Cfg 函数,使能 DAC,然后通过

WM8978_Output_Cfg 选择 DAC 输出,通过 WM8978_I2S_Cfg 配置 I2S 工作模式,最后设置音

量才可以接收 I2S 音频数据,实现音乐播放。这里设置音量、EQ、音效等函数,没有贴出了,

请大家参考光盘本例程源码。

接下来,看看 wavplay.c 里面的几个函数,代码如下:

__wavctrl wavctrl;

//WAV 控制结构体

vu8 wavtransferend=0; //i2s 传输完成标志

vu8 wavwitchbuf=0;

//i2sbufx 指示标志

//WAV 解析初始化

//fname:文件路径+文件名

//wavx:wav 信息存放结构体指针

//返回值:0,成功;1,打开文件失败;2,非 WAV 文件;3,DATA 区域未找到.

u8 wav_decode_init(u8* fname,__wavctrl* wavx)

{

FIL*ftemp; u32 br=0;

u8 *buf; u8 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;

//数据流开始的地方.

}else res=3;//data 区域未找到.

}else res=2;//非 wav 文件

}else res=1;//打开文件错误

}

f_close(ftemp);

myfree(SRAMIN,ftemp); myfree(SRAMIN,buf); //释放内存

return 0;

}

//填充 buf

//buf:数据区

//size:填充数据量

//bits:位数(16/24)

//返回值:读到的数据个数

u32 wav_buffill(u8 *buf,u16 size,u8 bits)

{

u16 readlen=0; u32 bread;

u16 i; u8 *p;

if(bits==24)//24bit 音频,需要处理一下

{

readlen=(size/4)*3;

//此次要读取的字节数

f_read(audiodev.file,audiodev.tbuf,readlen,(UINT*)&bread);

//读取数据

p=audiodev.tbuf;

for(i=0;i<size;)

{

buf[i++]=p[1]; buf[i]=p[2];

i+=2; buf[i++]=p[0];

p+=3;

}

bread=(bread*4)/3;

//填充后的大小.

}else

{

f_read(audiodev.file,buf,size,(UINT*)&bread);//16bit 音频,直接读取数据

if(bread<size) for(i=bread;i<size-bread;i++)buf[i]=0;//不够数据了,补充 0

}

return bread;

}

//WAV 播放时,I2S DMA 传输回调函数

void wav_i2s_dma_tx_callback(void)

{

u16 i;

if(DMA1_Stream4->CR&(1<<19))

{

wavwitchbuf=0;

if((audiodev.status&0X01)==0) //暂停

for(i=0;i<WAV_I2S_TX_DMA_BUFSIZE;i++)audiodev.i2sbuf1[i]=0;//填 0

}else

{

wavwitchbuf=1;

if((audiodev.status&0X01)==0) //暂停

for(i=0;i<WAV_I2S_TX_DMA_BUFSIZE;i++)audiodev.i2sbuf2[i]=0;//填 0

}

wavtransferend=1;

}

//播放某个 WAV 文件

//fname:wav 文件路径.

//返回值:

//KEY0_PRES:下一曲

//KEY1_PRES:上一曲

//其他:错误

u8 wav_play_song(u8* fname)

{

u8 key; u8 t=0; u8 res; u32 fillnum;

audiodev.file=(FIL*)mymalloc(SRAMIN,sizeof(FIL));

audiodev.i2sbuf1=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);

audiodev.i2sbuf2=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);

audiodev.tbuf=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);

if(audiodev.file&&audiodev.i2sbuf1&&audiodev.i2sbuf2&&audiodev.tbuf)

{

res=wav_decode_init(fname,&wavctrl);//得到文件的信息

if(res==0)//解析文件成功

{

if(wavctrl.bps==16)

{

WM8978_I2S_Cfg(2,0);//飞利浦标准,16 位数据长度

I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,

I2S_DataFormat_16bextended);

//飞利浦标准,主机发送,时钟低电平,16 位扩展帧长度

}else if(wavctrl.bps==24)

{

WM8978_I2S_Cfg(2,2);//飞利浦标准,24 位数据长度

I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,

I2S_DataFormat_24b);//飞利浦标准,主机发送,时钟低,24 位扩展帧长度

}

I2S2_SampleRate_Set(wavctrl.samplerate);//设置采样率

I2S2_TX_DMA_Init(audiodev.i2sbuf1,audiodev.i2sbuf2,

WAV_I2S_TX_DMA_BUFSIZE/2); //配置 TX DMA

i2s_tx_callback=wav_i2s_dma_tx_callback;//回调函数指 wav_i2s_dma_callback

audio_stop();

res=f_open(audiodev.file,(TCHAR*)fname,FA_READ);//打开文件

if(res==0)

{

f_lseek(audiodev.file, wavctrl.datastart);//跳过文件头

fillnum=wav_buffill(audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,

wavctrl.bps);

fillnum=wav_buffill(audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,

wavctrl.bps);

audio_start();

while(res==0)

{

while(wavtransferend==0);//等待 wav 传输完成;

wavtransferend=0;

if(fillnum!=WAV_I2S_TX_DMA_BUFSIZE)//播放结束?

{ res=KEY0_PRES; break; }

if(wavwitchbuf)fillnum=wav_buffill(audiodev.i2sbuf2,

WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充 buf2

else fillnum=wav_buffill(audiodev.i2sbuf1,

WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充 buf1

while(1)

{

key=KEY_Scan(0);

if(key==WKUP_PRES)//暂停

{

if(audiodev.status&0X01)audiodev.status&=~(1<<0);

else audiodev.status|=0X01;

}

if(key==KEY2_PRES||key==KEY0_PRES)//下一曲/上一曲

{ res=key; break; }

wav_get_curtime(audiodev.file,&wavctrl);//得到播放和总时间

audio_msg_show(wavctrl.totsec,wavctrl.cursec,wavctrl.bitrate);

t++;

if(t==20) { t=0; LED0=!LED0; }

if((audiodev.status&0X01)==0)delay_ms(10);

else break;

}

}

audio_stop();

}else res=0XFF;

}else res=0XFF;

}else res=0XFF;

myfree(SRAMIN,audiodev.tbuf); myfree(SRAMIN,audiodev.file);

//释放内存

myfree(SRAMIN,audiodev.i2sbuf1); myfree(SRAMIN,audiodev.i2sbuf2); //释放内存

return res;

}

以上,wav_decode_init 函数,用来对 wav 文件进行解析,得到 wav 的详细信息(音频采样

率,位数,数据流起始位置等);wav_buffill 函数,用 f_read 读取数据,填充数据到 buf 里面,

注意 24 位音频的时候,读出的数据需要经过转换后才填充到 buf;wav_i2s_dma_tx_callback 函

数,则是 DMA 发送完成的回调函数(i2s_tx_callback 函数指针指向该函数),这里面,我们并

没有对数据进行填充处理(暂停时进行了填 0 处理),而是采用 2 个标志量:wavtransferend 和

wavwitchbuf,来告诉 wav_play_song 函数是否传输完成,以及应该填充哪个数据 buf(i2sbuf1

或 i2sbuf2);

最后,wav_play_song 函数,是播放 WAV 的最终执行函数,该函数解析完 WAV 文件后,

设置 WM8978 和 I2S 的参数(采样率,位数等),并开启 DMA,然后不停填充数据,实现 WAV

播放,该函数还进行了按键扫描控制,实现上下取切换和暂停/播放等操作。该函数通过判断

wavtransferend 是否为 1 来处理是否应该填充数据,而到底填充到哪个 buf(i2sbuf1 或 i2sbuf2),

则是通过 wavwitchbuf 标志来确定的,当 wavwitchbuf=0 时,说明 DMA 正在使用 i2sbuf2,程

序应该填充 i2sbuf1;当 wavwitchbuf=1 时,说明 DMA 正在使用 i2sbuf1,程序应该填充 i2sbuf2;

接下来,看看 audioplay.c 里面的几个函数,代码如下:

//播放音乐

void audio_play(void)

{

u8 res; u8 key;u16 temp;

DIR wavdir;

//目录

FILINFO wavfileinfo; //文件信息

u8 *fn;

//长文件名

u8 *pname;

//带路径的文件名

u16 totwavnum;

//音乐文件总数

u16 curindex;

//图片当前索引

u16 *wavindextbl;

//音乐索引表

WM8978_ADDA_Cfg(1,0); //开启 DAC

WM8978_Input_Cfg(0,0,0);//关闭输入通道

WM8978_Output_Cfg(1,0); //开启 DAC 输出

while(f_opendir(&wavdir,"0:/MUSIC"))//打开音乐文件夹

{

Show_Str(60,190,240,16,"MUSIC 文件夹错误!",16,0); delay_ms(200);

LCD_Fill(60,190,240,206,WHITE); delay_ms(200);//清除显示

}

totwavnum=audio_get_tnum("0:/MUSIC"); //得到总有效文件数

while(totwavnum==NULL)//音乐文件总数为 0

{

Show_Str(60,190,240,16,"没有音乐文件!",16,0); delay_ms(200);

LCD_Fill(60,190,240,146,WHITE); delay_ms(200); //清除显示

}

wavfileinfo.lfsize=_MAX_LFN*2+1;

//长文件名最大长度

wavfileinfo.lfname=mymalloc(SRAMIN,wavfileinfo.lfsize);//为长文件缓存区分配内存

pname=mymalloc(SRAMIN,wavfileinfo.lfsize);

//为带路径的文件名分配内存

wavindextbl=mymalloc(SRAMIN,2*totwavnum);

//申请内存,用于存放音乐文件索引

while(wavfileinfo.lfname==NULL||pname==NULL||wavindextbl==NULL)//内存分配出错

{

Show_Str(60,190,240,16,"内存分配失败!",16,0); delay_ms(200);

LCD_Fill(60,190,240,146,WHITE); delay_ms(200);//清除显示

}

//记录索引

res=f_opendir(&wavdir,"0:/MUSIC"); //打开目录

if(res==FR_OK)

{

curindex=0;//当前索引为 0

while(1)//全部查询一遍

{

temp=wavdir.index;

//记录当前 index

res=f_readdir(&wavdir,&wavfileinfo); //读取目录下的一个文件

if(res!=FR_OK||wavfileinfo.fname[0]==0)break; //错误了/到末尾了,退出

fn=(u8*)(*wavfileinfo.lfname?wavfileinfo.lfname:wavfileinfo.fname);

res=f_typetell(fn);

if((res&0XF0)==0X40)//取高四位,看看是不是音乐文件

{

wavindextbl[curindex]=temp;//记录索引

curindex++;

}

}

}

curindex=0;

//从 0 开始显示

res=f_opendir(&wavdir,(const TCHAR*)"0:/MUSIC"); //打开目录

while(res==FR_OK)//打开成功

{

dir_sdi(&wavdir,wavindextbl[curindex]);

//改变当前目录索引

res=f_readdir(&wavdir,&wavfileinfo);

//读取目录下的一个文件

if(res!=FR_OK||wavfileinfo.fname[0]==0)break; //错误了/到末尾了,退出

fn=(u8*)(*wavfileinfo.lfname?wavfileinfo.lfname:wavfileinfo.fname);

strcpy((char*)pname,"0:/MUSIC/");

//复制路径(目录)

strcat((char*)pname,(const char*)fn);

//将文件名接在后面

LCD_Fill(60,190,240,190+16,WHITE);

//清除之前的显示

Show_Str(60,190,240-60,16,fn,16,0);

//显示歌曲名字

audio_index_show(curindex+1,totwavnum);

key=audio_play_song(pname);

//播放这个音频文件

if(key==KEY2_PRES)

//上一曲

{

if(curindex)curindex--;

else curindex=totwavnum-1;

}else if(key==KEY0_PRES)//下一曲

{

curindex++;

if(curindex>=totwavnum)curindex=0;//到末尾的时候,自动从头开始

}else break; //产生了错误

}

myfree(SRAMIN,wavfileinfo.lfname); //释放内存

myfree(SRAMIN,pname);

//释放内存

myfree(SRAMIN,wavindextbl);

//释放内存

}

//播放某个音频文件

u8 audio_play_song(u8* fname)

{ res=f_typetell(fname);

switch(res)

{

case T_WAV:

res=wav_play_song(fname); break;

default://其他文件,自动跳转到下一曲

printf("can't play:%srn",fname);

res=KEY0_PRES; break;

}

return res;

}

这里,audio_play 函数在 main 函数里面被调用,该函数首先设置 WM8978 相关配置,然

后查找 SD 卡里面的 MUSIC 文件夹,并统计该文件夹里面总共有多少音频文件(统计包括:

WAV/MP3/APE/FLAC 等),然后,该函数调用 audio_play_song 函数,按顺序播放这些音频文件。

在 audio_play_song 函数里面,通过判断文件类型,调用不同的解码函数,本章,只支持

WAV 文件,通过 wav_play_song 函数实现 WAV 解码。其他格式:MP3/APE/FLAC 等,在综合

实验我们会实现其解码函数,大家可以参考综合实验代码,这里就不做介绍了。

最后,我们看看主函数代码:

int main(void)

{

HAL_Init();

//初始化 HAL 库

Stm32_Clock_Init(336,8,2,7);

//设置时钟,168Mhz

delay_init(168);

//初始化延时函数

uart_init(115200);

//初始化 USART

usmart_dev.init(84);

//初始化 USMART

LED_Init();

//初始化 LED

KEY_Init();

//初始化 KEY

LCD_Init(); //初始化 LCD

SRAM_Init();

//初始化外部 SRAM

W25QXX_Init();

//初始化 W25Q128

WM8978_Init();

//初始化 WM8978

WM8978_HPvol_Set(40,40);

//耳机音量设置

WM8978_SPKvol_Set(50);

//喇叭音量设置

my_mem_init(SRAMIN);

//初始化内部内存池

my_mem_init(SRAMEX);

//初始化外部内存池

my_mem_init(SRAMCCM);

//初始化 CCM 内存池

exfuns_init();

//为 fatfs 相关变量申请内存

f_mount(fs[0],"0:",1);

//挂载 SD 卡

POINT_COLOR=RED;

while(font_init())

//检查字库

{

LCD_ShowString(30,50,200,16,16,"Font Error!");

delay_ms(200);

u8 res;

LCD_Fill(30,50,240,66,WHITE);//清除显示

delay_ms(200);

}

POINT_COLOR=RED;

Show_Str(60,50,200,16,"Explorer STM32F4 开发板",16,0);

Show_Str(60,70,200,16,"音乐播放器实验",16,0);

Show_Str(60,90,200,16,"正点原子@ALIENTEK",16,0);

Show_Str(60,110,200,16,"2017 年 5 月 3 日",16,0);

Show_Str(60,130,200,16,"KEY0:NEXT KEY2:PREV",16,0);

Show_Str(60,150,200,16,"KEY_UP:PAUSE/PLAY",16,0);

while(1)

{

audio_play();

}

}

该函数就相对简单了,在初始化各个外设后,通过 audio_play 函数,开始音频播放。软件

部分就介绍到这里,其他未贴出代码,请参考光盘本例程源码。

48.4 下载验证

在代码编译成功之后,我们下载代码到 ALIENTEK 探索者 STM32F4 开发板上,程序先执

行字库检测,然后当检测到 SD 卡根目录的 MUSIC 文件夹有有效音频文件(WAV 格式音频)

的时候,就开始自动播放歌曲了,如图 48.4.1 所示:

4aa98011ab7be0f348460b16a0d10887.png
图 48.4.1 MP3 播放中

从上图可以看出,当前正在播放第 4 首歌曲,总共 4 首歌曲,歌曲名、播放时间、总时长、

码率、音量等信息等也都有显示。此时 DS0 会随着音乐的播放而闪烁。

只要我们在开发板的 PHONE 端子插入耳机(或者在 P1 接口插入喇叭),就能听到歌曲的

声音了。同时,我们可以通过按 KEY0 和 KEY2 来切换下一曲和上一曲,通过 KEY_UP 控制暂 停和继续播放。

本实验,我们还可以通过 USMART 来测试 WM8978 的其他功能,通过将 wm8978.c 里面

的部分函数加入 USMART 管理,我们可以很方便的设置 wm8978 的各种参数(音量、3D、EQ

等都可以设置),达到验证测试的目的。有兴趣的朋友,可以实验测试一下。

至此,我们就完成了一个简单的音乐播放器了,虽然只支持 WAV 文件,但是大家可以在

此基础上,增加其他音频格式解码器(可参考综合实验),便可实现其他音频格式解码了。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值