第二十三章 音频播放实验
本章将介绍如何使用Kendryte K210的I2S功能,并将SD卡的音频数据通过I2S传输到扬声器实现音频的播放。通过学习本章内容,读者将掌握利用SDK编程技术实现wav音频解码并播放的方法。
本章分为如下几个小节:
23.1 WAV和I2S简介
23.2 硬件设计
23.3 程序设计
23.4 运行验证
23.1 WAV和I2S简介
23.1.1 WAV简介
WAV即WAVE文件,WAV是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),由于其扩展名为"*.wav"。它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW 等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16 位量化数字,因此在声音文件质量和CD相差无几!
WAV一般采用线性PCM(脉冲编码调制)编码,本章,我们也主要讨论PCM的播放,因为这个最简单。
WAV 文件是由若干个Chunk组成的。按照在文件中的出现位置包括:RIFF WAVE Chunk、Format Chunk、 Fact Chunk(可选)和Data Chunk。每个Chunk由块标识符、数据大小和数据三部分组成,如图 23.1.1.1 所示:

图23.1.1.1 Chunk组成结构
对于一个基本的WAVE文件而言,以下三种Chunk是必不可少的:文件中第一个Chunk是RIFF Chunk,然后是FMT Chunk,最后是Data Chunk。对于其他的Chunk,顺序没有严格的限制。使用WAVE文件的应用程序必须具有读取以上三种chunk信息的能力,如果程序想要复制WAVE文件,必须拷贝文件中所有的chunk。本章,我们主要讨论PCM,因为这个最简单,它只包含3个Chunk,我们看一下它的文件构成,如图23.1.2。

图23.1.2 PCM格式的wav文件构成
可以看到,不同的Chunk有不同的长度,编码文件时,按照Chunk的字节和位序排列好之后写入文件头,加上wav的后缀,就可以生成一个能被正确解析的wav文件了,对于PCM结构,我们只需要把获取到的音频数据填充到Data Chunk中即可。我们将利用VS1053实现16位,8Khz采样率的单声道WAV录音(PCM格式)。
首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:
typedef __PACKED_STRUCT
{
uint32_t ChunkID; /* chunk id;这里固定为"RIFF",即0X46464952 */
uint32_t ChunkSize ; /* 集合大小;文件总大小-8 */
uint32_t Format; /* 格式;WAVE,即0X45564157 */
}ChunkRIFF; /* RIFF块 */
接着,我们看看Format块(Format Chunk),该块以“fmt”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:
typedef __PACKED_STRUCT
{
uint32_t ChunkID; /* chunk id;这里固定为"fmt ",即0X20746D66 */
uint32_t ChunkSize ; /* 子集合大小(不包括ID和Size);这里为:20. */
uint16_t AudioFormat; /* 音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM */
uint16_t NumOfChannels; /* 通道数量;1,表示单声道;2,表示双声道; */
uint32_t SampleRate; /* 采样率;0X1F40,表示8Khz */
uint32_t ByteRate; /* /字节速率; */
uint16_t BlockAlign; /* 块对齐(字节); */
uint16_t BitsPerSample; /* 单个采样数据大小;4位ADPCM,设置为4 */
// uint16_t ByteExtraData; /* 附加的数据字节;2个; 线性PCM,没有这个参数 */
}ChunkFMT; /* fmt块 */
接下来,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:
typedef __PACKED_STRUCT
{
uint32_t ChunkID; /* chunk id;这里固定为"fact",即0X74636166; */
uint32_t ChunkSize; /* 子集合大小(不包括ID和Size);这里为:4. */
uint32_t NumOfSamples; /* 采样的数量; */
}ChunkFACT; /* fact块 */
DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。
最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”作为该Chunk的标示,然后是数据的大小。数据块的Chunk结构如下:
typedef __PACKED_STRUCT
{
uint32_t ChunkID; /* chunk id;这里固定为"data",即0X5453494C */
uint32_t ChunkSize ; /* 子集合大小(不包括ID和Size) */
}ChunkDATA; /* data块 */
ChunkSize后紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如表23.1.1.1所示的几种形式:

表23.1.1.1 WAVE文件数据采样格式
本章,我们播放的音频支持:16位和24位,立体声,所以每个取样为4/6个字节,低字节在前,高字节在后。在得到这些wav数据以后,通过I2S丢给ES8388,就可以欣赏音乐了。
23.1.2 I2S简介
I2S(Inter IC Sound)总线, 又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专责于音频设备之间的数据传输,广泛应用于各种多媒体系统。它采用了沿独立的导线传输时钟与数据信号的设计,通过将数据和时钟信号分离,避免了因时差诱发的失真,为用户节省了购买抵抗音频抖动的专业设备的费用。
Kendryte K210的I2S功能非常强大,集成电路内置音频总线共有3个(I2S0、I2S1、I2S2),都是MASTER模式。其中I2S0支持可配置连接语音处理模块,实现语音增强和声源定向的功能。下面是一些共有的特性:
1. 总线宽度可配置为8,16,和32位
2. 每个接口最多支持4个立体声通道
3. 由于发送器和接收器的独立性,所以支持全双工通讯
4. APB总线和I2S SCLK的异步时钟
5. 音频数据分辨率为12,16,20,24 和 32 位
6. I2S0发送FIFO深度为64字节,接收为8字节,I2S1和I2S2的发送和接收FIFO深度都为8字节
7. 支持DMA传输
6. 可编程FIFO阈值
Kendryte K210官方SDK提供了多个操作I2S接口的函数,这里我们只讲述本实验用到的函数,这些函数介绍如下:
1, i2s_init函数
该函数主要用于I2S的配置初始化,该函数原型及参数描述如下代码所示:
void i2s_init(i2s_device_number_t device_num, i2s_transmit_t rxtx_mode, uint32_t channel_mask);
/* I2S配总线号配置参数 */
typedef enum _i2s_device_number
{
I2S_DEVICE_0 = 0,
I2S_DEVICE_1 = 1,
I2S_DEVICE_2 = 2,
I2S_DEVICE_MAX
} i2s_device_number_t;
/* I2S工作模式配置参数 */
typedef enum _i2s_transmit
{
I2S_TRANSMITTER = 0,
I2S_RECEIVER = 1
} i2s_transmit_t;
该函数共有三个配置参数,第一个为配置I2S总线号,Kendryte K210共有三个I2S总线,第二个为I2S工作模式,可配置为接收或发送模式,第三个是配置I2S通道掩码,通常配置:通道0:0x03,通道1:0x0C,通道2:0x30,通道3:0xC0。
2,i2s_rx_channel_config函数
该函数用于设置I2S接收通道参数,该函数原型及参数描述如下代码所示:
void i2s_rx_channel_config(i2s_device_number_t device_num,
i2s_channel_num_t channel_num,
i2s_word_length_t word_length,
i2s_word_select_cycles_t word_select_size,
i2s_fifo_threshold_t trigger_level,
i2s_work_mode_t word_mode);
/* I2S通道号配置参数 */
typedef enum _i2s_channel_num
{
I2S_CHANNEL_0 = 0,
I2S_CHANNEL_1 = 1,
I2S_CHANNEL_2 = 2,
I2S_CHANNEL_3 = 3
} i2s_channel_num_t;
/* I2S接收数据位数配置参数 */
typedef enum _word_length
{
/* Ignore the word length */
IGNORE_WORD_LENGTH = 0x0,
/* 12-bit data resolution of the receiver */
RESOLUTION_12_BIT = 0x1,
/* 16-bit data resolution of the receiver */
RESOLUTION_16_BIT = 0x2,
/* 20-bit data resolution of the receiver */
RESOLUTION_20_BIT = 0x3,
/* 24-bit data resolution of the receiver */
RESOLUTION_24_BIT = 0x4,
/* 32-bit data resolution of the receiver */
RESOLUTION_32_BIT = 0x5
} i2s_word_length_t;
/* I2S单个数据时钟数配置参数 */
typedef enum _word_select_cycles
{
/* 16 sclk cycles */
SCLK_CYCLES_16 = 0x0,
/* 24 sclk cycles */
SCLK_CYCLES_24 = 0x1,
/* 32 sclk cycles */
SCLK_CYCLES_32 = 0x2
} i2s_word_select_cycles_t;
/*DMA触发时FIFO深度配置参数 */
typedef enum _fifo_threshold
{
/* Interrupt trigger when FIFO level is 1 */
TRIGGER_LEVEL_1 = 0x0,
/* Interrupt trigger when FIFO level is 2 */
TRIGGER_LEVEL_2 = 0x1,
/* Interrupt trigger when FIFO level is 3 */
TRIGGER_LEVEL_3 = 0x2,
/* Interrupt trigger when FIFO level is 4 */
TRIGGER_LEVEL_4 = 0x3,
/* Interrupt trigger when FIFO level is 5 */
TRIGGER_LEVEL_5 = 0x4,
/* Interrupt trigger when FIFO level is 6 */
TRIGGER_LEVEL_6 = 0x5,
/* Interrupt trigger when FIFO level is 7 */
TRIGGER_LEVEL_7 = 0x6,
/* Interrupt trigger when FIFO level is 8 */
TRIGGER_LEVEL_8 = 0x7,
/* Interrupt trigger when FIFO level is 9 */
TRIGGER_LEVEL_9 = 0x8,
/* Interrupt trigger when FIFO level is 10 */
TRIGGER_LEVEL_10 = 0x9,
/* Interrupt trigger when FIFO level is 11 */
TRIGGER_LEVEL_11 = 0xa,
/* Interrupt trigger when FIFO level is 12 */
TRIGGER_LEVEL_12 = 0xb,
/* Interrupt trigger when FIFO level is 13 */
TRIGGER_LEVEL_13 = 0xc,
/* Interrupt trigger when FIFO level is 14 */
TRIGGER_LEVEL_14 = 0xd,
/* Interrupt trigger when FIFO level is 15 */
TRIGGER_LEVEL_15 = 0xe,
/* Interrupt trigger when FIFO level is 16 */
TRIGGER_LEVEL_16 = 0xf
} i2s_fifo_threshold_t;
/* I2S工作模式配置参数 */
typedef enum _i2s_work_mode
{
STANDARD_MODE = 1,
RIGHT_JUSTIFYING_MODE = 2,
LEFT_JUSTIFYING_MODE = 4
} i2s_work_mode_t;
该函数涉及6个参数,参数可配置选项上面结构体中已列出,大家根据自己需要配置即可,这里不再详细介绍。
3,i2s_tx_channel_config函数
该函数用于设置I2S发送通道参数,该函数原型及参数描述如下代码所示:
void i2s_tx_channel_config(i2s_device_number_t device_num, i2s_channel_num_t channel_num, i2s_word_length_t word_length, i2s_word_select_cycles_t word_select_size, i2s_fifo_threshold_t trigger_level, i2s_work_mode_t word_mode);
该函数可配置6个参数,参数可配置选项和接收通道相同,大家根据自己需要配置即可。
4,i2s_play函数
该函数用来发送PCM数据,比如音乐播放,该函数原型及参数描述如下所示:
void i2s_play(i2s_device_number_t device_num, dmac_channel_number_t channel_num,
const uint8_t *buf, size_t buf_len, size_t frame, size_t bits_per_sample, uint8_t track_num);
/* 发送PCM数据的DMA通道配置参数 */
typedef enum _dmac_channel_number
{
DMAC_CHANNEL0 = 0,
DMAC_CHANNEL1 = 1,
DMAC_CHANNEL2 = 2,
DMAC_CHANNEL3 = 3,
DMAC_CHANNEL4 = 4,
DMAC_CHANNEL5 = 5,
DMAC_CHANNEL_MAX
} dmac_channel_number_t;
函数共有7个参数,分别配置I2S总线号、DMA通道号、PCM数据、PCM数据长度、单次发送数量,单次采样位宽和声道数。
5,i2s_set_sample_rate函数
该函数用来设置I2S总线的采样率,函数原型如下所示:
uint32_t i2s_set_sample_rate(i2s_device_number_t device_num, uint32_t sample_rate);
函数共有两个参数,第一个参数是选择总线号,第二个参数是设置I2S的采样率。
6,i2s_receive_data_dma函数
该函数的功能是从DMA读取PCM数据,函数原型如下所示:
void i2s_receive_data_dma(i2s_device_number_t device_num, uint32_t *buf, size_t buf_len, dmac_channel_number_t channel_num);
函数共有四个参数,第一个参数是选择总线号,第二个参数是接收数据的地址,第三个参数是接收数据的长度,第四个参数是DMA通道号。
7,i2s_send_data_dma函数
该函数的功能是通过DMA发送PCM数据,并等待DMA完成,函数原型如下所示:
void i2s_send_data_dma(i2s_device_number_t device_num, const void *buf, size_t buf_len, dmac_channel_number_t channel_num);
函数共有四个参数,第一个参数是选择总线号,第二个参数是要发送的数据地址,第三个参数是发送数据的长度,第四个参数是DMA通道号。
23.2 硬件设计
23.2.1 例程功能
1.开发板开机上电后自动播放SD卡目录MUSIC文件夹的play.wav音频文件(提前将音频文件拷贝到SD卡根目录的MUSIC文件夹中), LCD模块显示播放总时间和当前播放时间,播放完成后程序退出。
23.2.2 硬件资源
1. 扬声器与功放连接
扬声器正极 - VoP
扬声器负极 - VoN
2. 数字功放NS4168
SPK_CTRL - IO21
IIS_SDOUT - IO31
IIS_BCK - IO32
IIS_LRCK - IO33
3. LCD
LCD_RD - IO34
LCD_BL - IO35
LCD_CS - IO36
LCD_RST - IO37
LCD_RS - IO38
LCD_WR - IO39
LCD_D0~LCD_D7 - SPI0_D0~SPI0_D7
23.2.3 原理图
本章实验内容,需要解析文件系统中的WAV文件,然后将音频数据通过I2S接口发送到数字功放NS4168,随后NS4168便可根据配置,控制板载的扬声器发声。
DNK210开发板上的数字功放NS4168的连接原理图,如下图所示:

图23.2.3.1 数字功放NS4168连接原理图
关于数字功放NS4168的使用方法,可参考NS4168的数据手册,NS4168的数据手册可通过网站获取,网址为:http://www.nsiway.com.cn/product/18.html。
这里简单对NS4168的CTRL引脚进行说明,当CTRL引脚上的电压为0V~0.4V时,NS4168处于低功耗关断状态,当CTRL引脚上的电压为0.9V~1.15V时,NS4168控制扬声器播放I2S接口接收到音频数据中的左声道数据,当CTRL引脚上的电压为1.5V~VDD时,NS4168控制扬声器播放I2S接口接收到的音频数据中的右声道数据。
23.3 程序设计
23.3.1 speaker驱动代码
本实验是通过I2S将数据输入到数字功放实现音频的播放,数字功放也需要相应的驱动代码进行配置,驱动源码包括两个文件:speaker.c和speaker.h,我们先介绍speaker.h文件的内容。
/*****************************HARDWARE-PIN*********************************/ /* 硬件IO口,与原理图对应 */ #define PIN_SPK_CTRL (21) #define PIN_SPK_WS (33) #define PIN_SPK_DATA (31) #define PIN_SPK_BCK (32) /*****************************SOFTWARE-GPIO********************************/ /* 软件GPIO口,与程序对应 */ #define SPK_CTRL_GPIONUM (0) /*****************************FUNC-GPIO************************************/ /* GPIO口的功能,绑定到硬件IO口 */ #define FUNC_SPK_CTRL (FUNC_GPIO0 + SPK_CTRL_GPIONUM) #define FUNC_SPK_WS FUNC_I2S0_WS #define FUNC_SPK_DATA FUNC_I2S0_OUT_D0 #define FUNC_SPK_BCK FUNC_I2S0_SCLK
这个是驱动引脚功能的绑定,根据原理图可知,IO21为控制数字功放的引脚,用普通GPIO功能即可,其他三个为I2S功能引脚,I2S数据引脚配置为输出即可。
下面看speaker.c文件代码。
void speaker_i2s_hardware_init(void)
{
/* I2S 初始化 */
gpio_init(); /* 使能GPIO的时钟 */
fpioa_set_function(PIN_SPK_CTRL, FUNC_SPK_CTRL);
fpioa_set_function(PIN_SPK_WS, FUNC_SPK_WS);
fpioa_set_function(PIN_SPK_DATA, FUNC_SPK_DATA);
fpioa_set_function(PIN_SPK_BCK, FUNC_SPK_BCK);
gpio_set_drive_mode(SPK_CTRL_GPIONUM, GPIO_DM_OUTPUT); /*输出模式*/
gpio_set_pin(SPK_CTRL_GPIONUM, GPIO_PV_HIGH); /*输出为高,使能扬声器输出*/
}
可以看到,普通GPIO功能使用前需要进行GPIO使能,然后绑定引脚功能,最后将控制引脚拉高,使能数字功放输出。
23.3.2 wavplay代码
wavpaly主要用于wav格式的音频文件解码,接下来看看wavplay.c里面的几个函数,代码如下:
/**
* @brief WAV解析初始化
* @param fname : 文件路径+文件名
* @param wavx : 信息存放结构体指针
* @retval 0,打开文件成功
* 1,打开文件失败
* 2,非WAV文件
* 3,DATA区域未找到
*/
uint8_t wav_decode_init(uint8_t* fname, __wavctrl* wavx)
{
FIL *ftemp;
uint8_t *buf;
uint32_t br = 0;
uint8_t res = 0;
ChunkRIFF *riff;
ChunkFMT *fmt;
ChunkFACT *fact;
ChunkDATA *data;
ftemp = (FIL*)iomem_malloc(sizeof(FIL));
buf = iomem_malloc(512);
if (ftemp && buf) /* 内存申请成功 */
{
res = f_open(ftemp, (TCHAR*)fname, FA_READ); /* 打开文件 */
if (res==FR_OK)
{
f_read(ftemp, buf, 512, &br); /* 读取512字节在数据 */
riff = (ChunkRIFF *)buf; /* 获取RIFF块 */
if (riff->Format == 0X45564157) /* 是WAV文件 */
{
fmt = (ChunkFMT *)(buf + 12); /* 获取FMT块 */
fact = (ChunkFACT *)(buf + 12 + 8 + fmt->ChunkSize);
if (fact->ChunkID == 0X74636166 || fact->ChunkID == 0X5453494C)
{
wavx->datastart = 12 + 8 + fmt->ChunkSize + 8 + fact->
ChunkSize; /* 具有fact/LIST块的时候(未测试) */
}
else
{
wavx->datastart = 12 + 8 + fmt->ChunkSize;
}
data = (ChunkDATA *)(buf + wavx->datastart); /* 读取DATA块 */
if (data->ChunkID == 0X61746164) /* 解析成功! */
{
wavx->audioformat = fmt->AudioFormat; /* 音频格式 */
wavx->nchannels = fmt->NumOfChannels; /* 通道数 */
wavx->samplerate = fmt->SampleRate; /* 采样率 */
wavx->bitrate = fmt->ByteRate * 8; /* 得到位速 */
wavx->blockalign = fmt->BlockAlign; /* 块对齐 */
wavx->bps = fmt->BitsPerSample; /* 位数,16/24/32位 */
wavx->datasize = data->ChunkSize; /* 数据块大小 */
wavx->datastart = wavx->datastart + 8; /* 数据流开始的地方 */
printf("wavx->audioformat:%d\r\n", wavx->audioformat);
printf("wavx->nchannels:%d\r\n", wavx->nchannels);
printf("wavx->samplerate:%d\r\n", wavx->samplerate);
printf("wavx->bitrate:%d\r\n", wavx->bitrate);
printf("wavx->blockalign:%d\r\n", wavx->blockalign);
printf("wavx->bps:%d\r\n", wavx->bps);
printf("wavx->datasize:%d\r\n", wavx->datasize);
printf("wavx->datastart:%d\r\n", wavx->datastart);
}
else
{
res = 3; /* data区域未找到. */
}
}
else
{
res = 2; /* 非wav文件 */
}
}
else
{
res = 1; /* 打开文件错误 */
}
}
f_close(ftemp); /* 关闭文件 */
printf("f_clase\n");
iomem_free(ftemp); /* 释放内存 */
iomem_free(buf);
return 0;
}
/**
* @brief 填充buf
* @param buf : 填充区
* @param size : 填充数据量
* @param bits : 位数(16)
* @retval 读取到的数据长度
*/
uint32_t wav_buffill(uint8_t *buf, uint16_t size, uint8_t bits)
{
uint32_t bread;
uint16_t i;
if (bits == 16)
{
f_read(g_audiodev.file, buf, size, (UINT*)&bread);
if (bread < size) /* 不够数据了,补充0 */
{
for(i = bread; i < size - bread; i++)
{
buf[i] = 0;
}
}
}
// else if (bits == 24) /* 24bit音频,需要处理一下 */
// }
return bread;
}
/**
* @brief 音频播放的buf填充
* @param 无
* @retval 无
*/
void wav_play_buffill(void)
{
if(wavbuf1f1nish == 0 && wavplay == 1)
{
fillnum = wav_buffill(g_audiodev.i2sbuf1, WAV_I2S_TX_DMA_BUFSIZE,
wavctrl.bps); /* 填充buf1 */
wavbuf1f1nish = 1;
}
else if (wavbuf2f1nish == 0 && wavplay == 0)
{
fillnum = wav_buffill(g_audiodev.i2sbuf2, WAV_I2S_TX_DMA_BUFSIZE,
wavctrl.bps); /* 填充buf1 */
wavbuf2f1nish = 1;
}
}
/**
* @brief I2S音频文件传输
* @param device_num : I2S总线号
* @param channel_num : DM通道号
* @param buf : PCM数据
* @param buf_len : 发送德数据长度
* @param frame : 单次发送的声音数据量
* @param bits_per_sample : 位宽
* @param track_num : 声道数
* @retval 无
*/
void i2s_pcm_play(i2s_device_number_t device_num, dmac_channel_number_t channel_num,
const uint8_t *buf, size_t buf_len, size_t frame, size_t bits_per_sample, uint8_t track_num)
{
const uint8_t *trans_buf;
uint32_t i;
size_t sample_cnt = buf_len / (bits_per_sample / 8) / track_num;
size_t frame_cnt = sample_cnt / frame;
size_t frame_remain = sample_cnt % frame;
i2s_set_dma_divide_16(device_num, 0);
if(bits_per_sample == 16 && track_num == 2)
{
i2s_set_dma_divide_16(device_num, 1);
for(i = 0; i < frame_cnt; i++)
{
trans_buf = buf + i * frame * (bits_per_sample / 8) * track_num;
/* 按声音数量发送,一个声音大小和位数,声道相关 */
i2s_send_data_dma(device_num, trans_buf, frame, channel_num);
wav_play_buffill();
}
if(frame_remain)
{
trans_buf = buf + frame_cnt * frame * (bits_per_sample / 8) * track_num;
i2s_send_data_dma(device_num, trans_buf, frame_remain, channel_num);
wav_play_buffill();
}
}
}
/**
* @brief 获取当前播放时间
* @param fname : 文件指针
* @param wavx : wavx播放控制器
* @retval 无
*/
void wav_get_curtime(FIL *fx, __wavctrl *wavx)
{
long long fpos;
wavx->totsec = wavx->datasize / (wavx->bitrate / 8); /* 歌曲总长度(单位:秒) */
fpos = fx->fptr-wavx->datastart; /* 得到当前文件播放到的地方 */
wavx->cursec = fpos*wavx->totsec / wavx->datasize; /* 当前播放到第多少秒了? */
}
/**
* @brief 显示播放时间,比特率 信息
* @param totsec : 音频文件总时间长度
* @param cursec : 当前播放时间
* @retval 无
*/
void audio_msg_show(uint32_t totsec, uint32_t cursec)
{
char datashow[35];
static uint16_t playtime = 0XFFFF; /* 放时间标记 */
if (playtime != cursec) /* 需要更新显示时间 */
{
playtime = cursec;
/* 显示播放时间 */
sprintf((char *)datashow, "Play time:%02d:%02d/%02d:%02d", playtime / 60, playtime % 60, totsec / 60, totsec % 60);
// lcd_draw_fill_rectangle(10 + 8 * 11, 30, 10 + 8 * 15, 30 + 16, WHITE);
// lcd_draw_string(10, 30, (char *)datashow, BLUE); /* 显示SD卡容量 */
/* 显示字符的形式实在太慢了,会导致播放卡顿,所以用画图的形式显示 */
for (size_t i = 0; i < 320 * 16; i++)
{
lcd_gram[i] = 0xFFFF;
}
draw_string_rgb565_image(lcd_gram, 320, 240, 10, 0, datashow, BLUE);
lcd_draw_picture(0, 30, 320, 16, (uint16_t *)lcd_gram);
}
}
/**
* @brief 播放某个wav文件
* @param fname : 文件路径+文件名
* @retval KEY0_PRES,错误
* KEY1_PRES,打开文件失败
* 其他,非WAV文件
*/
uint8_t wav_play_song(uint8_t* fname)
{
uint8_t res;
g_audiodev.file = (FIL*)iomem_malloc(sizeof(FIL));
g_audiodev.i2sbuf1 = iomem_malloc(WAV_I2S_TX_DMA_BUFSIZE);
g_audiodev.i2sbuf2 = iomem_malloc(WAV_I2S_TX_DMA_BUFSIZE);
g_audiodev.tbuf = iomem_malloc(WAV_I2S_TX_DMA_BUFSIZE);
if (g_audiodev.file && g_audiodev.i2sbuf1 && g_audiodev.i2sbuf2 && g_audiodev.tbuf)
{
res = wav_decode_init(fname, &wavctrl); /* 得到文件的信息 */
if (res == 0) /* 解析文件成功 */
{
/*初始化I2S,第三个参数为设置通道掩码,通道0:0x03,通道1:0x0C,通道2:0x30,通道3:0xC0*/
i2s_init(I2S_DEVICE_0, I2S_TRANSMITTER, 0x03);
if (wavctrl.bps == 16)
{
/* 设置I2S发送数据的通道参数 */
i2s_tx_channel_config(
I2S_DEVICE_0, /* I2S设备号*/
I2S_CHANNEL_0, /* I2S通道 */
RESOLUTION_16_BIT, /* 接收数据位数 */
SCLK_CYCLES_32, /* 单个数据时钟数 */
TRIGGER_LEVEL_4, /* DMA触发时FIFO深度 */
STANDARD_MODE); /* 工作模式 */
}
// else if (wavctrl.bps == 24) /* 暂不支持 */
// {
// }
res = f_open(g_audiodev.file, (TCHAR*)fname, FA_READ); /* 打开文件 */
if (res == 0)
{
f_lseek(g_audiodev.file, wavctrl.datastart); /* 跳过文件头 */
fillnum = wav_buffill(g_audiodev.i2sbuf1, WAV_I2S_TX_DMA_BUFSIZE, wavctrl.bps);
// fillnum = wav_buffill(g_audiodev.i2sbuf2, WAV_I2S_TX_DMA_BUFSIZE, wavctrl.bps);
wavbuf1f1nish = 1;
wavbuf2f1nish = 0;
wavplay = 0;
while (1)
{
if (wavplay)
{
i2s_pcm_play(
I2S_DEVICE_0, /* I2S设备号 */
DMAC_CHANNEL1, /* DMA通道号 */
(uint8_t *)g_audiodev.i2sbuf2, /* 播放的PCM数据 */
WAV_I2S_TX_DMA_BUFSIZE, /* PCM数据的长度 */
1024, /* 单次发送数量 */
16, /* 单次采样位宽 */
2); /* 声道数 */
wavbuf2f1nish = 0;
wavplay = 0;
}
else
{
i2s_pcm_play(
I2S_DEVICE_0, /* I2S设备号 */
DMAC_CHANNEL1, /* DMA通道号 */
(uint8_t *)g_audiodev.i2sbuf1, /* 播放的PCM数据 */
WAV_I2S_TX_DMA_BUFSIZE, /* PCM数据的长度 */
1024, /* 单次发送数量 */
16, /* 单次采样位宽 */
2); /* 声道数 */
wavbuf1f1nish = 0;
wavplay = 1;
}
if (fillnum != WAV_I2S_TX_DMA_BUFSIZE)/*文件剩余不足,播放完束*/
{
dmac_wait_done(DMAC_CHANNEL0);
printf("play end\n");
break;
}
wav_get_curtime(g_audiodev.file, &wavctrl);
/* 得到总时间和当前播放的时间 */
audio_msg_show(wavctrl.totsec, wavctrl.cursec);
}
}
else
{
res = 0XFF;
}
}
else
{
res = 0XFF;
}
}
else
{
res = 0XFF;
}
iomem_free(g_audiodev.tbuf); /* 释放内存 */
iomem_free(g_audiodev.i2sbuf1); /* 释放内存 */
iomem_free(g_audiodev.i2sbuf2); /* 释放内存 */
iomem_free(g_audiodev.file); /* 释放内存 */
return res;
}
以上代码中,wav_decode_init函数,用来对wav音频文件进行解析,得到wav的详细信息(音频采样率,位数,数据流起始位置等);wav_buffill函数,通过f_read读取数据,然后将数据填充到buf里面;wav_play_buffill函数则是播放过程中提前对将要播放的数据buf进行填充,这里,用wavbuf1f1nish和wavbuf2f1nish两个变量判断buf是否填充完成,变量wavplay指示当前播放的buf,通过这几个变量,我们就能在音频播放前处理buf数据,i2s_pcm_play函数是我们参考官方API函数编写,用于播放PCM数据。
最后,wav_play_song函数,是播放WAV最终执行的函数,该函数解析完WAV文件后,设置I2S的参数(采样率,位数等),调用i2s_pcm_play开始播放,然后不断填充数据,实现WAV播放,播放完成后函数提示播放结束并退出。
23.3.3 main.c代码
main.c中的代码如下所示:
char *pname = {"0: MUSIC/play.wav"};
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);
speaker_i2s_hardware_init(); /* 扬声器接口初始化 */
/* 初始化中断,使能全局中断,初始化dmac */
plic_init();
sysctl_enable_irq();
dmac_init();
lcd_init(); /* 初始化LCD */
lcd_set_direction(DIR_YX_LRUD);
lcd_draw_string(10, 10, "MUSIC ", RED);
/* 初始化SD卡*/
while (sd_init() != 0) /* 检查字库 */
{
lcd_draw_string(10, 30, "SD Error!", BLUE); /* 提示SD卡错误 */
msleep(200);
lcd_draw_fill_rectangle(10, 30, 319, 50, WHITE); /* 清除显示 */
msleep(200);
}
printf("SD card initialization succeed!\n");
/* 挂载SD卡 */
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");
wav_play_song((uint8_t *)pname); /* 播放音频play.wav */
lcd_draw_string(10, 50, "End of play ", BLUE);
while (1)
{
}
}
在main函数前面,我们定义了pname数组,用于存放音频文件的SD卡路径。main函数代码具体流程大致是:首先完成系统级和用户级初始化工作,挂载SD卡,挂载成功后播放SD卡根目录中文件名为pname中的wav文件,播放结束后LCD显示“End of play”。
23.4 运行验证
将DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,程序启动后开始播放音频文件,可以看到LCD显示播放时间信息,LCD显示的内容如图23.4.1所示:

图23.4.1音频播放中运行效果图
等待音频播放结束,LCD提示播放结束信息,得到如图23.4.2所示:

图23.4.2播放结束后的显示效果图