发 帖  
原厂入驻New
「ALIENTEK 阿波罗 STM32F767 开发板资料连载」第五十三章 录音机实验
4 天前  147 STM32
分享
本帖最后由 正点原子运营官 于 2020-5-22 14:42 编辑

1)实验平台:alientek 阿波罗 STM32F767 开发板
2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子




第五十三章 录音机实验
上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,实现一个简
单的录音机,实现 WAV 录音。本章分为如下几个部:
53.1 SAI 录音简介
53.2 硬件设计
53.3 软件设计
53.4 下载验证
53.1 SAI 录音简介
本章涉及的知识点基本上在上一章都有介绍。本章要实现 WAV 录音,还是和上一章一样,
要了解:WAV 文件格式、wm8978 和 SAI 接口。WAV 文件格式,我们在上一章已经做了详细
介绍了,这里就不作介绍了。
ALIENTEK 阿波罗 STM32F767 开发板将板载的一个 MIC 分别接入到了 WM8978 的 2 个
差分输入通道(LIP/LIN 和 RIP/RIN,原理图见:图 52.2.1)。代码上,我们采用立体声 WAV
录音,不过,左右声道的音源都是一样的,录音出来的 WAV 文件,听起来就是个单声道效果。
WM8978 上一章也做了比较详细的介绍,本章我们主要看一下要进行 MIC 录音,WM8978
的配置步骤:
1,寄存器 R0(00h),该寄存器用于控制 WM8978 的软复位,写任意值到该寄存器地址,
即可实现软复位 WM8978。
2,寄存器 R1(01h),该寄存器主要要设置 MICBEN(bit4)和 BIASEN(bit3)两个位为 1,
开启麦克风(MIC)偏置,以及使能模拟部分放大器。
3,寄存器 R2(02h),该寄存器要设置 SLEEP(bit6)、INPGAENR(bit3)、INPGAENL(bit2)、
ADCENR(bit1)和ADCENL(bit0)等五个位。SLEEP设置为0,进入正常工作模式;INPGAENR
和 INPGAENL 设置为 1,使能 IP PGA 放大器;ADCENL 和 ADCENR 设置为 1,使能左
右通道 ADC。
4,寄存器 R4(04h),该寄存器要设置 WL(bit6:5)和 FMT(bit4:3)等 4 个位。WL(bit6:5)用
于设置字长(即设置音频数据有效位数),00 表示 16 位音频,10 表示 24 位音频;FMT(bit4:3)
用于设置 I2S 音频数据格式(模式),我们一般设置为 10,表示 I2S 格式,即飞利浦模式。
5,寄存器 R6(06h),该寄存器我们直接全部设置为 0 即可,设置 MCLK 和 BCLK 都来
自外部,即由 STM32F767 提供。
6,寄存器 R14(0Eh),该寄存器要设置 ADCOSR128(bit3)为 1,ADC 得到最好的 SNR。
7,寄存器 R44(2Ch),该寄存器我们要设置 LIP2INPPGA(bit0)、LIN2INPPGA(bit1)、
RIP2INPPGA(bit4)和 RIN2INPPGA(bit5)等 4 个位,将这 4 个位都设置为 1,将左右通道差
分输入接入 IN PGA。
8,寄存器 R45(2Dh)和 R46(2Eh),这两个寄存器用于设置 PGA 增益(调节麦克风增
益),一个用于设置左通道(R45),另外一个用于设置右通道(R46)。这两个寄存器的
最高位(INPPGAUPDATE)用于设置是否更新左右通道的增益,最低 6 位用于设置左右
通道的增益,我们可以先设置好两个寄存器的增益,最后设置其中一个寄存器最高位为 1,
即可更新增益设置。
9,寄存器 R47(2Fh)和 R48(30h),这两个寄存器也类似,我们只关心其最高位(bit8),
都设置为 1,可以让左右通道的 MIC 各获得 20dB 的增益。
10,寄存器 R49(31h),该寄存器我们要设置 TSDEN(bit1)这个位,设置为 1,开启过热
保护。
以上,就是我们用 WM8978 录音时的设置,按照以上所述,对各个寄存器进行相应的配置,
即可使用 WM8978 正常录音了。不过我们本章还要用到播放录音的功能,WM8978 的播放配置
在 50.1.2 节已经介绍过了,请大家参考这个章节。
上一章我们向大家介绍了 STM32F767 的 SAI 放音,通过上一章的了解,我们知道:
STM32F767 SAI 的全双工通信,需要用到 SAI 的两个子模块(SAI_A 和 SAI_B)),一个工作
在主模式,产生 FS、SCK 和 MCLK,一个工作在从模式,通过 SD 引脚接收数据。
本章我们必须向 WM8978 提供 WS(FS),CK(SCK)和 MCK(MCLK)等时钟,同时又要录音,
所以只能使用全双工模式。工作在主模式的 SAI 子模块循环发送数据 0X0000,给 WM8978,
以产生 CK、WS 和 MCK 等信号,工作在从模式的 SAI 子模块,则接收来自 WM8978 的 ADC
数据(ADCDAT),并保存到 SD 卡,实现录音。
本章我们将同时使用 SAI 的两个子模块,以实现录音功能,SAI 的相关寄存器,我们在上
一章已经介绍的差不多了,这里就不再进行寄存器介绍,大家可以参考《STM32F7 中文参考手
册.pdf》第 33.5 小节。
要实现录音功能,我们根据上一章,图 50.2.1 的连接关系可知,SAI_A 子模块必须工作在
主模式,循环发送 0X0000,以提供 FS、SCK 和 MCLK 等时钟信号,SAI_B 子模块则工作在从
模式,读取 ADCDAT 输出的数据流(SAI_SD_B),从而实现录音功能。
最后,我们看看要通过 STM32F767 的 SAI,驱动 WM8978 实现 WAV 录音的简要步骤,
如下:
1)初始化 WM8978
这个过程就是前面所讲的 WM8978 MIC 录音配置步骤,让 WM8978 的 ADC 以及其模拟部
分工作起来。
2)初始化 SAI_A 和 SAI_B
本章要用到 SAI 的全双工模式,所以,SAI_A 和 SAI_B 都需要配置,其中 SAI_A 配置为
主模式,SAI 设置为从模式,且与 SAI_A 同步。他们的其他配置(协议、时钟电平特性、slot
相关参数)基本一样,只是一个是发送一个是接收,且都要使能 DMA。同时,还需要设置音
采样率,不过这个只需要设置 SAI_A 的即可,还是通过上一章介绍的查表法设置。
3)设置发送和接收 DMA
放音和录音都是采用 DMA 传输数据的,本章放音其实就是个幌子,不过也得设置 DMA
(使用 DMA2 数据流 3 的通道 0),配置同上一章一模一样,不过不需要开启 DMA 传输完成
中断。对于录音,则使用的是 DMA2 数据流 5 的通道 0 实现的 DMA 数据接收,我们需要配置
DMA2 的数据流 5,本章将 DMA2 数据流 5 设置为:双缓冲循环模式,外设和存储器都是 16
位宽,并开启传输完成中断(方便接收数据)。
4)编写接收通道 DMA 传输完成中断服务函数
为了方便接收音频数据,我们使用 DMA 传输完成中断,每当一个缓冲接数据满了,硬件
自动切换为下一个缓冲,同时进入中断服务函数,将已满缓冲的数据写入 SD 卡的 wav 文件。
过程如图 53.1.1 所示:


图 53.1.1 DMA 双缓冲接收音频数据流框图
5)创建 WAV 文件,并保存 wav 头
前面 4 步完成,其实就可以开始读取音频数据了,不过在录音之前,我们需要先在创建一
个新的文件,并写入 wav 头,然后才能开始写入我们读取到的的 PCM 音频数据。
6)开启 DMA 传输,接收数据
然后,我们就只需要开启 DMA 传输,然后及时将 SAI_SD_B 读到的数据写入到 SD 卡之
前新建的 wav 文件里面,就可以实现录音了。
7)计算整个文件大小,重新保存 wav 头并关闭文件
在结束录音的时候,我们必须知道本次录音的大小(数据大小和整个文件大小),然后更新
wav 头,重新写入文件,最后因为 fatfs,在文件创建之后,必须调用 f_close,文件才会真正
体现在文件系统里面,否则是不会写入的!所以最后还需要调用 f_close,以保存文件。
53.2 硬件设计
本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,
再检测 SD 卡根目录是否存在 RECORDER 文件夹,如果不存在则创建,如果创建失败,则报
错。在找到 SD 卡的 RECORDER 文件夹后,即进入录音模式(包括配置 WM8978 和 SAI 等),
此时可以在耳机(或喇叭)听到采集到的音频。KEY0 用于开始/暂停录音,KEY2 用于保存并
停止录音,KEY_UP 用于播放最近一次的录音。
当我们按下 KEY0 的时候,可以在屏幕上看到录音文件的名字、码率以及录音时间等,然
后通过 KEY2 可以保存该文件,同时停止录音(文件名和时间也都将清零),在完成一段录音
后,我们可以通过按 KEY_UP 按键,来试听刚刚的录音。DS0 用于提示程序正在运行,DS1
用于提示是否处于暂停录音状态。
本实验用到的资源如下:
1) 指示灯 DS0,DS1
2) 三个按键(KEY_UP/KEY0/KEY2)
3) 串口
4) LCD 模块
5) SD 卡
6) SPI FLASH
7) WM8978
8) SAI
这些前面都已介绍过。本实验,大家需要准备 1 个 SD 卡和一个耳机,分别插入 SD 卡接
口和耳机接口(PHONE),然后下载本实验就可以实现一个简单的录音机了。
53.3 软件设计
打开本章实验工程可以看到我们在 APP 分组下新增了 recorder.c 文件,用来存放录音相关
源码。因为 recorder.c 代码比较多,我们这里仅介绍其中几个重要的函数,代码如下:
u8 *sairecbuf1; //SAI1 DMA 接收 BUF1
u8 *sairecbuf2; //SAI1 DMA 接收 BUF2
//REC 录音 FIFO 管理参数.
//由于 FATFS 文件写入时间的不确定性,如果直接在接收中断里面写文件,可能导致某次写
//入时间过长从而引起数据丢失,故加入 FIFO 控制,以解决此问题.
vu8 sairecfifordpos=0; //FIFO 读位置
vu8 sairecfifowrpos=0; //FIFO 写位置
u8 *sairecfifobuf[SAI_RX_FIFO_SIZE]; //定义 10 个录音接收 FIFO
FIL* f_rec=0; //录音文件
u32 wavsize; //wav 数据大小(字节数,不包括文件头!!)
u8 rec_sta=0; //录音状态
//[7]:0,没有开启录音;1,已经开启录音;
//[6:1]:保留
//[0]:0,正在录音;1,暂停录音;
//读取录音 FIFO
//buf:数据缓存区首地址
//返回值:0,没有数据可读;
// 1,读到了 1 个数据块
u8 rec_sai_fifo_read(u8 **buf)
{
if(sairecfifordpos==sairecfifowrpos)return 0;
sairecfifordpos++; //读位置加 1
if(sairecfifordpos>=SAI_RX_FIFO_SIZE)sairecfifordpos=0;//归零
*buf=sairecfifobuf[sairecfifordpos];
return 1;
}
//写一个录音 FIFO
//buf:数据缓存区首地址
//返回值:0,写入成功;
// 1,写入失败
u8 rec_sai_fifo_write(u8 *buf)
{
u16 i;
u8 temp=sairecfifowrpos;//记录当前写位置
sairecfifowrpos++; //写位置加 1
if(sairecfifowrpos>=SAI_RX_FIFO_SIZE)sairecfifowrpos=0;//归零
if(sairecfifordpos==sairecfifowrpos)
{
sairecfifowrpos=temp;//还原原来的写位置,此次写入失败
  return 1;
}
for(i=0;i<SAI_RX_DMA_BUF_SIZE;i++)sairecfifobuf[sairecfifowrpos]=buf;//拷贝
return 0;
}
//录音 SAI_DMA 接收中断服务函数.在中断里面写入数据
void rec_sai_dma_rx_callback(void)
{
if(rec_sta==0X80)//录音模式
{
if(DMA2_Stream5->CR&(1<<19))rec_sai_fifo_write(sairecbuf1);//sairecbuf1 写 FIFO
else rec_sai_fifo_write(sairecbuf2);//sairecbuf2 写入 FIFO
}
}
const u16 saiplaybuf[2]={0X0000,0X0000};//2 个数据,用于录音时 SAI_A 主机循环发送 0.
//进入 PCM 录音模式
void recoder_enter_rec_mode(void)
{
WM8978_ADDA_Cfg(0,1); //开启 ADC
WM8978_Input_Cfg(1,1,0); //开启输入通道(MIC&LINE IN)
WM8978_Output_Cfg(0,1); //开启 BYPASS 输出
WM8978_MIC_Gain(46); //MIC 增益设置
WM8978_SPKvol_Set(0); //关闭喇叭.
WM8978_I2S_Cfg(2,0); //飞利浦标准,16 位数据长度
SAIA_Init(SAI_modemASTER_TX,SAI_clockSTROBING_RISINGEDGE,
SAI_DATASIZE_16); //SAI1 Block A,主发送,16 位数据
SAIB_Init(SAI_MODESLAVE_RX,SAI_CLOCKSTROBING_RISINGEDGE,
SAI_DATASIZE_16);//SAI1 Block B 从模式接收,16 位
SAIA_SampleRate_Set(REC_SAMPLERATE);//设置采样率
SAIA_TX_DMA_Init((u8*)&saiplaybuf[0],(u8*)&saiplaybuf[1],1,1);//TX DMA,16 位
__HAL_DMA_DISABLE_IT(&SAI1_TXDMA_Handler,DMA_IT_TC);
//关闭传输完成中断(这里不用中断送数据)
SAIA_RX_DMA_Init(sairecbuf1,sairecbuf2,SAI_RX_DMA_BUF_SIZE/2,1);
//配置 RX DMA
sai_rx_callback=rec_sai_dma_rx_callback;//初始化回调函数指 sai_rx_callback
SAI_Play_Start(); //开始 SAI 数据发送(主机)
SAI_Rec_Start(); //开始 SAI 数据接收(从机)
recoder_remindmsg_show(0); }
//初始化 WAV 头.
void recoder_wav_init(__WaveHeader* wavhead) //初始化 WAV 头
{
wavhead->riff.ChunkID=0X46464952; //"RIFF"
wavhead->riff.ChunkSize=0; //还未确定,最后需要计算wavhead->riff.Format=0X45564157; //"WAVE"
wavhead->fmt.ChunkID=0X20746D66; //"fmt "
wavhead->fmt.ChunkSize=16; //大小为 16 个字节
wavhead->fmt.audioFormat=0X01; //0X01,表示 PCM;0X01,表示 IMA ADPCM
wavhead->fmt.NumOfChannels=2; //双声道
wavhead->fmt.SampleRate=REC_SAMPLERATE;//设置采样速率
wavhead->fmt.ByteRate=wavhead->fmt.SampleRate*4;//采样率*通道数*(ADC 位数/8)
wavhead->fmt.BlockAlign=4; //块大小=通道数*(ADC 位数/8)
wavhead->fmt.BitsPerSample=16; //16 位 PCM
wavhead->data.ChunkID=0X61746164; //"data"
wavhead->data.ChunkSize=0; //数据大小,还需要计算
}
//WAV 录音
void wav_recorder(void)
{
u8 res,i; u8 key; u8 rval=0;
__WaveHeader *wavhead=0;
DIR recdir; //目录
u8 *pname=0; u8 *pdatabuf;
u8 timecnt=0; //计时
u32 recsec=0; //录音时间
while(f_opendir(&recdir,"0:/RECORDER"))//打开录音文件夹
{
Show_Str(30,230,240,16,"RECORDER 文件夹错误!",16,0); delay_ms(200);
LCD_Fill(30,230,240,246,WHITE); delay_ms(200); //清除显示
f_mkdir("0:/RECORDER"); //尝试创建该目录
}
sairecbuf1=mymalloc(SRAMIN,SAI_RX_DMA_BUF_SIZE); //SAI 录音内存 1 申请
sairecbuf2=mymalloc(SRAMIN,SAI_RX_DMA_BUF_SIZE); //SAI 录音内存 2 申请
for(i=0;i<SAI_RX_FIFO_SIZE;i++)
{
sairecfifobuf=mymalloc(SRAMIN,SAI_RX_DMA_BUF_SIZE);//FIFO 内存申请
if(sairecfifobuf==NULL)break; //申请失败
}
f_rec=(FIL *)mymalloc(SRAMIN,sizeof(FIL)); //开辟 FIL 字节的内存区域
wavhead=(__WaveHeader*)mymalloc(SRAMIN,sizeof(__WaveHeader));//申请内存
pname=mymalloc(SRAMIN,30);//申请30字节内存,类似"0:RECORDER/REC00001.wav"
if(!sairecbuf1||!sairecbuf2||!f_rec||!wavhead||!pname||i!=SAI_RX_FIFO_SIZE)rval=1;
if(rval==0)
{
recoder_enter_rec_mode(); //进入录音模式,此时耳机可以听到咪头采集到的音频
pname[0]=0; //pname 没有任何文件名
while(rval==0){
key=KEY_Scan(0);
switch(key)
{
case KEY2_PRES: //STOP&SAVE
if(rec_sta&0X80)//有录音
{
rec_sta=0; //关闭录音
wavhead->riff.ChunkSize=wavsize+36; //整个文件的大小-8;
wavhead->data.ChunkSize=wavsize; //数据大小
f_lseek(f_rec,0); //偏移到文件头.
f_write(f_rec,(const void*)wavhead,sizeof(__WaveHeader),&bw);
f_close(f_rec);
wavsize=0;
sairecfifordpos=0; //FIFO 读写位置重新归零
sairecfifowrpos=0;
}
rec_sta=0; recsec=0;
LED1(1); //关闭 DS1
LCD_Fill(30,190,lcddev.width-1,lcddev.height-1,WHITE);//清除显示
break;
case KEY0_PRES: //REC/PAUSE
if(rec_sta&0X01) rec_sta&=0XFE;//原来是暂停,取消暂停,继续录音
else if(rec_sta&0X80) rec_sta|=0X01;//已经在录音了,则暂停
else //还没开始录音
{
recsec=0;
recoder_new_pathname(pname); //得到新的名字
Show_Str(30,190,lcddev.width,16,"录制:",16,0);
Show_Str(30+40,190,lcddev.width,16,pname+11,16,0);//显示名字
recoder_wav_init(wavhead); //初始化 wav 数据
res=f_open(f_rec,(const TCHAR*)pname,
FA_CREATE_ALWAYS | FA_WRITE);
if(res) //文件创建失败
{
rec_sta=0; //创建文件失败,不能录音
rval=0XFE; //提示是否存在 SD 卡
}else
{
res=f_write(f_rec,(const void*)wavhead,
sizeof(__WaveHeader),&bw);//写入头数据
recoder_msg_show(0,0);
rec_sta|=0X80; //开始录音}
}
if(rec_sta==0X80)LED1(0);//提示正在暂停
else LED1(1);
break;
case WKUP_PRES: //播放最近一段录音
if(rec_sta!=0X80)//没有在录音
{
if(pname[0])//如果触摸按键被按下,且 pname 不为空
{
Show_Str(30,190,lcddev.width,16,"播放:",16,0);
Show_Str(30+40,190,lcddev.width,16,pname+11,16,0);//显示
LCD_Fill(30,210,lcddev.width-1,230,WHITE);
recoder_enter_play_mode(); //进入播放模式
audio_play_song(pname); //播放 pname
LCD_Fill(30,190,lcddev.width-1,lcddev.height-1,WHITE);
recoder_enter_rec_mode(); //重新进入录音模式
} }
break;
}
if(rec_sai_fifo_read(&pdatabuf))//读取一次数据,读到数据了,写入文件
{
res=f_write(f_rec,pdatabuf,SAI_RX_DMA_BUF_SIZE,(UINT*)&bw);//写
if(res)printf("write error:%d\r\n",res);
wavsize+=SAI_RX_DMA_BUF_SIZE;
}else delay_ms(5);
timecnt++;
if((timecnt%20)==0)LED0_Toggle; //DS0 闪烁
if(recsec!=(wavsize/wavhead->fmt.ByteRate)) //录音时间显示
{
LED0_Toggle;//DS0 闪烁
recsec=wavsize/wavhead->fmt.ByteRate; //录音时间
recoder_msg_show(recsec,wavhead->fmt.SampleRate*wavhead->
fmt.NumOfChannels*wavhead->fmt.BitsPerSample);//显示码率
} }
}
myfree(SRAMIN,sairecbuf1); //释放内存
myfree(SRAMIN,sairecbuf2); //释放内存
for(i=0;i<SAI_RX_FIFO_SIZE;i++)myfree(SRAMIN,sairecfifobuf);//FIFO 内存释放
myfree(SRAMIN,f_rec); //释放内存
myfree(SRAMIN,wavhead); //释放内存myfree(SRAMIN,pname); //释放内存
}这里总共 6 个函数,接下来,我们分别介绍。
1,rec_sai_fifo_read 和 rec_sai_fifo_write 函数
这两个函数用于我们构建的 FIFO 里面的数据读取和写入,SAI 采集到的数据,通过 FATFS
写入 SD 卡的时候,因为 FATFS 写入时间不确定(有时候短,有时候长),可能导致数据写入
不及时,出现数据丢失,从而录音会有间隔(丢失一部分)。所以,我们构建了一个 FIFO,SAI
采集的数据,通过 rec_sai_fifo_write 函数写入 FIFO 里面,在主循环里面,我们通过
rec_sai_fifo_read 函数不停的读取 FIFO 里面的数据,并将数据通过 FATFS 写入 SD 卡里面,只
要 rec_sai_fifo_read 的速度,不小于 rec_sai_fifo_write 的速度,就可以保证数据不丢失,这个
FIFO 起到了一个缓冲的作用,从而保证录音文件的流畅性。
2,rec_sai_dma_rx_callback 函数
该函数用于 SAI_B 的 DMA 接收完成中断回调函数(通过 sai_rx_callback 指向该函数实现),
在该函数里面调用 rec_sai_fifo_write 函数,将采集到的音频数据,写入 FIFO。
3,recoder_enter_rec_mode 函数
该函数用于设置 WM8978 进入录音模式,并设置 SAI_A 和 SAI_B 的工作模式和位数等信
息,然后配置 DMA 和回调函数的指向,最后开启录音。调用该函数后,就可以开始录音了。
4,recoder_wav_init 函数
该函数初始化 wav 头的绝大部分数据,采样率通过 REC_SAMPLERATE 宏定义修改,默
认是 44.1Khz,位数为 16 位,线性 PCM 格式,另外由于录音还未真正开始,所以文件大小和
数据大小都还是未知的,要等录音结束才能知道。该函数__WaveHeader 结构体就是由上一章
(50.1.1 节)介绍的三个 Chunk 组成,结构为:
//wav 头
typedef __packed struct
{
ChunkRIFF riff;
//riff 块
ChunkFMT fmt; //fmt 块
//
ChunkFACT fact; //fact 块 线性 PCM,没有这个结构体
ChunkDATA data; //data 块
}__WaveHeader;
5,wav_recorder 函数
该函数实现了我们在硬件设计时介绍的功能(开始/暂停录音、保存录音文件、播放最近一
次录音等),实现方法请大家参考源码理解。另外,该函数使用上一章实现的 audio_play_song
函数,来播放最近一次录音。
recorder.c 的其他代码和 recorder.h 的代码我们这里就不再贴出了,请大家参考光盘本实验
的源码。然后,我们在 sai.c 里面也增加了几个函数,如下:
//SAI Block B 初始化,I2S,飞利浦标准

//mode:工作模式,可以设置:SAI_MODEMASTER_TX/

//SAI_MODEMASTER_RX/SAI_MODESLAVE_TX/SAI_MODESLAVE_RX

//cpol:数据在时钟的上升/下降沿选通,可以设置:

//SAI_CLOCKSTROBING_FALLINGEDGE/SAI_CLOCKSTROBING_RISINGEDGE

//datalen:数据大小,可以设置:SAI_DATASIZE_8/10/16/20/24/32

void SAIB_Init(u32 mode,u32 cpol,u32 datalen)

{
HAL_SAI_DeInit(&SAI1B_Handler); //清除以前的配置
SAI1B_Handler.Instance=SAI1_Block_B; //SAI1 Bock B
SAI1B_Handler.Init.AudioMode=mode; //设置 SAI1 工作模式
SAI1B_Handler.Init.Synchro=SAI_SYNCHRONOUS; //音频模块同步
SAI1B_Handler.Init.OutputDrive=SAI_OUTPUTDRIVE_ENABLE; //立即驱动输出
SAI1B_Handler.Init.NoDivider=SAI_MASTERDIVIDER_ENABLE; //使能主时钟分频器
SAI1B_Handler.Init.FIFOThreshold=SAI_FIFOTHRESHOLD_1QF //设置 FIFO 阈值
SAI1B_Handler.Init.ClockSource=SAI_CLKSOURCE_PLLI2S; //SIA 时钟源为 PLL2S
SAI1B_Handler.Init.MonoStereoMode=SAI_STEREOMODE; //立体声模式
SAI1B_Handler.Init.Protocol=SAI_FREE_PROTOCOL; //设置 SAI1 协议为自由协议
SAI1B_Handler.Init.DataSize=datalen; //设置数据大小
SAI1B_Handler.Init.FirstBit=SAI_FIRSTBIT_MSB; //数据 MSB 位优先
SAI1B_Handler.Init.ClockStrobing=cpol; //数据在时钟的上升/下降沿选通

//帧设置
SAI1B_Handler.FrameInit.FrameLength=64; //设置帧长度为 64,左/右通道各 32 个 SCK,
SAI1B_Handler.FrameInit.ActiveFrameLength=32; //设置帧同步有效电平长度
SAI1B_Handler.FrameInit.FSDefinition=SAI_FS_CHANNEL_IDENTIFICATION;
//FS 信号为 SOF 信号+通道识别信号
SAI1B_Handler.FrameInit.FSPolarity=SAI_FS_ACTIVE_LOW; //FS 低电平有效(下降沿)
SAI1B_Handler.FrameInit.FSOffset=SAI_FS_BEFOREFIRSTBIT;
//在 slot0 的第一位的前一位使能 FS,以匹配飞利浦标准
//SLOT 设置
SAI1B_Handler.SlotInit.FirstBitOffset=0; //slot 偏移(FBOFF)为 0
SAI1B_Handler.SlotInit.SlotSize=SAI_SLOTSIZE_32B; //slot 大小为 32 位
SAI1B_Handler.SlotInit.SlotNumber=2; //slot 数为 2 个
SAI1B_Handler.SlotInit.SlotActive=SAI_SLOTACTIVE_0|SAI_SLOTACTIVE_1;
//使能 slot0 和 slot1

HAL_SAI_Init(&SAI1B_Handler);
SAIB_DMA_Enable(); //使能 SAI 的 DMA 功能
__HAL_SAI_ENABLE(&SAI1B_Handler); //使能 SAI
}
//SAIA TX DMA 配置
//设置为双缓冲模式,并开启 DMA 传输完成中断
//buf0:M0AR 地址.
//buf1:M1AR 地址.
//num:每次传输数据量
//width:位宽(存储器和外设,同时设置),0,8 位;1,16 位;2,32 位;
void SAIA_RX_DMA_Init(u8* buf0,u8 *buf1,u16 num,u8 width)
{
  u32 memwidth=0,perwidth=0; //外设和存储器位宽
switch(width)
{
case 0: //8 位
memwidth=DMA_MDATAALIGN_BYTE;
perwidth=DMA_PDATAALIGN_BYTE;
break;
case 1: //16 位
memwidth=DMA_MDATAALIGN_HALFWORD;
perwidth=DMA_PDATAALIGN_HALFWORD;
break;
case 2: //32 位
memwidth=DMA_MDATAALIGN_WORD;
perwidth=DMA_PDATAALIGN_WORD;
break;

}
__HAL_RCC_DMA2_CLK_ENABLE(); //使能 DMA2 时钟
__HAL_LINKDMA(&SAI1B_Handler,hdmarx,SAI1_RXDMA_Handler);
//将 DMA 与 SAI 联系起来
SAI1_RXDMA_Handler.Instance=DMA2_Stream5; //DMA2 数据流 5
SAI1_RXDMA_Handler.Init.Channel=DMA_CHANNEL_0; //通道 0
SAI1_RXDMA_Handler.Init.Direction=DMA_PERIPH_TO_MEMORY; //外设到存储器
SAI1_RXDMA_Handler.Init.PeriphInc=DMA_PINC_DISABLE; //外设非增量模式
SAI1_RXDMA_Handler.Init.MemInc=DMA_MINC_ENABLE; //存储器增量模式
SAI1_RXDMA_Handler.Init.PeriphDataAlignment=perwidth; //外设数据长度:16/32 位
SAI1_RXDMA_Handler.Init.MemDataAlignment=memwidth; //存储器数据长度:16/32 位
SAI1_RXDMA_Handler.Init.Mode=DMA_CIRCULAR; //使用循环模式
SAI1_RXDMA_Handler.Init.Priority=DMA_PRIORITY_MEDIUM; //中等优先级
SAI1_RXDMA_Handler.Init.FIFOMode=DMA_FIFOMODE_DISABLE; //不使用 FIFO
SAI1_RXDMA_Handler.Init.Memburst=DMA_MBURST_SINGLE; //存储器单次突发
SAI1_RXDMA_Handler.Init.PeriphBurst=DMA_PBURST_SINGLE; //外设单次突发
HAL_DMA_DeInit(&SAI1_RXDMA_Handler); //先清除以前的设置
HAL_DMA_Init(&SAI1_RXDMA_Handler); //初始化 DMA

HAL_DMAEx_MultiBufferStart(&SAI1_RXDMA_Handler,
(u32)&SAI1_Block_B->DR,(u32)buf0,(u32)buf1,num);//开启双缓冲
__HAL_DMA_DISABLE(&SAI1_RXDMA_Handler); //先关闭接收 DMA
delay_us(10); //10us 延时,防止-O2 优化出问题
__HAL_DMA_CLEAR_FLAG(&SAI1_RXDMA_Handler,
DMA_FLAG_TCIF1_5); //清除 DMA 传输完成中断标志位
__HAL_DMA_ENABLE_IT(&SAI1_RXDMA_Handler,DMA_IT_TC);//开启传输完成中断
  HAL_NVIC_SetPriority(DMA2_Stream5_IRQn,0,1); //DMA 中断优先级
HAL_NVIC_EnableIRQ(DMA2_Stream5_IRQn);
}
void (*sai_rx_callback)(void); //RX 回调函数
//DMA2_Stream5 中断服务函数
void DMA2_Stream5_IRQHandler(void)
{
if(__HAL_DMA_GET_FLAG(&SAI1_RXDMA_Handler,
DMA_FLAG_TCIF1_5)!=RESET) //DMA 传输完成
{
__HAL_DMA_CLEAR_FLAG(&SAI1_RXDMA_Handler,DMA_FLAG_TCIF1_5);
//清除 DMA 传输完成中断标志位
if(sai_rx_callback!=NULL)sai_rx_callback();
//执行回调函数,读取数据等操作在这里面处理
}
}
//SAI 开始录音
void SAI_Rec_Start(void)
{
__HAL_DMA_ENABLE(&SAI1_RXDMA_Handler);//开启 DMA RX 传输
}
//关闭 SAI 录音
void SAI_Rec_Stop(void)
{
__HAL_DMA_DISABLE(&SAI1_RXDMA_Handler);//结束录音
}这里新增了5个函数,SAIB_Init函数完成SAI_B子模块的初始化,通过3个参数设置SAI_B
的详细配置信息。SAIB_RX_DMA_Init 函数,用于设置 SAI_B 的 DMA 接收,使用双缓冲循环
模式,接收来自 WM8978 的数据,并开启了传输完成中断。而 DMA2_Stream5_IRQHandler 函
数,则是 DMA2 数据流 5 传输完成中断的服务函数,该函数调用 sai_rx_callback 函数(函数指
针,使用前需指向特定函数)实现 DMA 数据接收保存。最后,SAI_ Rec_Start 和 SAI_ Rec_Stop,
用于开启和关闭 SAI_B 的 DMA 传输。
其他代码,我们就不再介绍了,请大家参考开发板光盘本例程源码。最后我们看看 main
函数源码:
int main(void)
{
Cache_Enable(); //打开 L1-Cache
HAL_Init(); //初始化 HAL 库
Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhz
delay_init(216); //延时初始化
uart_init(115200); //串口初始化
LED_Init(); //初始化
KEY_Init(); //初始化按键
SDRAM_Init(); //初始化 SDRAM
LCD_Init(); //初始化 LCD
W25QXX_Init(); //初始化 W25Q256
WM8978_Init(); //初始化 WM8978
WM8978_HPvol_Set(40,40); //耳机音量设置
WM8978_SPKvol_Set(40); //喇叭音量设置
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMEX); //初始化外部 SDRAM 内存池
my_mem_init(SRAMDTCM); //初始化内部 DTCM 内存池
exfuns_init(); //为 fatfs 相关变量申请内存
f_mount(fs[0],"0:",1); //挂载 SD 卡
f_mount(fs[1],"1:",1); //挂载 SPI FLASH.
f_mount(fs[2],"2:",1); //挂载 NAND FLASH.
POINT_COLOR=RED;
while(font_init()) //检查字库
{
LCD_ShowString(30,50,200,16,16,"Font Error!");
delay_ms(200);
LCD_Fill(30,50,240,66,WHITE);//清除显示
delay_ms(200);
}
POINT_COLOR=RED;
Show_Str(30,40,200,16,"阿波罗 STM32F4/F7 开发板",16,0);
Show_Str(30,60,200,16,"录音机实验",16,0);
Show_Str(30,80,200,16,"正点原子@ALIENTEK",16,0);
Show_Str(30,100,200,16,"2016 年 1 月 29 日",16,0);
while(1)
{
wav_recorder();
}
}该函数代码同上一章的 main 函数代码几乎一样,十分简单,我们就不再多说了。
至此,本实验的软件设计部分结束。
53.4 下载验证
在代码编译成功之后,我们下载代码到 ALIENTEK 阿波罗 STM32 开发板上,程序先检测
字库,然后检测 SD 卡的 RECORDER 文件夹,一切顺利通过之后,进入录音模式,得到,如
图 53.4.1 所示:


图 53.4.1 录音机界面
此时,我们按下 KEY0 就开始录音了,此时看到屏幕显示录音文件的名字、码率以及录音
时长,如图 53.4.2 所示:



图 53.4.2 录音进行中
在录音的时候按下 KEY0 则执行暂停/继续录音的切换,通过 DS1 指示录音暂停。通过按
下 KEY2,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通
过按 KEY_UP 按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。
我们将开发板的录音文件放到电脑上面,可以通过属性查看录音文件的属性,如图 53.4.3
所示:



图 53.4.3 录音文件属性
这和我们预期的效果一样,通过电脑端的播放器(winamp/千千静听等)可以直接播放我们
所录的音频。经实测,效果还是非常不错的。






2
分享淘帖 显示全部楼层

评论

高级模式
您需要登录后才可以回帖 登录 | 注册

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
发资料
关闭

站长推荐 上一条 /7 下一条

快速回复 返回顶部 返回列表