38. SAI—音频播放与录音输入

本章参考资料:《STM32H750用户手册》、《STM32H750xB规格书》、库帮助文档《STM32H753xx_User_Manual.chm》及《I2S BUS》。

若对I2S通讯协议不了解,可先阅读《I2S BUS》文档的内容学习。

关于音频编解码器WM8978,请参考其规格书《WM8978_v4.5》来了解。

38.1. SAI、I2S简介

SAI 接口(串行音频接口)是一种串行音频接口,具有灵活性高、配置多样等特点,可支持多种音频协议。

Inter-IC Sount Bus(I2S)是飞利浦半导体公司(现为恩智浦半导体公司)针对数字音频设备之间的音频数据传输而制定的一种总线标准。 在飞利浦公司的I2S标准中,既规定了硬件接口规范,也规定了数字音频数据的格式。

38.1.1. 数字音频技术

现实生活中的声音是通过一定介质传播的连续的波,它可以由周期和振幅两个重要指标描述。正常人可以听到的声音频率范围为20Hz~20KHz。 现实存在的声音是模拟量,这对声音保存和长距离传输造成很大的困难,一般的做法是把模拟量转成对应的数字量保存, 在需要还原声音的地方再把数字量的转成模拟量输出,参考图 音频转换过程

音频转换过程

模拟量转成数字量过程,一般可以分为三个过程,分别为采样、量化、编码,参考图 声音数字化过程 。用一个比源声音频率高的采样信号去量化源声音, 记录每个采样点的值,最后如果把所有采样点数值连接起来与源声音曲线是互相吻合的,只是它不是连续的。在图中两条蓝色虚线距离就是采样信号的周期, 即对应一个采样频率(FS),可以想象得到采样频率越高最后得到的结果就与源声音越吻合,但此时采样数据量越越大, 一般使用44.1KHz采样频率即可得到高保真的声音。每条蓝色虚线长度决定着该时刻源声音的量化值,该量化值有另外一个概念与之挂钩,就是量化位数。 量化位数表示每个采样点用多少位表示数据范围,常用有16bit、24bit或32bit,位数越高最后还原得到的音质越好,数据量也会越大。

声音数字化过程

WM8978是一个低功耗、高质量的立体声多媒体数字信号编解码器,集成DAC和ADC,可以实现声音信号量化成数字量输出, 也可以实现数字量音频数据转换为模拟量声音驱动扬声器。这样使用WM8978芯片解决了声音与数字量音频数据转换问题, 并且通过配置WM8978芯片相关寄存器可以控制转换过程的参数,比如采样频率,量化位数,增益、滤波等等。

WM8978芯片是一个音频编解码器,但本身没有保存音频数据功能,它只能接收其它设备传输过来的音频数据进行转换输出到扬声器, 或者把采样到的音频数据输出到其它具有存储功能的设备保存下来。该芯片与其他设备进行音频数据传输接口就是I2S协议的音频接口。

38.1.2. SAI总线接口

SAI 接口(串行音频接口)适用于许多立体声或单声道应用。例如,它可配置为支持 I2S 标准、LSB 或 MSB 对齐、PCM/DSP、TDM 和 AC’97 等协议。 将音频模块配置为发送器时,SAI 接口可提供 SPDIF 输出。SAI通过两个完全独立的音频子模块来实现这种灵活性和可配置性。 每个模块都有自己的时钟发生器和 I/O 线控制器。SAI 可以配置为主模式或配置为从模式。音频子模块既可作为接收器,又可作为发送器; 既可与另一模块同步,又可以不同步。SAI 可与其它 SAI 相连接来同步运行。

STM32H750的SAI总线接口有4个专用引脚信号:

(1) SAI_SCK_x: 位时钟,数字音频的每一位数据都对应有一个CK脉冲,它的频率为:2*采样频率*量化位数,2代表左右两个通道数据。

(2) SAI_MCLK_x: 主时钟,可用做外部解码器的参考时钟,帧同步与主时钟之间的频率比为固定值256或512,。

(3) SAI_SD_x : 音频模块数据线,用于发送或接收两个时分复用的数据通道上的数据。

(4) SAI_FS_x: 音频模块帧同步线,表明当前传输数据的声道,不同标准有不同的定义,频率等于采样频率(FS)。

38.1.3. 音频数据传输协议标准

随着技术的发展,在统一的I2S硬件接口下,出现了多种不同的数据格式,可分为左对齐(MSB)标准、右对齐(LSB)标准、I2S Philips标准。另外, STM32H750x系列控制器还支持PCM(脉冲编码调)音频传输协议。下面以STM32H750x系列控制器资源解释这四个传输协议。

STM32H750x系列控制器I2S的数据寄存器只有16bit,并且左右声道数据一般是紧邻传输,为正确得到左右两个声道数据, 需要软件控制数据对应通道数据写入或读取。另外,音频数据的量化位数可能不同,控制器支持16bit、24bit和32bit三种数据长度, 因为数据寄存器是16bit的,所以对于24bit和32bit数据长度需要发送两个。为此,可以产生四种数据和帧格式组合:

  • 将16位数据封装在16位帧中

  • 将16位数据封装在32位帧中

  • 将24位数据封装在32位帧中

  • 将32位数据封装在32位帧中

当使用32位数据包中的16位数据时,前16位(MSB)为有效位,16位LSB被强制清零,无需任何软件操作或DMA请求(只需一个读/写操作)。 如果程序使用DMA传输(一般都会用),则24位和32位数据帧需要对数据寄存器执行两次DMA操作。24位的数据帧,硬件会将8位非有效位扩展到带有0位的32位。 对于所有数据格式和通信标准而言,始终会先发送最高有效位(MSB优先)。

38.1.3.1. I2S Philips标准

使用WS信号来指示当前正在发送的数据所属的通道,为0时表示左通道数据。该信号从当前通道数据的第一个位(MSB)之前的一个时钟开始有效。 发送方在时钟信号(CK)的下降沿改变数据,接收方在上升沿读取数据。WS信号也在SCK的下降沿变化。参考图 I2S_Philips标准24bit传输 , 为24bit数据封装在32bit帧传输波形。正如之前所说,WS线频率对于采样频率FS,一个WS线周期包括发送左声道和右声道数据, 在图中实际需要64个CK周期来完成一次传输。

I2S_Philips标准24bit传输

38.1.3.2. 左对齐标准

在WS发生翻转同时开始传输数据,参考图 左对齐标准24bit传输 ,为24bit数据封装在32bit帧传输波形。该标准较少使用。注意此时WS为1时, 传输的是左声道数据,这刚好与I2S Philips标准相反。

左对齐标准24bit传输

38.1.3.3. 右对齐标准

与左对齐标准类似,参考图 右对齐标准24bit传输 ,为24bit数据封装在32bit帧传输波形。

右对齐标准24bit传输

38.1.3.4. PCM标准

PCM即脉冲编码调制,模拟语音信号经过采样量化以及一定数据排列就是PCM了。WS不再作为声道数据选择。它有两种模式,短帧模式和长帧模式, 以WS信号高电平保持时间为判别依据,长帧模式保持13个CK周期,短帧模式只保持1个CK周期,可以通过相关寄存器位选择。 如果有多通道数据是在一个WS周期内传输完成的,传完左声道数据就紧跟发送右声道数据。图 PCM标准16bit传输 为单声道数据16bit扩展到32bit数据帧发送波形。

PCM标准16bit传输

38.2. SAI功能框图

STM32H750有4个SAI(SAI1,SAI2,SAI3和SAI4),外设资源不仅相互独立,而且不像I2S那样占用SPI的大部分资源,但是还是共用了一些SPI的引脚。 这样I2S1和SPI1只能选择一个功能使用,I2S2和SPI2、I2S3和SPI3相同道理。资源共用包括引脚共用和部分寄存器共用, 当然也有部分是专用的。SPI已经在之前相关章节做了详细讲解,建议先看懂SPI相关内容再学习I2S。

控制器的SAI支持两种工作模式,主模式和从模式;主模式下使用自身时钟发生器生成通信时钟。SAI功能框图参考图 SAI功能框图

SAI功能框图

38.2.1. 功能引脚

4个SAI的4个功能引脚都与SPI有不同程度的的复用。SAI时钟发生器可以由控制器内部时钟源分频产生,亦可采用CKIN引脚输入时钟分频得到, 一般使用内部时钟源即可。控制器SAI引脚分布参考表 STM32H750x系列控制器SAI引脚分布

STM32H750x系列控制器SAI引脚分布

38.2.2. 数据寄存器

SAI在硬件寄存器上与SPI完全独立。SAI有一个独立的数据寄存器 (SAI_xDR),有效长度为32bit,用于SAI的数据发送和接收,它实际由两个部分组成, 一个32bit移位寄存器和一个32bit的FIFO,当处于发送模式且FIFO未满时,向SAI_xDR写入数据可向FIFO加载数据, 总线自动把FIFO中的内容转入到移位寄存器中进行传输;在接收模式下,实际接收到的数据先填充移位寄存器,然后自动转入FIFO, 软件读取SPI_DR时自动从FIFO内读取并清空FIFO。SAI是挂载在APB1总线上的。

38.2.3. 逻辑控制

SAI的逻辑控制通过设置相关寄存器位实现,比如通过配置SAI配置寄存器1(SAI_xCR1)的相关位可以实现音频传输协议的选择、 选择SAI工作在主模式还是从模式并且选择是发送还是接收、是否启用DMA、声道模式、LSB/MSB对齐、传输数据长度等等。 SAI配置寄存器2(SAI_xCR2)可用于设置音频压缩扩展模式、静音选项、FIFO阈值等。SAI中断屏蔽寄存器(SAI_xIM)可以配置中断使能与失能, I2S有7个中断源,分别为FIFO请求(FIFO空、四分之一满、半满、全满)、上溢或下溢错误、帧同步错误、帧同步滞后检测、编码器未就绪、 静音检测和时钟配置错误。SAI状态寄存器(SAI_xSR)用于指示当前SAI状态。

38.2.4. 时钟发生器

I2S比特率用来确定I2S数据线上的数据流和I2S时钟信号频率。I2S比特率=每个通道的位数×通道数×音频采样频率。

SAI时钟发生器内部结构。sai_x_ker_ck可以选择使用PLL1Q时钟作为I2S时钟源或I2S_CKIN引脚输入时钟作为SAI时钟源。 一般选择内部PLL2P(通过P分频系数)作为时钟源。例程程序设置PLL2P时钟为400MHz,R分频系数为5,此时sai_x_ker_ck时钟为80MHz。

SAI时钟发生器内部结构

SAI配置寄存器1寄存器(SAI_xCR1)的NOMCK位用于设置主时钟MCK引脚时钟输出使能;MCKDIV[5:0]位设置主时钟MCK预分频器的分频系数,实际分频系数可从0到32变化。

当使能MCK时钟输出,即NOMCK=0时,帧同步频率计算如下:

\[F_{FS\_ x} = \frac{F_{\text{sia}\_ x\_ ker\_ ck}}{MCKDIV \times \left( OSR + 1 \right) \times 256}\]

位时钟频率计算如下:

\[F_{SCK\_ x} = \frac{F_{MCLK\_ x} \times (FRL + 1)}{\left( OSR + 1 \right) \times 256}\]

此时帧长度(FRL+1)必须是2的幂数,且必须介于8到256之间。

当禁止MCK时钟输出,即NOMCK=1时,采样频率计算如下:

\[F_{FS\_ x} = \frac{F_{sai\_ x\_ ker\_ ck}}{(FRL + 1) \times MCKDIV}\]

位时钟频率计算与NOMCK=1时相同,此时(FRL+1)可以取8到256之间的任意值,MCKDIV=0和MCKDIV=1时结果相同。

38.3. WM8978音频编解码器

WM8978是一个低功耗、高质量的立体声多媒体数字信号编解码器。它主要应用于便携式应用。它结合了立体声差分麦克风的前置放大与扬声器、 耳机和差分、立体声线输出的驱动,减少了应用时必需的外部组件,比如不需要单独的麦克风或者耳机的放大器。

高级的片上数字信号处理功能,包含一个5路均衡功能,一个用于ADC和麦克风或者线路输入之间的混合信号的电平自动控制功能, 一个纯粹的录音或者重放的数字限幅功能。另外在ADC的线路上提供了一个数字滤波的功能,可以更好的应用滤波,比如“减少风噪声”。

WM8978可以被应用为一个主机或者一个从机。基于共同的参考时钟频率,比如 12MHz和13MHz,内部的PLL可以为编解码器提供所有需要的音频时钟。 与STM32控制器连接使用,STM32一般作为主机,WM8978作为从机。

WM8978内部结构 为WM8978芯片内部结构示意图,参考来自《WM8978_v4.5》。该图给人的第一印象感觉就是很复杂,密密麻麻很多内容, 特别有很多“开关”。实际上,每个开关对应着WM8978内部寄存器的一个位,通过控制寄存器的就可以控制开关的状态。

WM8978内部结构

38.3.1. 输入部分

WM8978结构图的左边部分是输入部分,可用于模拟声音输入,即用于录音输入。有三个输入接口,一个是由LIN和LIP、 RIN和RIP组合而成的伪差分立体声麦克风输入,一个是由L2和R2组合的立体声麦克风输入,还有一个是由AUXL和AUXR组合的线输入或用来传输告警声的输入。

38.3.2. 输出部分

WM8978结构图的右边部分是声音放大输出部分,LOUT1和ROUT1用于耳机驱动,LOUT2和ROUT2用于扬声器驱动, OUT3和OUT4也可以配置成立体声线输出,OUT4也可以用于提供一个左右声道的单声道混合。

38.3.3. ADC和DAC

WM8978结构图的中边部分是芯片核心内容,处理声音的AD和DA转换。ADC部分对声音输入进行处理,包括ADC滤波处理、 音量控制、输入限幅器/电平自动控制等等。DAC部分控制声音输出效果,包括DAC5路均衡器、DAC 3D放大、DAC输出限幅以及音量控制等等处理。

38.3.4. 通信接口

WM8978有两个通信接口,一个是数字音频通信接口,另外一个是控制接口。音频接口是采用I2S接口,支持左对齐、右对齐和I2S标准模式, 以及DSP模式A和模拟B。控制接口用于控制器发送控制命令配置WM8978运行状态,它提供2线或3线控制接口,对于STM32控制器,我们选择2线接口方式, 它实际就是I2C总线方式,其芯片地址固定为0011010。通过控制接口可以访问WM8978内部寄存器,实现芯片工作环境配置,总共有58个寄存器, 表示为R0至R57,限于篇幅问题这里不再深入探究,每个寄存器意义参考《WM8978_v4.5》了解。

WM8978寄存器是16bit长度,高7位([15:9]bit)用于表示寄存器地址,低9为有实际意义,比如对于图 WM8978内部结构 中的某个开关。 所以在控制器向芯片发送控制命令时,必须传输长度为16bit的指令,芯片会根据接收命令高7位值寻址。

38.3.5. 其他部分

WM8978作为主从机都必须对时钟进行管理,由内部PLL单元控制。另外还有电源管理单元。

38.4. WAV格式文件

WAV是微软公司开发的一种音频格式文件,用于保存Windows平台的音频信息资源,它符合资源互换文件格式(Resource Interchange File Format, RIFF)文件规范。标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几! WAVE是录音时用的标准的WINDOWS文件格式,文件的扩展名为“WAV”,数据本身的格式为PCM或压缩型,属于无损音乐格式的一种。

38.4.1. RIFF文件规范

RIFF有不同数量的chunk(区块)组成,每个chunk由“标识符”、“数据大小”和“数据”三个部分组成,“标识符”和“数据大小”都是占用4个字节空间。 简单RIFF格式文件结构参考图 RIFF文件格式结构 。最开始是ID为“RIFF”的chunk,Size为“RIFF”chunk数据字节长度,所以总文件大小为Size+8。 一般来说,chunk不允许内部再包含chunk,但有两个例外,ID为“RIFF”和“LIST”的chunk却是允许。 对此“RIFF”在其“数据”首4个字节用来存放“格式标识码(Form Type)”,“LIST”则对应“LIST Type”。

RIFF文件格式结构

38.4.2. WAVE文件

WAVE文件是非常简单的一种RIFF文件,其“格式标识码”定义为WAVE。RIFF chunk包括两个子chunk,ID分别为fmt和data,还有一个可选的fact chunk。 Fmt chunk用于表示音频数据的属性,包括编码方式、声道数目、采样频率、每个采样需要的bit数等等信息。factchunk是一个可选chunk, 一般当WAVE文件由某些软件转化而成就包含fact chunk。data chunk包含WAVE文件的数字化波形声音数据。WAVE整体结构如表 WAVE文件结构

WAVE文件结构

data chunk是WAVE文件主体部分,包含声音数据,一般有两个编码格式:PCM和ADPCM,ADPCM(自适应差分脉冲编码调制)属于有损压缩,现在几乎不用, 绝大部分WAVE文件是PCM编码。PCM编码声音数据可以说是在“数字音频技术”介绍的源数据,主要参数是采样频率和量化位数。

16bit声音数据格式 为量化位数为16bit时不同声道数据在data chunk数据排列格式。

16bit声音数据格式

38.4.3. WAVE文件实例分析

利用winhex工具软件可以非常方便以十六进制查看文件,图 WAV文件头实例 为名为“张国荣-一盏小明灯.wav”文件使用winhex工具打开的部分界面截图。 这部分截图是WAVE文件头部分,声音数据部分数据量非常大,有兴趣可以使用winhex查看。

WAV文件头实例

下面对文件头进行解读,参考表 WAVE文件格式说明

WAVE文件格式说明

38.5. SAI初始化结构体详解

HAL库函数对SAI外设建立了一个初始化结构体SAI_InitTypeDef。初始化结构体成员用于设置SAI工作环境参数,并由SAI相应初始化配置函数HAL_I2S_Init调用, 这些设定参数将会设置SAI相应的寄存器,达到配置I2S工作环境的目的。

初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。 初始化结构体定义在stm32h7xx_hal_sai.h文件中,初始化库函数定义在stm32h7xx_hal_sai.c文件中,编程时我们可以结合这两个文件内注释使用。

代码清单:SAI-1 I2S外设管理结构体(文件stm32h7xx_hal_sai.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct __SAI_HandleTypeDef {
    SAI_Block_TypeDef         *Instance;    /* SAI外设注册基地址 */
    SAI_InitTypeDef           Init;         /* SAI初始化结构体 */
    SAI_FrameInitTypeDef      FrameInit;    /* SAI帧配置结构体 */
    SAI_SlotInitTypeDef       SlotInit;     /* SAI Slot配置结构体 */
    uint8_t                  *pBuffPtr;     /* 指向SAI传输缓冲区的指针 */
    uint16_t                  XferSize;     /* SAI传输大小 */
    uint16_t                  XferCount;    /* SAI传输计数值 */
    DMA_HandleTypeDef         *hdmatx;      /* SAI发送DMA配置结构体 */
    DMA_HandleTypeDef         *hdmarx;      /* SAI接收DMA配置结构体 */
    SAIcallback               mutecallback; /* SAI 静音回调 */
    void (*InterruptServiceRoutine)(struct __SAI_HandleTypeDef *hsai); /* 中断处理程序的函数指针 */
    HAL_LockTypeDef           Lock;         /* SAI对象锁 */
    __IO HAL_SAI_StateTypeDef State;        /* SAI通信状态 */
    __IO uint32_t             ErrorCode;    /* SAI错误代码 */
} SAI_HandleTypeDef;

(1) Instance:SAI 寄存器基地址指针,所有参数都是指定基地址后才能正确写入寄存器。

(2) Init:SAI的初始化结构体,下面会详细讲解每一个成员。

(3) FrameInit:用来设置SAI帧同步参数的结构体。

(4) SlotInit:用来设置 SAI slot参数的结构体。

(5) pBuffPtr:用来存放接受和发送数据地址的指针

(6) XferSize:用来指定需要传输数据的大小。

(7) XferCount:用来指定需要传输数据的个数。

(8) hdmatx:SAI的发送DMA外设管理结构体,用来配置发送的DMA的请求和DMA的相关参数

(9) hdmarx:SAI的接收DMA外设管理结构体,用来配置接收的DMA的请求和DMA的相关参数

(10) mutecallback:静音回调函数地址的指针

(11) InterruptServiceRoutine:用于存放中断服务函数地址的指针

(12) Lock:SAI的外设对象资源锁。

(13) State:SAI的工作状态,正常工作的话,处于HAL_SAI_STATE_BUSY状态。出现等待超时,则会处于HAL_SAI_STATE_TIMEOUT状态。

(14) ErrorCode:SAI的错误操作值,提供给用户排查错误。

SAI的外设管理结构体的配置,我们一般只需要配置好SAI的外设寄存器基地址以及初始化结构体就可以了。其余的成员变量一般都是调用某个HAL库函数时, 函数内部会自动赋值,因此,我们不需要关心这部分的配置。

SAI初始化结构体用于配置SAI基本工作环境,比如SAI工作模式、通信标准选择等等。它被SAI_Init函数调用。

代码清单:SAI-2 I2S_InitTypeDef结构体(文件stm32h7xx_hal_sai.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
typedef struct {
    uint32_t AudioMode;           /* SAI音频模式选择 */
    uint32_t Synchro;             /* 设置SAI同步模式 */
    uint32_t SynchroExt;          /* 设置SAI外部同步 */
    uint32_t OutputDrive;         /* 设置何时驱动SAI输出 */
    uint32_t NoDivider;           /* 是否分频主时钟 */
    uint32_t FIFOThreshold;       /* SAI的FIFO阈值 */
    uint32_t AudioFrequency;      /* 音频采样频率 */
    uint32_t Mckdiv;              /* 选择主时钟分频器 */
    uint32_t MckOverSampling;     /* 主时钟过采样率 */
    uint32_t MonoStereoMode;      /* 声道模式选择 */
    uint32_t CompandingMode;      /* 压缩扩展模式选择 */
    uint32_t TriState;            /* 压缩扩展类型选择 */
    SAI_PdmInitTypeDef PdmInit;   /* PDM参数配置结构体 */
    uint32_t Protocol;            /* 选择SAI协议 */
    uint32_t DataSize;            /* SAI块数据大小 */
    uint32_t FirstBit;            /* 选择数据格式是MSB还是LSB */
    uint32_t ClockStrobing;       /* 选择SAI主时钟触发边沿 */
} SAI_InitTypeDef;

(1) AudioMode:SAI模式选择,可选主机发送、主机接收、从机发送,从机接收模式,从机全双工和主机全双工。 它设定SAI_xCR1寄存器MODE[1:0] 位的值。一般设置STM32控制器为主机模式,当播放声音时选择发送模式;当录制声音时选择接收模式。

(2) Synchro:SAI同步模式选择,可选异步、内部同步、SAI1外部同步和SAI2外部同步,它设定SAI_xCR1寄存器SYNCEN[1:0]位与SAI_GCR寄存器的SYNCIN[1:0]位的值。

(3) SynchroExt:SAI同步输出模式选择,可选禁止同步输出、使能SAI块A同步输出和使能SAI块B同步输出,它设定SAI_GCR寄存器的SYNCOUT[1:0]位的值。

(4) OutputDrive:SAI块输出驱动设置,可选立即使能输出或在音频模块使能后输出,它设定SAI_xCR1寄存器OUTDRIV位的值。

(5) NoDivider:主时钟分频器使能控制,可选择使能主时钟发生器或禁止主时钟发生器,它设定SAI_xCR1寄存器NOMCK 的值。

(6) FIFOThreshold:SAI块的FIFO阈值设置,可选择FIFO空、FIFO四分之一满、FIFO半满、FIFO四分之三满和FIFO全满,它设定SAI_xCR2寄存器FTH位[2:0]的值。

(7) AudioFrequency:SAI音频采样频率设置,可设置为192k、96k、48k、44k、32k、22k、16k、11k、8k和MCKDIV。

(8) Mckdiv:主时钟分频系数。默认情况下,主时钟频率是内核时钟输入 sai_x_ker_ck的1 分频。

(9) MckOverSampling:主时钟过采样率设置,可选择256倍过采样和512倍过采样,它设定SAI_xCR1寄存器OSR位的值。

(10) Data24BitAlignment:当传输的数据格式为24位时,选择左对齐或者是右对齐。

(11) FifoThreshold:FIFO的阈值,最大可以是16个数据。

(12) MasterKeepIOState:外设失能时,是否控制GPIO口。若为1,则外设保持对IO的控制权。

(13) SlaveExtendFREDetection:一般用于从机接受模式,选择在数据帧开始时检测欠载,在数据帧结束时检测欠载或者是SS信号有效。

38.6. 录音与回放实验

WAV格式文件在现阶段一般以无损音乐格式存在,音质可以达到CD格式标准。结合上一章SD卡操作内容,本实验通过FatFS文件系统函数从SD卡读取WAV格式文件数据, 然后通过SAI接口将音频数据发送到WM8978芯片,这样在WM8978芯片的扬声器接口即可输出声音,整个系统构成一个简单的音频播放器。 反过来的,我们可以实现录音功能,控制启动WM8978芯片的麦克风输入功能,音频数据从WM8978芯片的SAI接口传输到STM32控制器存储器中, 利用SD卡文件读写函数,根据WAV格式文件的要求填充文件头,然后就把WM8978传输过来的音频数据写入到WAV格式文件中, 这样就可以制成一个WAV格式文件,可以通过开发板回放也可以在电脑端回放。

38.6.1. 硬件设计

开发板板载WM8978芯片,具体电路设计参考图 WM8978电路设计 。WM8978与STM32H750有两个连接接口,SAI音频接口和两线I2C控制接口, 通过将WM8978芯片的MODE引脚拉低选择两线控制接口,符合I2C通信协议,这也导致WM8978是只写的,所以在程序上需要做一些处理。 WM8978输入部分有两种模式,一个是板载咪头输入,另外一个是通过3.5mm耳机插座引出。WM8978输出部分通过3.5mm耳机插座引出, 可直接接普通的耳机线或作为功放设备的输入源。

WM8978电路设计

38.6.2. 软件设计

这里只讲解核心的部分代码,有些变量的设置,头文件的包含等没有全部罗列出来,完整的代码请参考本章配套的工程。

上一章我们已经介绍了基于SD卡的文件系统,认识读写SD卡内文件方法,前面已经介绍了WAV格式文件结构以及WM8978芯片相关内容, 通过WM8978音频接口传输过来的音频数据可以直接作为WAV格式文件的音频数据部分,大致过程就是程序控制WM8978启动录音功能, 通过SAI音频数据接口WM8978的录音输出传输到STM32控制器指定缓冲区内,然后利用FatFs的文件写入函数把缓冲区数据写入到WAV格式文件中, 最终实现声音录制功能。同样的道理,WAV格式文件中的音频数据可以直接传输给WM8978芯片实现音乐播放,整个过程与声音录制工程相反。

STM32控制器与WM8978通信可分为两部分驱动函数,一部分是I2C控制接口,另一部分是SAI音频数据接口。

bsp_wm8978.c和bsp_wm8978.h两个是专门创建用来存放WM8978芯片驱动代码。

38.6.2.1. I2C控制接口

WM8978要正常工作并且实现符合我们的要求,我们必须对芯片相关寄存器进行必须要配置,STM32控制器通过I2C接口与WM8978芯片控制接口连接。 I2C接口内容也已经在以前做了详细介绍,这里主要讲解WM8978的功能函数。

bsp_wm8978.c文件中的I2cMaster_Init函数用于I2C通信接口GPIO和I2C相关配置,属于常规配置可以参考GPIO和I2C章节理解, 这里不再分析,代码具体见本章配套程序工程文件。

输入输出选择枚举

代码清单:SAI-3 输入输出选择枚举(文件bsp_wm8978.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* WM8978 音频输入通道控制选项, 可以选择多路,比如 MIC_LEFT_ON | LINE_ON */
typedef enum {
    IN_PATH_OFF   = 0x00, /* 无输入 */
    MIC_LEFT_ON   = 0x01, /* LIN,LIP脚,MIC左声道(接板载咪头)  */
    MIC_RIGHT_ON  = 0x02, /* RIN,RIP脚,MIC右声道(接板载咪头)  */
    LINE_ON       = 0x04, /* L2,R2 立体声输入(接板载耳机插座) */
    AUX_ON        = 0x08, /* AUXL,AUXR 立体声输入(开发板没用到) */
    DAC_ON        = 0x10, /* SAI数据DAC (CPU产生音频信号) */
    ADC_ON        = 0x20  /* 输入的音频馈入WM8978内部ADC (SAI录音) */
} IN_PATH_E;

/* WM8978 音频输出通道控制选项, 可以选择多路 */
typedef enum {
    OUT_PATH_OFF  = 0x00, /* 无输出 */
    EAR_LEFT_ON   = 0x01, /* LOUT1 耳机左声道(接板载耳机插座) */
    EAR_RIGHT_ON  = 0x02, /* ROUT1 耳机右声道(接板载耳机插座) */
    SPK_ON        = 0x04, /* LOUT2和ROUT2反相输出单声道(开发板没用到)*/
    OUT3_4_ON     = 0x08, /* OUT3 和 OUT4 输出单声道音频(开发板没用到)*/
} OUT_PATH_E;

IN_PATH_E和OUT_PATH_E枚举了WM8978芯片可用的声音输入源和输出端口,具体到开发板,如果进行录用功能, 设置输入源为(MIC_RIGHT_ON|ADC_ON)或(LINE_ON|ADC_ON),设置输出端口为OUT_PATH_OFF或(EAR_LEFT_ON |EAR_RIGHT_ON); 对于音乐播放功能,设置输入源为DAC_ON,设置输出端口为(EAR_LEFT_ON | EAR_RIGHT_ON)。

宏定义

代码清单:SAI-4 宏定义(文件bsp_wm8978.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* 定义最大音量 */
#define VOLUME_MAX                            63    /* 最大音量 */
#define VOLUME_STEP                           1    /* 音量调节步长 */

/* 定义最大MIC增益 */
#define GAIN_MAX                               63    /* 最大增益 */
#define GAIN_STEP                              1    /* 增益步长 */

/* WM8978 I2C从机地址 */
#define WM8978_SLAVE_ADDRESS                   0x34

/*等待超时时间*/
#define WM8978_I2C_FLAG_TIMEOUT             ((uint32_t)0x4000)
#define WM8978_I2C_LONG_TIMEOUT   ((uint32_t)(10 * WM8978_I2C_FLAG_TIMEOUT))

WM8978声音调节有一定的范围限制,比如R52(LOUT1 Volume Control)的LOUT1VOL[5:0]位用于设置LOUT1的音量大小,可赋值范围为0~63。 WM8978包含可调节的输入麦克风PGA增益,可对每个外部输入端口可单独设置增益大小, 比如R45(Left Channelinput PGA volume control)的INPPGAVOL[5:0]位用于设置左通道输入增益音量,最大可设置值为63。

WM8978控制接口被设置为I2C模式,其地址固定为0011010,为方便使用,直接定义为0x34。

最后定义I2C通信超时等待时间。

WM8978寄存器写入

代码清单:SAI-5 WM8978寄存器写入(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @brief  写寄存器,这是提供给上层的接口
* @param  slave_addr: 从机地址
* @param  reg_addr:寄存器地址
* @param len:写入的长度
* @param data_ptr:指向要写入的数据
* @retval 正常为0,不正常为非0
*/
int Sensors_I2C_WriteRegister(unsigned char slave_addr,
                            unsigned char reg_addr,
                            unsigned short len,
                            unsigned char *data_ptr)
{
    HAL_StatusTypeDef status = HAL_OK;
    status = HAL_I2C_Mem_Write(&I2C_Handle, slave_addr, reg_addr,
    I2C_MEMADD_SIZE_8BIT,data_ptr, len,
                                I2Cx_FLAG_TIMEOUT);
    /* 检查通讯状态 */
    if (status != HAL_OK) {
        /* 总线出错处理 */
        I2Cx_Error(slave_addr);
    }
    while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {

    }
    /* 检查SENSOR是否就绪进行下一次读写操作 */
    while (HAL_I2C_IsDeviceReady(&I2C_Handle, slave_addr, I2Cx_FLAG_TIMEOUT,
    I2Cx_FLAG_TIMEOUT) ==  HAL_TIMEOUT);
    /* 等待传输结束 */
    while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {

    }
    return status;
}

Sensors_I2C_WriteRegister用于向WM8978芯片寄存器写入数值,达到配置芯片工作环境,函数有四个形参,一个从机地址,一个是寄存器地址, 可设置范围为0~57;还有寄存器值和数据长度,WM8978芯片寄存器总共有16bit,前7bit用于寻址,后9位才是数据, 这里寄存器值形参使用uint16_t类型,只有低9位有效。

HAL_I2C_Mem_Write函数中还有I2C通信超时等待功能,防止出错时卡死。

WM8978寄存器读取

WM8978芯片是从硬件上选择I2C通信模式,该模式是只写的,STM32控制器无法读取WM8978寄存器内容,但程序有时需要用到寄存器内容, 为此我们创建了一个存放WM8978所有寄存器值的数组,在系统复位是将数组内容设置为WM8978缺省值,然后在每次修改寄存器内容时同步更新该数组内容, 这样可以达到该数组与WM8978寄存器内容相等的效果,参考 代码清单:SAI-6

代码清单:SAI-6 WM8978寄存器值缓冲区和读取(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
wm8978寄存器缓存
由于WM8978的I2C两线接口不支持读取操作,因此寄存器值缓存在内存中,
当写寄存器时同步更新缓存,读寄存器时直接返回缓存中的值。
寄存器MAP 在WM8978(V4.5_2011).pdf 的第89页,寄存器地址是7bit, 寄存器数据是9bit
*/
static uint16_t wm8978_RegCash[] = {
    0x000, 0x000, 0x000, 0x000, 0x050, 0x000, 0x140, 0x000,
    0x000, 0x000, 0x000, 0x0FF, 0x0FF, 0x000, 0x100, 0x0FF,
    0x0FF, 0x000, 0x12C, 0x02C, 0x02C, 0x02C, 0x02C, 0x000,
    0x032, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000,
    0x038, 0x00B, 0x032, 0x000, 0x008, 0x00C, 0x093, 0x0E9,
    0x000, 0x000, 0x000, 0x000, 0x003, 0x010, 0x010, 0x100,
    0x100, 0x002, 0x001, 0x001, 0x039, 0x039, 0x039, 0x039,
    0x001, 0x001
};

/**
* @brief  从cash中读回读回wm8978寄存器
* @param  _ucRegAddr : 寄存器地址
* @retval 寄存器值
*/
static uint16_t wm8978_ReadReg(uint8_t _ucRegAddr)
{
    return wm8978_RegCash[_ucRegAddr];
}

/**
* @brief  写wm8978寄存器
* @param  _ucRegAddr: 寄存器地址
* @param  _usValue: 寄存器值
* @retval 0:写入失败
*         1:写入成功
*/
static uint8_t wm8978_WriteReg(uint8_t _ucRegAddr, uint16_t _usValue)
{
    uint8_t res;
    res=WM8978_I2C_WriteRegister(_ucRegAddr,_usValue);
    wm8978_RegCash[_ucRegAddr] = _usValue;
    return res;
}

wm8978_WriteReg实现向WM8978寄存器写入数据并修改缓冲区内容。

输出音量修改与读取

代码清单:SAI-7 音量修改与读取(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* @brief  修改输出通道1音量
* @param  _ucVolume :音量值, 0-63
* @retval 无
*/
void wm8978_SetOUT1Volume(uint8_t _ucVolume)
{
    uint16_t regL;
    uint16_t regR;

    if (_ucVolume > VOLUME_MAX) {
        _ucVolume = VOLUME_MAX;
    }
    regL = _ucVolume;
    regR = _ucVolume;
    /*
    R52 LOUT1 Volume control
    R53 ROUT1 Volume control
    */
    /* 先更新左声道缓存值 */
    wm8978_WriteReg(52, regL | 0x00);

    /* 再同步更新左右声道的音量 */
    /* 0x180表示 在音量为0时再更新,避免调节音量出现的“嘎哒”声 */
    wm8978_WriteReg(53, regR | 0x100);
}

/**
* @brief  读取输出通道1音量
* @param  无
* @retval 当前音量值
*/
uint8_t wm8978_ReadOUT1Volume(void)
{
    return (uint8_t)(wm8978_ReadReg(52) & 0x3F );
}

/**
* @brief  输出静音.
* @param  _ucMute:模式选择
*         @arg 1:静音
*         @arg 0:取消静音
* @retval 无
*/
void wm8978_OutMute(uint8_t _ucMute)
{
    uint16_t usRegValue;
    if (_ucMute == 1) { /* 静音 */
        usRegValue = wm8978_ReadReg(52); /* Left Mixer Control */
        usRegValue |= (1u << 6);
        wm8978_WriteReg(52, usRegValue);

        usRegValue = wm8978_ReadReg(53); /* Left Mixer Control */
        usRegValue |= (1u << 6);
        wm8978_WriteReg(53, usRegValue);

        usRegValue = wm8978_ReadReg(54); /* Right Mixer Control */
        usRegValue |= (1u << 6);
        wm8978_WriteReg(54, usRegValue);

        usRegValue = wm8978_ReadReg(55); /* Right Mixer Control */
        usRegValue |= (1u << 6);
        wm8978_WriteReg(55, usRegValue);
    } else {  /* 取消静音 */
        usRegValue = wm8978_ReadReg(52);
        usRegValue &= ~(1u << 6);
        wm8978_WriteReg(52, usRegValue);

        usRegValue = wm8978_ReadReg(53); /* Left Mixer Control */
        usRegValue &= ~(1u << 6);
        wm8978_WriteReg(53, usRegValue);

        usRegValue = wm8978_ReadReg(54);
        usRegValue &= ~(1u << 6);
        wm8978_WriteReg(54, usRegValue);

        usRegValue = wm8978_ReadReg(55); /* Left Mixer Control */
        usRegValue &= ~(1u << 6);
        wm8978_WriteReg(55, usRegValue);
    }
}

wm8978_SetOUT1Volume函数用于修改OUT1通道的音量大小,有一个形参用于指示音量大小,要求范围为0~63。这里同时更新OUT1的左右两个声道音量, WM8978芯片的R52和R53分别用于设置OUT1的左声道和右声道音量,具体位段意义参考表 OUT1音量控制寄存器 。 wm8978_SetOUT1Volume函数会同时修改WM8978寄存器缓存区wm8978_RegCash数组内容。

OUT1音量控制寄存器

另外,wm8978_SetOUT2Volume用于设置OUT2的音量,程序结构与wm8978_SetOUT1Volume相同,只是对应修改R54和R55。

wm8978_ReadOUT1Volume函数用于读取OUT1的音量,它实际就是读取wm8978_RegCash数组对应元素内容。

wm8978_OutMute用于静音控制,它有一个形参用于设置静音效果,如果为1则为开启静音,如果为0则取消静音。静音控制是通过R52和R53的第6位实现的, 在进入静音模式时需要先保存OUT1和OUT2的音量大小,然后在退出静音模式时就可以正确返回到静音前OUT1和OUT2的配置。

输入增益调整

代码清单:SAI-8 输入增益调整(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* @brief  设置增益
* @param  _ucGain :增益值, 0-63
* @retval 无
*/
void wm8978_SetMicGain(uint8_t _ucGain)
{
    if (_ucGain > GAIN_MAX) {
        _ucGain = GAIN_MAX;
    }

    /* PGA 音量控制  R45, R46
    Bit8  INPPGAUPDATE
    Bit7  INPPGAZCL   过零再更改
    Bit6  INPPGAMUTEL   PGA静音
    Bit5:0  增益值,010000是0dB
    */
    wm8978_WriteReg(45, _ucGain);
    wm8978_WriteReg(46, _ucGain | (1 << 8));
}


/**
* @brief  设置Line输入通道的增益
* @param  _ucGain :音量值, 0-7. 7最大,0最小。 可衰减可放大。
* @retval 无
*/
void wm8978_SetLineGain(uint8_t _ucGain)
{
    uint16_t usRegValue;

    if (_ucGain > 7) {
        _ucGain = 7;
    }

    /*
    Mic 输入信道的增益由 PGABOOSTL 和 PGABOOSTR 控制
    Aux 输入信道的输入增益由 AUXL2BOOSTVO[2:0] 和 AUXR2BOOSTVO[2:0] 控制
    Line 输入信道的增益由 LIP2BOOSTVOL[2:0] 和 RIP2BOOSTVOL[2:0] 控制
    */
    /* R47(左声道),R48(右声道), MIC 增益控制寄存器
    R47 (R48定义与此相同)
B8 PGABOOSTL=1,0表示MIC信号直通无增益,1表示MIC信号+20dB增益(通过自举电路)
B7 = 0, 保留
B6:4  L2_2BOOSTVOL=x,0表示禁止,1-7表示增益-12dB ~ +6dB(可以衰减也可以放大)
B3 = 0, 保留
B2:0 AUXL2BOOSTVOL=x,0表示禁止,1-7表示增益-12dB~+6dB(可以衰减也可以放大)
    */

    usRegValue = wm8978_ReadReg(47);
    usRegValue &= 0x8F;/* 将Bit6:4清0   1000 1111*/
    usRegValue |= (_ucGain << 4);
    wm8978_WriteReg(47, usRegValue);  /* 写左声道输入增益控制寄存器 */

    usRegValue = wm8978_ReadReg(48);
    usRegValue &= 0x8F;/* 将Bit6:4清0   1000 1111*/
    usRegValue |= (_ucGain << 4);
    wm8978_WriteReg(48, usRegValue);  /* 写右声道输入增益控制寄存器 */
}

wm8978_SetMicGain用于设置麦克风输入的增益,可以设置增强或减弱输入效果,比如对于部分声音源本身就是比较微弱,我们就可以设置放大该信号, 从而得到合适的录制效果,该函数主要通过设置R45和R46实现,可设置的范围为0~63,默认值为16,没有增益效果。

wm8978_SetLineGain用于设置LINE输入的增益,对应芯片的L2和R2引脚组合的输入,开发板使用耳机插座引出拓展。它通过设置R47和R48寄存器实现, 可设置范围为0~7,默认值为0,没有增益效果。

音频接口标准选择

代码清单:SAI-9 wm8978_CfgAudioIF函数(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @brief  配置WM8978的音频接口(I2S)
* @param  _usStandard : 接口标准,
        I2S_Standard_Phillips, I2S_Standard_MSB 或I2S_Standard_LSB
* @param  _ucWordLen : 字长,16、24、32  (丢弃不常用的20bit格式)
* @retval 无
*/
void wm8978_CfgAudioIF(uint16_t _usStandard, uint8_t _ucWordLen)
{
    uint16_t usReg;

    /* WM8978(V4.5_2011).pdf 73页,寄存器列表 */

    /*  REG R4, 音频接口控制寄存器
    B8    BCP  = X, BCLK极性,0表示正常,1表示反相
    B7    LRCP = x, LRC时钟极性,0表示正常,1表示反相
    B6:5  WL = x, 字长,00=16bit,01=20bit,10=24bit,11=32bit
        (右对齐模式只能操作在最大24bit)
    B4:3  FMT = x,音频数据格式,00=右对齐,01=左对齐,10=SAI格式,11=PCM
    B2    DACLRSWAP = x, 控制DAC数据出现在LRC时钟的左边还是右边
    B1    ADCLRSWAP = x,控制ADC数据出现在LRC时钟的左边还是右边
    B0    MONO  = 0,0表示立体声,1表示单声道,仅左声道有效
    */
    usReg = 0;
    if (_usStandard == SAI_Standard_Phillips) { /* I2S飞利浦标准 */
        usReg |= (2 << 3);
    } else if (_usStandard == SAI_Standard_MSB) { /* MSB对齐标准(左对齐) */
        usReg |= (1 << 3);
    } else if (_usStandard == SAI_Standard_LSB) { /* LSB对齐标准(右对齐) */
        usReg |= (0 << 3);
    } else {  /*PCM标准(16位通道帧上带长或短帧同步或者16位数据帧扩展为32位通道帧)

        usReg |= (3 << 3);;
    }

    if (_ucWordLen == 24) {
        usReg |= (2 << 5);
    } else if (_ucWordLen == 32) {
        usReg |= (3 << 5);
    } else {
        usReg |= (0 << 5);    /* 16bit */
    }
    wm8978_WriteReg(4, usReg);

    /*
    R6,时钟产生控制寄存器
    MS = 0,  WM8978被动时钟,由MCU提供MCLK时钟
    */
    wm8978_WriteReg(6, 0x000);
}

wm8978_CfgAudioIF函数用于设置WM8978芯片的音频接口标准,它有两个形参,第一个是标准选择,可选I2S Philips标准(I2S_Standard_Phillips)、 左对齐标准(I2S_Standard_MSB)以及右对齐标准(I2S_Standard_LSB);另外一个形参是字长设置,可选16bit、24bit以及32bit,较常用16bit。 它函数通过控制WM8978芯片R4实现,最后还通过通用时钟控制寄存器R6设置芯片的SAI工作在从模式,时钟线为输入时钟。

输入输出通道设置

代码清单:SAI-10 wm8978_CfgAudioPath函数(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void wm8978_CfgAudioPath(uint16_t _InPath, uint16_t _OutPath)
{
    uint16_t usReg;
    if ((_InPath == IN_PATH_OFF) && (_OutPath == OUT_PATH_OFF)) {
        wm8978_PowerDown();
        return;
    }

    /*
    R1 寄存器 Power manage 1
    Bit8    BUFDCOPEN,  Output stage 1.5xAVDD/2 driver enable
    Bit7    OUT4MIXEN, OUT4 mixer enable
    Bit6    OUT3MIXEN, OUT3 mixer enable
    Bit5    PLLEN .不用
    Bit4   MICBEN ,Microphone Bias Enable (MIC偏置电路使能)
    Bit3   BIASEN ,Analogue amplifier bias control必须设置为1模拟放大器才工作
    Bit2    BUFIOEN , Unused input/output tie off buffer enable
    Bit1:0  VMIDSEL, 必须设置为非00值模拟放大器才工作
    */
    usReg = (1 << 3) | (3 << 0);
    if (_OutPath & OUT3_4_ON) { /* OUT3和OUT4使能输出 */
        usReg |= ((1 << 7) | (1 << 6));
    }
    if ((_InPath & MIC_LEFT_ON) || (_InPath & MIC_RIGHT_ON)) {
        usReg |= (1 << 4);
    }
    wm8978_WriteReg(1, usReg);  /* 写寄存器 */

    /**********************************************/
    /*         此处省略部分代码,具体参考工程文件          */
    /**********************************************/

    /*  R10 寄存器 DAC Control
    B8  0
    B7  0
    B6  SOFTMUTE, Softmute enable:
    B5  0
    B4  0
    B3  DACOSR128,  DAC oversampling rate: 0=64x (lowest power)
                                        1=128x (best performance)
    B2  AMUTE,    Automute enable
    B1  DACPOLR,  Right DAC output polarity
    B0  DACPOLL,  Left DAC output polarity:
    */
    if (_InPath & DAC_ON) {
        wm8978_WriteReg(10, 0);
    }
}

wm8978_CfgAudioPath函数用于配置声音输入输出通道,有两个形参,第一个形参用于设置输入源,可以使用IN_PATH_E枚举类型成员的一个或多个或运算结果; 第二个形参用于设置输出通道,可以使用OUT_PATH_E枚举类型成员的一个或多个或运算结果。具体到开发板,如果进行录用功能, 设置输入源为(MIC_RIGHT_ON|ADC_ON)或(LINE_ON|ADC_ON),设置输出端口为OUT_PATH_OFF或(EAR_LEFT_ON | EAR_RIGHT_ON); 对于音乐播放功能,设置输入源为DAC_ON,设置输出端口为(EAR_LEFT_ON | EAR_RIGHT_ON)。

wm8978_CfgAudioPath函数首先判断输入参数合法性,如果输入出错直接调用函数wm8978_PowerDown进入低功耗模式,并退出。

接下来使用wm8978_WriteReg配置相关寄存器值。大致可分三个部分,第一部分是电源管理部分,主要涉及到R1、R2和R3三个寄存器, 使用输入输出通道之前必须开启相关电源。第二部分是输入通道选择及相关配置,配置R44控制选择输入通道,R14设置输入的高通滤波器功能, R27、R28、R29和R30设置输入的可调陷波滤波器功能,R32、R33和R34控制输入限幅器/电平自动控制(ALC),R35设置ALC噪声门限, R47和R48设置通道增益参数,R15和R16设置ADC数字音量,R43设置AUXR功能。第三部分是输出通道选择及相关配置,控制R49选择输出通道, R50和R51设置左右通道混合输出效果,R56设置OUT3混合输出效果,R57设置OUT4混合输出效果,R11和R12设置左右DAC数字音量,R10设置DAC参数。

录音放音设置

代码清单:SAI-11 wm8978_CtrlGPIO1函数(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief  控制WM8978的GPIO1引脚输出0或1,
*     控制模拟开关来切换录音放音
*     1:放音
*     0:录音
* @param  _ucValue :GPIO1输出值,0或1
* @retval 无
*/
void wm8978_CtrlGPIO1(uint8_t _ucValue)
{
    uint16_t usRegValue;

    /* R8, pdf 62页 */
    if (_ucValue == 0) { /* 输出0 */
        usRegValue = 6; /* B2:0 = 110 */
    } else {
        usRegValue = 7; /* B2:0 = 111 */
    }
    wm8978_WriteReg(8, usRegValue);
}

由于H750的SAI是半双工,因此wm8978_CtrlGPIO1函数用于控制一个模拟开关来控制WM8978是录音还是放音,置高电平是放音,置低电平是录音。

软件复位

代码清单:SAI-12 wm8978_Reset函数(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint8_t wm8978_Reset(void)
{
    /* wm8978寄存器缺省值 */
    const uint16_t reg_default[] = {
        0x000, 0x000, 0x000, 0x000, 0x050, 0x000, 0x140, 0x000,
        0x000, 0x000, 0x000, 0x0FF, 0x0FF, 0x000, 0x100, 0x0FF,
        0x0FF, 0x000, 0x12C, 0x02C, 0x02C, 0x02C, 0x02C, 0x000,
        0x032, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000,
        0x038, 0x00B, 0x032, 0x000, 0x008, 0x00C, 0x093, 0x0E9,
        0x000, 0x000, 0x000, 0x000, 0x003, 0x010, 0x010, 0x100,
        0x100, 0x002, 0x001, 0x001, 0x039, 0x039, 0x039, 0x039,
        0x001, 0x001
    };
    uint8_t res;
    uint8_t i;

    res=wm8978_WriteReg(0x00, 0);

    for (i = 0; i < sizeof(reg_default) / 2; i++) {
        wm8978_RegCash[i] = reg_default[i];
    }
    return res;
}

wm8978_Reset函数用于软件复位WM8978芯片,通过写入R0完成,使其寄存器复位到缺省状态,同时会更新寄存器缓冲区数组wm8978_RegCash恢复到缺省状态。

38.6.2.2. SAI控制接口

WM8978集成I2S音频接口,用于与外部设备进行数字音频数据传输,芯片I2S接口属性通过wm8978_CfgAudioIF函数配置。 STM32控制器使用SAI与WM8978进行音频数据传输,一般设置STM32控制器为主机模式,WM8978作为从设备,SAI外设运行I2S协议。

SAI_GPIO_Config函数用于初始化SAI相关GPIO,具体参考工程文件。

SAI工作模式配置

代码清单:SAI-13 SAIxA_Tx_Config函数(文件bsp.sai.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @brief  配置STM32的SAI外设工作模式
* @param _usStandard:接口标准,SAI_STANDARD_PHILIPS, SAI_STANDARD_MSB 或 SAI_STANDARD_LSB
* @param  _usWordlen : 数据格式,16bit 或者24bit
* @param  _usAudioFreq : 采样频率,SAI_AUDIOFREQ_8K、SAI_AUDIOFREQ_16K、SAI_AUDIOFREQ_22K、
*         SAI_AUDIOFREQ_44K、SAI_AUDIOFREQ_48K
* @retval 无
*/
void SAIxA_Tx_Config(const uint16_t _usStandard, const uint16_t _usWordLen, const uint32_t _usAudioFreq)
{
    BSP_AUDIO_OUT_ClockConfig(_usAudioFreq);
    SAI_CLK_ENABLE();
    h_sai.Instance = SAI1_Block_A;

    h_sai.Init.AudioMode = SAI_MODEMASTER_TX;//配置为发送模式
    h_sai.Init.Synchro = SAI_ASYNCHRONOUS; //模块内部为异步
    h_sai.Init.OutputDrive = SAI_OUTPUTDRIVE_ENABLE;//立刻输出
    h_sai.Init.NoDivider=SAI_MASTERDIVIDER_ENABLE;//使能MCK输出NOACK
    h_sai.Init.FIFOThreshold=SAI_FIFOTHRESHOLD_1QF;//1/4FIFO
    h_sai.Init.MonoStereoMode=SAI_STEREOMODE;
    h_sai.Init.AudioFrequency = _usAudioFreq;
    h_sai.Init.Protocol=SAI_FREE_PROTOCOL;           //设置SAI1协议为:自由协议(支持I2S/LSB/MSB/TDM/PCM/DSP等协议)
    h_sai.Init.DataSize=SAI_DATASIZE_16;                     //设置数据大小
    h_sai.Init.FirstBit=SAI_FIRSTBIT_MSB;            //数据MSB位优先
    h_sai.Init.ClockStrobing=SAI_CLOCKSTROBING_RISINGEDGE;                   //数据在时钟的上升/下降沿选通

    //帧设置
    h_sai.FrameInit.FrameLength=32;
    h_sai.FrameInit.ActiveFrameLength=16;
    h_sai.FrameInit.FSDefinition=SAI_FS_CHANNEL_IDENTIFICATION;//FS信号为SOF信号+通道识别信号
    h_sai.FrameInit.FSPolarity=SAI_FS_ACTIVE_LOW;    //FS低电平有效(下降沿)
    h_sai.FrameInit.FSOffset=SAI_FS_BEFOREFIRSTBIT;  //在slot0的第一位的前一位使能FS,以匹配飞利浦标准

    //SLOT设置
    h_sai.SlotInit.FirstBitOffset=0;                 //slot偏移(FBOFF)为0
    h_sai.SlotInit.SlotSize=SAI_SLOTSIZE_16B;        //slot大小为32位
    h_sai.SlotInit.SlotNumber=2;                     //slot数为2个
    h_sai.SlotInit.SlotActive=SAI_SLOTACTIVE_0|SAI_SLOTACTIVE_1;//使能slot0和slot1

    HAL_SAI_Init(&h_sai);                            //初始化SAI

//  HAL_SAI_InitProtocol(&h_sai, SAI_I2S_STANDARD, _usWordLen, 2);//2--left_channel and right_channel
    SAI1_Block_A->CR1|=1<<17;         //写入CR1寄存器中
    __HAL_SAI_ENABLE(&h_sai);
}

SAIxA_Tx_Config函数用于配置STM32控制器的SAI接口工作模式,它有三个形参,第一个为指定SAI音频协议,一般设置为自由协议以支持I2S Philips标准, 第二个为字长设置,一般设置为16bit,第三个为采样频率,一般设置为44KHz既可得到高音质效果。

首先是时钟配置,使用BSP_AUDIO_OUT_ClockConfig函数选择SAI时钟源,一般选择内部PLL2P时钟,使能PLL2P时钟,并等待时钟正常后使用WM8978_CLK_ENABLE函数开启SAI外设时钟。

接下来通过给SAI_InitTypeDef结构体类型变量赋值设置SAI工作模式,并由HAL_SAI_Init函数完成SAI基本工作环境配置。

最后,__HAL_SAI_ENABLE函数用于使能SAI。

SAI数据发送(DMA传输)

代码清单:SAI-14 SAIA_TX_DMA_Init函数(文件bsp_sai.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @brief  SAIx TX DMA配置,设置为双缓冲模式,并开启DMA传输完成中断
* @param  buf0:M0AR地址.
* @param  buf1:M1AR地址.
* @param  num:每次传输数据量(以两个字节算的一个传输数据量,因为数据长度为HalfWord)
* @retval 无
*/
void SAIA_TX_DMA_Init(uint32_t buffer0,uint32_t buffer1,const uint32_t num)
{
    DMA_CLK_ENABLE();//DMA1时钟使能

    /* 配置 DMA Stream */
    h_txdma.Instance = DMA_Instance;
    h_txdma.Init.Request = DMA_REQUEST_SAI1_A;
    h_txdma.Init.Direction = DMA_MEMORY_TO_PERIPH;//存储器到外设模式
    h_txdma.Init.PeriphInc = DMA_PINC_DISABLE;//外设非增量模式
    h_txdma.Init.MemInc = DMA_MINC_ENABLE;//存储器增量模式
    h_txdma.Init.Mode = DMA_CIRCULAR;// 使用循环模式
    h_txdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;//外设数据长度:16位
    h_txdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD ;//存储器数据长度:16位
    h_txdma.Init.FIFOMode = DMA_FIFOMODE_DISABLE ;//不使用FIFO模式
    h_txdma.Init.Priority = DMA_PRIORITY_HIGH;//高优先级
    h_txdma.Init.MemBurst=DMA_MBURST_SINGLE;             //存储器单次突发传输
    h_txdma.Init.PeriphBurst=DMA_PBURST_SINGLE;          //外设突发单次传输

    __HAL_LINKDMA(&h_sai,hdmatx,h_txdma);        //将DMA与SAI联系起来
    HAL_DMA_Init(&h_txdma);                             //初始化DMA

    __HAL_DMA_ENABLE_IT(&h_txdma,DMA_IT_TC);
    HAL_DMAEx_MultiBufferStart(&h_txdma,(uint32_t)buffer0,(uint32_t)&SAI1_Block_A->DR,(uint32_t)buffer1,num);//开启双缓冲

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

}

SAIx_TX_DMA_Init函数用于初始化SAI数据发送DMA请求工作环境,并启动DMA传输。它有三个形参,第一个为缓冲区1地址,第二个为缓冲区2地址, 第三为缓冲区大小。这里使用DMA的双缓冲区模式,就是开辟两个缓冲区空间,当第一个缓冲区用于DMA传输时(不占用CPU),CPU可以往第二个缓冲区填充数据, 等到第一个缓冲区DMA传输完成后切换第二个缓冲区用于DMA传输,CPU往第一个缓冲区填充数据,如此不断循环切换,可以达到DMA数据传输不间断效果, 具体可参考DMA章节。这里为保证播放流畅性使用了DMA双缓冲区模式。

SAIx_TX_DMA_Init函数首先是使能SAI发送DMA流时钟,对DMA_HandleTypeDef结构体类型的变量赋值配置DMA流工作环境并通过HAL_DMA_Init完成配置。

HAL_DMAEx_MultiBufferStart_IT函数用于指定DMA双缓冲区模式下缓冲区地址。这里使能DMA传输完成中,用于指示其中一个缓冲区传输完成, 需要切换缓冲区,可以开始往缓冲区填充数据。

__HAL_LINKDMA用于DMA关联SAI外设。

最后配置DMA传输完成中断的优先级。

DMA数据发送传输完成中断服务函数

代码清单:SAI-15 DMA数据发送传输完成中断服务函数(文件bsp_sai.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
* @brief  SPIx_TX_DMA_STREAM中断服务函数
* @param  无
* @retval 无
*/
void SAI_TX_DMA_STREAM_IRQFUN(void)
{
    //执行回调函数,读取数据等操作在这里面处理
    h_txdma.XferCpltCallback = SAI_DMAConvCplt;
    h_txdma.XferM1CpltCallback = SAI_DMAConvCplt;
    HAL_DMA_IRQHandler(&h_txdma);

}

SAIx_TX_DMA_STREAM_IRQFUN函数是SAI的DMA传输中断服务函数,在判断是DMA传输完成中断后执行SAI_DMAConvCplt函数指针对应函数内容。

启动和停止播放控制

代码清单:SAI-16 启动和停止播放控制(文件bsp_sai.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @brief  SAI开始播放
* @param  无
* @retval 无
*/
void SAI_Play_Start(void)
{
    if (HAL_IS_BIT_CLR(SAI_InitStructure.Instance->CFG1, SPI_CFG1_TXDMAEN)) {
        if ((SAI_InitStructure.Instance->CR1 & SPI_CR1_SPE) == SPI_CR1_SPE) {
            /*开启DMA请求*/
            SET_BIT(SAI_InitStructure.Instance->CFG1, SPI_CFG1_TXDMAEN);
        } else {
            __HAL_SAI_DISABLE(&SAI_InitStructure);

            /*开启DMA请求*/
            SET_BIT(SAI_InitStructure.Instance->CFG1, SPI_CFG1_TXDMAEN);

            /* 使能SAI */
            __HAL_SAI_ENABLE(&SAI_InitStructure);
        }
    }
    /* 开始传输 */
    SET_BIT(SAI_InitStructure.Instance->CR1, SPI_CR1_CSTART);
}

/**
* @brief  关闭SAI播放
* @param  无
* @retval 无
*/
void SAI_Play_Stop(void)
{
    //关闭DMA TX传输,结束播放
    HAL_SAI_DMAStop(&SAI_InitStructure);
}

SAI_Play_Start用于开始播放,SAI_Play_Stop用于停止播放,实际是通过控制DMA传输使能来实现。值得注意的是,SAI_Play_Start中需要将寄存器CR1的位CSTART置一,来启动传输。

SAI录音功能模式配置

代码清单:SAI-17 SAIxB_Rx_Config函数(文件bsp_sai.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @brief  配置STM32的SAI外设工作模式
* @param  _usStandard : 接口标准,SAI_Standard_Phillips, SAI_Standard_MSB 或 SAI_Standard_LSB
* @param  _usWordlen : 数据格式,16bit 或者24bit
* @param  _usAudioFreq : 采样频率,SAI_AudioFreq_8K、SAI_AudioFreq_16K、SAI_AudioFreq_22K、SAI_AudioFreq_44K、SAI_AudioFreq_48
* @retval 无
*/
void SAIxB_Rx_Config(const uint16_t _usStandard, const uint16_t _usWordLen, const uint32_t _usAudioFreq)
{
    h_saib.Instance = SAI1_Block_B;

    h_saib.Init.AudioMode = SAI_MODESLAVE_RX;//配置为发送模式
    h_saib.Init.Synchro = SAI_SYNCHRONOUS; //模块内部为异步
    h_saib.Init.OutputDrive = SAI_OUTPUTDRIVE_ENABLE;//立刻输出
    h_saib.Init.NoDivider=SAI_MASTERDIVIDER_ENABLE;//使能MCK输出NOACK
    h_saib.Init.FIFOThreshold=SAI_FIFOTHRESHOLD_1QF;//1/4FIFO
    h_saib.Init.MonoStereoMode=SAI_STEREOMODE;
    h_saib.Init.AudioFrequency = _usAudioFreq;

    h_saib.Init.Protocol=SAI_FREE_PROTOCOL;          //设置SAI1协议为:自由协议(支持I2S/LSB/MSB/TDM/PCM/DSP等协议)
    h_saib.Init.DataSize=SAI_DATASIZE_16;                    //设置数据大小
    h_saib.Init.FirstBit=SAI_FIRSTBIT_MSB;           //数据MSB位优先
    h_saib.Init.ClockStrobing=SAI_CLOCKSTROBING_RISINGEDGE;        //数据在时钟的上升/下降沿选通

    //帧设置
    h_saib.FrameInit.FrameLength=32;                 //设置帧长度为64,左通道32个SCK,右通道32个SCK.
    h_saib.FrameInit.ActiveFrameLength=16;           //设置帧同步有效电平长度,在I2S模式下=1/2帧长.
    h_saib.FrameInit.FSDefinition=SAI_FS_CHANNEL_IDENTIFICATION;//FS信号为SOF信号+通道识别信号
    h_saib.FrameInit.FSPolarity=SAI_FS_ACTIVE_LOW;   //FS低电平有效(下降沿)
    h_saib.FrameInit.FSOffset=SAI_FS_BEFOREFIRSTBIT; //在slot0的第一位的前一位使能FS,以匹配飞利浦标准

    //SLOT设置
    h_saib.SlotInit.FirstBitOffset=0;                //slot偏移(FBOFF)为0
    h_saib.SlotInit.SlotSize=SAI_SLOTSIZE_16B;       //slot大小为32位
    h_saib.SlotInit.SlotNumber=2;                    //slot数为2个
    h_saib.SlotInit.SlotActive=SAI_SLOTACTIVE_0|SAI_SLOTACTIVE_1;//使能slot0和slot1

    HAL_SAI_Init(&h_saib);                            //初始化SAIB
    SAI1_Block_B->CR1|=1<<17;
    __HAL_SAI_ENABLE(&h_saib);
}

SAIxB_Rx_Config函数与SAIxB_Tx_Config函数类似,只不过一个用于输入即录音一个用于输出即放音。这里用于配置STM32控制器的SAI接口工作模式为录音, 它有三个形参,第一个为指定SAI接口标准,一般设置为SAIPhilips标准,第二个为字长设置,一般设置为16bit,第三个为采样频率,一般设置为44KHz既可得到高音质效果。

首先是时钟配置,使用BSP_AUDIO_OUT_ClockConfig函数选择SAI时钟源,一般选择内部PLLSAI时钟,使能PLLSAI时钟,并等待时钟正常后使用WM8978_CLK_ENABLE函数开启SAI外设时钟。

接下来通过给SAI_InitTypeDef结构体类型变量赋值设置SAI工作模式,并由HAL_SAI_Init函数完成SAI基本工作环境配置。

最后,__HAL_SAI_ENABLE函数用于使能SAI。

I2S扩展数据接收(DMA传输)

代码清单:SAI-18 SAIB_RX_DMA_Init函数(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @brief  SAIxext RX DMA配置,设置为双缓冲模式,并开启DMA传输完成中断
* @param  buf0:M0AR地址.
* @param  buf1:M1AR地址.
* @param  num:每次传输数据量
* @retval 无
*/
void SAIB_RX_DMA_Init(uint32_t buffer0,uint32_t buffer1,const uint32_t num)
{
    DMA_CLK_ENABLE();//DMA1时钟使能

    /* 配置 DMA Stream */
    h_rxdma.Instance = DMA1_Stream3;
    h_rxdma.Init.Request = DMA_REQUEST_SAI1_B;
    h_rxdma.Init.Direction = DMA_PERIPH_TO_MEMORY;//存储器到外设模式
    h_rxdma.Init.PeriphInc = DMA_PINC_DISABLE;//外设非增量模式
    h_rxdma.Init.MemInc = DMA_MINC_ENABLE;//存储器增量模式
    h_rxdma.Init.Mode = DMA_CIRCULAR;// 使用循环模式
    h_rxdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;//外设数据长度:16位
    h_rxdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD ;//存储器数据长度:16位
    h_rxdma.Init.FIFOMode = DMA_FIFOMODE_DISABLE ;//不使用FIFO模式
    h_rxdma.Init.Priority = DMA_PRIORITY_HIGH;//不使用FIFO模式
    h_rxdma.Init.MemBurst=DMA_MBURST_SINGLE;             //存储器单次突发传输
    h_rxdma.Init.PeriphBurst=DMA_PBURST_SINGLE;          //外设突发单次传输

    __HAL_LINKDMA(&h_sai,hdmarx,h_rxdma);        //将DMA与SAI联系起来
    HAL_DMA_Init(&h_rxdma);                             //初始化DMA

    __HAL_DMA_ENABLE_IT(&h_rxdma,DMA_IT_TC);
    HAL_DMAEx_MultiBufferStart(&h_rxdma,(uint32_t)&SAI1_Block_B->DR,(uint32_t)buffer0,(uint32_t)buffer1,num);//开启双缓冲

    HAL_NVIC_SetPriority(DMA1_Stream3_IRQn,0,0);                    //DMA中断优先级
    HAL_NVIC_EnableIRQ(DMA1_Stream3_IRQn);
}

SAIB_RX_DMA_Init函数配置SAI的数据接收功能,使用DMA传输方式接收数据,程序结构与SAIx_TX_DMA_Init函数一致,只是DMA传输方向不同, SAIB_RX_DMA_Init函数是从外设到存储器传输,SAIx_TX_DMA_Init函数是存储器到外设传输。

SAIB_RX_DMA_Init函数也是使用DMA的双缓冲区模式传输数据。最后使能了DMA传输完成中断,并使能DMA数据接收请求。

DMA数据接收传输完成中断服务函数

代码清单:SAI-19 DMA数据接收传输完成中断服务函数(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
* @brief  SAIxext_RX_DMA_STREAM中断服务函数
* @param  无
* @retval 无
*/
void SAIxext_RX_DMA_STREAM_IRQFUN(void)
{
    //执行回调函数,读取数据等操作在这里面处理
    hdma_spi2_rx.XferCpltCallback = SAIxext_DMAConvCplt;
    hdma_spi2_rx.XferM1CpltCallback = SAIxext_DMAConvCplt;
    HAL_DMA_IRQHandler(&hdma_spi2_rx);
}

与DMA数据发送传输完成中断服务函数类似,SAIxext_RX_DMA_STREAM_IRQFUN函数在判断得到是数据接收传输完成后执行SAIxext_DMAConvCplt函数,SAIxext_DMAConvCplt实际也是一个函数指针。

启动和停止录音

代码清单:SAI-20 启动和停止录音(文件bsp_wm8978.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @brief  SAI开始录音
* @param  无
* @retval 无
*/
void SAIxext_Recorde_Start(void)
{
    /* Check if the SAI Tx request is already enabled */
    if (HAL_IS_BIT_CLR(SAIext_InitStructure.Instance->CFG1, SPI_CFG1_RXDMAEN)) {
        /* Check if the SPSAI is disabled to edit CFG1 register */
        if ((SAIext_InitStructure.Instance->CR1 & SPI_CR1_SPE) == SPI_CR1_SPE) {
            /* Enable Tx DMA Request */
            SET_BIT(SAIext_InitStructure.Instance->CFG1, SPI_CFG1_RXDMAEN);
        } else {
            /* Disable SPI peripheral */
            __HAL_SAI_DISABLE(&SAIext_InitStructure);

            /* Enable Tx DMA Request */
            SET_BIT(SAIext_InitStructure.Instance->CFG1, SPI_CFG1_RXDMAEN);
            /* Enable SPI peripheral */
            __HAL_SAI_ENABLE(&SAIext_InitStructure);
        }
        /* Master transfer start */
        SET_BIT(SAIext_InitStructure.Instance->CR1, SPI_CR1_CSTART);
    }
}
/**
* @brief  关闭SAI录音
* @param  无
* @retval 无
*/
void SAIxext_Recorde_Stop(void)
{
    HAL_SAI_DMAStop(&SAIext_InitStructure);
}

SAIxext_Recorde_Start函数用于启动录音,SAIxext_Recorde_Stop函数用于停止录音,实际是通过控制DMA传输使能来实现。

至此,关于WM8978芯片的驱动程序已经介绍完整了,该部分程序都是在bsp_wm8978.c文件中的,接下来我们就可以使用这些驱动程序实现录音和回放功能了。

38.6.2.3. 录音和回放功能

录音和回放功能是在WM8978驱动函数基础上搭建而成的,实现代码存放在Recorder.c和Recorder.h文件中。启动录音功能后会在SD卡内创建一个WAV格式文件, 把音频数据保存在该文件中,录音结束后既可得到一个完整的WAV格式文件。回放功能用于播放录音文件,实际上回放功能的实现函数也是适用于播放其他WAV格式文件的。

枚举和结构体类型定义

代码清单:SAI-21 枚举和结构体类型定义(文件Recorder.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 录音机状态 */
enum {
    STA_IDLE = 0, /* 待机状态 */
    STA_RECORDING,  /* 录音状态 */
    STA_PLAYING,  /* 放音状态 */
    STA_ERR,      /*  error  */
};

typedef struct {
    uint8_t ucInput;      /* 输入源:0:MIC, 1:线输入 */
    uint8_t ucFmtIdx;     /* 音频格式:标准、位长、采样频率 */
    uint8_t ucVolume;     /* 当前放音音量 */
    uint8_t ucGain;       /* 当前增益 */
    uint8_t ucStatus;     /* 录音机状态,0表示待机,1表示录音中,2表示播放中 */
} REC_TYPE;

/* WAV文件头格式 */
typedef __packed struct {
    uint32_t  riff;             /* = "RIFF" 0x46464952*/
    uint32_t  size_8;           /* 从下个地址开始到文件尾的总字节数 */
    uint32_t  wave;             /* = "WAVE" 0x45564157*/

    uint32_t  fmt;              /* = "fmt " 0x20746d66*/
    uint32_t  fmtSize;          /* 下一个结构体的大小(一般为16) */
    uint16_t  wFormatTag;       /* 编码方式,一般为1  */
    uint16_t  wChannels;        /* 通道数,单声道为1,立体声为2 */
    uint32_t  dwSamplesPerSec;  /* 采样率 */
    uint32_t  dwAvgBytesPerSec; /* 每秒字节数(= 采样率 × 每个采样点字节数) */
    uint16_t  wBlockAlign;      /* 每个采样点字节数(=量化比特数/8*通道数) */
    uint16_t  wBitsPerSample;   /* 量化比特数(每个采样需要的bit数) */

    uint32_t  data;             /* = "data" 0x61746164*/
    uint32_t  datasize;         /* 纯数据长度 */
} WavHead;

首先,定义一个枚举类型罗列录音和回放功能的状态,录音和回放功能是不能同时使用的,使用枚举类型区分非常有效。

REC_TYPE结构体类型定义了录音和回放功能相关可控参数,包括WM8978声音源输入端,可选板载咪头或板载耳机插座的LINE线输入;音频格式选择, 一般选择SAI Philips标准、16bit字长、44KHz采样频率;音频输出耳机音量控制;录音时声音增益;当前状态。

WavHead结构体类型定义了WAV格式文件头,具体参考“WAV格式文件”,这里没有用到fact chunk。需要注意的是这里使用__packed关键字,它表示结构字节对齐。

启动播放WAV格式音频文件

代码清单:SAI-22 StartPlay函数(文件Recorder.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @brief  配置WM8978和STM32的SAI开始放音。
* @param  无
* @retval 无
*/
static void StartPlay(const char *filename)
{
    printf("当前播放文件 -> %s\n",filename);

    result=f_open(&file,filename,FA_READ);
    if (result!=FR_OK) {
        printf("打开音频文件失败!!!->%d\r\n",result);
        result = f_close (&file);
        Recorder.ucStatus = STA_ERR;
        return;
    }
    //读取WAV文件头
    result = f_read(&file,&rec_wav,sizeof(rec_wav),&bw);
    //先读取音频数据到缓冲区
    result = f_read(&file,(uint16_t *)buffer0,RECBUFFER_SIZE*2,&bw);
    result = f_read(&file,(uint16_t *)buffer1,RECBUFFER_SIZE*2,&bw);

    Delay_ms(10); /* 延迟一段时间,等待SAI中断结束 */
    SAI_Stop();     /* 停止SAI录音和放音 */
    wm8978_Reset();   /* 复位WM8978到复位状态 */
    wm8978_CtrlGPIO1(1);
    Recorder.ucStatus = STA_PLAYING;    /* 放音状态 */

    /* 配置WM8978芯片,输入为DAC,输出为耳机 */
    wm8978_CfgAudioPath(DAC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
    /* 调节音量,左右相同音量 */
    wm8978_SetOUT1Volume(Recorder.ucVolume);
    /* 配置WM8978音频接口为飞利浦标准SAI接口,16bit */
    wm8978_CfgAudioIF(SAI_STANDARD_PHILIPS, 16);
    SAIx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
    g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
    I2x_TX_DMA_Init((uint32_t)&buffer0,(uint32_t)&buffer1, ECBUFFER_SIZE);

    SAI_Play_Start();
}

StartPlay函数用于启动播放WAV格式音频文件,它有一个形参,用于指示待播放文件名称。函数首先检查待播放文件是否可以正常打开, 如果打开失败则退出播放。如果可以正常打开文件则先读取WAV格式文件头,保存在WavHead结构体类型变量rec_wav中, 同时先读取音频数据填充到两个缓冲区中,这两个缓冲区buffer0和buffer1是定义的全局数组变量,用于DMA双缓冲区模式。

接下来,配置WM8978的工作环境,首先停止SAI并复位WM8978芯片。这里是播放音频功能,所以设置WM8978的输入是DAC,播放SAI接口接收到的音频数据, 输出设置为耳机输出。STM32控制器的SAI接口和WM8978的SAI接口都设置为SAI Philips标志、字长为16bit。

然后,调用SAIx_TX_DMA_Init配置SAI的DMA发送请求,并调用SAI_Play_Start函数使能DMA数据传输。

启动录音功能

代码清单:SAI-23 StartRecord函数(文件Recorder.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @brief  配置WM8978和STM32的SAI开始录音。
* @param  无
* @retval 无
*/
static void StartRecord(const char *filename)
{
    printf("当前录音文件 -> %s\n",filename);
    result = f_close (&file);
    result=f_open(&file,filename,FA_CREATE_ALWAYS|FA_WRITE);
    if (result!=FR_OK) {
        printf("Open wavfile fail!!!->%d\r\n",result);
        result = f_close (&file);
        Recorder.ucStatus = STA_ERR;
        return;
    }

    // 写入WAV文件头,这里必须写入写入后文件指针自动偏移到sizeof(rec_wav)位置,
    // 接下来写入音频数据才符合格式要求。
    result=f_write(&file,(const void *)&rec_wav,sizeof(rec_wav),&bw);

    Delay_ms(10);   /* 延迟一段时间,等待SAI中断结束 */
    SAI_Stop();     /* 停止SAI录音和放音 */
    wm8978_Reset();   /* 复位WM8978到复位状态 */
    wm8978_CtrlGPIO1(0);
    Recorder.ucStatus = STA_RECORDING;    /* 录音状态 */

    /* 调节放音音量,左右相同音量 */
    wm8978_SetOUT1Volume(Recorder.ucVolume);

    if (Recorder.ucInput == 1) { /* 线输入 */
        /* 配置WM8978芯片,输入为线输入,输出为耳机 */
        wm8978_CfgAudioPath(LINE_ON | ADC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
        wm8978_SetLineGain(Recorder.ucGain);
    } else { /* MIC输入 */
        /* 配置WM8978芯片,输入为Mic,输出为耳机 */
//wm8978_CfgAudioPath(MIC_LEFT_ON | ADC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
//wm8978_CfgAudioPath(MIC_RIGHT_ON |MIC_LEFT_ON | ADC_ON, OUT_PATH_OFF);
wm8978_CfgAudioPath(MIC_LEFT_ON | MIC_RIGHT_ON | ADC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
        wm8978_SetMicGain(Recorder.ucGain);
    }

    /* 配置WM8978音频接口为飞利浦标准SAI接口,16bit */
    wm8978_CfgAudioIF(SAI_STANDARD_PHILIPS, 16);

    SAIxB_Rx_Config(g_FmtList[Recorder.ucFmtIdx][0],
    g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
    SAIB_RX_DMA_Init(buffer0,buffer1,RECBUFFER_SIZE);

    SAIxext_Recorde_Start();
}

StartRecord函数在结构上与StartPlay函数类似,它实现启动录音功能,它有一个形参,指示保存录音数据的文件名称。StartRecord函数会首先创建录音文件, 因为用到FA_CREATE_ALWAYS标志位,f_open函数会总是创建新文件,如果已存在该文件则会覆盖原先的文件内容。

这些必须先写入WAV格式文件头数据,这样当前文件指针自动移动到文件头的下一个字节,即是存放音频数据的起始位置。

开发板支持LINE线输入和板载咪头输入,程序默认使用咪头输入,在录音同时使能耳机输出,这样录音时在耳机接口是有相同的声音输出的。

录音功能需要 使能模拟开关切换SAI输入通道。

录音和回放功能选择

代码清单:SAI-24 RecorderDemo函数(文件Recorder.c)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
void RecorderDemo(void)
{
    uint8_t i;
    uint8_t ucRefresh;  /* 通过串口打印相关信息标志 */
    DIR dir;

    Recorder.ucStatus=STA_IDLE;    /* 开始设置为空闲状态  */
    Recorder.ucInput=0;            /* 缺省MIC输入  */
    Recorder.ucFmtIdx=3;           /* 缺省飞利浦SAI标准,16bit数据长度,44K采样率
                    */
    Recorder.ucVolume=35;          /* 缺省耳机音量  */
    if (Recorder.ucInput==0) { //MIC
        Recorder.ucGain=50;          /* 缺省MIC增益  */
        rec_wav.wChannels=1;         /* 缺省MIC单通道 */
    } else {                //LINE
        Recorder.ucGain=6;           /* 缺省线路输入增益 */
        rec_wav.wChannels=2;         /* 缺省线路输入双声道 */
    }

    rec_wav.riff=0x46464952;       /* “RIFF”; RIFF 标志 */
    rec_wav.size_8=0;              /* 文件长度,未确定 */
    rec_wav.wave=0x45564157;       /* “WAVE”; WAVE 标志 */

    rec_wav.fmt=0x20746d66;        /* “fmt ”; fmt 标志,最后一位为空 */
    rec_wav.fmtSize=16;            /* sizeof(PCMWAVEFORMAT) */
    rec_wav.wFormatTag=1;          /* 1 表示为PCM 形式的声音数据 */
    /* 每样本的数据位数,表示每个声道中各个样本的数据位数。 */
    rec_wav.wBitsPerSample=16;
    /* 采样频率(每秒样本数) */
    rec_wav.dwSamplesPerSec=g_FmtList[Recorder.ucFmtIdx][2];
    /* 每秒数据量;其值为通道数×每秒数据位数×每样本的数据位数/ 8。 */
    rec_wav.dwAvgBytesPerSec=rec_wav.wChannels*rec_wav.dwSamplesPerSec*rec_wav.wBitsPerSample/8;
    /*
    数据块的调整数(按字节算的),其值为通道数×每样本的数据位值/8。
    */
    rec_wav.wBlockAlign=rec_wav.wChannels*rec_wav.wBitsPerSample/8;

    rec_wav.data=0x61746164;       /* “data”; 数据标记符 */
    rec_wav.datasize=0;            /* 语音数据大小 目前未确定*/

    /*  如果路径不存在,创建文件夹  */
    result = f_opendir(&dir,RECORDERDIR);
    while (result != FR_OK) {
        f_mkdir(RECORDERDIR);
        result = f_opendir(&dir,RECORDERDIR);
    }

    /*  初始化并配置SAI  */
    SAI_Stop();
    SAI_GPIO_Config();
    SAIx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[
                Recorder.ucFmtIdx][2]);
    SAIxB_Rx_Config(g_FmtList[Recorder.ucFmtIdx][0],g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[
                    Recorder.ucFmtIdx][2]);

    SAI_DMA_TX_Callback=MusicPlayer_SAI_DMA_TX_Callback;
    SAI_Play_Stop();

    SAI_DMA_RX_Callback=Recorder_SAI_DMA_RX_Callback;
    SAIxext_Recorde_Stop();

    ucRefresh = 1;
    bufflag=0;
    Isread=0;
    /* 进入主程序循环体 */
    while (1) {
        /* 如果使能串口打印标志则打印相关信息 */
        if (ucRefresh == 1) {
            DispStatus();   /* 显示当前状态,频率,音量等 */
            ucRefresh = 0;
        }
        if (Recorder.ucStatus == STA_IDLE) {
            /*  KEY2开始录音  */
            if (Key_Scan(KEY2_GPIO_PORT,KEY2_PIN)==KEY_ON) {
                /* 寻找合适文件名 */
                for (i=1; i<0xff; ++i) {
                    sprintf(recfilename,"0:/recorder/rec%03d.wav",i);
                    result=f_open(&file,(const TCHAR *)recfilename,FA_READ);
                    if (result==FR_NO_FILE)break;
                }
                f_close(&file);

                if (i==0xff) {
                    Recorder.ucStatus =STA_ERR;
                    continue;
                }
                /* 开始录音 */
                StartRecord(recfilename);
                ucRefresh = 1;
            }
            /*  TouchPAD开始回放录音  */
            if (TPAD_Scan(0)) {
                /* 开始回放 */
                StartPlay(recfilename);
                ucRefresh = 1;
            }
        } else {
            /*  KEY1停止录音或回放  */
            if (Key_Scan(KEY1_GPIO_PORT,KEY1_PIN)==KEY_ON) {
                /* 对于录音,需要把WAV文件内容填充完整 */
                if (Recorder.ucStatus == STA_RECORDING) {
                    SAIxext_Recorde_Stop();
                    SAI_Play_Stop();
                    rec_wav.size_8=wavsize+36;
                    rec_wav.datasize=wavsize;
                    result=f_lseek(&file,0);
                    result=f_write(&file,(const void *)&rec_wav,sizeof(rec_wav),&bw);
                    result=f_close(&file);
                    printf("录音结束\r\n");
                }
                ucRefresh = 1;
                Recorder.ucStatus = STA_IDLE;   /* 待机状态 */
                SAI_Stop();   /* 停止SAI录音和放音 */
                wm8978_Reset(); /* 复位WM8978到复位状态 */
            }
        }
        /* DMA传输完成 */
        if (Isread==1) {
            Isread=0;
            switch (Recorder.ucStatus) {
            case STA_RECORDING:  // 录音功能,写入数据到文件
                if (bufflag==0) {
                    result=f_write(&file,buffer0,RECBUFFER_SIZE*2,(UINT*)&bw);//写入文件
                } else {
                    result=f_write(&file,buffer1,RECBUFFER_SIZE*2,(UINT*)&bw);//写入文件.
                }
                wavsize+=RECBUFFER_SIZE*2;
                break;
            case STA_PLAYING:   // 回放功能,读取数据到播放缓冲区
                if (bufflag==0) {
                    result = f_read(&file,buffer0,RECBUFFER_SIZE*2,&bw);
                    printf("OK\r\n");
                } else {
                    result = f_read(&file,buffer1,RECBUFFER_SIZE*2,&bw);
                    printf("OK\r\n");
                }
                /* 播放完成或读取出错停止工作 */
                if ((result!=FR_OK)||(file.fptr==file.fsize)) {
                    printf("播放完或者读取出错退出...\r\n");
                    SAI_Play_Stop();
                    file.fptr=0;
                    f_close(&file);
                    Recorder.ucStatus = STA_IDLE;   /* 待机状态 */
                    SAI_Stop();   /* 停止SAI录音和放音 */
                    wm8978_Reset(); /* 复位WM8978到复位状态 */
                }
                break;
            }
        }

    }
}

RecorderDemo函数实现录音和回放功能。Recorder是一个REC_TYPE结构体类型变量,指示录音和回放功能相关参数,这里通过赋值缺省选择板载咪头输入、 使用SAI Philips标准、16bit字长、44KHz采样频率,并设置了音量和增益,对于LINE输入增益范围为0~7。rec_wav是WavHead结构体类型变量, 用于设置WAV格式文件头,很多成员赋值为缺省值即可,成员size_8和datasize变量表示数据大小,因为录音时间长度直接影响这两个变量大小, 现在并无法确定它们大小,需要在停止录音后才可计算得到它们的值。

接下来是使用FatFs的功能函数f_opendir和f_mkdir组合判断SD卡内是否有名为“recorder”的文件夹,如果没有改文件夹就创建它,因为我们打算把录音文件存放在该文件夹内。

接下来就是调用SAI相关函数完成SAI工作环境配置。

开始时停止录音和回放功能。

ucRefresh变量作为通过串口打印相关操作和状态信息到串口调试助手“刷新”标志。bufflag变量用于指示当前空闲缓冲区,工程定义了两个缓冲区buffer0和buffer1用于DMA双缓冲区模式, 对于录音功能,buffag为0表示当前DMA使用buffer1填充,buffer0已经填充完整,为1表示当前DMA使用buffer0填充,buffer1已经填充完整;对于回放功能, buffag为0表示buffer1用于当前播放,buffer0已经播放完成需要读取新数据填充,为1表示buffer0用于当前播放,buffer1已经播放完成需要读取新数据填充。 Isread变量用于指示DMA传输完成状态,为1时表示DMA传输完成,只有在DMA传输完成中断服务函数中才会被置1。

接下来就是无限循环函数了,先判断ucRefresh变量状态,如果为1就执行DispStatus函数,该函数只是使用printf函数打印相关信息。开发板集成有两个独立按键K1和K2, 还有一个电容按键,程序设置按下K2按键开始录音功能,触摸电容按键开始回放功能,按下K1按键用于停止录音和回放功能,这里K2按键和电容按键是互斥的。

在空闲状态下,允许按下K2按键启动录音,录音功能是通过调用StartRecord函数启动的,该函数需要一个参数指示录音文件名称, 使用在执行StartRecord函数之前会循环使用f_open函数获取SD卡内不存在的文件,防止覆盖已存在文件。

在空闲状态下,允许触摸电容按键启动回放功能,它直接使用StartPlay函数启动上一个录音文件播放。

不在空闲状态下,按下K1按键可以停在录音或回放功能,回放功能只需要停在SAI的DMA数据发送接口并复位WM8978即可,录音功能需要停在扩展SAI的DMA数据接收功能, 并且需要填充完整WAV格式文件头并写入到录音文件中。

无限循环还要不懂检测Isread变量的状态,当它在DMA传输完成中断服务函数被置1时说明双缓冲区状态发生改变,对于录音功能, 需要把以填充满的缓冲区数据通过f_write写入到文件中;对于回放功能,需要利用f_read函数读取新数据填充到已播缓冲区中, 如果遇到读取出错或文件已经播放完全需要停止SAI的DMA传输并复位WM8978。

DMA传输完成中断回调函数

代码清单:SAI-25 DMA传输完成中断回调函数(文件Recorder.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DMA发送完成中断回调函数 */
/* 缓冲区内容已经播放完成,需要切换缓冲区,进行新缓冲区内容播放
同时读取WAV文件数据填充到已播缓冲区  */
void MusicPlayer_SAI_DMA_TX_Callback(void)
{
    if (Recorder.ucStatus == STA_PLAYING) {
        if (SAIx_TX_DMA_STREAM->CR&(1<<19)) { //当前使用Memory1数据
            bufflag=0;                       //可以将数据读取到缓冲区0
        } else {                           //当前使用Memory0数据
            bufflag=1;                       //可以将数据读取到缓冲区1
        }
        Isread=1;                          // DMA传输完成标志
    }
}

/* DMA接收完成中断回调函数 */
/* 录音数据已经填充满了一个缓冲区,需要切换缓冲区,
同时可以把已满的缓冲区内容写入到文件中 */
void Recorder_SAI_DMA_RX_Callback(void)
{
    if (Recorder.ucStatus == STA_RECORDING) {
        if (SAIx_RX_DMA_STREAM->CR&(1<<19)) { //当前使用Memory1数据
            bufflag=0;
        } else {                             //当前使用Memory0数据
            bufflag=1;
        }
        Isread=1;                            // DMA传输完成标志
    }
}

这两个函数用于在DMA传输完成后切换缓冲区。DMA数据流x配置寄存器的CT位用于指示当前目标缓冲区,如果为1,当前目标缓冲区为存储器1;如果为0,则为存储器0。

主函数

代码清单:SAI-26 main函数(文件main.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int main(void)
{
    FRESULT result;
    /* 系统时钟初始化成480MHz */
    SystemClock_Config();

    CPU_CACHE_Enable();
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_0);
    /* 初始化按键 */
    Key_GPIO_Config();

    /* 初始化调试串口,一般为串口1 */
    DEBUG_USART_Config();

    //链接驱动器,创建盘符
    FATFS_LinkDriver(&SD_Driver, SDPath);
    //在外部SD卡挂载文件系统,文件系统挂载时会对SD卡初始化
    result = f_mount(&fs,"0:",1);
    if (result!=FR_OK) {
        printf("\n SD卡文件系统挂载失败\n");
        while (1);
    }
    printf("WM8978录音和回放功能\n");

    /*  初始化电容按键  */
    TPAD_Init();

    /* 检测WM8978芯片,此函数会自动配置CPU的GPIO */
    if (wm8978_Init()==0) {
        printf("检测不到WM8978芯片!!!\n");
        while (1);  /* 停机 */
    }
    printf("初始化WM8978成功\n");

    /* 录音与回放功能 */
    RecorderDemo();
}

main函数主要完成各个外设的初始化,包括独立按键初始化、电容按键初始化、调试串口初始化、SD卡文件系统挂载还有系统定时器初始化。

wm8978_Init初始化I2C接口用于控制WM8978芯片,并复位WM8978芯片,如果初始化成功则运行RecorderDemo函数进行录音和回放功能测试。

38.6.3. 下载验证

把Micro SD卡插入到开发板右侧的卡槽内,使用USB线连接开发板上的“USB TO UART”接口到电脑,将耳机插入到开发板上边沿左侧的耳机插座, 电脑端配置好串口调试助手参数。编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板发过来的提示信息,按下开发板左下边沿的K2按键, 开始执行录音功能测试,不断对着咪头说话,就可以把声音录制下来,按下K1按键可以停止录音。然后触摸电容按键就可以在耳机接口听到之前录音内容了, 按下K1按键可停止播放。录音完成后也可以在电脑端打开SD卡,找到其中的录音文件,可在电脑端音频播放器播放录音文件。

38.7. MP3播放器

MP3格式音乐文件普遍存在我们生活中,实际上MP3本身是一种音频编码方式,全称为Moving Picture Experts Group Audio Layer III(MPEG Audio Layer 3)。 MPEG音频文件是MPEG标准中的声音部分,根据压缩质量和编码复杂程度划分为三层,即Layer-1、Layer2、Layer3,且分别对应MP1、MP2、MP3这三种声音文件, 其中MP3压缩率可达到10:1至12:1,可以大大减少文件占用存储空间大小。MPEG音频编码的层次越高,编码器越复杂,压缩率也越高。MP3是利用人耳对高频声音信号不敏感的特性, 将时域波形信号转换成频域信号,并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)对低频信号使用小压缩比, 保证信号不失真。这样一来就相当于抛弃人耳基本听不到的高频声音,只保留能听到的低频部分,这样可得到很高的压缩率。

38.7.1. MP3文件结构

MP3文件大致分为3个部分:TAG_V2(ID3V2),音频数据,TAG_V1(ID3V1)。ID3是MP3文件中附加关于该MP3文件的歌手、标题、专辑名称、年代、风格等等信息, 有两个版本ID3V1和ID3V2。ID3V1固定存放在MP3文件末尾,固定长度为128字节,以TAG三个字符开头,后面跟上歌曲信息。因为ID3V1可存储信息量有限, 有些MP3文件添加了ID3V2,ID3V2是可选的,如果存在ID3V2那它必然存在在MP3文件起始位置,它实际是ID3V1的补充。

38.7.1.1. ID3V2

ID3V2以灵活管理方法MP3文件附件信息,比ID3V1可以存储更多的信息,同时也比ID3V1在结构上复杂得多。常用是ID3V2.3版本, 一个MP3文件最多就一个ID3V2.3标签。ID3V2.3标签由一个标签头和若干个标签帧或一个扩展标签头组成。关于曲目的信息如标题、作者等都存放在不同的标签帧中, 扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧。标签头和标签帧一起顺序存放在MP3文件的首部。

标签头结构如表 标签头

标签头

其中标签大小计算如下:

Total_size=(Size[0]&0x7F)*0x200000+(Size[1]&0x7F)*0x400+(Size[2]&0x7F)*0x80+(Size[3]&0x7F)

每个标签帧都有一个10个字节的帧头和至少一个字节的不定长度内容,在文件中是连续存放的。标签帧头由三个部分组成,即Frame[4]、Size[4]和Flags[2]。 Frame是用四个字符表示帧内容含义,比如TIT2为标题、TPE1为作者、TALB为专辑、TRCK为音轨、TYER为年代等等信息。 Size用四个字节组成32bit数表示帧大小。Flags是标签帧的标志位,一般为0即可。

38.7.1.2. ID3V1

ID3V1是早期的版本,可以存放的信息量有限,但编程比ID3V2简单很多,即使到现在使用还是很多。ID3V1是固定存放在MP3文件末尾的128字节,组成结构见表 ID3V1结构

ID3V1结构

其中,歌名是固定分配为30个字节,如果歌名太短则以0填充完整,太长则被截断,其他信息类似情况存储。MP3音乐类别总共有147种, 每一种对应一个数组,比如0对应“Blues”、1对应“Classic Rock”、2对应“Country”等等。

38.7.1.3. MP3数据帧

音频数据是MP3文件的主体部分,它由一系列数据帧(Frame)组成,每个Frame包含一段音频的压缩数据,通过解码库解码即可得到对应PCM音频数据, 就可以通过SAI发送到WM8978芯片播放音乐,按顺序解码所有帧就可以得到整个MP3文件的音轨。

每个Frame由两部分组成,帧头和数据实体,Frame长度可能不同,由位率决定。帧头记录了MP3数据帧的位率、采样率、版本等等信息,总共有4个字节,见表 数据帧头结构

数据帧头结构

位率位在不同版本和层都有不同的定义,具体参考表 位率选择 ,单位为kbps。其中V1对应MPEG-1,V2对应MPEG-2和MPEG-2.5;L1对应Layer1, L2对应Layer2,L3对应Layer3,free表示位率可变,bad表示该定义不合法。

位率选择

例如,当Version=11,Layer=01,Bitrate_incdex=0101时,描述为MPEG-1 Layer3(MP3)位率为64kbps。

数据帧长度取决于位率(Bitrate)和采样频率(Sampling_freq),具体计算如下:

对于MPEG-1标准,Layer1时:

Size=(48000*Bitrate)/Sampling_freq + Padding;

Layer2或Layer3时:

Size=(144000*Bitrate)/Sampling_freq + Padding;

对于MPEG-2或MPEG-2.5标准,Layer1时:

Size=(24000*Bitrate)/Sampling_freq + Padding;

Layer2或Layer3时:

Size=(72000*Bitrate)/Sampling_freq + Padding;

如果有CRC校验,则存放在在帧头后两个字节。接下来就是帧主数据(MAIN_DATA)。一般来说一个MP3文件每个帧的Bitrate是固定不变的, 所以每个帧都有相同的长度,称为CBR,还有小部分MP3文件的Bitrate是可变,称之为VBR,它是XING公司推出的算法。

MAIN_DATA保存的是经过压缩算法压缩后得到的数据,为得到大的压缩率会损失部分源声音,属于失真压缩。

38.7.2. MP3解码库

MP3文件是经过压缩算法压缩而存在的,为得到PCM信号,需要对MP3文件进行解码,解码过程大致为:比特流分析、霍夫曼编码、逆量化处理、立体声处理、 频谱重排列、抗锯齿处理、IMDCT变换、子带合成、PCM输出。整个过程涉及很多算法计算,要自己编程实现不是一件现实的事情, 还好有很多公司经过长期努力实现了解码库编程。

现在合适在小型嵌入式控制器移植运行的有两个版本的开源MP3解码库,分别为Libmad解码库和Helix解码库,Libmad是一个高精度MPEG音频解码库, 支持MPEG-1、MPEG-2以及MPEG-2.5标准,它可以提供24bitPCM输出,完全是定点计算,更多信息可参考网站:http://www.underbit.com/。

Helix解码库支持浮点和定点计算实现,将该算法移植到STM32控制器运行使用定点计算实现,它支持MPEG-1、MPEG-2以及MPEG-2.5标准的Layer3解码。 Helix解码库支持可变位速率、恒定位速率,以及立体声和单声道音频格式。更多信息可参考网站:https://datatype.helixcommunity.org/Mp3dec。

因为Helix解码库需要占用的资源比Libmad解码库更少,特别是RAM空间的使用,这对STM32控制器来说是比较重要的, 所以在实验工程中我们选择Helix解码库实现MP3文件解码。这两个解码库都是一帧为解码单位的,一次解码一帧,这在应用解码库时是需要着重注意的。

Helix解码库涉及算法计算,整个界面过程复杂,有兴趣可以深入探究,这里我们着重讲解Helix移植和使用方法。

Helix网站有提供解码库代码,经过整理,移植Helix解码库需要用到的的文件如图 Helix解码库文件结构 。有优化解码速度,部分解码过程使用汇编实现。

Helix解码库文件结构

38.7.3. Helix解码库移植

在“录音与回放实验”已经实现了WM8978驱动代码,现在我们可以移植Helix解码库工程中,实现MP3文件解码,将解码输出的PCM数据通过SAI接口发送到WM8978芯片实现音乐播放。

我们在“录音与回放实验”工程文件基础上移植Helix解码,首先将需要用到的文件添加到工程中,如图 添加Helix解码库文件到工程 。MP3文件夹下文件是Helix解码库源码, 工程移植中是不需要修改文件夹下代码的,我们只需直接调用相关解码函数即可。建议自己移植时直接使用例程中mp3文件夹内文件。 我们是在mp3Player.c文件中调用Helix解码库相关函数实现MP3文件解码的,该文件是我们自己创建的。

添加Helix解码库文件到工程

接下来还需要在工程选项中添加Helix解码库的文件夹路径,编译器可以寻找到相关头文件,见图 添加Helix解码库文件夹路径

添加Helix解码库文件夹路径

38.7.4. MP3播放器功能实现

“录音与回放实验”中的回放功能实际上就是从SD卡内读取WAV格式文件数据,然后提取里边音频数据通过SAI传输到WM8978芯片内实现声音播放。 MP3播放器的功能也是类似的,只不过现在音频数据提取方法不同,MP3需要先经过解码库解码后才可得到“可直接”播放的音频数据。由此可以看到, MP3播放器只是添加了MP3解码库实现代码,在硬件设计上并没有任何改变,即这里直接使用“录音与回放实验”中硬件设计即可。

实验工程代码中创建mp3Player.c和mp3Player.h两个文件存放MP3播放器实现代码。Helix解码库是用来解码MP3数据帧,一次解码一帧, 它是不能用来检索ID3V1和ID3V2标签的,如果需要获取歌名、作者等信息需要自己编程实现。解码过程可能用到的Helix解码库函数有:

  • MP3InitDecoder:初始化解码器函数

  • MP3FreeDecoder:关闭解码器函数

  • MP3FindSyncWord:寻找帧同步函数

  • MP3Decode:解码MP3帧函数

  • MP3GetLastFrameInfo:获取帧信息函数

MP3InitDecoder函数初始化解码器,它会申请分配一个存储空间用于存放解码器状态的一个数据结构并将其初始化,该数据结构由MP3DecInfo结构体定义, 它封装了解码器内部运算数据信息。MP3InitDecoder函数会返回指向该数据结构的指针。

MP3FreeDecoder函数用于关闭解码器,释放由MP3InitDecoder函数申请的存储空间,所以一个MP3InitDecoder函数都需要有一个MP3FreeDecoder函数与之对应。 它有一个形参,一般由MP3InitDecoder函数的返回指针赋值。

MP3FindSyncWord函数用于寻址数据帧同步信息,实际上就是寻址数据帧开始的11bit都为“1”的同步信息。它有两个形参,第一个为源数据缓冲区指针, 第二个为缓冲区大小,它会返回一个int类型变量,用于指示同步字较缓冲区起始地址的偏移量,如果在缓冲区中找不到同步字,则直接返回-1。

MP3Decode函数用于解码数据帧,它有五个形参,第一个为解码器数据结构指针,一般由MP3InitDecoder函数返回值赋值;第二个参数为指向解码源数据缓冲区开始地址的一个指针, 注意这里是地址的指针,即是指针的指针;第三个参数是一个指向存放解码源数据缓冲区有效数据量的变量指针;第四个参数是解码后输出PCM数据的指针, 一般由我们定义的缓冲区地址赋值,对于双声道输出数据缓冲区以LRLRLR…顺序排列;第五个参数是数据格式选择,一般设置为0表示标准的MPEG格式。 函数还有一个返回值,用于返回解码错误,返回ERR_MP3_NONE说明解码正常。

MP3GetLastFrameInfo函数用于获取数据帧信息,它有两个形参,第一个为解码器数据结构指针,一般由MP3InitDecoder函数返回值赋值; 第二个参数为数据帧信息结构体指针,该结构体定义见 代码清单:SAI-27

代码清单:SAI-27 MP3数据帧信息结构体(mp3dec.h文件)
1
2
3
4
5
6
7
8
9
typedef struct _MP3FrameInfo {
    int bitrate;        //位率
    int nChans;         //声道数
    int samprate;       //采样率
    int bitsPerSample;  //采样位数
    int outputSamps;    //PCM数据数
    int layer;          //层级
    int version;        //版本
} MP3FrameInfo;

该结构体成员包括了该数据帧的位率、声道、采样频率等等信息,它实际上是从数据帧的帧头信息中提取的。

MP3播放器实现

代码清单:SAI-28 mp3PlayerDemo函数(mp3Player.c文件)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
void mp3PlayerDemo(const char *mp3file)
{
    uint8_t *read_ptr=inputbuf;
    uint32_t frames=0;
    int err=0, i=0, outputSamps=0;
    int read_offset = 0;        /* 读偏移指针 */
    int bytes_left = 0;         /* 剩余字节数 */

    mp3player.ucFreq=SAI_AUDIOFREQ_DEFAULT;
    mp3player.ucStatus=STA_IDLE;
    mp3player.ucVolume=40;

    result=f_open(&file,mp3file,FA_READ);
    if (result!=FR_OK) {
        printf("Open mp3file :%s fail!!!->%d\r\n",mp3file,result);
        result = f_close (&file);
        return; /* 停止播放 */
    }
    printf("当前播放文件 -> %s\n",mp3file);

    //初始化MP3解码器
    Mp3Decoder = MP3InitDecoder();
    if (Mp3Decoder==0) {
        printf("初始化helix解码库设备\n");
        return; /* 停止播放 */
    }
    printf("初始化中...\n");

    Delay_ms(10); /* 延迟一段时间,等待SAI中断结束 */
    wm8978_Reset();   /* 复位WM8978到复位状态 */
    /* 配置WM8978芯片,输入为DAC,输出为耳机 */
    wm8978_CfgAudioPath(DAC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);

    /* 调节音量,左右相同音量 */
    wm8978_SetOUT1Volume(mp3player.ucVolume);

    /* 配置WM8978音频接口为飞利浦标准SAI接口,16bit */
    wm8978_CfgAudioIF(SAI_STANDARD_PHILIPS, 16);

    /*  初始化并配置SAI  */
    SAI_Stop();
    SAI_GPIO_Config();
    SAIx_Mode_Config(SAI_STANDARD_PHILIPS,SAI_DATAFORMAT_16B,mp3player.ucFreq);
    SAIx_TX_DMA_Init((uint32_t)&outbuffer[0],(uint32_t)&outbuffer[1],MP3BUFFER_SIZE);

    bufflag=0;
    Isread=0;

    mp3player.ucStatus = STA_PLAYING;   /* 放音状态 */
    result=f_read(&file,inputbuf,INPUTBUF_SIZE,&bw);
    if (result!=FR_OK) {
        printf("读取%s失败 -> %d\r\n",mp3file,result);
        MP3FreeDecoder(Mp3Decoder);
        return;
    }
    read_ptr=inputbuf;
    bytes_left=bw;
    /* 进入主程序循环体 */
    while (mp3player.ucStatus == STA_PLAYING) {
        read_offset = MP3FindSyncWord(read_ptr, bytes_left);  //寻找帧同步,返回第一个同步字的位置
        if (read_offset < 0) {                  //没有找到同步字
            result=f_read(&file,inputbuf,INPUTBUF_SIZE,&bw);
            if (result!=FR_OK) {
                printf("读取%s失败 -> %d\r\n",mp3file,result);
                break;
            }
            read_ptr=inputbuf;
            bytes_left=bw;
            continue;   //跳出循环2,回到循环1
        }
        read_ptr += read_offset;        //偏移至同步字的位置
        bytes_left -= read_offset;        //同步字之后的数据大小
        if (bytes_left < 1024) {            //补充数据
            /* 注意这个地方因为采用的是DMA读取,所以一定要4字节对齐  */
            i=(uint32_t)(bytes_left)&3;     //判断多余的字节
            if (i) i=4-i;           //需要补充的字节
            memcpy(inputbuf+i, read_ptr, bytes_left); //从对齐位置开始复制
            read_ptr = inputbuf+i;                    //指向数据对齐位置
            result = f_read(&file, inputbuf+bytes_left+i, INPUTBUF_SIZE-bytes_left-i, &bw);//补充数据
            if (result!=FR_OK) {
                printf("读取%s失败 -> %d\r\n",mp3file,result);
                break;
            }
            bytes_left += bw;   //有效数据流大小
        }
        err = MP3Decode(Mp3Decoder, &read_ptr,&bytes_left,outbuffer[bufflag], 0);
        //bufflag开始解码 参数:mp3解码结构体、输入流指针、输入流大小、输出流指针、数据格式
        frames++;
        if (err != ERR_MP3_NONE) {  //错误处理
            switch (err) {
            case ERR_MP3_INDATA_UNDERFLOW:
                printf("ERR_MP3_INDATA_UNDERFLOW\r\n");
                result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);
                read_ptr = inputbuf;
                bytes_left = bw;
                break;
            case ERR_MP3_MAINDATA_UNDERFLOW:
        /* do nothing - next call to decode will provide more mainData */
                printf("ERR_MP3_MAINDATA_UNDERFLOW\r\n");
                break;
            default:
                printf("UNKNOWN ERROR:%d\r\n", err);
                // 跳过此帧
                if (bytes_left > 0) {
                    bytes_left --;
                    read_ptr ++;
                }
                break;
            }
            Isread=1;
        } else {  //解码无错误,准备把数据输出到PCM
        MP3GetLastFrameInfo(Mp3Decoder, &Mp3FrameInfo);   //获取解码信息
            /* 输出到DAC */
            outputSamps = Mp3FrameInfo.outputSamps;         //PCM数据个数
            if (outputSamps > 0) {
                if (Mp3FrameInfo.nChans == 1) { //单声道
                    //单声道数据需要复制一份到另一个声道
                for (i = outputSamps - 1; i >= 0; i--) {
                outbuffer[bufflag][i * 2] = outbuffer[bufflag][i];
                outbuffer[bufflag][i * 2 + 1] = outbuffer[bufflag][i];
                    }
                    outputSamps *= 2;
                }//if (Mp3FrameInfo.nChans == 1)  //单声道
            }//if (outputSamps > 0)

            /* 根据解码信息设置采样率 */
            if (Mp3FrameInfo.samprate != mp3player.ucFreq) {  //采样率
                mp3player.ucFreq = Mp3FrameInfo.samprate;

                printf(" \r\n Bitrate       %dKbps", Mp3FrameInfo.bitrate/1000);
                printf(" \r\n Samprate      %dHz", mp3player.ucFreq);
                printf(" \r\n BitsPerSample %db", Mp3FrameInfo.bitsPerSample);
                printf(" \r\n nChans        %d", Mp3FrameInfo.nChans);
                printf(" \r\n Layer         %d", Mp3FrameInfo.layer);
                printf(" \r\n Version       %d", Mp3FrameInfo.version);
                printf(" \r\n OutputSamps   %d", Mp3FrameInfo.outputSamps);
                printf("\r\n");
                if (mp3player.ucFreq >= SAI_AUDIOFREQ_DEFAULT) {//SAI_AudioFreq_Default =2,正常的帧,每次都要改速率
                SAIx_Mode_Config(SAI_STANDARD_PHILIPS,SAI_DATAFORMAT_16B,mp3player.ucFreq);
                                                    //根据采样率修改iis速率
                //MP3BUFFER_SIZE);
                SAIx_TX_DMA_Init((uint32_t)&outbuffer[0],(uint32_t)&outbuffer[1],outputSamps); }
                SAI_Play_Start();
            }
        }//else 解码正常

        if (file.fptr==file.fsize) {  //mp3文件读取完成,退出
            printf("END\r\n");
            break;
        }

        while (Isread==0) {
            led_delay++;
            if (led_delay==0xffffff) {
                led_delay=0;
                LED4_TOGGLE;
            }
            //Input_scan();   //等待DMA传输完成,此间可以运行按键扫描及处理事件
        }
        Isread=0;
    }
    SAI_Stop();
    mp3player.ucStatus=STA_IDLE;
    MP3FreeDecoder(Mp3Decoder);
    f_close(&file);
}

mp3PlayerDemo函数是MP3播放器的实现函数,篇幅很长,需要我们仔细分析。它有一个形参,用于指定待播放的MP3文件,需要用MP3文件的绝对路径加全名称赋值。

read_ptr是定义的一个指针变量,它用于指示解码器源数据地址,把它初始化为用来存放解码器源数据缓冲区(inputbuf数组)的首地址。 read_offset和bytes_left主要用于MP3FindSyncWord函数,read_offset用来指示帧同步相对解码器源数据缓冲区首地址的偏移量, bytes_left用于指示解码器源数据缓冲区有效数据量。

mp3player是一个MP3_TYPE结构体类型变量,指示音量、状态和采样频率信息。

f_open函数用于打开文件,如果文件打开失败则直接退出播放。MP3InitDecoder函数用于初始化Helix解码器,分配解码器必须内存空间,如果初始化解码器失败直接退出播放。

接下来配置WM8978芯片功能,使能耳机输出,设置音量,使用SAI Philips标准和16bit数据长度。还要设置SAI外设工作环境,同样是SAI Philips标准和16bit数据长度, 采样频率先使用SAI_AudioFreq_Default,在运行MP3GetLastFrameInfo函数获取数据帧的采样频率后需要再次修改。MusicPlayer_SAI_DMA_TX_Callback是一个函数名, 该函数实现DMA双缓冲区标志位切换以及指示DMA传输完成,它在SAI的DMA发送传输完成中断服务函数中调用。SAIx_TX_DMA_Init用于初始化SAI的DMA发送请求, 使用双缓冲区模式。bufflag用于指示当前DMA传输的缓冲区号,Isread用于指示DMA传输完成。

f_read函数从SD卡读取MP3文件数据,存放在inputbuf缓冲区中,bw变量保存实际读取到的数据的字节数。如果读取数据失败则运行MP3FreeDecoder函数关闭解码器后退出播放器。

接下来是循环解码帧数据并播放。MP3FindSyncWord用于选择帧同步信息,如果在源数据缓冲区中找不到同步信息,read_offset值为-1, 需要读取新的MP3文件数据,重新寻找帧同步信息。一般MP3起始部分是ID3V2信息,所以可能需要循环几次才能寻找到帧同步信息。如果找到帧同步信息说明找到 数据帧,接下来数据就是数据帧的帧头以及MAIN_DATA。

有时找到了帧同步信息,但可能源数据缓冲区并没有包括整帧数据,这时需要把从帧同步信息开始的源数据,复制到源数据缓冲区起始地址上, 再使用f_read函数读取新数据填充满整个源数据缓冲区,保证源数据缓冲区保存有整帧源数据。

MP3Decode函数开始对源数据缓冲区中帧数据进行解码,通过函数返回值可判断得到解码状态,如果发生解码错误则执行对应的代码。

在解码无错误时,就可以使用MP3GetLastFrameInfo函数获取帧信息,如果有数据输出并且是单声道需要把数据复制成双声道数据格式。 如果采样频率与上一次有所不同则执行通过串口打印帧信息到串口调试助手,可能还需要调整SAI的工作环境,还调用SAI_Play_Start启动播放。 因为mp3player.ucFreq缺省值为SAI_AudioFreq_Default,一般都不会与Mp3FrameInfo.samprate相等,所以会至少进入if语句内,即会执行SAI_Play_Start函数。

如果文件读取已经都了文件末尾就退出循环,MP3文件已经播放完整。循环中还需要等待DMA数据传输完成才进行下一帧的解码操作。

mp3PlayerDemo函数最后停止SAI,关闭解码器和MP3文件。

DMA发送完成中断回调函数

代码清单:SAI-29 MusicPlayer_SAI_DMA_TX_Callback函数(mp3Player.c文件)
1
2
3
4
5
6
7
8
9
void MusicPlayer_SAI_DMA_TX_Callback(void)
{
    if (SAIx_TX_DMA_STREAM->CR&(1<<19)) { //当前使用Memory1数据
        bufflag=0;                       //可以将数据读取到缓冲区0
    } else {                           //当前使用Memory0数据
        bufflag=1;                       //可以将数据读取到缓冲区1
    }
    Isread=1;                          // DMA传输完成标志
}

MusicPlayer_SAI_DMA_TX_Callback函数用于在DMA发送完成后切换缓冲区。DMA数据流x配置寄存器的CT位用于指示当前目标缓冲区, 如果为1,当前目标缓冲区为存储器1;如果为0,则为存储器0。

主函数

代码清单:SAI-30 main函数(main.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main(void)
{
    /* 系统时钟初始化成480MHz */
    SystemClock_Config();

    CPU_CACHE_Enable();

    /* 初始化LED */
    LED_GPIO_Config();
    LED_BLUE;
    /* 初始化触摸按键 */
    TPAD_Init();
    /* 初始化调试串口,一般为串口1 */
    DEBUG_USART_Config();

    printf("Music Player\n");
    //链接驱动器,创建盘符
    FATFS_LinkDriver(&SD_Driver, SDPath);
    //在外部SD卡挂载文件系统,文件系统挂载时会对SD卡初始化
    res_sd = f_mount(&fs,"0:",1);

    if (res_sd!=FR_OK) {
        printf("!!SD卡挂载文件系统失败。(%d)\r\n",res_sd);
        printf("!!可能原因:SD卡初始化不成功。\r\n");
        while (1);
    } else {
        printf("SD卡挂载成功\r\n");
    }
    /* 检测WM8978芯片,此函数会自动配置CPU的GPIO */
    if (wm8978_Init()==0) {
        printf("检测不到WM8978芯片!!!\n");
        while (1);  /* 停机 */
    }
    printf("初始化WM8978成功\n");


    while (1) {
        mp3PlayerDemo("0:/mp3/张国荣-玻璃之情.mp3");
        mp3PlayerDemo("0:/mp3/张国荣-全赖有你.mp3");
    }
}

main函数主要完成各个外设的初始化,包括调试串口初始化、SD卡文件系统挂载还有系统滴答定时器初始化。

wm8978_Init初始化I2C接口用于控制WM8978芯片,并复位WM8978芯片,如果初始化成功则进入无限循环,执行MP3播放器实现函数mp3PlayerDemo,它有一个形参,用于指定播放文件。

另外,为使程序正常运行还需要适当增加控制器的栈空间,见 代码清单:SAI-5,Helix解码过程需要用到较多局部变量,需要调整栈空间,防止栈空间溢出。

代码清单:SAI-31 栈空间大小调整
1
2
3
4
Stack_Size      EQU     0x00001000

AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size

38.7.5. 下载验证

将工程文件夹中的“音频文件放在SD卡根目录下”文件夹的内容拷贝到Micro SD卡根目录中,把Micro SD卡插入到开发板右侧的卡槽内, 使用USB线连接开发板上的“USB TOUART”接口到电脑,电脑端配置好串口调试助手参数,在开发板的上边沿的耳机插座(左边那个)插入耳机。 编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板发过来的提示信息,如果没有提示错误信息则直接在耳机可听到音乐,播放完后自动切换下一首,如此循环。

实验主要展示MP3解码库移植过程和实现简单MP3文件播放,跟实际意义上的MP3播放器在功能上还有待完善,比如快进快退功能、声音调节、音效调节等等。