完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
|
第五十二章 音乐播放器实验 ALIENTEK 阿波罗 STM32F767 开发板拥有串行音频接口(SAI),支持 I2S、LSB/MSB 对其、PCM/DSP、TDM 和 AC’97 等协议,且外扩了一颗 HIFI 级 CODEC 芯片:WM8978G,支 持最高 192K 24BIT 的音频播放,并且支持录音(下一章介绍)。本章,我们将利用阿波罗 STM32F767 开发板实现一个简单的音乐播放器(仅支持 WAV 播放)。本章分为如下几个部: 52.1 WAV&WM8978&SAI 简介 52.2 硬件设计 52.3 软件设计 52.4 下载验证 52.1 WAV&WM8978&SAI 简介 本章新知识点比较多,包括:WAV、WM8978 和 SAI 等三个知识点。下面我们将分别向 大家介绍。 52.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 由块标识符、数据大小和数 据三部分组成,如图 52.1.1.1 所示: 图 52.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 位置可以分成如表 52.1.1.1 所示的几种形式: 表 52.1.1.1 WAVE 文件数据采样格式 本章,我们播放的音频支持:16 位和 24 位,立体声,所以每个取样为 4/6 个字节,低字 节在前,高字节在后。在得到这些 wav 数据以后,通过 I2S 丢给 WM8978,就可以欣赏音乐了。 52.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 数据传输协议如图 52.1.2.1 所示: 图 52.1.2.1 飞利浦标准模式 I2S 数据传输图 图中,fs 即音频信号的采样率,比如 44.1Khz,因此可以知道,LRC 的频率就是音频信号 的采样率。另外,WM8978 还需要一个 MCLK,本章我们采用 STM32F767 为其提供 MCLK 时钟,MCLK 的频率必须等于 256fs,也就是音频采样率的 256 倍。 WM8978 的框图如图 52.1.2.2 所示: 图 52.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 都来 自外部,即由 STM32F767 提供。 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 的数据手册自行研究下即可。 52.1.3 SAI 简介 STM32F767 自带了两个串行音频接口(SAI1 和 SAI2),SAI 具有灵活性高、配置多样的 特点。可以支持:I2S 标准、LSB 或 MSB 对齐、PCM/DSP、TDM 和 AC’97 等协议,适用于 多声道或单声道应用。 SAI 通过两个完全独立的音频子模块来实现这种灵活性与可配置性,每个音频子模块与多 达 4 个引脚(SD、SCK、FS 和 MCLK)相连。如果将两个子模块声明为同步模块,则其中一 些引脚可以共用,从而可释放一些引脚用作通用 I/O。MCLK 引脚是否用作输出引脚取决于实 际应用和解码的要求以及音频模块是否配置为主模块。SAI 可以配置为主模式或配置为从模式。 音频子模块既可作为接收器,又可作为发送器;既可与另一模块同步,又可以不同步。 STM32F767 自带的 SAI 接口特点有: ●具有两个独立的音频子模块,子模块既可作为接收器,也可作为发送器,并自带 FIFO ●每个音频子模块集成多达 8 个字,每个字 32 位的 FIFO ●两个音频子模块间可以是同步或异步模式 ●两个音频子模块的主/从配置相互独立 ●当两个音频子模块都配置为主模式时,每个子模块可设置互相独立的采样率 ●数据大小可配置:8 位、10 位、16 位、20 位、24 位或 32 位 ●支持:I2S、LSB 或 MSB 对齐、PCM/DSP、TDM 和 AC’97 等音频协议 ●高达 16 个大小可配置的 Slot,可选择音频帧中的哪些 Slot 有效 ●支持 LSB 或 MSB 数据传输 ●支持 DMA,有 2 个专用通道,用于处理对每个 SAI 音频子模块的专用集成 FIFO 的访问 STM32F767 的 SAI 框图如图 52.1.3.1 所示: 图 52.1.3.1 SAI 框图 本章,我们将用 SAI 接口来驱动 WM8978,而 WM8978 的接口是 I2S 接口的,所以,本章 我们只介绍 SAI 支持 I2S 协议使用的方法,其他协议的使用介绍,请看《STM32F7 中文参考手 册.pdf》第 33 章。 (1)SAI I2S 信号线 SAI 作为 I2S 使用的时候,同 I2S 接口连接的信号线如表 52.1.3.1 所示: 表 52.1.3.1 SAI 同 I2S 接口连接关系表 表中,A/B 表示 SAI 内部的两个独立的音频子模块,可以独立的连接 I2S,也可以共同连 接同一个 I2S(主从同步模式),主从同步模式,常用于全双工 I2S 通信(读/写同时进行), 主从同步模式,还可以省略一些信号线(SCK/FS/MCLK 等)。 FS_A/B:连接 I2S 的 LRC 脚,用于切换左右声道的数据,它的频率等于音频信号采样率 (fs)。 SCK_A/B:连接 I2S 的 BLCK 脚,用作位时钟,是 I2S 主模式下的串行时钟输出以及从模 式下的串行时钟输入。SCK_A/B 频率= FS_A/B 频率(fs)*slot 个数*单个 slot 大小(slot 后面 介绍)。 SD_A/B:连接 I2S 的 DACDAT/ADCDAT 脚,是数据输入/输出脚,用于发送或接收数据 (单个音频子模块,只能做半双工通信。全双工需要 2 个音频子模块同时工作,使用主从同步 模式)。 MCLK_A/B:连接 I2S 的 MCLK 脚,是主时钟输出脚,固定输出频率为 256×fs,fs 即音 频信号采样频率(fs)。 (2)SAI slot 简介 slot 是 SAI 音频帧中的基本元素,音频帧中 slot 的数目通过 SAI_xSLOTR 寄存器配置,每 个音频帧的 slot 数,最大是 16。在 I2S 模式下,SAI 中 slot 的传输方式如图 52.1.3.2 所示: 图 52.1.3.2 SAI I2S 模式下 slot 传输示意图 上图中,一个音频帧中,slot 的个数为 6 个,每个半帧有 3 个 slot,根据 slot 数与音频帧的 对齐与否又分为两种情况,我们一般设计为 slot 数与音频帧对齐,也就是图 52.1.3.2 中下部分 图所示的传输方式:一个半帧刚好是 3 个 slot,每个 slot 可以传输一个声道的音频数据,这样, 6 个 slot 就可以传输 6 个声道的音频数据。一般音频文件都是立体声,所以只需要 2 个 slot 即 可,每个半帧一个 slot。STM32 的 SAI 最多可以实现 16 声道数据传输(16 个 slot)。 每个 slot 的大小是可以配置的,如图 52.1.3.3 所示: 图 52.1.3.3 slot 大小配置 由图可知,数据大小(DS)可以和 slot 相等,也可以不相等(16bit/32bit),当数据大小 小于 slot 大小的时候,可以通过 SAI_xSLOTR 寄存器的 FBOFF 位设置数据的偏移。各种设置 的约束条件为: FBOFF≤(SLOTSZ-DS) DS≤SLOTSZ NBSLOT*SLOTSZ≤FRL 其中:FBOFF 为数据在 slot 里面的偏移量;SLOTSZ 为单个 slot 的位数;DS 为数据大小 位数;NBSLOT 为一帧中 slot 的个数;FRL 为帧长度(位数)。 在 I2S 模式下,我们配置 slot 大小为 32 位,每一帧 slot 个数为 2 个,偏移量为 0,这样就 可以支持 16~32 位的立体声音乐播放了。 (3)SAI 时钟发生器 SAI 每个音频子模块都有自己的时钟发生器,这样两个模块完全独立,可以同时工作,并 互不干扰。当音频模块定义为主模块时,时钟发生器将产生位时钟(SCK)以及用于外部解码 器的主时钟(MCLK),当音频模块定义为从模块时,时钟发生器将关闭(关 SCK 和 MCLK)。 SAI 的时钟发生器架构如图 52.1.3.4 所示: 图 52.1.3.4 SAI 时钟发生器架构 图中,NODIV,可以用于控制是否使能分频器,我们一般设置为 0,使能分频器。如果设 置为 1,那么分频器将关闭(主分频器和位时钟分频器都关闭),MCLK_x(x=A/B,下同)将 无输出,而 SCK_x 则等于 SAI_CK_x。 SAI_CK_x 时钟来自PLLSAI或PLLI2S 的Q分频输出,随后经过主时钟分频器(MCKDIV) 分频,作为主时钟(MCLK)提供给 WM8978,同时,经主分频(MCKDIV)分频后,还会经 由位时钟分频器(FRLP[7:0])分频,作为位时钟(SCK)提供给 WM8978。 当 MCKDIV[3:0] !=0000 的时候,MCLK_x=SAI_CK_x/(MCKDIV[3:0]*2) 当 MCKDIV[3:0] ==0000 的时候,MCLK_x=SAI_CK_x 位时钟(SCK)计算公式:SCK_x=MCLK_x*(FRL[7:0]+1)/256 其中:256 是 MCLK 和音频采样率之间的固定比率(MCLK 恒等于 fs*256);FRL[7:0]是音频 帧中的位时钟-1(在 I2S 协议下,必须是奇数,+1 后为偶数);因此,我们可以得到音频采样 率(fs)的计算公式为: 当 MCKDIV[3:0] !=0000 的时候:fs= SAI_CK_x/(MCKDIV[3:0]*512) 当 MCKDIV[3:0] ==0000 的时候:fs= SAI_CK_x/(256) 其中:SAI_CK_x 来自 PLLI2S/PLLSAI 的 Q 分频,以来自 PLLI2S 为例,计算公式为: SAI_CK_x=(HSE/pllm)*PLLI2SN/PLLI2SQ/(PLLI2SDIVQ+1) HSE 我们是 25Mhz,而 pllm 在系统时钟初始化就确定了,是 25,这样结合以上公式,可 得 fs 的计算公式如下: MCKDIV[3:0] !=0000 时:fs= 1000*PLLI2SN/PLLI2SQ/(PLLI2SDIVQ+1)/(MCKDIV*512) MCKDIV[3:0] ==0000 时:fs=1000*PLLI2SN/PLLI2SQ/(PLLI2SDIVQ+1)/(256) fs 单位是:Khz。其中:PLL2SN 取值范围:192~432;PLLI2SQ 取值范围:2~15;PLLI2SDIVQ 取值范围:0~31;MCKDIV 的值范围:0~15。根据以上约束条件,我们便可以根据 fs 来设置 各个系数的值了,不过很多时候,并不能取得和 fs 一模一样的频率,只能近似等于 fs,比如 44.1Khz 采样率,我们设置 PLL2SN=429,PLL2SQ=2,PLLI2SDIVQ=18,MCKDIV=0,得到 fs=44.0995Khz,误差为:0.0011%。晶振频率决定了有时无法通过分频得到我们所要的 fs,所 以,某些 fs 如果要实现 0 误差,大家必须得选用外部时钟才可以。 如果要通过程序去计算这些系数的值,是比较麻烦的,所以,我们事先计算好常用 fs 对应 的系数值,建立一个表,这样,用的时候,只需要查表取值就可以了,大大简化了代码,常用 fs 对应系数表如下: //表格式:采样率/10,PLLI2SN,PLLI2SQ, PLLI2SDIVQ, MCKDIV const u16 SAI_PSC_TBL[][5]= { {800 ,344,7,0,12}, //8Khz 采样率 {1102,429,2,18,2}, //11.025Khz 采样率 {1600,344,7, 0,6}, //16Khz 采样率 {2205,429,2,18,1}, //22.05Khz 采样率 {3200,344,7, 0,3}, //32Khz 采样率 {4410,429,2,18,0}, //44.1Khz 采样率 {4800,344,7, 0,2}, //48Khz 采样率 {8820,271,2, 2,1}, //88.2Khz 采样率 {9600,344,7, 0,1}, //96Khz 采样率 {17640,271,6,0,0}, //176.4Khz 采样率 {19200,295,6,0,0}, //192Khz 采样率 }; 有了上面的 fs-系数对应表,我们可以很方便的完成 SAI 的时钟配置。 (4)SAI 相关寄存器 接下来,我们看看本章需要用到的一些相关寄存器。 首先,是 SAI 配置寄存器 1:SAI_xCR1(x=A/B,下同),该寄存器各位描述如图 52.1.3.5 所示: 图 52.1.3.5 寄存器 SAI_xCR1 各位描述 该寄存器的配置,需要在禁止 SAI 的状况下配置,接下来,看看本章我们需要用到的各位 的描述: MODE[1:0]位:00,主发送器;01,主接收器;10,从发送器;11,从接收器。我们用来 播放音乐,设置为 00,就可以了。 PRTCFG[1:0]位:00,自由协议(I2S/LSB/MSB/TDM/PCM/DSP 等);10,AC’97 协议。 我们使用 I2S 协议,需要设置为 00。 DS[2:0]位:010~111,表示 8/10/16/20/24/32 位数据大小,我们使用的音频一般是 16/24 位, 所以设置这三个位为:100(16 位)或 110(24 位)。 LSBFIRST 位:控制数据传输时是 MSB 还是 LSB,I2S 为 MSB,我们设置该位为 0。 CKSTR 位:设置时钟选通边沿,我们设置为 1,即数据在时钟的上升沿选通。 SYNCEN[1:0]位:00,音频模块异步工作;01,音频模块与另外一个音频模块同步。我们 要控制 WM8978 播放音乐,需要设置音频模工作在异步模式,即设置 SYNCEN[1:0]=00。 MONO 位:用于设置单声道/立体声模式,我们设置为 0,工作在立体声模式。 OUTDIV 位:0,当 SAIEN 置 1 时,驱动音频模块输出;1,在该位设置为 1 后,立即驱 动音频模块输出。这里我们设置为 1。 SAIxEN 位:0,禁止音频模块;1,使能音频模块;注意:必须在所有 SAI 配置完成以后, 才设置该位为 1。 DMAEN 位:DMA 使能位,0,禁止 DMA;1,使能 DMA;我们设置为 1,使能 DMA。 NODIV 位:0,使能主时钟和位时钟分频器;1,禁止主时钟和位时钟分频器;我们一般设 置为 0。 MCKDIV[3:0]:主时钟分频器,当设置为 0000 时,表示 1 分频;其他情况,则分频值为: MCKDIV[3:0]*2;我们需要根据音频采样率(fs)的不同来设置不同的值。 第二个是 SAI 帧配置寄存器:SAI_xFRCR,该寄存器各位描述如图 52.1.3.6 所示: 图 52.1.3.6 寄存器 SAI_xFRCR 各位描述 FRL[7:0]位:帧长度设置位,它等于音频帧中 SCK 的个数-1。FRL 的最小值为 8,最大值 为 256,且 FRL+1 应该为偶数,并且是 2 的指数倍关系。 FSALL[6:0]位:帧同步有效电平长度,用于指定 FS 信号的有效电平长度,即高电平/低电 平的宽度,应该等于帧长度的一半。计算方法为:FSALL=(FRL+1)/2-1。 FSDEF 位:帧同步定义,0,FS 信号为起始帧信号;1,FS 信号为 SOF 信号+通道识别信 号。使用 I2S 协议的时候,我们设置 FS 为 1。 FSPOL 位:帧同步极性设置,0,FS 低电平有效(下降沿);1,FS 高电平有效(上升沿); 我们设置为 FS 低电平有效,即 FSPOL 位为 0。 FSOFF:帧同步偏移,0,在 slot0 的第一位上使能 FS;1,在 slot0 的第一位的前一位上使 能 FS。使用 I2S 协议时,需要设置 FSOFF 位为 1,以匹配 I2S 协议(见图 52.1.2.1)。 第三个是 SAI slot 寄存器:SAI_xSLOTR,该寄存器各位描述如图 52.1.3.7 所示: 图 52.1.3.7 寄存器 SAI_xSLOTR 各位描述 FBOFF[4:0]位:设置第一个位的偏移量,用于设置 slot 中第一个数据传输位的位置,它表 示一个偏移值。由于前面我们设置了 FSOFF 位,所以,我们设置 FBOFF=0 即可。 SLOTSZ[1:0]位:设置 slot 大小,00,slot 大小等于数据大小;01,16 位;10,32 位;我 们设置 SLOTSZ 为 10(32 位),以支持最高 32 位音频的播放。 NBSLOT[3:0]位:设置音频帧中 slot 的个数(设置值+1)。比如我们用立体声,使用 2 个 slot 就够了,所以设置 NBSLOT=1 即可。 SLOTEN[15:0]位:设置 slot 使能,每个位表示 1 个 slot,最多是 16 个 slot。我们使用 2 个 slot(即 slot0 和 slot1),所以,设置 SLOTEN 的最低 2 位为 1 即可。 第四个是 PLLI2S 配置寄存器:RCC_PLLI2SCFGR,该寄存器各位描述如图 52.1.3.8 所示: 图 52.1.3.8 寄存器 RCC_PLLI2SCFGR 各位描述 该寄存器用于配置 PLLI2SQ 和 PLLI2SN 两个系数,PLLI2SQ 的取值范围是:2~15,PLLI2SN 的取值范围是:49~432。同样,这两个也是根据 fs 的值来设置的。 第五个是 RCC 专用时钟配置寄存器 1:RCC_DCKCFGR1,该寄存器各位描述如图 52.1.3.9 所示: 图 52.1.3.9 寄存器 RCC_DCKCFGR1 各位描述 该寄存器用于配置 SAI1 和 SAI2 的时钟源(SAI1SEL[1:0]和 SAI2SEL[1:0]),以及分频系 数(PLLSAIDIVQ 和 PLLI2SDIVQ)。我们使用 SAI 1 来驱动 WM8978,且时钟源为 PLLI2S, 所以,需要设置 SAI1SEL[1:0]为 01,得到 SAI1_CK_x=PLLI2S_Q/PLLI2SDIVQ,其中,PLLI2S_Q 和 PLLI2SDIVQ 是根据 fs 的值来设置的。 第六个是 SAI 数据寄存器:SAI_xDR,该寄存器各位描述如图 52.1.3.10 所示: 图 52.1.3.10 寄存器 SAI_xDR 各位描述 当我们需要向 WM8978 发送音频数据的时候,通过写这个寄存器,就可以实现。不过,我 们采用 DMA 来传输,所以直接设置 DMA 的外设地址为 SAI_xDR 即可。 此外,使用 FIFO 的时候,还要用到 SAI_xCR2 寄存器设置 FIFO 阈值和刷新,我们这里就 不做多的介绍了,请大家参考《STM32F7 中文参考手册.pdf》第 33.3.8 节。 SAI 的相关寄存器,就给大家介绍到这里。 (5)SAI 初始化步骤 最后,我们看看要通过 STM32F7 的 SAI,驱动 WM8978 播放音乐的简要步骤。SAI 相关 的库函数定义和声明分布在源文件 stm32f7xx_hal_sai.c/stm23f7xx_hal_sai_ex.c 以及头文件 stm32f7xx_hal_sai.h 中。具体步骤如下: 1)初始化 WM8978 这个过程就是在 50.1.2 节最后那十几个寄存器的配置,包括软复位、DAC 设置、输出设置 和音量设置等。 2)初始化 SAI 此过程主要设置 SAI_xCR1、SAI_xFRCR 和 SAI_xSLOTR 等寄存器,设置 SAI 工作模式、 协议、时钟电平特性、slot 相关参数等。HAL 库 SAI 初始化函数为:HAL_SAI_Init,声明如下: HAL_StatusTypeDef HAL_SAI_Init(SAI_HandleTypeDef *hsai); 该函数只有一个入口参数 hsai,该参数为 SAI_HandleTypeDef 结构体指针类型。 SAI_HandleTypeDef 结构体定义如下: typedef struct __SAI_HandleTypeDef{ SAI_Block_TypeDef*Instance; SAI_InitTypeDefInit; SAI_FrameInitTypeDefFrameInit; SAI_SlotinitTypeDefSlotInit; uint8_t*pBuffPtr; uint16_tXferSize; uint16_tXferCount; DMA_HandleTypeDef*hdmatx; DMA_HandleTypeDef*hdmarx; SAIcallbackmutecallback; void (*InterruptServiceRoutine)(struct __SAI_HandleTypeDef *hsai); HAL_LockTypeDef Lock; __IO HAL_SAI_StateTypeDef State; __IO uint32_t ErrorCode; }SAI_HandleTypeDef;该结构体成员变量比较多,大致会分为如下几种: 第一种是初始化结构体变量 Init,FrameInit,和 SlotInit,这三个成员变量都是结构体类型, 分别用来初始化 SAI 的工作模式,协议,时钟电平特性和 slot 相关参数。 第二种是 HAL 库中处理 SAI 接口通信的数据指针 pBuffPtr,传输数据大小 XferSize 和剩余 数据量 XferCount 三个变量,这和串口通信很相似。 第三种是 hdmatx 和 hdmarx,为 DMA_HandleTypeDef 结构体指针类型,指向 DMA 句柄。 第四种是回调函数 mutecallback 和 InterruptServiceRoutine。 第五种是 HAL 库中间过程变量。 这里我们主要讲解 Init,FrameInit,和 SlotInit 三个初始化结构体变量。 成员变量 Init 是 SAI_InitTypeDef 结构体类型,该结构体定义为: typedef struct { uint32_t AudioMode; //音频模块模式 主/从 发送/接收 器 uint32_t Synchro; //同步使能:异步/同步 uint32_t SynchroExt; uint32_t OutputDrive;//输出驱动:立即驱动音频模块输出还是当 SAIEN 置 1 后输出 uint32_t NoDivider; //主时钟分频器使能/失能 uint32_t FIFOThreshold; //FIFO 阈值 uint32_t ClockSource; //SAI 时钟源选择 uint32_t AudioFrequency; //音频频率 uint32_t Mckdiv; //主时钟分频器系数 uint32_t MonoStereoMode; //模式:单声道还是立体声 uint32_t CompandingMode;//压扩模式设置 uint32_t TriState; //数据线的三态管理 uint32_t Protocol;//协议配置:自由协议还是 AC’97 协议 uint32_t DataSize;//数据大小:8/10/16/20/24/32 位 uint32_t FirstBit;// MSB 还是 LSB 在先 uint32_t ClockStrobing; //时钟选通边沿,SCK 上升沿还是下降沿 }SAI_InitTypeDef; 该结构体主要用来配置 SAI_xCR1 寄存器,各个成员变量含义我们已经注释了,如有不理 解的地方请参考 SAI_xCR1 寄存器定义。 成员变量 FrameInit 是 SAI_FrameInitTypeDef 结构体类型,用来进行帧设置,主要是配置 SAI_xFRCR 寄存器。结构体 SAI_FrameInitTypeDef 定义为: typedef struct { uint32_t FrameLength; //帧长度 uint32_t ActiveFrameLength; //帧同步有效电平长度 uint32_t FSDefinition; //帧同步定义 uint32_t FSPolarity; //帧同步极性设置 uint32_t FSOffset; //帧同步偏移设置 }SAI_FrameInitTypeDef; 该结构体各个成员变量含义就比较好理解了,在讲解SAI_xFRCR寄存器的时候都有提到, 这里我们同样也在每个成员变量后面注释了。 成员变量 SlotInit 是 SAI_SlotInitTypeDef 结构体类型,用来进行 SLOT 设置,主要是配置 SAI_xSLOTR 寄存器。结构体 SAI_SlotInitTypeDef 定义为: typedef struct { uint32_t FirstBitOffset; //第一个位的偏移量 uint32_t SlotSize; //设置 slot 大小 uint32_t SlotNumber; //设置音频帧中 slot 个数 uint32_t SlotActive; //设置 slot 使能 }SAI_SlotInitTypeDef; 该结构体各个成员变量含义同样比较好理解,我们都有注释,不理解的地方请参考寄存器 SAI_xSLOTR 各位定义。 关于 HAL_SAI_Init 的使用实例这里基于篇幅考虑我们就不列出来了,详情请参考软件设 计小节源码。 HAL 库同样提供了 SAI 初始化 MSP 回调函数 HAL_SAI_MspInit,定义如下: void HAL_SAI_MspInit(SAI_HandleTypeDef *hsai); 关于回调函数使用方法,这里我们就不做过多讲解了。 3)解析 WAV 文件,获取音频信号采样率和位数并设置 SAI 时钟分频器 这里,要先解析 WAV 文件,取得音频信号的采样率(fs)和位数(16 位或 24 位),根据 这两个参数,来设置 SAI 的时钟分频,这里我们用前面介绍的查表法来设置即可。我们在设置 完采样率和时钟分频后,便可以使能 SAI 了。 4)设置 DMA SAI 播放音频的时候,一般都是通过 DMA 来传输数据的,所以必须配置 DMA,本章我们 用 SAI 的子模块 A,其 TX 是使用的 DMA2 数据流 3 的通道 0 来传输的。并且,STM32F7 的 DMA 具有双缓冲机制,这样可以提高效率,大大方便了我们的数据传输,本章将 DMA2 数据 流 3 设置为:双缓冲循环模式,外设和存储器宽度相同(16 位/32 位),并开启 DMA 传输完 成中断(方便填充数据)。DMA 配置过程请参考实验源码,并对照第二十八章 DMA 实验讲解 学习。 5)编写 DMA 传输完成中断服务函数 为了方便填充音频数据,我们使用 DMA 传输完成中断,每当一个缓冲数据发送完后,硬 件自动切换为下一个缓冲,同时进入中断服务函数,填充数据到发送完的这个缓冲。过程如图 50.1.3.11 所示: 图 50.1.3.11 DMA 双缓冲发送音频数据流框图 6)开启 DMA 传输,填充数据 最后,我们就只需要开启 DMA 传输,然后及时填充 WAV 数据到 DMA 的两个缓存区即 可。此时,就可以在 WM8978 的耳机和喇叭通道听到所播放音乐了。 52.2 硬件设计 本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题, 则开始循环播放 SD 卡 MUSIC 文件夹里面的歌曲(必须在 SD 卡根目录建立一个 MUSIC 文件 夹,并存放歌曲(仅支持 wav 格式)在里面),在 TFTLCD 上显示歌曲名字、播放时间、歌 曲总时间、歌曲总数目、当前歌曲的编号等信息。KEY0 用于选择下一曲,KEY2 用于选择上 一曲,KEY_UP 用来控制暂停/继续播放。DS0 还是用于指示程序运行状态。 本实验用到的资源如下: 1) 指示灯 DS0 2) 三个按键(KEY_UP/KEY0/KEY1) 3) 串口 4) LCD 模块 5) SD 卡 6) SPI FLASH 7) WM8978 8) SAI 这些硬件我们都已经介绍过了,不过 WM8978 和 STM32F767 的连接,还没有介绍,连接 如图 52.2.1 所示: 图 52.2.1 WM8978 与 STM32F767 连接原理图 图中,PHONE 接口,可以用来插耳机,SPK+和 SPK-连接了板载的喇叭(在开发板底部)。 硬件上,IIC 接口和 24C02 等芯片共用。 本实验,大家需要准备 1 个 SD 卡(在里面新建一个 MUSIC 文件夹,并存放一些 wav 歌 曲在 MUSIC 文件夹下),然后下载本实验就可以听歌了。 52.3 软件设计 打开本章实验工程可以看到,我们在工程中新建了 AUDIOCODEC 分组和 APP 分组,分别 添加了 wavplay.c 文件和 audioplay.c 文件。同时在 HARDWARE 分组之下添加了 wm8978.c 文件 和 sai.c 文件。 本章代码比较多,我们就不全部贴出来给大家介绍了,这里仅挑一些重点函数给大家介绍 下。首先是 sai.c 里面,重点函数代码如下: //SAI Block A 初始化,I2S,飞利浦标准//mode:工作模式,可以设置:SAI_MODEMASTER_TX///SAI_MODEMASTER_RX/SAI_MODESLAVE_TX/SAI_MODESLAVE_RX//cpol:数据在时钟的上升/下降沿选通,可以设置//SAI_CLOCKSTROBING_FALLINGEDGE/SAI_CLOCKSTROBING_RISINGEDGE//datalen:数据大小,可以设置:SAI_DATASIZE_8/10/16/20/24/32void SAIA_Init(u32 mode,u32 cpol,u32 datalen){HAL_SAI_DeInit(&SAI1A_Handler);//清除以前的配置 SAI1A_Handler.Instance=SAI1_Block_A; //SAI1 Bock A SAI1A_Handler.Init.AudioMode=mode;//设置 SAI1 工作模式 SAI1A_Handler.Init.Synchro=SAI_ASYNCHRONOUS; //音频模块异步 SAI1A_Handler.Init.OutputDrive=SAI_OUTPUTDRIVE_ENABLE;//立即驱动音频模块输出 SAI1A_Handler.Init.NoDivider=SAI_MASTERDIVIDER_ENABLE;//使能主时钟分频器 SAI1A_Handler.Init.FIFOThreshold=SAI_FIFOTHRESHOLD_1QF;//设置 FIFO 阈值,1/4 FIFO SAI1A_Handler.Init.ClockSource=SAI_CLKSOURCE_PLLI2S; //SIA 时钟源为 PLL2S SAI1A_Handler.Init.MonoStereoMode=SAI_STEREOMODE; //立体声模式 SAI1A_Handler.Init.Protocol=SAI_FREE_PROTOCOL; //设置 SAI1 协议为:自由协议 SAI1A_Handler.Init.DataSize=datalen; //设置数据大小 SAI1A_Handler.Init.FirstBit=SAI_FIRSTBIT_MSB;//数据 MSB 位优先 SAI1A_Handler.Init.ClockStrobing=cpol; //数据在时钟的上升/下降沿选通 //帧设置 SAI1A_Handler.FrameInit.FrameLength=64; //设置帧长度为 64,左/右通道 32 个 SCK SAI1A_Handler.FrameInit.ActiveFrameLength=32;//设置帧同步有效电平长度//在 I2 模式下=1/2 帧长. SAI1A_Handler.FrameInit.FSDefinition=SAI_FS_CHANNEL_IDENTIFICATION;//FS 信号为 SOF 信号+通道识别信号 SAI1A_Handler.FrameInit.FSPolarity=SAI_FS_ACTIVE_LOW; //FS 低电平有效(下降沿) SAI1A_Handler.FrameInit.FSOffset=SAI_FS_BEFOREFIRSTBIT; //在 slot0 的第一位的//前一位使能 FS,以匹配飞利浦标准 //SLOT 设置 SAI1A_Handler.SlotInit.FirstBitOffset=0;//slot 偏移(FBOFF)为 0 SAI1A_Handler.SlotInit.SlotSize=SAI_SLOTSIZE_32B; //slot 大小为 32 位 SAI1A_Handler.SlotInit.SlotNumber=2; //slot 数为 2 个 SAI1A_Handler.SlotInit.SlotActive=SAI_SLOTACTIVE_0|SAI_SLOTACTIVE_1;//使能 slot0 和 slot1 HAL_SAI_Init(&SAI1A_Handler);//初始化 SAI __HAL_SAI_ENABLE(&SAI1A_Handler);//使能 SAI}void HAL_SAI_MspInit(SAI_HandleTypeDef *hsai){……//省略 IO 口初始化代码}const u16 SAI_PSC_TBL[][5]={……//省略代码,见 50.1.3 节};//开启 SAI 的 DMA 功能,HAL 库没有提供此函数//因此我们需要自己操作寄存器编写一个void SAIA_DMA_Enable(void){ u32 tempreg=0; tempreg=SAI1_Block_A->CR1; //先读出以前的设置tempreg|=1<<17; //使能 DMASAI1_Block_A->CR1=tempreg; //写入 CR1 寄存器中}//设置 SAIA 的采样率(@MCKEN)//samplerate:采样率,单位:Hz//返回值:0,设置成功;1,无法设置.u8 SAIA_SampleRate_Set(u32 samplerate){ u8 i=0; RCC_PeriphCLKInitTypeDef RCCSAI1_Sture;for(i=0;i<(sizeof(SAI_PSC_TBL)/10);i++)//看看改采样率是否可以支持{if((samplerate/10)==SAI_PSC_TBL[0])break;} if(i==(sizeof(SAI_PSC_TBL)/10))return 1;//搜遍了也找不到 RCCSAI1_Sture.PeriphClockSelection=RCC_PERIPHCLK_SAI1; //外设时钟源选择 RCCSAI1_Sture.Sai1ClockSelection=RCC_SAI1CLKSOURCE_PLLSAI; RCCSAI1_Sture.PLLSAI.PLLSAIN=(u32)SAI_PSC_TBL[1]; //设置 PLLSAIN RCCSAI1_Sture.PLLSAI.PLLSAIQ=(u32)SAI_PSC_TBL[2]; //设置 PLLSAIQ RCCSAI1_Sture.PLLSAIDivQ=SAI_PSC_TBL[3]; //设置 PLLSAIDivQ HAL_RCCEx_PeriphCLKConfig(&RCCSAI1_Sture); //设置时钟 __HAL_SAI_DISABLE(&SAI1A_Handler); //关闭 SAI SAI1A_Handler.Init.AudioFrequency=samplerate; //设置播放频率 HAL_SAI_Init(&SAI1A_Handler); //初始化 SAI SAIA_DMA_Enable(); //开启 SAI 的 DMA 功能 __HAL_SAI_ENABLE(&SAI1A_Handler); //开启 SAI return 0;}//SAIA TX DMA 配置//设置为双缓冲模式,并开启 DMA 传输完成中断//buf0:M0AR 地址.//buf1:M1AR 地址.//num:每次传输数据量//width:位宽(存储器和外设,同时设置),0,8 位;1,16 位;2,32 位;void SAIA_TX_DMA_Init(u8* buf0,u8 *buf1,u16 num,u8 width){ u32 memwidth=0,perwidth=0; //外设和存储器位宽 switch(width) { case 0: //8 位 memwidth=DMA_MDATAALIGN_BYTE; perwidth=DMA_PDATAALIGN_BYTE; break; case 1: //16 位 memwidth=DMA_MDATAALIGN_HALFWORD; perwidth=DMA_PDATAALIGN_HALFWORD; break; case 2: //32 位 memwidth=DMA_MDATAALIGN_WORD; perwidth=DMA_PDATAALIGN_WORD; break; } __HAL_RCC_DMA2_CLK_ENABLE(); //使能 DMA2 时钟 __HAL_LINKDMA(&SAI1A_Handler,hdmatx,SAI1_TXDMA_Handler); //将 DMA 与 SAI 联系起来 SAI1_TXDMA_Handler.Instance=DMA2_Stream3; //DMA2 数据流 3 SAI1_TXDMA_Handler.Init.Channel=DMA_CHANNEL_0; //通道 0 SAI1_TXDMA_Handler.Init.Direction=DMA_MEMORY_TO_PERIPH; //存储器到外设 SAI1_TXDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外设非增量模式 SAI1_TXDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存储器增量模式 SAI1_TXDMA_Handler.Init.PeriphDataAlignment=perwidth; //外设数据长度:16/32 位 SAI1_TXDMA_Handler.Init.MemDataAlignment=memwidth; //存储器数据长度:16/32 位 SAI1_TXDMA_Handler.Init.Mode=DMA_CIRCULAR; //使用循环模式 SAI1_TXDMA_Handler.Init.Priority=DMA_PRIORITY_HIGH; //高优先级 SAI1_TXDMA_Handler.Init.FIFOMode=DMA_FIFOMODE_DISABLE; //不使用 FIFO SAI1_TXDMA_Handler.Init.MemBurst=DMA_MBURST_SINGLE;//单次突发传输 SAI1_TXDMA_Handler.Init.PeriphBurst=DMA_PBURST_SINGLE; //外设突发单次传输 HAL_DMA_DeInit(&SAI1_TXDMA_Handler); //先清除以前的设置 HAL_DMA_Init(&SAI1_TXDMA_Handler); //初始化 DMA HAL_DMAEx_MultiBufferStart(&SAI1_TXDMA_Handler,(u32)buf0,(u32)&SAI1_Block_A->DR,(u32)buf1,num);//开启双缓冲 __HAL_DMA_DISABLE(&SAI1_TXDMA_Handler); //先关闭 DMA delay_us(10); //10us 延时,防止-O2 优化出问题 __HAL_DMA_ENABLE_IT(&SAI1_TXDMA_Handler,DMA_IT_TC);//开启传输完成中断 __HAL_DMA_CLEAR_FLAG(&SAI1_TXDMA_Handler,DMA_FLAG_TCIF3_7); //清除 DMA 传输完成中断标志位 HAL_NVIC_SetPriority(DMA2_Stream3_IRQn,0,0); //DMA 中断优先级 HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);} //SAI DMA 回调函数指针void (*sai_tx_callback)(void); //TX 回调函数//DMA2_Stream3 中断服务函数void DMA2_Stream3_IRQHandler(void){ if(__HAL_DMA_GET_FLAG(&SAI1_TXDMA_Handler,DMA_FLAG_TCIF3_7)!=RESET) //DMA 传输完成 { __HAL_DMA_CLEAR_FLAG(&SAI1_TXDMA_Handler,DMA_FLAG_TCIF3_7); //清除 DMA 传输完成中断标志位 sai_tx_callback(); //执行回调函数,读取数据等操作在这里面处理 } } //SAI 开始播放void SAI_Play_Start(void){ __HAL_DMA_ENABLE(&SAI1_TXDMA_Handler);//开启 DMA TX 传输 }//关闭 I2S 播放void SAI_Play_Stop(void){ __HAL_DMA_DISABLE(&SAI1_TXDMA_Handler); //结束播放 }其中,SAIA_Init 完成 SAI_A 子模块的的初始化,通过 3 个参数设置 SAI_A 的详细配置信 息。另外一个函数:SAIA_SampleRate_Set,则是用前面介绍的查表法,根据音频采样率来设置 SAI_A 的时钟部分。函数 SAIA_TX_DMA_Init,用于设置 SAI_A 的 DMA 发送,使用双缓冲循 环模式,发送数据给 WM8978,并开启了发送完成中断。而 DMA2_Stream3_IRQHandler 函数, 则是 DMA2 数据流 3 发送完成中断的服务函数,该函数调用 sai_tx_callback 函数(函数指针, 使用前需指向特定函数)实现 DMA 数据填充。函数 SAI_Play_Start 和 SAI_Play_Stop,用于开 启和关闭 DMA 传输。 再来看 wm8978.c 里面的几个函数,代码如下: //WM8978 初始化//返回值:0,初始化正常// 其他,错误代码u8 WM8978_Init(void){u8 res;IIC_Init(); //初始化 IIC 接口res=WM8978_Write_Reg(0,0);//软复位 WM8978if(res)return 1; //发送指令失败,WM8978 异常//以下为通用设置WM8978_Write_Reg(1,0X1B); //R1,MICEN 设置为 1(MIC 使能),BIASEN 设置为 1WM8978_Write_Reg(2,0X1B0); //R2,ROUT1,LOUT1 输出使能(耳机可以工作) WM8978_Write_Reg(3,0X6C); //R3,LOUT2,ROUT2 输出使能(喇叭工作)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(49,1<<2); //R49,SPEAKER BOOST,1.5x 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); //读取 R3if(dacen)regval|=3<<0; //R3 最低 2 个位设置为 1,开启 DACR&DACLelse regval&=~(3<<0); //R3 最低 2 个位清零,关闭 DACR&DACL.WM8978_Write_Reg(3,regval); //设置 R3regval=WM8978_Read_Reg(2); //读取 R2if(adcen)regval|=3<<0; //R2 最低 2 个位设置为 1,开启 ADCR&ADCLelse 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; //sai 传输完成标志vu8 wavwitchbuf=0; //saibufx 指示标志//WAV 解析初始化//fname:文件路径+文件名//wavx:wav 信息存放结构体指针//返回值:0,成功;1,打开文件失败;2,非 WAV 文件;3,DATA 区域未找到.u8 wav_decode_init(u8* fname,__wavctrl* wavx){FIL*ftemp;u8 *buf; u32 br=0;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; //数据流开始的地方. printf("wavx->audioformat:%drn",wavx->audioformat);printf("wavx->nchannels:%drn",wavx->nchannels);printf("wavx->samplerate:%drn",wavx->samplerate);printf("wavx->bitrate:%drn",wavx->bitrate);printf("wavx->blockalign:%drn",wavx->blockalign);printf("wavx->bps:%drn",wavx->bps);printf("wavx->datasize:%drn",wavx->datasize);printf("wavx->datastart:%drn",wavx->datastart); }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;u32 *p,*pbuf;if(bits==24)//24bit 音频,需要处理一下{readlen=(size/4)*3; //此次要读取的字节数f_read(audiodev.file,audiodev.tbuf,readlen,(UINT*)&bread);//读取数据pbuf=(u32*)buf;for(i=0;i 率,位数,数据流起始位置等);wav_buffill 函数,用 f_read 读取数据,填充数据到 buf 里面, 注意 24 位音频的时候,读出的数据需要扩展为 32 位才可填充到 buf;wav_sai_dma_tx_callback 函数,则是 DMA 发送完成的回调函数(sai_tx_callback 函数指针指向该函数),这里面,我们 并没有对数据进行填充处理(暂停时进行了填 0 处理),而是采用 2 个标志量:wavtransferend 和 wavwitchbuf,来告诉 wav_play_song 函数是否传输完成,以及应该填充哪个数据 buf(saibuf1 或 saibuf2); 最后,wav_play_song 函数,是播放 WAV 的最终执行函数,该函数解析完 WAV 文件后, 设置 WM8978 和 I2S 的参数(采样率,位数等),并开启 DMA,然后不停填充数据,实现 WAV 播放,该函数还进行了按键扫描控制,实现上下取切换和暂停/播放等操作。该函数通过判断 wavtransferend 是否为 1 来处理是否应该填充数据,而到底填充到哪个 buf(saibuf1 或 saibuf2), 则是通过 wavwitchbuf 标志来确定的,当 wavwitchbuf=0 时,说明 DMA 正在使用 saibuf2,程 序应该填充 saibuf1;当 wavwitchbuf=1 时,说明 DMA 正在使用 saibuf1,程序应该填充 saibuf2; 接下来,看看 audioplay.c 里面的几个函数,代码如下: void audio_play(void){u8 res;DIR wavdir; //目录FILINFO *wavfileinfo; //文件信息u8 *pname; //带路径的文件名u16 totwavnum; //音乐文件总数u16 curindex; //当前索引u8 key; //键值 u32 temp;u32 *wavoffsettbl; //音乐 offset 索引表 WM8978_ADDA_Cfg(1,0); //开启 DACWM8978_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=(FILINFO*)mymalloc(SRAMIN,sizeof(FILINFO)); //申请内存 pname=mymalloc(SRAMIN,_MAX_LFN*2+1); //为带路径的文件名分配内存wavoffsettbl=mymalloc(SRAMIN,4*totwavnum);//申请 4*totwavnum 个字节的内存,用于存放音乐文件 off block 索引while(!wavfileinfo||!pname||!wavoffsettbl)//内存分配出错 { 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;//当前索引为 0while(1)//全部查询一遍{temp=wavdir.dptr; //记录当前 index res=f_readdir(&wavdir,wavfileinfo); //读取目录下的一个文件 if(res!=FR_OK||wavfileinfo->fname[0]==0)break;//错误了/到末尾了,退出res=f_typetell((u8*)wavfileinfo->fname);if((res&0XF0)==0X40)//取高四位,看看是不是音乐文件{wavoffsettbl[curindex]=temp;//记录索引curindex++;} } } curindex=0; //从 0 开始显示 res=f_opendir(&wavdir,(const TCHAR*)"0:/MUSIC"); //打开目录while(res==FR_OK)//打开成功{dir_sdi(&wavdir,wavoffsettbl[curindex]); //改变当前目录索引 res=f_readdir(&wavdir,wavfileinfo); //读取目录下的一个文件 if(res!=FR_OK||wavfileinfo->fname[0]==0)break; //错误了/到末尾了,退出strcpy((char*)pname,"0:/MUSIC/"); //复制路径(目录)strcat((char*)pname,(const char*)wavfileinfo->fname);//将文件名接在后面LCD_Fill(60,190,lcddev.width-1,190+16,WHITE); //清除之前的显示Show_Str(60,190,lcddev.width-60,16,(u8*)wavfileinfo->fname,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); //释放内存 myfree(SRAMIN,pname); //释放内存 myfree(SRAMIN,wavoffsettbl); //释放内存}这里,audio_play 函数在 main 函数里面被调用,该函数首先设置 WM8978 相关配置,然 后查找 SD 卡里面的 MUSIC 文件夹,并统计该文件夹里面总共有多少音频文件(统计包括: WAV/MP3/APE/FLAC 等),然后,该函数调用 audio_play_song 函数,按顺序播放这些音频文件。 在 audio_play_song 函数里面,通过判断文件类型,调用不同的解码函数,本章,只支持 WAV 文件,通过 wav_play_song 函数实现 WAV 解码。其他格式:MP3/APE/FLAC 等,在综合 实验我们会实现其解码函数,大家可以参考综合实验代码,这里就不做介绍了。 最后我们看看 main 函数源码: int main(void){ Cache_Enable(); //打开 L1-Cache HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhz delay_init(216); //延时初始化uart_init(115200); //串口初始化 LED_Init(); //初始化 LED KEY_Init(); //初始化按键 SDRAM_Init(); //初始化 SDRAM LCD_Init(); //初始化 LCDW25QXX_Init(); //初始化 W25Q256 W25QXX_Init(); //初始化 W25Q256 WM8978_Init(); //初始化 WM8978WM8978_HPvol_Set(40,40); //耳机音量设置WM8978_SPKvol_Set(50); //喇叭音量设置 my_mem_init(SRAMIN); //初始化内部内存池 my_mem_init(SRAMEX); //初始化外部 SDRAM 内存池 my_mem_init(SRAMDTCM); //初始化内部 DTCM 内存池 exfuns_init(); //为 fatfs 相关变量申请内存 f_mount(fs[0],"0:",1); //挂载 SD 卡f_mount(fs[1],"1:",1); //挂载 SPI FLASH. 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,"阿波罗 STM32F4/F7 开发板",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,"2016 年 7 月 18 日",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 函数,开始音频播放。软件 部分就介绍到这里,其他未贴出代码,请参考光盘本例程源码。 52.4 下载验证 在代码编译成功之后,我们下载代码到 ALIENTEK 阿波罗 STM32 开发板上,程序先执行 字库检测,然后当检测到 SD 卡根目录的 MUSIC 文件夹有有效音频文件(WAV 格式音频)的 时候,就开始自动播放歌曲了,如图 52.4.1 所示: 图 52.4.1 音乐播放中 从上图可以看出,当前正在播放第 17 首歌曲,总共 18 首歌曲,歌曲名、播放时间、总时 长、码率、音量等信息等也都有显示。此时 DS0 会随着音乐的播放而闪烁。 图中我们播放的是 192Khz,24 位的音乐,码率=192*24*2=9216Kbps,这比最好的 MP3 (320Kbps)足足高了 28 倍多!!!因而可以带来更好的音质享受,发烧友的最爱。 我们在开发板的 PHONE 端子插入耳机,就可以通过耳机欣赏音乐了。同时,我们可以通 过按 KEY0 和 KEY2 来切换下一曲和上一曲,通过 KEY_UP 控制暂停和继续播放。 本实验,我们还可以通过 USMART 来测试 WM8978 的其他功能,通过将 wm8978.c 里面 的部分函数加入 USMART 管理,我们可以很方便的设置 wm8978 的各种参数(音量、3D、EQ 等都可以设置),达到验证测试的目的。有兴趣的朋友,可以实验测试一下。 至此,我们就完成了一个简单的音乐播放器了,虽然只支持 WAV 文件,但是大家可以在 此基础上,增加其他音频格式解码器(可参考综合实验),便可实现其他音频格式解码了。 |
|
|
相关推荐
|
|
636 浏览 1 评论
976 浏览 0 评论
858 浏览 0 评论
STM32F405驱动DS1302时钟模块,输出时间错乱该怎么排查?
4810 浏览 2 评论
stm32f405rgt6驱动DS1302ZN出现时间错乱问题
3612 浏览 1 评论
/9
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2025-12-11 19:34 , Processed in 0.959382 second(s), Total 64, Slave 46 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191

淘帖