WAV 即 WAVE 文件, 标准格式化的 WAV 文件和 CD 格式一样,也是 44.1K 的取样频率, 16 位量化数字,因此在声音文件质量和 CD 相差无几!
WAV 一般采用线性 PCM(脉冲编码调制)编码, WAV 是由若干个 Chunk 组成的。
RIFF WAVE Chunk、Format Chunk、 Fact Chunk(可选)和 Data Chunk。 每个 Chunk 由块标识符、数据大小和数据三部分组成,
块标识符由 4 个 ASCII 码构成,数据大小则标出紧跟其后的数据的长度(单位为字节),注意这个长度不包含块标识符和数据大小的长度,即不包含最前面的 8 个字节。所以实际 Chunk的大小为数据大小加 8。
RIFF 块(RIFF WAVE Chunk),该块以“RIFF” 作为标示,紧跟 wav文件大小(该大小是 wav 文件的总大小-8),
然后数据段为“WAVE”,表示是 wav 文件。
//RIFF 块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"RIFF",即 0X46464952
u32 ChunkSize ; //集合大小;文件总大小-8
u32 Format; //格式;WAVE,即 0X45564157
}ChunkRIFF ;
Format 块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!)Format 块的 Chunk 结构如下:
//fmt 块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"fmt ",即 0X20746D66
u32 ChunkSize ; //子集合大小(不包括 ID 和 Size);这里为:20.
u16 AudioFormat; //音频格式;0X10,表示线性 PCM;0X11 表示 IMA ADPCM
u16 NumOfChannels; //通道数量;1,表示单声道;2,表示双声道;
u32 SampleRate; //采样率;0X1F40,表示 8Khz
u32 ByteRate; //字节速率;
u16 BlockAlign; //块对齐(字节);
u16 BitsPerSample; //单个采样数据大小;4 位 ADPCM,设置为 4
}ChunkFMT;
Fact 块(Fact Chunk),该块为可选块,以“fact”作为标示,非 PCM 格式的文件中, 一般会在 Format 结构后面加入一个 Fact 块,该块 Chunk 结构如下:
//fact 块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"fact",即 0X74636166;
u32 ChunkSize ; //子集合大小(不包括 ID 和 Size);这里为:4.
u32 DataFactSize; //数据转换为 PCM 格式后的大小
}ChunkFACT;
我们使用的是 PCM 格式,所以不存在这个块
数据块(Data Chunk),该块是真正保存 wav 数据的地方,以“data”'作为该 Chunk 的标示, 然后是数据的大小。数据块的 Chunk 结构如下:
//data 块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"data",即 0X61746164
u32 ChunkSize ; //子集合大小(不包括 ID 和 Size);文件大小-60.
}ChunkDATA;
ChunkSize 后紧接着就是 wav 数据。低字节在前,高字节在后。在得到这些 wav 数据以后,通过 I2S 丢给 WM8978,
一般使用 WM8978 作为从机,接收 LRC 和 BLCK。
飞利浦( I2S)标准模式,数据在跟随 LRC 传输的 BCLK 的第二个上升沿时传输 MSB,其他位一直到 LSB 按顺序传输。
fs 即音频信号的采样率,比如 44.1Khz,LRC 的频率就是音频信号的采样率。
WM8978 还需要一个 MCLK,本章我们采用 STM32F4 为其提供 MCLK 时钟, MCLK的频率必须等于 256fs,也就是音频采样率的 256 倍。
我们通过 IIC 接口(MODE=0)连接 WM8978,用 WM8978 播放音乐时的设置,对各个寄存器进行相应的配置,即可使用 WM8978 正常播放音乐了。
STM32F4 自带了 2 个全双工 I2S 接口,STM32F4 的 I2S 是与 SPI 部分共用的,通过设置 SPI_I2SCFGR 寄存器的 I2SMOD 位即可开启 I2S 功能, I2S 接口使用了几乎与 SPI 相同的引脚、标志和中断。
I2S 用到的信号有:
1) SD:串行数据(映射到 MOSI 引脚),用于发送或接收两个时分复用的数据通道上的数据
2)WS:字选择(映射到 NSS 引脚) ,即帧时钟, 用于切换左右声道的数据。 WS 频率等于音频信号采样率(fs) 。
3)CK:串行时钟(映射到 SCK 引脚) ,即位时钟,是主模式下的串行时钟输出以及从模式下的串行时钟输入。 CK 频率=WS 频率(fs) 216(16 位宽),
4)I2S2ext_SD 和 I2S3ext_SD:用于控制 I2S 全双工模式的附加引脚(映射到 MISO 引脚)。为支持 I2S 全双工模式,除了 I2S2 和 I2S3,还可以使用两个额外的 I2S,它们称为扩展 I2S(I2S2_ext、 I2S3_ext),
5)MCK:即主时钟输出, 当 I2S 配置为主模式(并且 SPI_I2SPR 寄存器中的 MCKOE 位置 1)时,使用此时钟,该时钟输出频率 256× fs, fs 即音频信号采样频率(fs) 。
I2Sx 可以在主模式下工作。
只有 I2Sx 可在半双工模式下输出 SCK 和 WS,只有 I2Sx 可在全双工模式下向 I2S2_ext 和 I2S3_ext 提供 SCK 和 WS。扩展 I2S (I2Sx_ext)只能用于全双工模式。 I2Sx_ext 始终在从模式下工作。
对于所有数据格式和通信标准而言,始终会先发送最高有效位(MSB 优先)。
I2S 飞利浦标准, 使用 WS 信号来指示当前正在发送的数据所属的通道。该信号从当前通道数据的第一个位(MSB)之前的一个时钟开始有效。发送方在时钟信号(CK)的下降沿改变数据,接收方在上升沿读取数据。 WS 信号也在 CK 的下降沿变化。
我们使用 16 位/24 位数据格式, 16 位时采用扩展帧格式(即将 16 位数据封装在 32 位帧中),
在 24 位模式下数据传输,需要对 SPI_DR 执行两次读取或写入操作。比如我们要发送 0X8EAA33 这个数据,就要分两次写入 SPI_DR,第一次写入:0X8EAA,第二次写入 0X33xx(xx 可以为任意数值),这样就把 0X8EAA33 发送出去了。
SD 卡读取到的 24 位 WAV 数据流,是低字节在前,高字节在后的,
一个声道的数据(24bit),存储在 buf[3]里面,通过 SPI_DR 发送这个 24 位数据,
过程如下:
SPI_DR=((u16)buf[2]<<8)+buf[1];
SPI_DR=(u16)buf[0]<<8;
第一次发送高 16 位数据,第二次发送低 8 位数据,完成一次 24bit 数据的发送。
I2SxCLK 可以来自 PLLI2S 输出(通过 R 系数分频)或者来自外部时钟(I2S_CKIN引脚),一般我们使用PLLI2S作为 I2SxCLK 输入时钟。
需要根据音频采样率(fs,即 CK 的频率)来计算各个分频器的值,常用的音频采样率有: 22.05Khz、 44.1Khz、 48Khz、 96Khz、 196Khz 等。
当 MCK 输出使能时, fs 频率计算公式如下:
fs=I2SxCLK/[256*(2*I2SDIV+ODD)]
其中:
I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR
HSE 我们是 8Mhz,而 pllm 在系统时钟初始化就确定了,是 8,这样结合以上 2 式,可得计算公式如下:
fs= (1000*PLLI2SN/PLLI2SR )/[256*(2*I2SDIV+ODD)]
很多时候,并不能取得和 fs 一模一样的频率,只能近似等于 fs,比如 44.1Khz采样率,
我们设置 PLL2SN=271, PLL2SR=2, I2SDIV=6, ODD=0,得到 fs=44.108073Khz,
我们事先计算好常用 fs 对应的系数值,建立一个表,这样,用的时候,只需要查表取值就可以了,大大简化了代码,常用
fs 对应系数表如下:
//表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD
const u16 I2S_PSC_TBL[][5]=
{
{800 ,256,5,12,1}, //8Khz 采样率
{1102,429,4,19,0}, //11.025Khz 采样率
{1600,213,2,13,0}, //16Khz 采样率
{2205,429,4, 9,1}, //22.05Khz 采样率
{3200,213,2, 6,1}, //32Khz 采样率
{4410,271,2, 6,0}, //44.1Khz 采样率
{4800,258,3, 3,1}, //48Khz 采样率
{8820,316,2, 3,1}, //88.2Khz 采样率
{9600,344,2, 3,1}, //96Khz 采样率
{17640,361,2,2,0}, //176.4Khz 采样率
{19200,393,2,2,0}, //192Khz 采样率
};
+++++++++++++++++++++++++++++++++++++++++
SPI_I2S 配置寄存器: SPI_I2SCFGR,
I2SMOD 位,设置为 1,选择 I2S 模式,注意,必须在 I2S/SPI 禁止的时候,设置该位。
I2SE 位,设置为 1,使能 I2S 外设,该位必须在 I2SMOD 位设置之后再设置。
I2SCFG[1:0]位, 这两个位用于配置 I2S 模式,设置为 10,选择主模式(发送)。
I2SSTD[1:0]位,这两个位用于选择 I2S 标准,设置为 00,选择飞利浦模式。
CKPOL 位,用于设置空闲时时钟电平,设置为 0,空闲时时钟低电平。
DATLEN[1:0]位,用于设置数据长度, 00,表示 16 位数据; 01 表示 24 位数据。
CHLEN 位,用于设置通道长度,即帧长度, 0,表示 16 位; 1,表示 32 位。
SPI_I2S 预分配器寄存器: SPI_I2SPR,
MCKOE 为 1,开启 MCK 输出,
ODD 和 I2SDIV 则根据不同的 fs,查表进行设置。
PLLI2S 配置寄存器: RCC_PLLI2SCFGR,
该寄存器用于配置 PLLI2SR 和 PLLI2SN 两个系数, PLLI2SR 的取值范围是: 2~7, PLLI2SN的取值范围是: 192~432。同样,这两个也是根据 fs 的值来设置的。
SPI_CR2 寄存器的 bit1 位,设置 I2S TX DMA 数据传输, SPI_DR 寄存器用于传输数据,我们用 DMA 来传输,所以直接设置 DMA 的外设地址为SPI_DR的基地址 即可。
++++++++++++++++++++++++++++++++++++++
I2S 相关的库函数申明和定义在 stm32f4xx_hal_i2s.c/ stm32f4xx_hal_i2s_ex.c 以及头文件 stm32f4xx_hal_i2s.h/ stm32f4xx_hal_i2s_ex.h 中。
初始化 I2S
主要设置 SPI_I2SCFGR 寄存器,设置 I2S 模式、 I2S 标准、时钟空闲电平和数据帧长等,最后开启 I2S TX DMA,使能 I2S 外设。
在库函数中初始化 I2S 调用的函数为
HAL_StatusTypeDef HAL_I2S_Init(I2S_HandleTypeDef *hi2s)
结构体 I2S_HandleTypeDef的定义为:
typedef struct __I2S_HandleTypeDef
{
SPI_TypeDef *Instance;
I2S_InitTypeDef Init;
uint16_t *pTxBuffPtr;
__IO uint16_t TxXferSize;
__IO uint16_t TxXferCount;
uint16_t *pRxBuffPtr;
__IO uint16_t RxXferSize;
__IO uint16_t RxXferCount;
void (*IrqHandlerISR)(struct __I2S_HandleTypeDef *hi2s);
DMA_HandleTypeDef *hdmatx;
DMA_HandleTypeDef *hdmarx;
__IO HAL_LockTypeDef Lock;
__IO HAL_I2S_StateTypeDef State;
__IO uint32_t ErrorCode;
} I2S_HandleTypeDef;
初始化了I2S,就需要启动I2S。
__HAL_I2S_ENABLE(&I2S2_Handler); //使能 I2S2
I2S2,其 TX 是使用的 DMA1 数据流 4 的通道 0 来传输的。 DMA1 数据流 4 设置为:
双缓冲循环模式,外设和存储器都是 16 位宽,并开启 DMA 传输完成中断(方便填充数据)。
使用 DMA 传输完成中断,每当一个缓冲数据发送完后,硬件自动切换为下一个缓冲,同时进入中断服务函数,填充数据到发送完的这个缓冲。
开启 DMA 传输,然后及时填充 WAV 数据到 DMA 的两个缓存区即可。
__HAL_DMA_ENABLE(&I2S2_TXDMA_Handler);//开启 DMA TX 传输
++++++++++++++++++++++++++++++++++++++++++++++++++
实战,
在cubemx中,配置I2S2。
设置为half duplexer master,
勾选mclk output,
设置mode为 master transmit,
设置communication standard 为 philips,
设置data and format 为 16bits on 32bits frame,
设置audio freq 为44k,
设置clock polarity 为low,
设置NVIC spi interrupt为 enable,
设置DMA,
add一个spi2-tx的request,
设置stream为 dma1_stream4,
设置directionwei mem to peri,
设置priority为very high,
设置mode 为 circular,
不勾选use fifo,
设置inc addr为mem,
设置data width为 half word,
设置burst size 为 single,
++++++++++++++++++++++++++++++++++++++++++++++++++++
HAL库提供了基于DMA的传输函数,
HAL_StatusTypeDef HAL_I2S_Transmit_DMA(I2S_HandleTypeDef *hi2s, uint16_t *pData, uint16_t Size);
我们需要覆盖定义弱函数
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s);
当DMA传输完成时,发送下一个需要传输的数据。
++++++++++++++++++++++++++++++++++++++++++++++++++
我们需要自建实用库,用来初始化配置WM8978。
配置WM8978,需要使用I2C总线来配置,我们需要首先自建I2C的实用库。用GPIO来模拟I2C。
在头文件中定义相关的宏。
#define SDA_IN() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2;} //PB9输入模式
#define SDA_OUT() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2;} //PB9输出模式
//IO操作
#define IIC_SCL PBout(8) //SCL
#define IIC_SDA PBout(9) //SDA
#define READ_SDA PBin(9) //输入SDA
在C文件中,定义需要的函数。
//IIC初始化
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB时钟
//PH4,5初始化设置
GPIO_Initure.Pin=GPIO_PIN_8|GPIO_PIN_9;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST; //快速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
IIC_SDA=1;
IIC_SCL=1;
}
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;//START:when CLK is high,DATA change form high to low
delay_us(4);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
IIC_SDA=1;//发送I2C总线结束信号
delay_us(4);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出0
return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
+++++++++++++++++++++++++++++++++++++++
我们需要自建实用库,用来控制I2S。
//采样率计算公式:Fs=I2SxCLK/[256*(2*I2SDIV+ODD)]
//I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR
//一般HSE=8Mhz
//pllm:在Sys_Clock_Set设置的时候确定,一般是8
//PLLI2SN:一般是192~432
//PLLI2SR:2~7
//I2SDIV:2~255
//ODD:0/1
//I2S分频系数表@pllm=8,HSE=8Mhz,即vco输入频率为1Mhz
//表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD
const u16 I2S_PSC_TBL[][5]=
{
{800 ,256,5,12,1}, //8Khz采样率
{1102,429,4,19,0}, //11.025Khz采样率
{1600,213,2,13,0}, //16Khz采样率
{2205,429,4, 9,1}, //22.05Khz采样率
{3200,213,2, 6,1}, //32Khz采样率
{4410,271,2, 6,0}, //44.1Khz采样率
{4800,258,3, 3,1}, //48Khz采样率
{8820,316,2, 3,1}, //88.2Khz采样率
{9600,344,2, 3,1}, //96Khz采样率
{17640,361,2,2,0}, //176.4Khz采样率
{19200,393,2,2,0}, //192Khz采样率
};
//设置SAIA的采样率(@MCKEN)
//samplerate:采样率,单位:Hz
//返回值:0,设置成功;1,无法设置.
u8 I2S2_SampleRate_Set(u32 samplerate)
{
u8 i=0;
u32 tempreg=0;
RCC_PeriphCLKInitTypeDef RCCI2S2_ClkInitSture;
for(i=0;i<(sizeof(I2S_PSC_TBL)/10);i++)//看看改采样率是否可以支持
{
if((samplerate/10)==I2S_PSC_TBL[i][0])break;
}
if(i==(sizeof(I2S_PSC_TBL)/10))return 1;//搜遍了也找不到
RCCI2S2_ClkInitSture.PeriphClockSelection=RCC_PERIPHCLK_I2S; //外设时钟源选择
RCCI2S2_ClkInitSture.PLLI2S.PLLI2SN=(u32)I2S_PSC_TBL[i][1]; //设置PLLI2SN
RCCI2S2_ClkInitSture.PLLI2S.PLLI2SR=(u32)I2S_PSC_TBL[i][2]; //设置PLLI2SR
HAL_RCCEx_PeriphCLKConfig(&RCCI2S2_ClkInitSture); //设置时钟
RCC->CR|=1<<26; //开启I2S时钟
while((RCC->CR&1<<27)==0); //等待I2S时钟开启成功.
tempreg=I2S_PSC_TBL[i][3]<<0; //设置I2SDIV
tempreg|=I2S_PSC_TBL[i][4]<<8; //设置ODD位
tempreg|=1<<9; //使能MCKOE位,输出MCK
SPI2->I2SPR=tempreg; //设置I2SPR寄存器
return 0;
}
+++++++++++++++++++++++++++++++++++++
我们需要自建实用库,用来解析wav文件。
在H文件中,定义所需要的结构体。
#define WAV_I2S_TX_DMA_BUFSIZE 8192 //定义WAV TX DMA 数组大小(播放192Kbps@24bit的时候,需要设置8192大才不会卡)
//RIFF块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"RIFF",即0X46464952
u32 ChunkSize ; //集合大小;文件总大小-8
u32 Format; //格式;WAVE,即0X45564157
}ChunkRIFF ;
//fmt块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"fmt ",即0X20746D66
u32 ChunkSize ; //子集合大小(不包括ID和Size);这里为:20.
u16 AudioFormat; //音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM
u16 NumOfChannels; //通道数量;1,表示单声道;2,表示双声道;
u32 SampleRate; //采样率;0X1F40,表示8Khz
u32 ByteRate; //字节速率;
u16 BlockAlign; //块对齐(字节);
u16 BitsPerSample; //单个采样数据大小;4位ADPCM,设置为4
// u16 ByteExtraData; //附加的数据字节;2个; 线性PCM,没有这个参数
}ChunkFMT;
//fact块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"fact",即0X74636166;
u32 ChunkSize ; //子集合大小(不包括ID和Size);这里为:4.
u32 NumOfSamples; //采样的数量;
}ChunkFACT;
//LIST块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"LIST",即0X74636166;
u32 ChunkSize ; //子集合大小(不包括ID和Size);这里为:4.
}ChunkLIST;
//data块
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"data",即0X5453494C
u32 ChunkSize ; //子集合大小(不包括ID和Size)
}ChunkDATA;
//wav头
typedef __packed struct
{
ChunkRIFF riff; //riff块
ChunkFMT fmt; //fmt块
// ChunkFACT fact; //fact块 线性PCM,没有这个结构体
ChunkDATA data; //data块
}__WaveHeader;
//wav 播放控制结构体
typedef __packed struct
{
u16 audioformat; //音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM
u16 nchannels; //通道数量;1,表示单声道;2,表示双声道;
u16 blockalign; //块对齐(字节);
u32 datasize; //WAV数据大小
u32 totsec ; //整首歌时长,单位:秒
u32 cursec ; //当前播放时长
u32 bitrate; //比特率(位速)
u32 samplerate; //采样率
u16 bps; //位数,比如16bit,24bit,32bit
u32 datastart; //数据帧开始的位置(在文件里面的偏移)
}__wavctrl;
//音乐播放控制器
typedef __packed struct
{
//2个I2S解码的BUF
u8 *i2sbuf1;
u8 *i2sbuf2;
u8 *tbuf; //零时数组,仅在24bit解码的时候需要用到
FIL *file; //音频文件指针
u8 status; //bit0:0,暂停播放;1,继续播放
//bit1:0,结束播放;1,开启播放
}__audiodev;
这里,我们使用了attribute-pack属性,将结构体紧凑排布,中间不留PAD。