1)实验平台:alientek 阿波罗 STM32F767 开发板
2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

第四十八章 音乐*放播**器实验
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 所示:

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


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

图 48.1.2.1 飞利浦标准模式 I2S 数据传输图
图中,fs 即音频信号的采样率,比如 44.1Khz,因此可以知道,LRC 的频率就是音频信号
的采样率。另外,WM8978 还需要一个 MCLK,本章我们采用 STM32F4 为其提供 MCLK 时钟,MCLK
的频率必须等于 256fs,也就是音频采样率的 256 倍。
WM8978 的框图如图 48.1.2.2 所示:

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

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

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

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

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

图 48.1.3.6 寄存器 SPI_ I2SPR 各位描述
本章我们设置 MCKOE 为 1,开启 MCK 输出,ODD 和 I2SDIV 则根据不同的 fs,查表进
行设置。
第三个是 PLLI2S 配置寄存器:RCC_PLLI2SCFGR,该寄存器各位描述如图 48.1.3.7 所示:

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

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

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

图 48.4.1 MP3 *放播**中
从上图可以看出,当前正在*放播**第 4 首歌曲,总共 4 首歌曲,歌曲名、*放播**时间、总时长、
码率、音量等信息等也都有显示。此时 DS0 会随着音乐的*放播**而闪烁。
只要我们在开发板的 PHONE 端子插入耳机(或者在 P1 接口插入喇叭),就能听到歌曲的
声音了。同时,我们可以通过按 KEY0 和 KEY2 来切换下一曲和上一曲,通过 KEY_UP 控制暂
停和继续*放播**。
本实验,我们还可以通过 USMART 来测试 WM8978 的其他功能,通过将 wm8978.c 里面
的部分函数加入 USMART 管理,我们可以很方便的设置 wm8978 的各种参数(音量、3D、EQ
等都可以设置),达到验证测试的目的。有兴趣的朋友,可以实验测试一下。
至此,我们就完成了一个简单的音乐*放播**器了,虽然只支持 WAV 文件,但是大家可以在
此基础上,增加其他音频格式解码器(可参考综合实验),便可实现其他音频格式解码了。