TinyMP3音乐播放器-基于MAX78000
基于MAX78000设计的简易音乐播放器,主要目标是读取存储在Sd卡中的音乐文件,解码,然后利用板子自带的codec,然后通过line-out接口输出
标签
嵌入式系统
音乐播放器
MAX78000
littlestudent
更新2023-01-31
1314

首先非常感谢硬禾学堂和ADI公司联合举办的这次MAX78000项目活动,本次的板卡非常强大,具有非常丰富的板卡资源。

我在本次活动中实现的TinyMP3音乐播放器可以实现读取SD卡里的音乐文件,然后通过开源的libmad算法进行解码,然后通过耳机或者外接扬声器播放出来。

一、板卡资源介绍

MAX78000FTHR为快速开发平台,核心集成了卷积神经网络加速器,帮助工程师帮助工程师快速实现超低功耗、人工智能(AI)方案的搭建。Maxim提供的library中有关MAX78000的是“AI85”相关的源文件。

AI84 Unreleased test chip
AI85 MAX78000 (full production)
AI87 MAX78002 (engineering samples)


1. 核心芯片:MAX78000微控制器

2. 电池电源管理:MAX20303 PMIC

3. 规格:0.9in x 2.6in、双排连接器,兼容Adafruit Feather Wing外设扩展板

4. 音频处理:多关键词识别、声音分类、消噪声

5. 面部识别

6. 目标检测和分类

7. 时间序列数据处理:心率/健康信号分析、多传感器分析、预测性维护

8. 集成外设:

  • RGB指示LED
  • 用户按钮
  • CMOS VGA图像传感器(OVM769)
  • 低功耗、立体声音频编解码器(MAX9867)
  • SPH0645LM4H-B数字麦克风
  • SWD调试器
  • 虚拟UART控制台
  • 10引脚Cortex调试接头,用于RISC-V协处理器

FjSp_lOeoCfIGUYs8_BG1qmEnI2C

二、OLED屏幕驱动

之所以尝试驱动屏幕,是希望音乐在播放的时候,能够通过屏幕显示歌曲相关信息,比如歌名等。这次我使用的屏幕是第一次参加Funpack活动时候购买的OLED屏幕,如下所示。

Fo3mMpfY2XN8nVJIKuf4_4QFFJpt

此次利用MAX78000的模拟SPI接口来驱动屏幕:

屏幕序号 屏幕引脚标号 说明 MAX7800引脚
1 GND 接地 GND
2 VCC 5V/3.3V电源输入 3.3V
3 SCL SPI总线时钟信号
MXC_GPIO2, MXC_GPIO_PIN_3
4 SDA SPI总线“写”数据信号
MXC_GPIO2, MXC_GPIO_PIN_4
5 RES 液晶屏复位信号,低电平复位
MXC_GPIO1, MXC_GPIO_PIN_1
6 DC 液晶屏寄存器/数据选择信号,低电平:寄存器;高电平:数据
MXC_GPIO1, MXC_GPIO_PIN_0
7 CS 屏幕片选信号,低电平使能
MXC_GPIO1, MXC_GPIO_PIN_6

首先进行OLED相关GPIO初始化:

void OLED_PIN_Init(void)
{
    mxc_gpio_cfg_t oled_scl_pin = {MXC_GPIO2, MXC_GPIO_PIN_3, 
    MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    MXC_GPIO_Config(&oled_scl_pin);

    mxc_gpio_cfg_t oled_sda_pin = {MXC_GPIO2, MXC_GPIO_PIN_4, 
    MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    MXC_GPIO_Config(&oled_sda_pin);

    mxc_gpio_cfg_t oled_res_pin = {MXC_GPIO1, MXC_GPIO_PIN_1, 
    MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    MXC_GPIO_Config(&oled_res_pin);

    mxc_gpio_cfg_t oled_dc_pin = {MXC_GPIO1, MXC_GPIO_PIN_0, 
    MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    MXC_GPIO_Config(&oled_dc_pin);

    mxc_gpio_cfg_t oled_cs_pin = {MXC_GPIO1, MXC_GPIO_PIN_6, 
    MXC_GPIO_FUNC_OUT, MXC_GPIO_PAD_NONE, MXC_GPIO_VSSEL_VDDIOH};
    MXC_GPIO_Config(&oled_cs_pin);
}

之后需要定义好相关OLED屏幕操作的函数:

#define OLED_SCL_Clr() MXC_GPIO_OutClr(MXC_GPIO2, MXC_GPIO_PIN_3)        //0
#define OLED_SCL_Set() MXC_GPIO_OutSet(MXC_GPIO2, MXC_GPIO_PIN_3)	//1

#define OLED_SDA_Clr() MXC_GPIO_OutClr(MXC_GPIO2, MXC_GPIO_PIN_4)       //0
#define OLED_SDA_Set() MXC_GPIO_OutSet(MXC_GPIO2, MXC_GPIO_PIN_4)  	//1

#define OLED_RES_Clr() MXC_GPIO_OutClr(MXC_GPIO1, MXC_GPIO_PIN_1)        //0
#define OLED_RES_Set() MXC_GPIO_OutSet(MXC_GPIO1, MXC_GPIO_PIN_1)   	//1

#define OLED_DC_Clr()  MXC_GPIO_OutClr(MXC_GPIO1, MXC_GPIO_PIN_0)        //0 
#define OLED_DC_Set()  MXC_GPIO_OutSet(MXC_GPIO1, MXC_GPIO_PIN_0)       //1 
 		     
#define OLED_CS_Clr()  MXC_GPIO_OutClr(MXC_GPIO1, MXC_GPIO_PIN_6)   	//0
#define OLED_CS_Set()  MXC_GPIO_OutSet(MXC_GPIO1, MXC_GPIO_PIN_6)   	//1

然后添加相应的路径到makefile文件,这样在编译的时候才能找到正确的源文件。

VPATH += oled
IPATH += oled

上电测试,屏幕工作正常。

Fju3zMQsCYAa08qGnUv0N5LXtL21

三、SD卡读取MP3文件

MP3 音频的播放涉及到音频驱动、SD 卡读写文件的实现,我这里参考的是MAXIAM官方例程中的ImgCapture。主要是通过sd_mount来挂载SD卡,然后通过调用sd_ls()来打印出sd卡当前目录下的内容。

FiB7048T2VfQu3do7COF4sO8Z1Ub

到此,OLED屏幕显示,SD读取MP3文件都正常工作,下一步是调用开源的libmad算法并设计好I2S与DMA的配合,从而才能正常的播放音乐。

四、I2S, DMA, CODEC初始化

声音是通过一定介质传播的连续的波,由振幅和周期两个重要指标。我们正常人可以听到的声音频率范围是20 Hz ~ 20 kHz。声音的存储(模拟转数字)或者播放(数字转模拟)都要涉及到如下的重要参数:

  • 采样频率:每秒抽取声波振幅样本的次数。常用的采样频率有:11.025 kHz, 22.05 kHz, 44.1 kHz和96 kHz。
  • 量化位数:每个采样点用多少个二进制位表示数据范围。
  • 声道数:使用声道的个数。
  • 音频数据量:采样频率(Hz)✖ 量化位数 ✖ 声道数/8,单位: B/s

TinyMP3音乐播放器的实现离不开I2S, DMA CODEC等重要外设的支持,这里要特别注意是I2S_TxStart(), I2S_TxStop()以及I2S, DMA如何协同工作。首先是I2S模块初始化:

/**
 * @brief I2S - 数字音频接口初始化
 * 
 */
void i2s_init(void)
{
    mxc_i2s_req_t req;

#define I2S_CRUFT_PTR (void *)UINT32_MAX
#define I2S_CRUFT_LEN UINT32_MAX

    req.wordSize = MXC_I2S_DATASIZE_WORD;
    req.sampleSize = MXC_I2S_SAMPLESIZE_THIRTYTWO;
    req.justify = MXC_I2S_MSB_JUSTIFY;
    req.wsPolarity = MXC_I2S_POL_NORMAL;
    req.channelMode = MXC_I2S_INTERNAL_SCK_WS_0;
    req.stereoMode = MXC_I2S_STEREO;

    req.bitOrder = MXC_I2S_MSB_FIRST;
    req.clkdiv = CLK_DIV;

    req.rawData = NULL;
    req.txData = I2S_CRUFT_PTR;
    req.rxData = I2S_CRUFT_PTR;
    req.length = I2S_CRUFT_LEN;

    if (MXC_I2S_Init(&req) != E_NO_ERROR)
        blink_halt("Error initializing I2S");

    MXC_I2S_SetFrequency(MXC_I2S_EXTERNAL_SCK_EXTERNAL_WS, 0);
}

然后是DMA初始化:

void dma_init(void)
{
    MXC_NVIC_SetVector(DMA0_IRQn, dma_handler);
    MXC_NVIC_SetVector(DMA1_IRQn, dma_handler);
    NVIC_EnableIRQ(DMA0_IRQn);
    NVIC_EnableIRQ(DMA1_IRQn);
}

音频编解码器初始化:

/**
 * @brief 音频编解码器初始化
 * 
 */
void codec_init(void)
{
    if (max9867_init(CODEC_I2C, CODEC_MCLOCK, 1) != E_NO_ERROR)
        blink_halt("Error initializing MAX9867 CODEC");

    if (max9867_enable_playback(1) != E_NO_ERROR)
        blink_halt("Error enabling playback path");

    if (max9867_playback_volume(-34, -34) != E_NO_ERROR)
        blink_halt("Error setting playback volume");
}

然后注册dma传输完成的callback函数:

MXC_I2S_RegisterDMACallback(dma_callback);

void dma_callback(int channel, int result)
{
    uint8_t *tx_buf = (volatile uint8_t *)I2SState.TxBuffer[I2SState.TxReadIndex];
    /* Enqueue the same original buffer all over again */
    
	if (I2SState.TxReadIndex != I2SState.TxWriteIndex) {
		if (I2SState.TxReadIndex >= AUDIO_NUM_BUFFERS-1) {
			I2SState.TxReadIndex = 0;
		} else {
			I2SState.TxReadIndex++;
		}
	}
    MXC_DMA_ReleaseChannel(dma_ch_tx);
    dma_ch_tx = MXC_I2S_TXDMAConfig(tx_buf, sizeof(I2SState.TxBuffer[I2SState.TxReadIndex]));    
	I2SState.TxEvent = 1;
}

最后是I2S_TxStart(), I2S_TxStop()的实现。

void I2S_TxStart(void)
{
    MXC_I2S_TXEnable();

    static uint8_t *tx_buf = (volatile uint8_t *)I2SState.TxBuffer[0];

	I2SState.TxReadIndex = 0;
    dma_ch_tx = MXC_I2S_TXDMAConfig(tx_buf, sizeof(I2SState.TxBuffer[0]));

	I2SState.TxWriteIndex = 1;	
    dma_ch_tx = MXC_I2S_TXDMAConfig(tx_buf, sizeof(I2SState.TxBuffer[0]));

}

void I2S_TxStop(void){
    MXC_I2S_TXDisable();
}

 

五、MP3文件解码

MP3是一种音频压缩技术,其全称是动态影像专家压缩标准音频层面3(Moving Picture Experts Group Audio Layer III),简称为MP3。它被设计用来大幅度地降低音频数据量。利用 MPEG Audio Layer 3 的技术,将音乐以1:10 甚至 1:12 的压缩率,压缩成容量较小的文件,而对于大多数用户来说重放的音质与最初的不压缩音频相比没有明显的下降。它是在1991年由位于德国埃尔朗根的研究组织Fraunhofer-Gesellschaft的一组工程师发明和标准化的。用MP3形式存储的音乐就叫作MP3音乐,能播放MP3音乐的机器就叫作MP3播放器。

MP3文件的解码是一项非常复杂的工作,感谢开源界前辈的奉献,我在此次活动中采样了开源的libmad算法。MAD (libmad)是一个开源的高精度 MPEG 音频解码库,支持 MPEG-1(Layer I, Layer II 和 LayerIII(也就是 MP3)。LIBMAD 提供 24-bit 的 PCM 输出,完全是定点计算,非常适合没有浮点支持的平台上使用。使用 libmad 提供的一系列 API,就可以非常简单地实现 MP3 数据解码工作。在 libmad 的源代码文件目录下的 mad.h 文件中,可以看到绝大部分该库的数据结构和 API 等。libmad的源码可以从网站下载到:

https://www.linuxfromscratch.org/blfs/view/svn/multimedia/libmad.html

将下载好的源文件放入项目下面的libmad文件夹。

FqMgwP_DpRFGxTvS2mxGYIjYlWs3

然后添加相应的路径到makefile文件,这样在编译的时候才能找到正确的源文件。

VPATH += libmad
IPATH += libmad

libmad算法的调用在主函数中非常的简单:

	while (1) {
		if (ret == 0) {
            OLED_ShowString(10,20,"PLAYing: start.mp3",8,1);
            OLED_Refresh();

			ret = MP3_Play("start.mp3");

            OLED_ShowString(10,40,"PLAY Finished",8,1);
            OLED_Refresh();
		}
	}  

在MP3_Play()函数中会调用libmad算法的decode()来开始解码mp3文件。其中mad_decoder_run()过程中,会通过input()回调函数加载更多的码流用于解码,解码完一帧,会通过output()回调函数播放等处理解码出来的音频数据,如果解码出现错 误,通过error()回调函数进行错误的处理。

static int decode(void)
{
	struct mad_decoder decoder;
	int result;

	/* configure input, output, and error functions */
	mad_decoder_init(&decoder, 0,
		input, 0 /* header */, 0 /* filter */, output,
		error, 0 /* message */);

	/* start decoding */
	result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);

	/* release the decoder */
	mad_decoder_finish(&decoder);
	return result;
}

input()回调函数加载更多的码流用于解码,从 SD 卡加载码流填充满缓存 MadInputBuffer,libmad 会读取一帧的码流长度数据用于解码,剩余的数据会被留作 下一帧的解码,通过 mad_stream_buffer()上报码流的位置及可用大小。文件结束后, 停止播放,关闭文件,返回 MAD_FLOW_STOP 告知码流结束。

static enum mad_flow input(void *data,
		    struct mad_stream *stream)
{
    unsigned char *ReadStart;
	unsigned int ReadSize, ReturnSize;
	int Remaining;
	FRESULT Res;	
	
	switch (FileState) {
	case 0:
		ReadStart = MadInputBuffer;
		ReadSize = FILE_IO_BUFFER_SIZE;
		Remaining = 0;
		FileState = 1;
		break;
	case 1:
		/* Get the remaining frame */
		Remaining = stream->bufend - stream->next_frame;
		memmove(MadInputBuffer, stream->next_frame, Remaining);
		ReadStart = MadInputBuffer + Remaining;
		ReadSize = FILE_IO_BUFFER_SIZE - Remaining;		
		break;
	default:
		I2S_TxStop();
		f_close(&file);
		return MAD_FLOW_STOP;
	}

	/* read the file from SDCard */
	Res = f_read(&file, ReadStart, ReadSize, &ReturnSize);
	if (Res != RES_OK) {
		f_close(&file);
		return MAD_FLOW_BREAK;
	}
	/* if the file is over */
	if (ReadSize > ReturnSize) {
		FileState = 2;
	}
	mad_stream_buffer(stream, MadInputBuffer, ReturnSize+Remaining);
	return MAD_FLOW_CONTINUE;
}

output()回调函数把解码出来的音频数据加载到音频输出流进行播放。第一帧解码完 成后,可以从mad_header 结构体获取 MP3 的采样率、通道数等等音频格式,对 I2S 音频驱动初始化。解码的左声道数据放在 pcm->samples[0]缓存,右声道数据放在 pcm->samples[1]缓存,每次解码一帧包含 pcm->length 个音频数据,一个一个填充 到音频输出缓存,如果输出缓存满,则等待播放完一帧后,继续填充。

static enum mad_flow output(void *data,
		     struct mad_header const *header,
		     struct mad_pcm *pcm)
{
	unsigned int nchannels, nsamples;
	mad_fixed_t const *left_ch, *right_ch;	
	static int Index;

	if (!Playing) {
		printf("Mode: %s\r\n", header->mode==1?"Mono":"Stereo");
		printf("Samplerate: %d Hz\r\n", header->samplerate);
		printf("Bitrate: %d bps\r\n", header->bitrate);
#if 0
		I2S_SetSamplerate(header->samplerate);	
#endif
		
		uint32_t sampleRate_debug = MXC_I2S_SetSampleRate(header->samplerate,MXC_I2S_DATASIZE_HALFWORD, (header->samplerate)<<5);

		I2S_TxStart();
		
		WriteIndex = I2SState.TxWriteIndex + 1;
		Index = 0;
		Playing = 1;
	}

	/* pcm->samplerate contains the sampling frequency */
	nchannels = pcm->channels;
	nsamples  = pcm->length;
	left_ch   = pcm->samples[0];
	right_ch  = pcm->samples[1];

	while (nsamples--) {
		signed short letf_sample, right_sample;
		/* output sample(s) in 16-bit signed little-endian PCM */
		letf_sample = scale(*left_ch++);
		if (nchannels == 2) {
			right_sample = scale(*right_ch++);
		} else {
			right_sample = letf_sample;
		}
		while (WriteIndex == I2SState.TxReadIndex) {
				
		}
		//Sample pair is 4 bytes, 16-bit mode
		if (WriteIndex != I2SState.TxReadIndex) {
			I2SState.TxBuffer[I2SState.TxWriteIndex][Index] = (letf_sample&0xffff) | (right_sample<<16);
			Index++;
			if (Index >= AUDIO_FRAME_SIZE) {
				Index = 0;
				I2SState.TxWriteIndex = WriteIndex;
				if (WriteIndex >= AUDIO_NUM_BUFFERS-1) {
					WriteIndex = 0;
				} else {
					WriteIndex++;
				}					
			}
		}
	}

	return MAD_FLOW_CONTINUE;
}

 

程序烧录进板子后,可以通过串口助手和OLED屏幕来查看当前打印的一些信息:

Establishing communication with SD Card...
Mounting SD card...
SD card mounted.
Volume label: MAXIM-SD
Trying to List all content in current directory
.
System Volume Information/
start.mp3
Initializing DMA
I2S Init Done
Ready to play mp3!
Playing start.mp3
Mode: Stereo
Samplerate: 44100 Hz
Bitrate: 320000 bps

实物展示如下:

FtH1NnuvvKaFKUaxGEDn3W_BH7fC

Fg8wSOiVa1UXioARZMtVLFtgdbcE

FuTCHWoyPqXCFsTPqcIaqfpiqjSu

六、总结

非常感谢ADI和硬禾学堂联合举办的这次项目,非常开心能够借助这次的学习机会跟众多才华横溢的工程师朋友互相交流和鼓励,自己才能在截止日期前一天提交作业。

自己在这个过程中也学到了很多音频方面的知识,这对我来说是第一次来从sd卡读取mp3文件并进行扬声器播放,感觉非常开心。当然这个作品还有比较多的改进之处,比如通过扩展板上的按键来暂停/继续播放,调节音量,静音等功能。

自己也是几经打算放弃,到感染新冠觉得完成任务遥不可及,到最终提交作业,心情犹如过山车,非常刺激。

感谢硬禾提供的平台,咱们后续活动再见面。

 

 

 

附件下载
TinyMP3-Final.7z
团队介绍
爱好者一个人
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号