|
第六十一章 SPDIF(光纤音频)实验
1)实验平台:正点原子STM32H7R7开发板
2)章节摘自【正点原子】STM32H7R7开发指南 V1.1
3)购买链接: https://detail.tmall.com/item.htm?id=820823382459
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h7rx.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32开发板技术交流群:756580169
前面,我们介绍了STM32H7R7的SAI接口,实现了音乐播放器、录音机等功能,本章,我们将介绍STM32H7R7的SPDIF接口,结合SAI接口和ES8388解码器,实现对光纤音频信号的解码。
本章分为如下几个部分:
61.1 SPDIF简介
61.2 硬件设计
61.3 软件设计
61.4 下载验证
61.1 SPDIF简介
SPDIF是Sony/Philip Digital Interface Format的缩写,是由索尼和飞利浦公司联合开发的数字音频接口简称,分为SPDIF输入(IN)和SPDIF输出(OUT)两种,STM32H7R7的SPDIF接口,仅支持SPDIF IN,称之为SPDIF RX。
STM32H7R7的SPDIF接口的主要特点有:
提供多达4路输入
自动符号率检测
最大符号率:12.288MHz
支持8KHz到192KHz的立体声
支持IEC-60958和IEC-61937音频标准,消费类应用
奇偶校验位管理
使用DMA通信进行音频采样
支持控制和用户信息DMA传输
STM32H7R7的SPDIFRX接口支持符合IEC-60958和IEC-61937标准的SPDIF数据流。支持高采样率的简单立体声以及压缩的多通道环绕声(Dolby或DTS音频)。SPDIFRX接口框图如图61.1.1所示:
图61.1.1 SPDIF RX接口框图
图中SPDIFRX_DC模块负责解码从SPDIFX_IN[4:1]输入接收的SPDIF数据流。该模块重新采样传入的信号、解码曼彻斯特数据流、并识别帧、子帧和块元素,它传送到寄存器接口、解码数据和相关的状态标志。关于SPDIFRX_DC模块的详细介绍,请参考《STM32H7Rx参考手册_V6(英文版).pdf》第57.3.1节。
SPDIF RX接口通过APB1总线完全控制并且能够处理两个DMA通道:
1、专用于传输音频采样的DMA通道
2、专用于传输IEC-60958通道状态和用户信息的DMA通道
此外,还提供了中断服务,既可用作DMA的复用功能,也可用来指示外设的错误或关键状态。接下来,我们简单介绍一下SPDIF协议。
1)SPDIF块
一个SPDIF块由192个SPDIF帧组成,如图61.1.2所示:
图61.1.2 SPDIF块格式
每个SPDIF帧又由两个子帧组成,如图61.1.3所示:
图61.1.3 SPDIF子帧格式
每个子帧包含32个位,他们的组成如下:
1,位0到3包含同步报头之一(B/M/W);
2,位4到27包含以线性2的补码表示的音频采样字。最高有效位(MSB)为位27;
3,使用20位编码范围时,位8到27包含音频采样字,其中位8为LSB。
4,位28(有效性位“V”)表示数据是否有效(例如转换为模拟数据)。
5,位29(用户数据位“U”)包含用户数据信息,如光盘的音轨编号。
6,位30(通道状态位“C”)包含通道状态信息,如采样率和复制保护。
7,位31(奇偶捡验位“P”)包含奇偶校验位,位4到31将包含偶数个1和0(偶校验)。
对于线性编码音频应用,第一个子帧(立体声操作中的左声道或“A”通道以及单声道操作中的主通道)通常以报头“M”开始。但是,报头每192帧切换为报头“B”一次,以识别用于组织通道状态和用户信息的块结构的开始。第二个子帧(立体声操作中的右声道或“B”通道以及单声道操作中的辅助通道)始终以报头“W”开始。
2)同步报头
SPDIF协议规定,总共有三种同步报头:B、M、W(也可以称为Z、X、Y)。同步报头总是以和前半个位相反的电平开始的。使能第一个帧的第一个“B”报头的传输前,此前半位值为线路的电平。对于其他报头,此前半位值为之前子帧的奇偶校验位的第二个半位。
SPDIF三种同步报头的编码方式如图61.1.4所示:
图61.1.4 SPDIF同步报头
3)位编码
为最大程度减小传输线路上的直流分量值,并加快时钟从数据流中恢复的速度,从第4位开始,到第31位,全部采用双相符号编码。
双相符号编码原理:要传输的各个位通过由两个连续二进制状态构成的符号表示。符号的第一个状态始终不同于前一个符号的第二个状态。如果要传输的位为逻辑0,则符号的第二个状态与第一个状态相同。但如果该位为逻辑1,则两个状态不同。在IEC-60958规范中,这些状态称为“UI”(单位间隔)。
SPDIF的位编码原理如图61.1.5所示:
图61.1.5 通道编码示例
图中比特流实际上就是通道解码时钟,每2个时钟解码一个位,在两个时钟内,通道解码状态有变化的(10/01),解码为逻辑1;在两个解码时钟内,通道解码状态没有变化的(00/11),解码为逻辑0;
结合通道位编码原理,以及图61.1.4所示的同步报头编码方式,我们可以看出,前面4个位是不能用这个位编码原理来编码的(因为有3个UI的状态!!)。
SPDIF协议我们就介绍到这里,接下来,我们看看STM32H7R7 SPDIF RX的状态流程。如图61.1.6所示:
图61.1.6 STM32H7R7 SPDIF RX状态流程
由图可知,STM32H7R7的SPDIF RX接口总共有四种状态:
1,STATE_IDLE
该状态下,禁止SPDIF外设,spdifrx_ker_ck域复位,spdifrx_pclk域功能正常。
2,STATE_SYNC
该状态下,SPDIF将与数据流同步(同步过程请参考《STM32H7Rx参考手册_V6(中文版)》57.3.6节),阈值定期更新,可通过中断或DMA读取用户和通道状态。
3,STATE_RCV
该状态下,SPDIF将与数据流同步,阈值定期更新,可通过中断或DMA通道读取用户、通道状态和音频采样。当检测到“B”报头后,开始保存音频数据。
4,STOP_STATE
该状态下,SPDIF将不再同步,用户、通道状态和音频数据的接收都将停止,我们可以通过SPDIF_CR寄存器的SPDIFRXEN字段,控制SPDIFRX的状态。
当SPDIF处于STATE_IDLE时:
通过将SPDIFRXEN设置为01或11,将切换到STATE_SYNC状态。
当SPDIFRX处于STATE_SYNC时:
如果同步失败或者接收的数据未正确解码且无法在不进行再同步的情况下恢复(FERR或SERR或TERR=1),则SPDIFRX将进入STATE_STOP状态并且等待软件应答。
当同步阶段完成时,如果SPDIFRXEN=01,将保持该状态。
将SPDIFRXEN设置为0,SPDIFRX将立刻返回至STATE_IDLE状态。
当SPDIFRXEN=11,且SYNCD=1,则SPDIFRX进入STATE_RCV状态。
当SPDIFRX处于STATE_RCV时:
如果接收的数据未正确解码且无法在不进行再同步的情况下恢复(FERR或SERR或TERR=1),则SPDIFRX将进入STATE_STOP状态并且等待软件应答。
将SPDIFRXEN设置为0,SPDIFRX将立刻返回至STATE_IDLE状态。
在此状态下,如果数据接收正确,我们将其通过解码器播放出来,就能实现解码了。
当SPDIFRX处于STATE_STOP时:
SPDIFRX停止接收和同步,并等待软件将SPDIFRXEN位设置为0,以清零错误标志。
当SPDIFRXEN设置为0时,IP被禁止,这意味着所有状态机被复位,并且RX_BUF被刷新。还要注意,标志FERR、SERR和TERR也会复位。
数据接收
SPDIF RX为音频采样接收提供了两个32位双缓冲区:RX_BUF和SPDIF_DR。如果SPDIF_DR为空,则包含在RX_BUF中的有效数据将立刻传输至SPDIF_DR。
SPDIFRX可以使用DMA或中断将音频采样传输到存储器中,我们一般使用DMA来传输,这样效率比较高。
SPDIFRX提供多种处理接收数据的方法,用户可以分别处理控制信息流和音频采样流,或将二者一起处理。对于各子帧,数据接收寄存器SPDIFRX_FMT0_DR包含24个数据位和可选的V、U、C、PE状态位以及PT,这些可选的位,通过SPDIFRX_CR寄存器的VMSK、CUMSK、PMSK和PTMSK等位控制,我们将在寄存器介绍的时候,给大家详细讲解。
PE位表示奇偶校验错误位,该位将在解码的子帧中检测到奇偶校验错误时置1;PT字段包含报头类型(B、M或W);V、U和C则是从SPDIF接口接收的值的直接副本。
通过SPDIFRX_CR寄存器的DRFMT位,我们可以选择三种音频格式,如图61.1.7所示:
图61.1.7 SPDIFRX_DR寄存器格式
将DRFMT设置为00或01,可以使数据在SPDIFRX_DR寄存器中右对齐或左对齐,此时我们可以根据软件所需的处理方式启用状态信息(V/C/U/PE/PT等)或将其强制设置为零。
将DRFMT设置为10得出的格式在非线性模式下相关,因为每个子帧仅使用16位。使用此格式,两个连续子帧的数据存储到SPDIFRX_DR中,存储器占用量将减半。
本章我们将DRFMT设置为00,使用右对齐格式,且可以支持24位音频数据传输。
时钟策略
SPDIFRX块需要两个不同的时钟:
1,APB1时钟(spdifrx_pclk),用于寄存器接口,其频率必须至少大于符号率;
2,spdifrx_ker_ck时钟,主要由SPDIFRX_DC部分使用;
为正确解码传入的SPDIF数据流,SPDIFRX_DC应以至少比最大符号率(符号率=采样率*32*2)高11倍或者比音频采样率高704倍的时钟重新采样接收的数据。
例如,如果用户预期接收到最高12.288MHz的符号率(对应音频采样率为192KHz),则采样率至少为135.2MHz。
61.1.1 SPDIF寄存器
SPDIFRX控制寄存器(SPDIFRX_CR)
图61.1.2.1 SPDIFRX_CR寄存器各位描述
该寄存器我们只介绍本章需要用到的一些位(下同):
INSEL[2:0]:这三个位,用于选择SPDIFRX的输入通道,STM32H7R7总共有4个输入通道(0~3),我们硬件上连接在通道1上面,所以设置INSEL=001即可。
NBTR[1:0]:这两个位用于设置在同步阶段允许的最大重试次数:00,表示不允许重试;01,表示允许重试3次;10,表示允许重试15次;11,表示允许重试63次。本章我们设置为10,允许重试15次。
CHSEL:用于选择通道状态获取路径。我们设置为0,选择从通道A获取通道状态。
CBDMAEN:用于设置通道状态和用户数据是否使能DMA接收。我们设置为0,禁止DMA方式接收通道状态和用户数据。
PTMSK:用于设置报头类型屏蔽位。我们设置为1,禁止将报头数据写入SPDIFRX_DR。
CUMSK:用于设置通道状态和用户数据屏蔽位。我们设置为1,禁止将通道状态和用户数据写入SPDIFRX_DR。
VMSK:用于设置有效性屏蔽位。我们设置为1,禁止将有效性位写入SPDIFRX_DR。
PMSK:用于设置奇偶检验屏蔽位。我们设置为1,禁止将奇偶检验写入SPDIFRX_DR。
DRFMT:用于设置SPDIFRX_DR的数据格式。我们设置为00,选择右对齐格式(LSB)。
RXSTEO:用于设置是否使能立体声模式。我们设置为1,使能立体声模式。
RXDMAEN:用于设置是否使能数据流DMA接收。我们设置为1,使能DMA接收。
SPDIFRXEN:用于设置SPDIFRX的使能。实际上就是控制SPDIFRX的工作状态:00,禁止SPDIFRX;01,使能SPDIFRX同步;10,保留;11,使能SPDIFRX接收器。关于该寄存器的设置对SPDIFRX工作状态的影响,请参考图61.1.6。
SPDIFRX中断屏蔽寄存器(SPDIFRX_IMR)
图61.1.2.2 SPDIFRX_IMR寄存器各位描述
IFEIE:串行接口错误中断使能屏蔽位。我们设置为1,当SPDIFRX_SR寄存器中的SERR=1、TERR=1或者FERR=1时,将产生SPDIFRX中断。
PERRIE:奇偶检验错误中断屏蔽位。我们设置为1,当SPDIFRX_SR寄存器中的PERR=1时,将产生SPDIFRX中断。
SPDIFRX状态寄存器(SPDIFRX_SR)
图61.1.2.3 SPDIFRX_SR寄存器各位描述
WIDTH5:使用spdifrx_ker_ck计数5个符号的持续时间。它表示5个连续符号的时间内包含的spdifrx_ker_ck时钟周期数。该值可用于估算SPDIFRX的音频采样率。其精度受spdifrx_ker_ck的频率限制。其估算公式为:
Fs = 5*spdifrx_ker_ck/(WIDTH5*64) Fs为估算的音频采样率。假定spdifrx_ker_ck为84MHz,WIDTH5为147。计算可得Fs为44.6KHz,最接近的标准采样率为44.1KHz,所以,我们基本可以确定采样率为44.1KHz。
注意,需要在同步完成以后(SYNCD=1),才可以读取WIDTH5的值,判断采样率。
TERR:超时错误标志。当该位为1时,表示检测到序列错误。
SERR:同步错误标志。当该位为1时,表示检测到同步错误。
FERR:帧错误标志。当该位为1时,表示检测到曼彻斯特编码错误(帧错误)。
SYNCD:同步完成标志。当该位为1时,表示同步完成。
OVR:上溢错误标志。当该位为1时,表示检测到上溢错误。
PERR:奇偶校验错误标志。当该位为1时,表示检测到奇偶校验错误。
SPDIFRX中断标志清零寄存器(SPDIFRX_IFCR)
图61.1.2.4 SPDIFRX_IFCR寄存器各位描述
OVRCF:清零上溢错误标志。向该位写1,可以清除上溢错误标志。
PERRCF:清零奇偶校验错误标志。向该位写1,可以清除奇偶校验错误标志。
SPDIFRX数据寄存器(SPDIFRX_DR)
图61.1.2.5 SPDIFRX_DR寄存器各位描述
该寄存器有三种数据格式可选(FMT0/FMT1/FMT2,见图61.1.7),此图为DRFMT=00时的格式(FMT0),使用右对齐(LSB)格式的SPDIFRX_DR寄存器各位描述。该寄存器里面的PT/C/U/V/PE等位,我们都不用,我们只用低24位(DR[23:0]),用于读取音频数据。当进入接收模式以后(SPDIFRXEN=11),我们不停的读取SPDIFRX_DR的数据,并传送给SAI接口,再由SAI传输给ES8388,就可以播放来自SPDIFRX接收到的音乐了。不过,我们采用DMA来传输,所以直接设置DMA的外设地址为SPDIFRX_DR即可。
SPDIFRX的相关寄存器,就给大家介绍到这里,更详细的介绍,请大家参考《STM32H7Rx参考手册_V6(英文版).pdf》第57.5节。
61.2 硬件设计
1. 例程功能
本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则初始化ES8388的DAC工作,并开启DAC输出,随后设置SPDIFRX的DMA和回调函数。然后进入死循环等待,不停的检测SPDIF的连接状态;当SPDIFRX同步完成,且与SAI时钟同步完成后,开启SPDIFRX和SAI的DMA数据传输。此时,在屏幕上会显示当前的音频信号采用率,同时在喇叭/耳机,可以听到音乐。另外,我们可以通过KEY_UP和KEY1来调节音量,KEY_UP用于增加音量,KEY1用于减少音量。
2. 硬件资源
实验用到的硬件资源如下:
1)LED灯
LED0 – PD14
LED1 – PC0
2)独立按键
KEY0 - PE9
KEY1 - PE8
KEY_UP - PC13 (程序中的宏名:WK_UP)
3)串口1 (PB14/PB15连接在板载USB转串口芯片CH340上面)
4)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(包括MCU屏和RGB屏,都支持)
5)SD卡:通过SDMMC连接
6)NOR FLASH
7)SAI,驱动ES8388芯片
8)开发板板载的咪头或自备麦克风输入
9)喇叭或耳机
其中,除了光纤接口,其他资源在之前的学习中都已经介绍过了。光纤接口我们使用的是DLR1150光纤座,该接口和STM32H7R7的连接,如图61.2.1所示:
图61.2.1 光纤接口与STM32H7R7连接原理图
图中DLR1150就是我们所使用的光纤座(即光纤接口),它连接在STM32H7R7的PG12脚上,是SPDIFRX的输入通道1。
另外,本实验还需要用到一个音频光纤信号发送设备和一条光纤线,这些都需要大家自备。光纤音频发送设备推荐购买ALIENTEK的贝斯(BASE)蓝牙音频接收器,支持光纤输出,购买地址:http://www.openedv.com/thread-86409-1-1.html。
61.3 程序设计
我们要通过STM32的SPDIFRX接口接收光纤音频数据,并通过SAI驱动ES8388播放音乐的简要步骤。HAL库中SPDIFRX相关的库函数定义和声明分布在源文件stm32h7rsxx_hal_spdifrx.c和头文件stm32h7rsxx_hal_spdifrx.h中。
具体配置步骤
1)初始化ES8388
本实验最终要通过ES8388来输出音乐,我们需要先对其进行配置,详细的配置过程,请参考第五十九章:
2)初始化SPDIF
此步需要初始化SPDIFRX对应的IO口、开启SPDIFRX时钟、设置SPDIFRX的模式和相关配置,主要通过对SPDIFRX_CR寄存器的配置来实现。
HAL库中初始化SPDIF函数为HAL_SPDIFRX_Init,该函数声明如下:
- HAL_StatusTypeDef HAL_SPDIFRX_Init(SPDIFRX_HandleTypeDef *hspdif)
复制代码 该函数只有一个SPDIFRX_HandleTypeDef结构体类型指针类型入口参数hspdif,接下来我们看看结构体SPDIFRX_HandleTypeDef定义,如下:
- typedef struct
- {
- SPDIFRX_TypeDef *Instance;
- SPDIFRX_InitTypeDef Init;
- uint32_t *pRxBuffPtr;
- uint32_t *pCsBuffPtr;
- __IO uint16_t RxXferSize;
- __IO uint16_t RxXferCount;
- __IO uint16_t CsXferSize;
- __IO uint16_t CsXferCount;
- DMA_HandleTypeDef *hdmaCsRx;
- DMA_HandleTypeDef *hdmaDrRx;
- __IO HAL_LockTypeDef Lock;
- __IO HAL_SPDIFRX_StateTypeDef State;
- __IO uint32_t ErrorCode;
- } SPDIFRX_HandleTypeDef;
复制代码 对于SPDIFRX初始化,这里我们主要讲解Init成员变量,该成员变量用来设置SPDIFRX的初始化参数,为SPDIFRX_InitTypeDef结构体类型,该结构体定义为:
- typedef struct
- {
- uint32_t InputSelection; /* 选择SPDIFRX的输入通道 */
- uint32_t Retries; /* 设置在同步阶段允许的最大重试次数 */
- uint32_t WaitForActivity; /* 执行同步前是否等待SPDIFRX_IN线路上的活动 */
- uint32_t ChannelSelection; /* 选择通道状态获取路径 */
- uint32_t DataFormat; /* 设置SPDIFRX_DR的数据格式 */
- uint32_t StereoMode; /* 设置是否使能立体声模式 */
- uint32_t PreambleTypeMask; /* 设置报头类型屏蔽位 */
- uint32_t ChannelStatusMask; /* 设置通道状态和用户数据屏蔽位 */
- uint32_t ValidityBitMask; /* 设置有效性位屏蔽位 */
- uint32_t ParityErrorMask; /* 设置奇偶校验屏蔽位 */
- FunctionalState SymbolClockGen;
- FunctionalState BackupSymbolClockGen;
- } SPDIFRX_InitTypeDef;
复制代码 该结构体各个成员变量的含义都在上面注释了,实际上配置的是SPDIFRX_CR寄存器各个位,有不理解的地方可以对照中文参考手册中寄存器位定义描述来理解。
和其他外设接口一样,HAL库同样提供了SPDIFRX初始化回调函数:
- void HAL_SPDIFRX_MspInit(SPDIFRX_HandleTypeDef *hspdif);
复制代码
3)设置SPDIFRX的DMA
我们通过DMA双缓冲模式来接收SPDIF RX接收到的数据,从而提高效率。每当一个缓冲区数据接收满以后,硬件自动切换为下一个缓冲区,同时可以将满的缓冲区数据通过SAI接口,传输给ES8388,从而实现音乐播放。SPDIFRX使用DMA双缓冲接收数据的过程如图61.3.1所示:
图61.3.1 DMA双缓冲发送音频数据流框图
4)配置SAI
在SPDIFRX接口同步完成,且与SAI接口完成时钟同步(后续介绍),并成功获取音频数据流的采样率以后,就可以配置SAI接口,然后将SPDIFRX接收到的音频数据流,传输给ES8388,实现音频播放。此过程主要配置SAI的采样率、工作模式、协议、时钟电平特性、slot相关参数和DMA等。我们同样通过DMA双缓冲模式将数据传输给ES8388。SAI接口的DMA处理流程,请参考第五十九章。
5)编写SPDIFRX中断服务函数
当SPDIFRX传输出现错误的时候(比如突然断开光纤线或采样率发生了变化),需要在SPDIFRX中断服务函数里面对其进行处理,当发生不可恢复的错误时,需要重新设置SPDIFRX进入IDLE状态,以便重新同步。SPDIFRX中断服务函数为SPDIF_RX_IRQHandler。
6)开启DMA传输
最后,我们就只需要开启SPDIFRX和SAI的DMA传输,就可以实现SPDIF接收光纤音频数据,并通过ES8388播放出来。此时,就可以在ES8388的耳机和喇叭通道听到光纤传输过来的音乐了。
61.3.1 程序解析
1. spdifrx驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,SPDIFRX的驱动主要包括两个文件:spdifrx.c和spdifrx.h。spdifrx.c我们编写代码如下:
1,spdif_rx_init函数
- /**
- * @brief 初始化SPDIF
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 无
- */
- void spdif_rx_init(void)
- {
- spdif_dev.clock = 200000000;
- /* 设置SPDIF CLK的频率,为200Mhz,要支持192Khz采样率必须保证clock≥135.2Mhz */
- g_spdifin1_handle.Instance = SPDIFRX;
- g_spdifin1_handle.Init.InputSelection = SPDIFRX_INPUT_IN1;
- /* SPDIF输入1 */
- g_spdifin1_handle.Init.Retries = SPDIFRX_MAXRETRIES_15;
- /* 同步阶段允许重试次数 */
- g_spdifin1_handle.Init.WaitForActivity = SPDIFRX_WAITFORACTIVITY_ON;
- /* 等待同步 */
- g_spdifin1_handle.Init.ChannelSelection = SPDIFRX_CHANNEL_A;
- /* 控制流从通道A获取通道状态 */
- g_spdifin1_handle.Init.DataFormat = SPDIFRX_DATAFORMAT_LSB; /* 右对齐 */
- g_spdifin1_handle.Init.StereoMode = SPDIFRX_STEREOMODE_ENABLE;
- /* 使能立体声模式 */
- g_spdifin1_handle.Init.PreambleTypeMask = SPDIFRX_PREAMBLETYPEMASK_OFF;
- /* 报头类型不复制到SPDIFRX_DR中 */
- g_spdifin1_handle.Init.ChannelStatusMask = SPDIFRX_CHANNELSTATUS_OFF;
- /* 通道状态和用户位不复制到SPDIFRX_DR中 */
- g_spdifin1_handle.Init.ValidityBitMask = SPDIFRX_VALIDITYMASK_ON;
- /* 有效性位不复制到SPDIFRX_DR中 */
- g_spdifin1_handle.Init.ParityErrorMask = SPDIFRX_PARITYERRORMASK_ON;
- /* 奇偶校验错误位不复制到SPDIFRX_DR中 */
- HAL_SPDIFRX_Init(&g_spdifin1_handle);
- g_spdifin1_handle.Instance->CR |= SPDIFRX_CR_RXDMAEN;
- /* SPDIF音频数据使用DMA来接收 */
-
- /* 使能SPDIF的上溢错误和奇偶校验错误 */
- __HAL_SPDIFRX_ENABLE_IT(&g_spdifin1_handle, SPDIFRX_IT_IFEIE |
- SPDIFRX_IT_PERRIE);
- }
复制代码 该函数用于初始化SPDIFRX接口,包括使能SPDIFRX时钟、设置IO口复用功能、设置SPDIFRX相关参数等,最后开启了奇偶检验错误中断和接口错误中断,用于处理当接收数据出现错误时(包括光纤断开、采样率切换等),重新同步。
该函数还设置了SPDIFRX_CLK的时钟频率,频率为SPDIFRX_CLK=200MHz,其频率大于135.2MHz,因此可以支持最高192KHz的音频采样率。
2,HAL_SPDIFRX_MspInit函数
- /**
- * @brief SPDIF底层IO初始化和时钟使能
- * [url=home.php?mod=space&uid=60778]@note[/url] 此函数会被HAL_SPDIF_Init()调用
- * @param hspdif : SPDIF句柄
- * @retval 无
- */
- void HAL_SPDIFRX_MspInit(SPDIFRX_HandleTypeDef *hspdif)
- {
- GPIO_InitTypeDef gpio_init_struct;
- RCC_PeriphCLKInitTypeDef rcc_spdifrx_sture;
-
- __HAL_RCC_SPDIFRX_CLK_ENABLE(); /* 使能SPDIF RX时钟 */
- __HAL_RCC_GPIOG_CLK_ENABLE(); /* 使能GPIOG时钟 */
-
-
- rcc_spdifrx_sture.PeriphClockSelection = RCC_PERIPHCLK_SPDIFRX;
- rcc_spdifrx_sture.SpdifrxClockSelection = RCC_SPDIFRXCLKSOURCE_PLL2R;
- HAL_RCCEx_PeriphCLKConfig(&rcc_spdifrx_sture);
-
- /* 初始化PG12,SPDIF IN引脚 */
- gpio_init_struct.Pin = GPIO_PIN_12; /* PG12,SPDIF IN引脚 */
- gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用 */
- gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
- gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
- gpio_init_struct.Alternate = GPIO_AF8_SPDIFRX; /* 复用为SPDIF RX */
- HAL_GPIO_Init(GPIOG, &gpio_init_struct);
- HAL_NVIC_SetPriority(SPDIF_RX_IRQn, 1, 0); /* SPDIF中断 */
- HAL_NVIC_EnableIRQ(SPDIF_RX_IRQn);
- }
复制代码 该函数是SPDIFRX的初始化回调函数,主要用来配置时钟使能,IO口模式和中断分组等。
3,spdif_rx_dma_init函数
- /**
- * @brief SPDIF RX数据DMA配置
- * @note 设置为双缓冲模式,并开启DMA传输完成中断
- * @param buf0 : M0AR地址.
- * @param buf1 : M1AR地址.
- * @param num : 每次传输数据量
- * @param width : 位宽(存储器和外设,同时设置),0,8位;1,16位;2,32位;
- * @retval 无
- */
- void spdif_rx_dma_init(uint32_t *buf0, uint32_t *buf1, uint16_t num, uint8_t width)
- {
- uint32_t memwidth = 0, perwidth = 0; /* 外设和存储器位宽 */
- DMA_NodeConfTypeDef dma_node_conf_struct = {0};
-
- switch (width)
- {
- case 0: /* 8位 */
- num = num;
- memwidth = DMA_SRC_DATAWIDTH_BYTE;
- perwidth = DMA_DEST_DATAWIDTH_BYTE;
- break;
- case 1: /* 16位 */
- num *= 2;
- memwidth = DMA_SRC_DATAWIDTH_HALFWORD;
- perwidth = DMA_DEST_DATAWIDTH_HALFWORD;
- break;
- case 2: /* 32位 */
- num *= 4;
- memwidth = DMA_SRC_DATAWIDTH_WORD;
- perwidth = DMA_DEST_DATAWIDTH_WORD;
- break;
- }
- __HAL_RCC_GPDMA1_CLK_ENABLE(); /* 使能GPDMA1时钟 */
- /* 复位链表 */
- HAL_DMAEx_List_ResetQ(&g_spdif_rx_dma_qlist_struct);
-
- /* 构建链表节点 */
- dma_node_conf_struct.NodeType = DMA_GPDMA_LINEAR_NODE;
- dma_node_conf_struct.Init.Request = GPDMA1_REQUEST_SPDIF_RX_DT;
- dma_node_conf_struct.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
- dma_node_conf_struct.Init.Direction = DMA_PERIPH_TO_MEMORY;
- dma_node_conf_struct.Init.SrcInc = DMA_SINC_FIXED;
- dma_node_conf_struct.Init.DestInc = DMA_DINC_INCREMENTED;
- dma_node_conf_struct.Init.SrcDataWidth = perwidth;
- dma_node_conf_struct.Init.DestDataWidth = memwidth;
- dma_node_conf_struct.Init.SrcBurstLength = 1;
- dma_node_conf_struct.Init.DestBurstLength = 1;
- dma_node_conf_struct.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0
- | DMA_DEST_ALLOCATED_PORT1;
- dma_node_conf_struct.Init.TransferEventMode =
- DMA_TCEM_EACH_LL_ITEM_TRANSFER;
- dma_node_conf_struct.Init.Mode = DMA_NORMAL;
- dma_node_conf_struct.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
- dma_node_conf_struct.DataHandlingConfig.DataAlignment =
- DMA_DATA_RIGHTALIGN_ZEROPADDED;
- dma_node_conf_struct.TriggerConfig.TriggerPolarity =
- DMA_TRIG_POLARITY_MASKED;
- dma_node_conf_struct.SrcAddress = (uint32_t)&SPDIFRX->DR;
- dma_node_conf_struct.DataSize = num;
- dma_node_conf_struct.DstAddress = (uint32_t)buf0;
- HAL_DMAEx_List_BuildNode(&dma_node_conf_struct,
- &g_spdif_rx_dma_node_struct[0]);
- HAL_DMAEx_List_InsertNode_Tail(&g_spdif_rx_dma_qlist_struct,
- &g_spdif_rx_dma_node_struct[0]);
- dma_node_conf_struct.DstAddress = (uint32_t)buf1;
- HAL_DMAEx_List_BuildNode(&dma_node_conf_struct,
- &g_spdif_rx_dma_node_struct[1]);
- HAL_DMAEx_List_InsertNode_Tail(&g_spdif_rx_dma_qlist_struct,
- &g_spdif_rx_dma_node_struct[1]);
-
- /* 循环链表 */
- HAL_DMAEx_List_SetCircularMode(&g_spdif_rx_dma_qlist_struct);
-
- /* 初始化DMA通道为链表模式 */
- g_spdif_dtdma_handle.Instance = GPDMA1_Channel1;
- g_spdif_dtdma_handle.InitLinkedList.Priority = DMA_HIGH_PRIORITY;
- g_spdif_dtdma_handle.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
- g_spdif_dtdma_handle.InitLinkedList.LinkAllocatedPort =
- DMA_LINK_ALLOCATED_PORT1;
- g_spdif_dtdma_handle.InitLinkedList.TransferEventMode =
- DMA_TCEM_EACH_LL_ITEM_TRANSFER;
- g_spdif_dtdma_handle.InitLinkedList.LinkedListMode =
- DMA_LINKEDLIST_CIRCULAR;
- HAL_DMAEx_List_DeInit(&g_spdif_dtdma_handle);
- HAL_DMAEx_List_Init(&g_spdif_dtdma_handle);
-
- /* 链接链表到DMA通道 */
- HAL_DMAEx_List_LinkQ(&g_spdif_dtdma_handle, &g_spdif_rx_dma_qlist_struct);
-
- /* 链接到外设 */
- __HAL_LINKDMA(&g_spdifin1_handle, hdmaDrRx, g_spdif_dtdma_handle);
-
- /* 配置DMA通道的安全和特权属性 */
- HAL_DMA_ConfigChannelAttributes(&g_spdif_dtdma_handle, DMA_CHANNEL_NPRIV);
-
- /* 以链表模式(阻塞模式)启动DMA通道传输 */
- HAL_DMAEx_List_Start(&g_spdif_dtdma_handle);
- }
复制代码 该函数用于配置SPDIFRX接口的数据接收DMA,使用双缓冲模式,将SPDIFRX_DR的数据,存储到内存里面,实现音频数据接收。
4,spdif_rx_waitsync函数
- /**
- * @brief 等待进入同步状态,同步完成以后自动进入接收状态
- * @param 无
- * @retval 返回值:0,未同步;1,已同步
- */
- uint8_t spdif_rx_waitsync(void)
- {
- uint8_t res = 0;
- uint8_t timeout = 0;
- spdif_rx_mode(SPDIF_RX_SYNC); /* 设置为同步模式 */
- while(1)
- {
- timeout++;
- delay_ms(2);
- if (timeout > 100)
- {
- break;
- }
- if (__HAL_SPDIFRX_GET_FLAG(&g_spdifin1_handle, SPDIFRX_FLAG_SYNCD))
- /* 同步完成? */
- {
- res = 1; /* 标记同步完成 */
- spdif_rx_mode(SPDIF_RX_RCV);/* 进入接收模式 */
- break;
- }
- }
- return res;
- }
复制代码 该函数用于等待SPDIFRX同步完成,SPDIF必须在等待同步完成以后,才可以获取音频采样率,然后进入接收状态,接收音频数据。
5,spdif_rx_getsamplerate函数
- /**
- * @brief 获取SPDIF RX收到的音频采样率
- * @param 无
- * @retval 返回值:0,错误的采样率
- * 其他值,音频采样率
- */
- uint32_t spdif_rx_getsamplerate(void)
- {
- uint16_t spdif_w5;
- uint32_t samplerate;
-
- spdif_w5 = SPDIFRX->SR >> 16;
- samplerate = (spdif_dev.clock * 5) / (spdif_w5 & 0X7FFF);
- samplerate >>= 6; /* 除以64 */
- if ((8000 - 1500 <= samplerate) && (samplerate <= 8000 + 1500))
- {
- samplerate = 8000; /* 8K的采样率 */
- }
- else if ((11025 - 1500 <= samplerate) && (samplerate <= 11025 + 1500))
- {
- samplerate = 11025; /* 11.025K的采样率 */
- }
- else if ((16000 - 1500 <= samplerate) && (samplerate <= 16000 + 1500))
- {
- samplerate= 16000; /* 16K的采样率 */
- }
- else if ((22050 - 1500 <= samplerate) && (samplerate <= 22050 + 1500))
- {
- samplerate = 22050; /* 22.05K的采样率 */
- }
- else if ((32000 - 1500 <= samplerate) && (samplerate <= 32000 + 1500))
- {
- samplerate = 32000; /* 32K的采样率 */
- }
- else if ((44100 - 1500 <= samplerate) && (samplerate <= 44100 + 1500))
- {
- samplerate = 44100; /* 44.1K的采样率 */
- }
- else if ((48000 - 1500 <= samplerate) && (samplerate <= 48000 + 1500))
- {
- samplerate = 48000; /* 48K的采样率 */
- }
- else if ((88200 - 1500 <= samplerate) && (samplerate <= 88200 + 1500))
- {
- samplerate = 88200; /* 88.2K的采样率 */
- }
- else if ((96000 - 1500 <= samplerate) && (samplerate <= 96000 + 1500))
- {
- samplerate = 96000; /* 96K的采样率 */
- }
- else if ((176400 - 6000 <= samplerate) && (samplerate <= 176400 + 6000))
- {
- samplerate = 176400; /* 176.4K的采样率 */
- }
- else if ((192000 - 6000 <= samplerate) && (samplerate <= 192000 + 6000))
- {
- samplerate = 192000; /* 192K的采样率 */
- }
- else
- {
- samplerate = 0;
- }
- return samplerate;
- }
复制代码 该函数用于获取SPDIFRX识别到的音频采样率。该函数首先通过SPDIFRX_SR寄存器的WIDTH5位,获取大概的音频采样率,然后标准化为最接近的音频采样率。该函数必须在SPDIF同步完成以后才可以调用。
6,SPDIF_RX_IRQHandler函数
- /**
- * @brief SPDIF接收中断服务函数
- * @param 无
- * @retval 无
- */
- void SPDIF_RX_IRQHandler(void)
- {
- /* 发生超时、同步和帧错误中断,这三个中断一定要处理! */
- if ( __HAL_SPDIFRX_GET_FLAG(&g_spdifin1_handle, SPDIFRX_FLAG_FERR)||\
- __HAL_SPDIFRX_GET_FLAG(&g_spdifin1_handle, SPDIFRX_FLAG_SERR)||\
- __HAL_SPDIFRX_GET_FLAG(&g_spdifin1_handle, SPDIFRX_FLAG_TERR))
- {
- spdif_play_stop(); /* 发生错误,关闭SPDIF播放 */
- spdif_rx_stop_callback(); /* 调用回调函数 */
- spdif_rx_mode(SPDIF_RX_IDLE);
- /* 当发生超时、同步和帧错误的时候要将SPDIFRXEN写0来清除中断 */
- }
- if (__HAL_SPDIFRX_GET_FLAG(&g_spdifin1_handle,SPDIFRX_FLAG_OVR))
- /* 上溢错误 */
- {
- __HAL_SPDIFRX_CLEAR_IT(&g_spdifin1_handle, SPDIFRX_FLAG_OVR);
- /* 清除上溢错误中断 */
- }
- if (__HAL_SPDIFRX_GET_FLAG(&g_spdifin1_handle,SPDIFRX_FLAG_PERR))
- /* 奇偶校验错误 */
- {
- __HAL_SPDIFRX_CLEAR_IT(&g_spdifin1_handle, SPDIFRX_FLAG_PERR);
- /* 清除奇偶校验错误 */
- }
- }
复制代码 该函数用于处理SPDIFRX接口的错误中断,当发生超时错误、同步错误和帧错误的时候,必须停止SPDIFRX接口,然后重新进入IDLE状态,以便重新同步。
7,spdif_play_start和spdif_play_stop函数
- /**
- * @brief SPDIF开始播放
- * @param 无
- * @retval 无
- */
- void spdif_play_start(void)
- {
- spdif_dev.consta = 1; /* 标记已经打开SPDIF */
- __HAL_DMA_ENABLE(&g_spdif_dtdma_handle); /* 开启DMA TX传输 */
- }
- /**
- * @brief SPDIF关闭
- * @param 无
- * @retval 无
- */
- void spdif_play_stop(void)
- {
- spdif_dev.consta = 0; /* 标记已经关闭SPDIF */
- spdif_dev.saisync = 0; /* 清空同步状态 */
- __HAL_DMA_DISABLE(&g_spdif_dtdma_handle); /* 关闭DMA TX传输 */
- }
复制代码 这两个函数,前者用于启动SPDIFRX DMA传输,在所有配置和状态都正常的情况下,用于启动SPDIF音频播放。后者用于关闭SPDIFRX DMA传输,停止SPDIF播放。其中spdif_dev结构体用于控制SPDIF接收状态,结构体定义如下:
- /* SPDIF控制结构体 */
- typedef struct
- {
- uint8_t consta; /* 连接状态,0,未连接;1,连接上了 */
- uint8_t saisync; /* 与SAI时钟是否同步,0,未同步;1,已同步. */
- uint32_t samplerate; /* SPDIF采样率 */
- uint32_t clock; /* SPDIF时钟频率 */
- }spdif_rx_dev;
复制代码 consta表示SPDIF连接状态,当SPDIF开启DMA传输的时候,设置为1,表示SPDIF正在播放。当其为0的时候,表示SPDIF停止播放。
saisync表示当前的同步状态。
samplerate表示SPDIF识别到的音频采样率,单位为Hz。
clock表示spdifrx_ker_ck的时钟频率,单位为Hz。
2. main.c代码
最后我们在main.c中实现代码如下:
- /* SPDIF音频数据接收缓冲区定义 */
- #define SPDIF_DBUF_SIZE 1024
- static uint32_t spdif_audiobuf[2][SPDIF_DBUF_SIZE];
- /**
- * @brief 显示采样率
- * @param samplerate: 音频采样率(单位:Hz)
- * @retval 无
- */
- static void spdif_show_samplerate(uint32_t samplerate)
- {
- char *buf = NULL;
-
- /* 申请内存 */
- buf = (char *)mymalloc(SRAMIN, 100);
- if (buf == NULL)
- {
- return;
- }
-
- /* 显示采样率 */
- if (samplerate)
- {
- sprintf((char *)buf, "%dHz", samplerate);
- }
- else
- {
- sprintf((char *)buf, "正在识别...");
- }
- lcd_fill(30 + 56, 170, 230, 170 + 16, WHITE);
- text_show_string(30 + 56, 170, 200, 16, (char *)buf, 16, 0, RED);
- /* 释放内存 */
- myfree(SRAMIN, buf);
- }
- /**
- * @brief SAI DMA发送完成中断回调函数
- * @param 无
- * @retval 无
- */
- void sai_dma_tx_callback(void)
- {
- return;
- }
- /**
- * @brief SPDIF RX结束时的回调函数
- * @param 无
- * @retval 无
- */
- void spdif_rx_stopplay_callback(void)
- {
- sai1_play_stop();
- SPDIFRX->IFCR |= SPDIFRX_IFCR_SYNCDCF;
- spdif_show_samplerate(0);
- memset(&spdif_audiobuf[0], 0, SPDIF_DBUF_SIZE * sizeof(uint32_t));
- memset(&spdif_audiobuf[1], 0, SPDIF_DBUF_SIZE * sizeof(uint32_t));
- }
- int main(void)
- {
- uint8_t t = 0;
- uint8_t key;
- uint8_t vol;
-
- sys_mpu_config(); /* 配置MPU */
- sys_cache_enable(); /* 使能Cache */
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(300, 6, 2); /* 配置时钟,600MHz */
- delay_init(600); /* 初始化延时 */
- usart_init(115200); /* 初始化串口 */
- led_init(); /* 初始化LED */
- key_init(); /* 初始化按键 */
- hyperram_init(); /* 初始化HyperRAM */
- lcd_init(); /* 初始化LCD */
- es8388_init(); /* 初始化ES8388 */
- spdif_rx_init(); /* 初始化SPDIF */
- my_mem_init(SRAMIN); /* 初始化AXI-SRAM1~4内存池 */
- my_mem_init(SRAMEX); /* 初始化XSPI2 HyperRAM内存池 */
- my_mem_init(SRAM12); /* 初始化AHB-SRAM1~2内存池 */
- my_mem_init(SRAMDTCM); /* 初始化DTCM内存池 */
- my_mem_init(SRAMITCM); /* 初始化ITCM内存池 */
- exfuns_init(); /* 为exfuns申请内存 */
- f_mount(fs[0], "0:", 1); /* 挂载SD卡 */
- f_mount(fs[1], "1:", 1); /* 挂载NOR Flash */
- f_mount(fs[2], "2:", 1); /* 挂载NAND Flash */
-
- /* 初始化SD卡 */
- while (sd_init() != 0)
- {
- lcd_show_string(30, 30, 200, 16, 16, "SD Card Error!", RED);
- delay_ms(500);
- lcd_show_string(30, 30, 200, 16, 16, "Please Check! ", RED);
- delay_ms(500);
- }
- lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
-
- /* 检查字库 */
- while (fonts_init() != 0)
- {
- lcd_show_string(30, 30, 200, 16, 16, "Font Error! ", RED);
- delay_ms(500);
- lcd_show_string(30, 30, 200, 16, 16, "Please Check!", RED);
- delay_ms(500);
- }
- lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
-
- text_show_string(30, 50, 200, 16, "正点原子STM32开发板",16,0, RED);
- text_show_string(30, 70, 200, 16, "SPDIF(光纤音频)实验", 16, 0, RED);
- text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
- text_show_string(30, 110, 200, 16, "KEY_UP:VOL+", 16, 0, RED);
- text_show_string(30, 130, 200, 16, "KEY1:VOL-", 16, 0, RED);
- text_show_string(30, 150, 200, 16, "音量:", 16, 0, RED);
- text_show_string(30, 170, 200, 16, "采样率:", 16, 0, RED);
-
- /* 配置ES8388 */
- vol = 10;
- es8388_hpvol_set(vol);
- es8388_spkvol_set(vol);
- es8388_adda_cfg(1, 0);
- es8388_input_cfg(0);
- es8388_output_cfg(1, 1);
- lcd_show_num(30 + 40, 150, vol, 2, 16, BLUE);
-
- /* 配置SPDIF */
- spdif_rx_dma_init(spdif_audiobuf[0], spdif_audiobuf[1],SPDIF_DBUF_SIZE,2);
- spdif_rx_stop_callback = spdif_rx_stopplay_callback;
- spdif_show_samplerate(0);
-
- while (1)
- {
- key = key_scan(1);
- if ((key == WKUP_PRES) || (key == KEY1_PRES))
- {
- /* 改变音量 */
- if (key == WKUP_PRES)
- {
- if (++vol > 33)
- {
- vol = 33;
- }
- }
- else
- {
- if (vol > 0)
- {
- vol--;
- }
- }
-
- es8388_hpvol_set(vol);
- es8388_spkvol_set(vol);
- lcd_show_num(30 + 40, 150, vol, 2, 16, BLUE);
- }
-
- if (spdif_dev.consta == 0) /* 未连接 */
- {
- if (spdif_rx_waitsync()) /* 等待同步 */
- {
- /* 获取采样率 */
- spdif_dev.samplerate = spdif_rx_getsamplerate();
- if (spdif_dev.samplerate != 0)
- {
- if (spdif_dev.saisync == 0)
- {
- /* 同步SAI与SPDIF时钟 */
- es8388_sai_cfg(0, 0);
- sai1_saib_init(SAI_MODEMASTER_TX,
- SAI_CLOCKSTROBING_RISINGEDGE, SAI_DATASIZE_24);
- sai1_samplerate_set(spdif_dev.samplerate);
- spdif_dev.saisync = 1;
- }
- else
- {
- /* 开始播放 */
- spdif_play_start();
- sai1_tx_dma_init((uint8_t *)&spdif_audiobuf[0],
- (uint8_t *)&spdif_audiobuf[1], SPDIF_DBUF_SIZE, 2);
- sai1_tx_callback = sai_dma_tx_callback;
- sai1_play_start();
- spdif_show_samplerate(spdif_dev.samplerate);
- }
- }
- else
- {
- /* 采样率错误,则停止播放 */
- spdif_play_stop();
- }
- }
- }
-
- if (++t == 20)
- {
- t = 0;
- LED0_TOGGLE();
- }
-
- delay_ms(10);
- }
- }
复制代码 在main函数里面我们初始化ES8388和SPDIFRX等外设,随后在死循环里面等待SPDIF同步完成,在首次同步完成以后,获取音频采样率,然后设置SAI接口,设置SAI的音频采样率,标记SAI与SPDIF时钟同步完成,然后重新同步(SPDIF和光纤信号同步),重新同步完成以后,开启SPDIF和SAI的DMA传输,启动音频播放,此时就可以从喇叭或者耳机听到光纤传输过来的音频信号了。在主循环里面我们还加入了音量调节的代码,可以通过KEY_UP和KEY1调节音量的大小。
61.4 下载验证
在代码编译成功之后,我们下载代码到正点原子STM32H7R7开发板上,先检测字库是否存在,然后进入主循环 ,等待SPDIF信号(光纤信号)的输入,屏幕提示:正在识别…,如图61.4.1所示:
图61.4.1 等待SPDIF信号输入(光纤音频信号)
此时,我们利用光纤线连接开发板的光纤座和光纤音频输出设备(比如贝斯蓝牙音频接收器),然后播放歌曲,就可以看到屏幕显示了音频信号的采样率,然后,通过耳机和板载喇叭就可以欣赏所播放的歌曲了。如图61.4.2所示:
图61.4.2 识别采样率,播放音乐
此时,我们按下KEY_UP按键,可以提高音频,按KEY1按键,可以降低音量。STM32H7R7自带的SPDIFRX功能,可以实现对光纤、同轴等数字音频信号的解码,在HiFi解码方面可以有广泛的应用前景。 |