打印
[其他ST产品]

【正点原子K210连载】第二十四章 音频录制实验《DNK210使用指南-SDK版》

[复制链接]
372|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
第二十四章 音频录制实验

本章将介绍如何使用Kendryte K210的I2S功能,并将麦克风输入的声音数据以wav格式保存在SD卡中。通过学习本章内容,读者将掌握利用SDK编程技术实现wav音频文件的编码并保存的方法。
本章分为如下几个小节:
24.1 I2S录音简介
24.2 硬件设计
24.3 程序设计
24.4 运行验证


24.1 WAVI2S简介
本章涉及的知识点基本上在上一章都有介绍。本章要实现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.cmic.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.crecoder.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;        /* 16PCM */
    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 = iomem_malloc(REC_I2S_RX_DMA_BUF_SIZE);
/* I2S录音FIFO内存申请 */
        if (p_i2s_recfifo_buf == 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 = 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);   /* 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卡插入读取器在电脑中播放录音。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

93

主题

94

帖子

2

粉丝