本帖最后由 正点原子运营 于 2023-9-12 17:50 编辑
第五十六章 录音机实验 1)实验平台:正点原子探索者STM32F407开发板
2) 章节摘自【正点原子】STM32F407开发指南 V1.1
6)STM32技术交流QQ群:151941872
上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,继续用ES8388实现一个简单的录音机,录制WAV格式的录音。 56.1 I2S录音简介 56.2 硬件设计 56.3 软件设计 56.4 下载验证
56.1 I2S录音简介本章涉及的知识点基本上在上一章都有介绍。本章要实现WAV录音,还是和上一章一样,要了解:WAV文件格式、ES8388和I2S接口。WAV文件格式,我们在上一章已经做了详细介绍了,这里就不作介绍了。
ALIENTEK探索者STM32F407开发板将板载的一个MIC分别接入到了ES8388的2个差分输入通道(LIP/LIN和RIP/RIN,原理图见:图54.2.1)。代码上,我们采用立体声WAV录音,不过,左右声道的音源都是一样的,录音出来的WAV文件,听起来就是个单声道效果。
关于ES8388的录音设置(即ADC的相关设置),我们在上一站的介绍已经顺带都介绍了,所以本章我们就不再介绍ES8388的寄存器了,想了解读者可以参考第54章的介绍,或者参考本例程源代码和ES8388的pdf数据手册理解。
上一章我们向大家介绍了STM32F407的I2S放音,通过上一章的了解,我们知道:STM32F407的全双工需要用到扩展的I2Sx_ext(x=2/3),和I2Sx组成全双工I2S。在全双工模式下,I2Sx向I2Sx_ext提供CK和WS时钟信号。
本章我们必须向ES8388提供WS(FS),CK(SCK)和MCK(MCLK)等时钟,同时又要录音,所以只能使用全双工模式。工作在主模式的主I2Sx循环发送数据0X0000,给ES8388,以产生CK、WS和MCK等信号,工作在从模式的从I2Sx_ext,则接收来自ES8388的ADC数据(ADCDAT),并保存到SD卡,实现录音。
本章我们主要通过STM32F407的I2S,驱动ES8388实现WAV录音的简要步骤,如下:
1)初始化ES8388 这个过程就是前面所讲的ES8388 MIC录音配置步骤,让ES8388的ADC以及其模拟部分工作起来。
2)初始化I2S2和I2S2_ext 本章要用到I2S2的全双工模式,所以,I2S2和I2S2_ext都需要配置,其中I2S2配置为主模式,I2S2_ext设置为从模式。他们的其他配置(协议、时钟电平特性、slot相关参数)基本一样,只是一个是发送一个是接收,且都要使能DMA。同时,还需要设置音频采样率,不过这个只需要设置I2S2的即可,还是通过上一章介绍的查表法设置。
3)设置发送和接收DMA 放音和录音都是采用DMA传输数据的,本章放音其实就是个幌子,不过也得设置DMA(使用DMA1数据流4的通道0),配置同上一章一模一样,不过不需要开启DMA传输完成中断。对于录音,则使用的是DMA1数据流3的通道3实现的DMA数据接收,我们需要配置DMA1的数据流3,本章将DMA1数据流3设置为:双缓冲循环模式,外设和存储器都是16位宽,并开启传输完成中断(方便接收数据)。
4)编写接收通道DMA传输完成中断服务函数 为了方便接收音频数据,我们使用DMA传输完成中断,每当一个缓冲接数据满了,硬件自动切换为下一个缓冲,同时进入中断服务函数,将已满缓冲的数据写入SD卡的wav文件。过程如图56.1.1所示: 5)创建WAV文件,并保存wav头 前面4步完成,其实就可以开始读取音频数据了,不过在录音之前,我们需要先在创建一个新的文件,并写入wav头,然后才能开始写入我们读取到的的PCM音频数据。
6)开启DMA传输,接收数据 然后,我们就只需要开启DMA传输,然后及时将I2S2_ext读到的数据写入到SD卡之前新建的wav文件里面,就可以实现录音了。
7)计算整个文件大小,重新保存wav头并关闭文件 在结束录音的时候,我们必须知道本次录音的大小(数据大小和整个文件大小),然后更新wav头,重新写入文件,最后因为FATFS,在文件创建之后,必须调用f_close,文件才会真正体现在文件系统里面,否则是不会写入的!所以最后还需要调用f_close,以保存文件。
56.2 硬件设计
1. 例程功能本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,再检测SD卡根目录是否存在RECORDER文件夹,如果不存在则创建,如果创建失败,则报错。在找到SD卡的RECORDER文件夹后,即进入录音模式(包括配置ES8388和I2S等),此时可以在耳机(或喇叭)听到采集到的音频。KEY0用于开始/暂停录音,KEY2用于保存并停止录音,KEY_UP用于播放最近一次的录音。
当我们按下KEY0的时候,可以在屏幕上看到录音文件的名字、码率以及录音时间等,然后通过KEY2可以保存该文件,同时停止录音(文件名和时间也都将清零),在完成一段录音后,我们可以通过按KEY_UP按键,来试听刚刚的录音。LED0用于提示程序正在运行,LED1用于提示是否处于暂停录音状态。
2. 硬件资源本实验,大家需要准备1个microSD/SD卡(在里面新建一个MUSIC文件夹,并存放一些歌曲在MUSIC文件夹下)和一个耳机(非必备),分别插入SD卡接口和耳机接口,然后下载本实验就可以实现录音机的效果。实验用到的硬件资源如下: 1)LED灯 LED0 – PF9 LED1 – PF10 2)独立按键 KEY0 - PE4 KEY1 - PE3 KEY2 - PE2 KEY_UP - PA0 (程序中的宏名:WK_UP) 3)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面) 4)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动) 5)SD卡:通过SDIO连接 6)NOR FLASH(SPI FLASH芯片,连接在SPI1上) 7)I2S,驱动ES8388芯片 8)开发板板载的咪头或自备麦克风输入 9)喇叭或耳机 录音机实验与上一章(音乐播放器实验)用到的硬件资源基本一样,我们这里就不重复介绍了,有差异的是这次我们用到板载的咪头用于信号输入,也可以通过3.5mm的音频接口通过LINE_IN接入麦克风输入录音音源。
56.3 程序设计
56.3.1 程序流程图程序的设计流程如下: 我们通过板载的按键控制录音的开始和停止,检测到录音开始后在录音目录下随机生成一个wav后缀的文件名并写入文件头信息,通过ES8388的录音模式不断采集声音信息并定入文件,录音结束后,我们保存文件并修改对应的文件头信息以便文件能被解码。最后,我们设计了用KEY_UP按键来播放上一次录音的文件,以便查看录音效果。
56.3.2 程序解析1. recorder驱动代码 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,RECORDER的驱动主要包括两个文件:recorder.c和recorder.h。
音乐播放器实验中我们已经学过配置ES8388的方法,我们在recoder.c编写函数配置ES8388工作在PCM录音模式,我们编写代码如下: - /**
- *@brief 进入PCM 录音模式
- *@param 无
- *@retval 无
- */
- voidrecoder_enter_rec_mode(void)
- {
- /* 关闭传输完成中断(这里不用中断送数据) (如果在这里不关闭dma就会卡在清空数据过程)*/
- I2S_TX_DMASx->CR &= ~(1 << 4);
- es8388_adda_cfg(0, 1); /* 开启ADC */
- es8388_input_cfg(0); /* 开启输入通道(通道1,MIC所在通道) */
- es8388_mic_gain(8); /* MIC增益设置为最大 */
- es8388_alc_ctrl(3, 4, 4); /* 开启立体声ALC控制,以提高录音音量 */
- es8388_output_cfg(0, 0); /* 关闭通道1和2的输出 */
- es8388_spkvol_set(0); /* 关闭喇叭. */
- es8388_i2s_cfg(0, 3); /* 飞利浦标准,16位数据长度 */
-
- i2s_init(I2S_STANDARD_PHILIPS, I2S_MODE_MASTER_TX, I2S_CPOL_LOW,
- I2S_DATAFORMAT_16B); /* 飞利浦标准,主机发送,时钟低电平有效,16位帧长度 */
- i2sext_init(I2S_STANDARD_PHILIPS, I2S_MODE_SLAVE_RX, I2S_CPOL_LOW,
- I2S_DATAFORMAT_16B); /* 飞利浦标准,从机接收,时钟低电平有效,16位帧长度 */
- i2s_samplerate_set(REC_SAMPLERATE); /* 设置采样率 */
- /* 配置TX DMA */
- i2s_tx_dma_init((uint8_t *)&I2S_PLAY_BUF[0], (uint8_t *)&I2S_PLAY_BUF[1],1);
- I2S_TX_DMASx->CR &= ~(1 << 4); /* 关闭传输完成中断(这里不用中断送数据) */
- /* 配置RX DMA */
- i2sext_rx_dma_init(p_i2s_recbuf1, p_i2s_recbuf2, REC_I2S_RX_DMA_BUF_SIZE/2);
- i2s_rx_callback = rec_i2s_dma_rx_callback; /*回调函数指wav_i2s_dma_callback*/
- i2s_play_start(); /* 开始I2S数据发送(主机) */
- i2s_rec_start(); /* 开始I2S数据接收(从机) */
- recoder_remindmsg_show(0);
- }
复制代码该函数就是用我们前面介绍的方法,激活ES8388的PCM模式,本章,我们使用的是44.1Khz采样率,16位单声道线性PCM模式,以及发送与接收DMA配置。
由于最后要把录音写入到文件,这里需要准备wav的文件头,为方便,我们定义了一个__WaveHeader结构体来定义文件头的数据字节,这个结构体包含了前面提到的wav文件的数据结构块: - typedef __PACKED_STRUCT
- {
- ChunkRIFF riff; /* riff块 */
- ChunkFMT fmt; /* fmt块 */
- //ChunkFACT fact; /*fact块 线性PCM,没有这个结构体 */
- ChunkDATA data; /* data块 */
- } __WaveHeader;
复制代码我们定义一个recoder_wav_init()函数方便初始化文件信息,代码如下: - void recoder_wav_init(__WaveHeader *wavhead)
- {
- 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; 0x00, 表示IMA ADPCM; */
- wavhead->fmt.NumOfChannels = 2; /* 双声道 */
- wavhead->fmt.SampleRate = REC_SAMPLEREATE; /*采样速率 */
- /* 字节速率=采样率*通道数*(ADC位数/8) */
- wavhead->fmt.ByteRate = wavhead->fmt.SampleRate * 4;
- wavhead->fmt.BlockAlign = 4; /* 块大小=通道数*(ADC位数/8) */
- wavhead->fmt.BitsPerSample = 16; /* 16位PCM */
- wavhead->data.ChunkID = 0X61746164; /*"data" */
- wavhead->data.ChunkSize = 0; /* 数据大小, 还需要计算 */
- }
复制代码录音完成我们还要重新计算录音文件的大小写入文件头,以保证音频文件能正常被解析。我们把这些数据直接按顺序写入文件即可完成录音操作,结合文件操作和按键功能定义,我们用wav_recoder()函数实现录音过程,代码如下: - /**
- *@brief WAV录音
- *@param 无
- *@retval 无
- */
- void wav_recorder(void)
- {
- uint8_t res, i;
- uint8_t key;
- uint8_t rval = 0;
- uint32_t bw;
-
- __WaveHeader *wavhead = 0;
- DIRrecdir; /* 目录 */
- FIL*f_rec = 0; /* 录音文件 */
-
- uint8_t *pdatabuf; /* 数据缓存指针 */
- char *pname = 0;
- uint8_t timecnt = 0; /* 计时器 */
- uint32_t recsec = 0; /* 录音时间 */
- while (f_opendir(&recdir, "0:/RECORDER")) /* 打开录音文件夹 */
- {
- lcd_show_string(30, 230, 240, 16, 16, "RECORDER文件夹错误!", RED);
- delay_ms(200);
- lcd_fill(30, 230, 240, 246, WHITE); /* 清除显示 */
- delay_ms(200);
- f_mkdir("0:/RECORDER"); /* 创建该目录 */
- }
- /* 申请内存 */
- for (i = 0; i < REC_I2S_RX_FIFO_SIZE; i++)
- {
- /* I2S录音FIFO内存申请 */
- p_i2s_recfifo_buf = mymalloc(SRAMIN, REC_I2S_RX_DMA_BUF_SIZE);
- if (p_i2s_recfifo_buf == NULL)
- {
- break; /* 申请失败 */
- }
- }
-
- p_i2s_recbuf1 = mymalloc(SRAMIN,REC_I2S_RX_DMA_BUF_SIZE);/*I2S录音内存1申请*/
- p_i2s_recbuf2 = mymalloc(SRAMIN,REC_I2S_RX_DMA_BUF_SIZE);/*I2S录音内存2申请*/
- f_rec = (FIL *)mymalloc(SRAMIN, sizeof(FIL)); /* 开辟FIL字节的内存区域 */
-
- /* 开辟__WaveHeader字节的内存区域 */
- wavhead = (__WaveHeader *)mymalloc(SRAMIN, sizeof(__WaveHeader));
- pname = mymalloc(SRAMIN, 30); /* 申请30个字节内存,文件名类似 REC00001.wav"*/
- if (!p_i2s_recbuf2 || !f_rec || !wavhead || !pname)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 (g_rec_sta & 0x80) /* 有录音 */
- {
- g_rec_sta = 0; /* 关闭录音 */
- /* 整个文件的大小-8; */
- wavhead->riff.ChunkSize = g_wav_size + 36;
- wavhead->data.ChunkSize = g_wav_size; /* 数据大小 */
- f_lseek(f_rec, 0); /* 偏移到文件头. */
- f_write(f_rec, (const void *)wavhead, sizeof(__WaveHeader),
- &bw); /* 写入头数据 */
- f_close(f_rec);
- g_wav_size = 0;
- }
- g_rec_sta = 0;
- recsec = 0;
- LED1(1); /* 关闭DS1 */
- /* 清除显示,清除之前显示的录音文件名 */
- lcd_fill(30, 190, lcddev.width, lcddev.height, WHITE);
- break;
- case KEY0_PRES: /* REC/PAUSE*/
- if (g_rec_sta & 0x01) /* 如果是暂停,继续录音 */
- {
- g_rec_sta &= 0xFE; /* 取消暂停 */
- }
- else if (g_rec_sta & 0x80) /* 已经在录音了,暂停 */
- {
- g_rec_sta |= 0x01; /* 暂停 */
- }
- else /* 还没开始录音 */
- {
- recsec = 0;
- recoder_new_pathname(pname); /* 得到新的名字 */
- text_show_string(30, 190,lcddev.width,16,"录制:",16,0,RED);
- text_show_string(30 + 40, 190, lcddev.width, 16, pname +
- 11, 16, 0, RED); /* 显示当前录音文件名字 */
- recoder_wav_init(wavhead); /* 初始化wav数据 */
- res = f_open(f_rec, (const TCHAR *)pname, FA_CREATE_ALWAYS
- | FA_WRITE);
- if (res) /* 文件创建失败 */
- {
- g_rec_sta = 0; /* 创建文件失败,不能录音 */
- rval = 0xFE; /* 提示是否存在SD卡 */
- }
- else
- {
- res = f_write(f_rec, (const void *)wavhead,
- sizeof(__WaveHeader), &bw); /* 写入头数据 */
- recoder_msg_show(0, 0);
- g_rec_sta |= 0x80; /* 开始录音 */
- }
- }
- if (g_rec_sta & 0x01)
- {
- LED1(0); /* 提示正在暂停 */
- }
- else
- {
- LED1(1);
- }
- break;
- case WKUP_PRES: /* 播放最近一段录音 */
- if (g_rec_sta != 0x80) /* 没有在录音 */
- {
- if (pname[0]) /* 如果按键被按下,且pname不为空 */
- {
- text_show_string(30, 190, lcddev.width, 16, "播放:", 16,
- 0, RED);
- text_show_string(30 + 40, 190, lcddev.width, 16,
- (char*)pname + 11, 16, 0, RED); /* 显示当播放的文件名字 */
- recoder_enter_play_mode(); /* 进入播放模式 */
- audio_play_song(pname); /* 播放pname */
- /* 清除显示,清除之前显示的录音文件名 */
- lcd_fill(30, 190, lcddev.width, lcddev.height, WHITE);
- recoder_enter_rec_mode(); /* 重新进入录音模式 */
- }
- }
- break;
- }
- if (recoder_i2s_fifo_read(&pdatabuf)) /*读取一次数据,读到数据了,写入文件*/
- {
- /* 写入文件 */
- res = f_write(f_rec, pdatabuf, REC_I2S_RX_DMA_BUF_SIZE, &bw);
- if (res)
- {
- printf("writeerror:%d\r\n", res);
- }
- g_wav_size +=REC_I2S_RX_DMA_BUF_SIZE; /* WAV数据大小增加 */
- }
- else
- {
- delay_ms(5);
- }
-
- timecnt++;
- if ((timecnt % 20) == 0)
- {
- LED0_TOGGLE(); /* LED0闪烁 */
- }
- if (recsec != (g_wav_size / wavhead->fmt.ByteRate)) /* 录音时间显示 */
- {
- LED1_TOGGLE(); /* LED0闪烁 */
- recsec = g_wav_size / wavhead->fmt.ByteRate; /* 录音时间 */
- recoder_msg_show(recsec, wavhead->fmt.SampleRate *
- wavhead->fmt.NumOfChannels *
- wavhead->fmt.BitsPerSample); /* 显示码率 */
- }
- }
- }
-
- for (i = 0; i < REC_I2S_RX_FIFO_SIZE; i++)
- {
- myfree(SRAMIN, p_i2s_recfifo_buf); /* 录音FIFO内存释放 */
- }
-
- myfree(SRAMIN, p_i2s_recbuf1); /* 释放内存 */
- myfree(SRAMIN, p_i2s_recbuf2); /* 释放内存 */
- myfree(SRAMIN, f_rec); /* 释放内存 */
- myfree(SRAMIN, wavhead); /* 释放内存 */
- myfree(SRAMIN, pname); /* 释放内存 */
- }
复制代码2. main.c代码 由于我们把大部分功能已经在wav_recoder()中实现了,main函数进行必要的外设初始化,显示相关的数据信息后,调用该接口即可实现我们需要的录音机功能了,最后我们在main.c中实现代码如下: - int main(void)
- {
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口初始化为115200 */
- usmart_dev.init(84); /* 初始化USMART */
- led_init(); /* 初始化LED */
- lcd_init(); /* 初始化LCD */
- key_init(); /* 初始化按键 */
- sram_init(); /* SRAM初始化 */
- norflash_init(); /* 初始化NORFLASH */
- my_mem_init(SRAMIN); /* 初始化内部SRAM内存池 */
- my_mem_init(SRAMCCM); /* 初始化内部SRAMCCM内存池 */
- my_mem_init(SRAMEX); /* 初始化外部SRAM内存池 */
- while (fonts_init()) /* 检查字库 */
- {
- lcd_show_string(30, 150, 200, 16, 16, "SD Card Error!", RED);
- delay_ms(500);
- lcd_show_string(30, 150, 200, 16, 16, "PleaseCheck! ", RED);
- delay_ms(500);
- LED0_TOGGLE(); /* LED0闪烁 */
- }
- exfuns_init(); /* 为fatfs相关变量申请内存 */
- f_mount(fs[0], "0:", 1); /* 挂载SD卡 */
- f_mount(fs[1], "1:", 1); /* 挂载FLASH */
- while (fonts_init()) /* 检查字库 */
- {
- lcd_show_string(30, 50, 200, 16, 16, "FontError!", RED);
- delay_ms(200);
- lcd_fill(30, 50, 240, 66, WHITE); /* 清除显示 */
- delay_ms(200);
- }
- text_show_string(30, 30, 200, 16, "正点原子STM32开发板", 16, 0, RED);
- text_show_string(30, 50, 200, 16, "WAV录音机 实验", 16, 0, RED);
- text_show_string(30, 70, 200, 16, "2021年11月16日", 16, 0, RED);
- es8388_init(); /* ES8388初始化 */
- es8388_adda_cfg(1, 0); /* 开启DAC关闭ADC */
- es8388_hpvol_set(25); /* 设置耳机音量 */
- es8388_spkvol_set(30); /* 设置喇叭音量 */
- while (1)
- {
- wav_recoder(); /* 录音 */
- }
- }
复制代码可以看到main函数与音乐播放器实验十分类似,封装好了APP,main函数会精简很多。
56.4 下载验证在代码编译成功之后,我们下载代码到正点原子探索者STM32F407开发板上,先初始化各外设,然后检测字库是否存在,如果检测无问题,再检测SD卡根目录是否存在RECORDER文件夹,如果不存在则创建,如果创建失败,则报错。在找到SD卡的RECORDER文件夹后,即进入录音模式(包括配置ES8388和I2S等),此时可以在耳机(或喇叭)听到采集到的音频。KEY0用于开始/暂停录音,KEY2用于保存并停止录音,KEY_UP用于播放最近一次的录音。 此时,我们按下KEY0就开始录音了,此时看到屏幕显示录音文件的名字以及录音时长,如图56.4.2所示: 在录音的时候按下KEY0则执行暂停/继续录音的切换,通过LED0指示录音暂停。通过按下KEY2,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按KEY_UP按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。
我们可以把录音完成的wav文件放到电脑上,可以通过一些播放软件播放并查看详细的音频编码信息,本例程使用的是KMPlayer播放,查看到的信息如图56.4.3所示: 这和我们程序设计时的效果一样,通过电脑端的播放器可以直接播放我们所录的音频。经实测,效果还是非常不错的。 |