感谢EEPW提供的“换取逻辑分析仪”活动,继续发帖,本次展示的是自己前段时间搞的基于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协处理器
二、OLED屏幕驱动
之所以尝试驱动屏幕,是希望音乐在播放的时候,能够通过屏幕显示歌曲相关信息,比如歌名等。这次我使用的屏幕是第一次参加Funpack活动时候购买的OLED屏幕,如下所示。
此次利用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
上电测试,屏幕工作正常。
三、SD卡读取MP3文件
MP3 音频的播放涉及到音频驱动、SD 卡读写文件的实现,我这里参考的是MAXIAM官方例程中的ImgCapture。主要是通过sd_mount来挂载SD卡,然后通过调用sd_ls()来打印出sd卡当前目录下的内容。
到此,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文件夹。
然后添加相应的路径到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 个音频数据,一个一个填充 到音频输出缓存,如果输出缓存满,则等待播放完一帧后,继续填充。
程序烧录进板子后,可以通过串口助手和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
实物展示如下: