探索者stm32f (探索者stm32f4怎么供电)

1)实验平台:alientek 阿波罗 STM32F767 开发板

2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

探索者stm32f,探索者完整版

第四十八章 音乐*放播**器实验

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 所示:

探索者stm32f,探索者完整版

图 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 所示的几种形式:

探索者stm32f,探索者完整版

探索者stm32f,探索者完整版

表 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:数据左/右对齐时钟

探索者stm32f,探索者完整版

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

所示:

探索者stm32f,探索者完整版

图 48.1.2.1 飞利浦标准模式 I2S 数据传输图

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

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

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

WM8978 的框图如图 48.1.2.2 所示:

探索者stm32f,探索者完整版

图 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 所示:

探索者stm32f,探索者完整版

图 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:

探索者stm32f,探索者完整版

图 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 所示:

探索者stm32f,探索者完整版

图 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 所示:

探索者stm32f,探索者完整版

图 48.1.3.4 I2S 时钟发生器架构

图中 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 对应系数表如下:

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

const u16 I2S_PSC_TBL[][5]=

{

{800 ,256,5,12,1},

//8Khz 采样率

{1102,429,4,19,0},

//11.025Khz 采样率

{1600,213,2,13,0},

//16Khz 采样率

{2205,429,4, 9,1},

//22.05Khz 采样率

{3200,213,2, 6,1},

//32Khz 采样率

{4410,271,2, 6,0},

//44.1Khz 采样率

{4800,258,3, 3,1},

//48Khz 采样率

{8820,316,2, 3,1},

//88.2Khz 采样率

{9600,344,2, 3,1},

//96Khz 采样率

{17640,361,2,2,0}, //176.4Khz 采样率

{19200,393,2,2,0}, //192Khz 采样率

};

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

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

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

图 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 所示:

探索者stm32f,探索者完整版

图 48.1.3.6 寄存器 SPI_ I2SPR 各位描述

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

行设置。

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

探索者stm32f,探索者完整版

图 48.1.3.7 寄存器 RCC_ PLLI2SCFGR 各位描述

该寄存器用于配置 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 所示:

探索者stm32f,探索者完整版

图 48.1.3.8 DMA 双缓冲发送音频数据流框图

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 所示:

探索者stm32f,探索者完整版

图 48.2.1 WM8978 与 STM32F4 连接原理图

图中,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 时钟

//初始化 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)

{

u8 res;

res=f_typetell(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 函数里面被调用,该函数首先设置 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);

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 所示:

探索者stm32f,探索者完整版

图 48.4.1 MP3 *放播**中

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

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

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

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

停和继续*放播**。

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

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

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

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

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