第二十四章 音频录制实验
本章将介绍如何使用Kendryte K210的I2S功能,并将麦克风输入的声音数据以wav格式保存在SD卡中。通过学习本章内容,读者将掌握利用SDK编程技术实现wav音频文件的编码并保存的方法。
本章分为如下几个小节:
24.1 I2S录音简介
24.2 硬件设计
24.3 程序设计
24.4 运行验证
24.1 WAV和I2S简介
本章涉及的知识点基本上在上一章都有介绍。本章要实现WAV录音,还是和上一章一样,要了解:WAV文件格式和I2S接口。WAV文件格式和Kendryte K210的I2S功能,我们在上一章已经做了详细介绍了,这里就不作介绍了。
24.2 硬件设计
24.2.1 例程功能
1. 开机后,先初始化各外设,然后检测SD卡是否存在,如果不存在,则报错。在找到SD卡的RECORDER文件夹后,即进入录音模式(配置I2S和开启中断),此时LCD模块显示进入音频录制实验以及控制信息。KEY0用于开始录音,KEY1用于暂停或开启录音,KEY2用于保存并停止录音。
当我们按下KEY0的时候,可以在屏幕上看到录音时间,然后通过KEY2可以保存该文件,同时停止录音(文件名和时间也都将清零),按下KEY0开始录音的途中我们也可以通过KEY1暂停录音,再次按下KEY1会继续录音,直至按下KEY2可以保存该文件并停止录音。
24.2.2 硬件资源
1. 数字麦克风
IIS_SDIN - IO30
IIS_BCK - IO32
IIS_LRCK - IO33
24.2.3 原理图
本章实验内容,需要获取板载数字麦克风的音频数据,然后使用WAV编码后保存到文件系统中。
DNK210开发板上的数字麦克风的连接原理图,如下所示:

图24.2.3.1 数字功放NS4168连接原理图
关于该数字麦克风的使用方法,可参考MSM261S4030H0R的数据手册——《MSM261S4030H0R.pdf》,读者可在A盘→硬件资料→芯片资料下找到这份文档。
24.3 程序设计
24.3.1 mic驱动代码
本实验是通过I2S将数据输入到数字功放实现音频的播放,数字功放也需要相应的驱动代码进行配置,驱动源码包括两个文件:mic.c和mic.h,我们先介绍mic.h文件的内容。
/*****************************HARDWARE-PIN*********************************/ /* 硬件IO口,与原理图对应 */ #define PIN_SPK_CTRL (21) #define PIN_MIC_SDIN (30) #define PIN_MIC_BCK (32) #define PIN_MIC_WS (33) /*****************************SOFTWARE-GPIO********************************/ /* 软件GPIO口,与程序对应 */ #define SPK_CTRL_GPIONUM (0) /*****************************FUNC-GPIO************************************/ /* GPIO口的功能,绑定到硬件IO口 */ #define FUNC_SPK_CTRL (FUNC_GPIO0 + SPK_CTRL_GPIONUM) #define FUNC_MIC_WS FUNC_I2S0_WS #define FUNC_MIC_SDIN FUNC_I2S0_IN_D1 #define FUNC_MIC_BCK FUNC_I2S0_SCLK
这个是驱动引脚功能的绑定的宏,除了数据引脚不同外,其他和扬声器的引脚是相同的,也就是说DNK210开发板的音频播放和音频录制两个功能不可以同时使用,只能分步进行。
下面看mic.c文件代码。
/**
* @brief 麦克风引脚初始化,绑定GPIO功能
* @param 无
* @retval 返回值 : 无
*/
void mic_i2s_hardware_init(void)
{
/* I2S 初始化 */
gpio_init(); /* 使能GPIO的时钟 */
/* mic */
fpioa_set_function(PIN_MIC_WS, FUNC_MIC_WS);
fpioa_set_function(PIN_MIC_SDIN, FUNC_MIC_SDIN);
fpioa_set_function(PIN_MIC_BCK, FUNC_MIC_BCK);
fpioa_set_function(PIN_SPK_CTRL, FUNC_SPK_CTRL);
gpio_set_drive_mode(SPK_CTRL_GPIONUM, GPIO_DM_OUTPUT); /*输出模式*/
gpio_set_pin(SPK_CTRL_GPIONUM, GPIO_PV_LOW); /*输出为低,使能麦克风输入*/
}
麦克风的初始化代码和扬声器的也相识,不同的是数据引脚绑定的I2S输入功能,用于数据的接收,控制引脚拉低,使能数字功放输入。
24.3.2 recoder代码
recoder程序文件主要用于实现音频录制功能,驱动源码包括两个文件:recoder.c和recoder.h,我们先看下recoder.h文件。
#define REC_I2S_RX_DMA_BUF_SIZE 4096 /* 定义RX DMA 数组大小 */ #define REC_I2S_RX_FIFO_SIZE 10 /* 定义接收FIFO大小 */ #define MIC_GAIN 5 /* 麦克风增益值,可以根据实际调大录音的音量 */ #define REC_SAMPLERATE 16000 /* 采样率,44.1Khz */
这四个宏分别是设置音频存放的缓存区大小、接收FIFO的数量、麦克风增益以及设置采样率。
下面看看recoder.c里面的几个函数,代码如下:
/**
* @brief 进入PCM 录音模式
* @param 无
* @retval 无
*/
void recoder_enter_rec_mode(void)
{
/* I2S设备0初始化为接收模式 */
i2s_init(I2S_DEVICE_0, I2S_RECEIVER, 0x0C);
/* 通道参数设置 */
i2s_rx_channel_config(
I2S_DEVICE_0, /* I2S设备0 */
I2S_CHANNEL_1, /* 通道1 */
RESOLUTION_16_BIT, /* 接收数据16bit */
SCLK_CYCLES_32, /* 单个数据时钟为32 */
TRIGGER_LEVEL_4, /* FIFO深度为4 */
STANDARD_MODE); /* 标准模式 */
/* 设置采样率 */
i2s_set_sample_rate(I2S_DEVICE_0, REC_SAMPLERATE);
/* 设置DMA中断回调 */
dmac_set_irq(DMAC_CHANNEL1, i2s_receive_dma_cb, NULL, 4);
}
该函数用于初始化I2S外设,并将I2S配置为接收模式,设置采样率和DMA中断回调函数,我们使用的采样率为44.1Khz。
/**
* @brief 初始化WAV头
* @param wavhead : wav文件头指针
* @retval 无
*/
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;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; /* 数据大小,还需要计算 */
}
recoder_wav_init()函数方便初始化wav文件信息。
/**
* @brief WAV录音
* @param 无
* @retval 无
*/
void wav_recorder(void)
{
uint8_t res, i;
uint8_t key;
uint8_t rval = 0;
uint32_t bw;
char datashow[15];
__WaveHeader *wavhead = 0;
DIR recdir; /* 目录 */
FIL *f_rec = 0; /* 录音文件 */
uint8_t *pdatabuf; /* 数据缓存指针 */
uint8_t *pname = 0;
uint32_t recsec = 0; /* 录音时间 */
while (f_opendir(&recdir, "0:/RECORDER")) /* 打开录音文件夹 */
{
msleep(200);
f_mkdir("0:/RECORDER"); /* 创建该目录 */
}
/* 申请内存 */
for (i = 0; i < REC_I2S_RX_FIFO_SIZE; i++)
{
p_i2s_recfifo_buf[i] = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE);
/* I2S录音FIFO内存申请 */
if (p_i2s_recfifo_buf[i] == NULL)
{
break; /* 申请失败 */
}
}
p_i2s_recbuf1 = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE / 2);
p_i2s_recbuf2 = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE / 2);
/* I2S录音内存申请 */
rx_buf = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE / 2);
f_rec = (FIL *)iomem_malloc(sizeof(FIL)); /* 开辟FIL字节的内存区域 */
wavhead = (__WaveHeader *)iomem_malloc(sizeof(__WaveHeader));
/* 开辟__WaveHeader字节的内存区域 */
pname = iomem_malloc(30);
/* 申请30个字节内存,类似"0:RECORDER/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 KEY0_PRES: /* 开始录音 */
recsec = 0;
recoder_new_pathname(pname); /* 得到新的名字 */
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); /* 写入头数据 */
g_rec_sta |= 0X80; /* 开始录音 */
/* I2S通过DMA接收数据,保存到rx_buf中 */
i2s_receive_data_dma(I2S_DEVICE_0, p_i2s_recbuf1,
REC_I2S_RX_DMA_BUF_SIZE, DMAC_CHANNEL1);
}
break;
case KEY1_PRES: /*REC/PAUSE */
if (g_rec_sta & 0X01) /* 原来是暂停,继续录音 */
{
g_rec_sta &= 0XFE; /* 取消暂停 */
/* I2S通过DMA接收数据,保存到rx_buf中 */
i2s_receive_data_dma(I2S_DEVICE_0, p_i2s_recbuf1,
REC_I2S_RX_DMA_BUF_SIZE, DMAC_CHANNEL1);
}
else if (g_rec_sta & 0X80) /* 已经在录音了,暂停 */
{
g_rec_sta |= 0X01; /* 暂停 */
}
else /* 还没开始录音 */
{
recsec = 0;
recoder_new_pathname(pname); /* 得到新的名字 */
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); /* 写入头数据 */
g_rec_sta |= 0X80; /* 开始录音 */
/* I2S通过DMA接收数据,保存到rx_buf中 */
i2s_receive_data_dma(I2S_DEVICE_0, p_i2s_recbuf1,
REC_I2S_RX_DMA_BUF_SIZE, DMAC_CHANNEL1);
}
}
key = 0;
break;
case KEY2_PRES: /* STOP&SAVE */
if (g_rec_sta & 0X80) /* 有录音 */
{
g_rec_sta = 0; /* 关闭录音 */
wavhead->riff.ChunkSize = g_wav_size + 36;
/* 整个文件的大小-8; */
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;
g_index = 0;
key = 0;
break;
}
if (recoder_i2s_fifo_read(&pdatabuf))/*读取一次数据,读到数据了,写入文件*/
{
res = f_write(f_rec, pdatabuf, REC_I2S_RX_DMA_BUF_SIZE, &bw);
/* 写入文件 */
if (res)
{
printf("write error:%d\r\n", res);
}
g_wav_size += REC_I2S_RX_DMA_BUF_SIZE; /* WAV数据大小增加 */
}
else
{
msleep(1);
}
if (recsec != (g_wav_size / wavhead->fmt.ByteRate)) /* 录音时间显示 */
{
recsec = g_wav_size / wavhead->fmt.ByteRate; /* 录音时间 */
sprintf((char *)datashow, "time:%02d:%02d", (uint16_t)(recsec /
60), (uint16_t)(recsec % 60));
for (size_t i = 0; i < 320 * 16; i++)
{
lcd_gram[i] = 0xFFFF;
}
draw_string_rgb565_image(lcd_gram, 320, 240, 10, 0,
(char *)datashow, BLUE);
lcd_draw_picture(0, 70, 320, 16, (uint16_t *)lcd_gram);
}
}
}
for (i = 0; i < REC_I2S_RX_FIFO_SIZE; i++)
{
iomem_free(p_i2s_recfifo_buf[i]); /* SAI录音FIFO内存释放 */
}
iomem_free(p_i2s_recbuf1); /* 释放内存 */
iomem_free(p_i2s_recbuf2); /* 释放内存 */
iomem_free(f_rec); /* 释放内存 */
iomem_free(wavhead); /* 释放内存 */
iomem_free(pname); /* 释放内存 */
}
wav_recorder函数是我们实现录音功能的主要函数,它首先是申请数个缓存区,然后将I2S按顺序存入这些缓存区中,再一个一个写入到SD卡保存,我们通过相应的按键控制录音的开始、暂停与继续、停止并保存录音文件等操作,录音完成我们还要重新计算录音文件的大小并写入wav文件头,以保证音频文件能正常被解析。
24.3.3 main.c代码
main.c中的代码如下所示:
int main(void)
{
FRESULT res;
FATFS fs;
sysctl_pll_set_freq(SYSCTL_PLL0, 800000000);
sysctl_pll_set_freq(SYSCTL_PLL1, 400000000);
sysctl_pll_set_freq(SYSCTL_PLL2, 45158400);
sysctl_set_power_mode(SYSCTL_POWER_BANK6, SYSCTL_POWER_V18);
sysctl_set_power_mode(SYSCTL_POWER_BANK7, SYSCTL_POWER_V18);
sysctl_set_spi0_dvp_data(1);
plic_init();
sysctl_enable_irq();
dmac_init();
key_init(); /* 按键初始化 */
mic_i2s_hardware_init(); /* 麦克风初始化 */
lcd_init(); /* 初始化LCD */
lcd_set_direction(DIR_YX_LRUD);
lcd_draw_string(10, 10, "RECODER ", RED);
lcd_draw_string(10, 30, "KEY0:START ", RED);
lcd_draw_string(10, 50, "KEY1:REC/PAUSE KEY2:STOP&SAVE", RED);
/* 初始化SD卡*/
if (sd_init() != 0)
{
printf("SD card initialization failed!\n");
while (1);
}
printf("SD card initialization succeed!\n");
/* Filesystem mount SD card */
res = f_mount(&fs, _T("0:"), 1);
if (res != FR_OK)
{
printf("SD card mount failed! Error code: %d\n", res);
while (1);
}
printf("SD card mount succeed!\n");
while (1)
{
wav_recorder(); /* 开始录音 */
}
}
main函数代码具体流程大致是:首先完成系统级和用户级初始化工作,完成LCD、按键、麦克风、SD卡等初始化,然后挂载SD卡,挂载成功后进入录音函数,LCD模块显示按键控制相关信息。
24.4 运行验证
将DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,程序启动后进入录音模式,我们按下KEY0控制DNK210开发板开始录音,可以看到LCD显示录音时间信息,LCD显示的内容如图24.4.1所示:

图24.4.1音频录制中运行效果图
录音完成后我们可以将SD卡插入读取器在电脑中播放录音。