38. SAI

您所在的位置:网站首页 数字音频无法切换到扬声器 38. SAI

38. SAI

2024-07-11 05:23| 来源: 网络整理| 查看: 265

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周期来完成一次传输。

38.1.3.2. 左对齐标准¶

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

38.1.3.3. 右对齐标准¶

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

38.1.3.4. PCM标准¶

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

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功能框图 。

38.2.1. 功能引脚¶

4个SAI的4个功能引脚都与SPI有不同程度的的复用。SAI时钟发生器可以由控制器内部时钟源分频产生,亦可采用CKIN引脚输入时钟分频得到, 一般使用内部时钟源即可。控制器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配置寄存器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内部寄存器的一个位,通过控制寄存器的就可以控制开关的状态。

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”。

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文件结构 。

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

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

38.4.3. WAVE文件实例分析¶

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

下面对文件头进行解读,参考表 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 16typedef 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 19typedef 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耳机插座引出, 可直接接普通的耳机线或作为功放设备的输入源。

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 CR&(1 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 9void MusicPlayer_SAI_DMA_TX_Callback(void) { if (SAIx_TX_DMA_STREAM->CR&(1


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3