STM32F407使用Helix库软解MP3并通过DAC输出,最精简的STM32+SD卡实现MP3播放器

您所在的位置:网站首页 mp3播放代码 STM32F407使用Helix库软解MP3并通过DAC输出,最精简的STM32+SD卡实现MP3播放器

STM32F407使用Helix库软解MP3并通过DAC输出,最精简的STM32+SD卡实现MP3播放器

2023-12-07 02:02| 来源: 网络整理| 查看: 265

只用STM32单片机+SD卡+耳机插座,实现播放MP3播放器!

看过很多STM32软解MP3的方案,即不通过类似VS1053之类的解码器芯片,直接用STM32和软件库解码MP3文件,通常使用了labmad或者Helix解码库实现,Helix相对labmad占用的RAM更少。但是大多数参考的方案还是用了外接IIS接口WM98xx之类的音频DAC芯片播放音频,稍显复杂繁琐。STM32F407Vx本身就自带了2路12位DAC输出,最高刷新速度333kHz,除了分辨率差点意思,速度上对于MP3通常44.1kHz采样率来说,用来播放音频绰绰有余了。本文给的方案和源码,直接用STM32软解码MP3并使用自带的2个DAC输出引脚输出音频左右声道。

原理:STM32从SD读取MP3文件原始数据,发送给Helix库解码,Helix解码后输出PCM数据流,将此数据进一步处理转换后,按照左右声道分别存入DAC输出1和2缓存,通过定时器以MP3文件的采样率的频率提供DAC触发节拍,通过DMA取缓存中高12位数据给DAC,在DAC1和2引脚产生音频波形,通过电容耦合到耳机的左右声道上。

MP3源文件是一种经过若干算法,将原始音频数据压缩得来的,软件解码的过程是逆过程,将压缩的音频反向转换为记录了左右声道、幅值的数据流,通常是PCM格式。

PCM:是模拟信号以固定的采样频率转换成数字信号后的表现形式。记录了音频采样的数据,双通道、16bit的PCM数据格式是以0轴为中心,范围为-32768~32767的数值,每个数据占用2字节,左声道和右声道交替存储,如图。

 软解码得到的PCM数据到STM32的DAC缓存需要进一步处理。STM32的DAC是12位的,其输入范围0~4095,而双通道16位的PCM音频数据是左右声道交替存储,且数据范围-32768~32767,因此PCM到STM32的DAC缓存要按照顺序一拆为二,分为左右声道,每个数据再加上32768,使其由short int的范围转换为unsigned short int,即0~65535。由于PCM数据是对音频的采样,因此调节音量(幅值)可以在此步骤一并处理,即音频数据 x 音量 /最大音量。至于DAC是12位,只需将DAC模式设置为左对齐12位,舍弃低4位即可。

到此,STM32的DAC输出引脚上应该已经有音频信号了,通常DAC引脚上串联一个1~10uF的电容用来耦合音频信号,电容越大音质越好,电容另一端接耳机插座的左声道/右声道,插上耳机就可以欣赏音乐啦!音质嘛,反正我是听不出来好不好,跟商品MP3播放器差不多。如果不串联电容,DAC引脚直连耳机插座左右声道也能听到声音,就是有些数字信号噪声也会传进来。如果希望噪声小一些,DAC引脚输出端加一个下图的低通滤波电路也是可以的。

 

  

Helix移植:

Helix源码的官网我没找到,直接用了野火的例程里面的代码,移植也很简单,不用改任何代码,只需要将Helix文件夹拷贝到工程目录里,然后在Keil中添加好文件,以及添加头文件途径,编译即可。工程目录如图。

源码:dac配置

dac.c

/** ****************************************************************************** * @file dac.c * @author ZL * @version V0.0.1 * @date September-20-2019 * @brief DAC configuration. ****************************************************************************** */ /* Includes ------------------------------------------------------------------*/ #include "dac.h" /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ #define CNT_FREQ 84000000 // TIM6 counter clock (prescaled APB1) /* DHR registers offsets */ #define DHR12R1_OFFSET ((uint32_t)0x00000008) #define DHR12R2_OFFSET ((uint32_t)0x00000014) #define DHR12RD_OFFSET ((uint32_t)0x00000020) /* Private macro -------------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ uint32_t DAC_DHR12R1_ADDR = (uint32_t)DAC_BASE + DHR12R1_OFFSET + DAC_Align_12b_L; uint32_t DAC_DHR12R2_ADDR = (uint32_t)DAC_BASE + DHR12R2_OFFSET + DAC_Align_12b_L; uint16_t DAC_buff[2][DAC_BUF_LEN]; //DAC1、DAC2输出缓冲 /* Private function prototypes -----------------------------------------------*/ static void TIM6_Config(void); /* Private functions ---------------------------------------------------------*/ /** * @brief DAC初始化 * @param none * @retval none */ void DAC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_InitStructure.DAC_Trigger = DAC_Trigger_T6_TRGO; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Init(DAC_Channel_2, &DAC_InitStructure); //配置DMA DMA_InitTypeDef DMA_InitStruct; DMA_StructInit(&DMA_InitStruct); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); DMA_InitStruct.DMA_PeripheralBaseAddr = (u32)DAC_DHR12R1_ADDR; DMA_InitStruct.DMA_Memory0BaseAddr = (u32)&DAC_buff[0];//DAC1 DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral; DMA_InitStruct.DMA_BufferSize = DAC_BUF_LEN; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; DMA_InitStruct.DMA_Priority = DMA_Priority_High; DMA_InitStruct.DMA_Channel = DMA_Channel_7; DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_Init(DMA1_Stream5, &DMA_InitStruct); DMA_InitStruct.DMA_PeripheralBaseAddr = (u32)DAC_DHR12R2_ADDR; DMA_InitStruct.DMA_Memory0BaseAddr = (u32)&DAC_buff[1];//DAC2 DMA_Init(DMA1_Stream6, &DMA_InitStruct); //开启DMA传输完成中断 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream6_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_TCIF6); DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_HTIF6); DMA_ITConfig(DMA1_Stream6, DMA_IT_TC, ENABLE); DMA_ITConfig(DMA1_Stream6, DMA_IT_HT, ENABLE); // DMA_Cmd(DMA1_Stream5, ENABLE); // DMA_Cmd(DMA1_Stream6, ENABLE); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_Cmd(DAC_Channel_2, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); DAC_DMACmd(DAC_Channel_2, ENABLE); TIM6_Config(); } //配置DAC采样率和DMA数据长度,并启动DMA DAC void DAC_DMA_Start(uint32_t freq, uint16_t len) { //设置DMA缓冲长度需要停止DMA DAC_DMA_Stop(); //设置DMA DAC缓冲长度 DMA_SetCurrDataCounter(DMA1_Stream5, len); DMA_SetCurrDataCounter(DMA1_Stream6, len); //设置定时器 TIM_SetAutoreload(TIM6, (uint16_t)((CNT_FREQ)/freq)); //启动 DMA_Cmd(DMA1_Stream5, ENABLE); DMA_Cmd(DMA1_Stream6, ENABLE); } //停止DMA DAC void DAC_DMA_Stop(void) { DMA_Cmd(DMA1_Stream5, DISABLE); DMA_Cmd(DMA1_Stream6, DISABLE); } //定时器6用于设置DAC刷新率 static void TIM6_Config(void) { TIM_TimeBaseInitTypeDef TIM6_TimeBase; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_TimeBaseStructInit(&TIM6_TimeBase); TIM6_TimeBase.TIM_Period = (uint16_t)((CNT_FREQ)/44100); TIM6_TimeBase.TIM_Prescaler = 0; TIM6_TimeBase.TIM_ClockDivision = 0; TIM6_TimeBase.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, &TIM6_TimeBase); TIM_SelectOutputTrigger(TIM6, TIM_TRGOSource_Update); TIM_Cmd(TIM6, ENABLE); } /** * @brief DAC out1 PA4输出电压 * @param dat:dac数值:,0~4095 * @retval none */ void DAC_Out1(uint16_t dat) { DAC_SetChannel1Data(DAC_Align_12b_R, dat); DAC_SoftwareTriggerCmd(DAC_Channel_1, ENABLE); } /** * @brief DAC out2 PA5输出电压 * @param dat:dac数值:,0~4095 * @retval none */ void DAC_Out2(uint16_t dat) { DAC_SetChannel2Data(DAC_Align_12b_R, dat); DAC_SoftwareTriggerCmd(DAC_Channel_2, ENABLE); } /********************************************* *****END OF FILE****/

源码:MP3播放流程 (原创野火,参考了野火的例程,本人进行整理和修改)

MP3player.c

/* ****************************************************************************** * @file mp3Player.c * @author fire * @version V1.0 * @date 2023-08-13 * @brief mp3解码 ****************************************************************************** */ #include #include #include "ff.h" #include "mp3Player.h" #include "mp3dec.h" #include "dac.h" #include "led.h" /* 推荐使用以下格式mp3文件: * 采样率:44100Hz * 声 道:2 * 比特率:320kbps */ /* 处理立体声音频数据时,输出缓冲区需要的最大大小为2304*16/8字节(16为PCM数据为16位), * 这里我们定义MP3BUFFER_SIZE为2304 */ #define MP3BUFFER_SIZE 2304 #define INPUTBUF_SIZE 3000 static HMP3Decoder Mp3Decoder; /* mp3解码器指针 */ static MP3FrameInfo Mp3FrameInfo; /* mP3帧信息 */ static MP3_TYPE mp3player; /* mp3播放设备 */ volatile uint8_t Isread = 0; /* DMA传输完成标志 */ volatile uint8_t dac_ht = 0; //DAC dma 半传输标志 uint32_t led_delay = 0; uint8_t inputbuf[INPUTBUF_SIZE]={0}; /* 解码输入缓冲区,1940字节为最大MP3帧大小 */ static short outbuffer[MP3BUFFER_SIZE]; /* 解码输出缓冲区*/ static FIL file; /* file objects */ static UINT bw; /* File R/W count */ FRESULT result; //从SD卡读取MP3源文件进行解码,并传入DAC缓冲区 int MP3DataDecoder(uint8_t **read_ptr, int *bytes_left) { int err = 0, i = 0, outputSamps = 0; //bufflag开始解码 参数:mp3解码结构体、输入流指针、输入流大小、输出流指针、数据格式 err = MP3Decode(Mp3Decoder, read_ptr, bytes_left, outbuffer, 0); if (err != ERR_MP3_NONE) //错误处理 { switch (err) { case ERR_MP3_INDATA_UNDERFLOW: printf("ERR_MP3_INDATA_UNDERFLOW\r\n"); result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw); *read_ptr = inputbuf; *bytes_left = bw; break; case ERR_MP3_MAINDATA_UNDERFLOW: /* do nothing - next call to decode will provide more mainData */ printf("ERR_MP3_MAINDATA_UNDERFLOW\r\n"); break; default: printf("UNKNOWN ERROR:%d\r\n", err); // 跳过此帧 if (*bytes_left > 0) { (*bytes_left) --; read_ptr ++; } break; } return 0; } else //解码无错误,准备把数据输出到PCM { MP3GetLastFrameInfo(Mp3Decoder, &Mp3FrameInfo); //获取解码信息 /* 输出到DAC */ outputSamps = Mp3FrameInfo.outputSamps; //PCM数据个数 if (outputSamps > 0) { if (Mp3FrameInfo.nChans == 1) //单声道 { //单声道数据需要复制一份到另一个声道 for (i = outputSamps - 1; i >= 0; i--) { outbuffer[i * 2] = outbuffer[i]; outbuffer[i * 2 + 1] = outbuffer[i]; } outputSamps *= 2; }//if (Mp3FrameInfo.nChans == 1) //单声道 }//if (outputSamps > 0) //将数据传送至DMA DAC缓冲区 for (i = 0; i < outputSamps/2; i++) { if(dac_ht == 1) { DAC_buff[0][i] = outbuffer[2*i] * mp3player.ucVolume /100 + 32768; DAC_buff[1][i] = outbuffer[2*i+1] * mp3player.ucVolume /100 + 32768; } else { DAC_buff[0][i+outputSamps/2] = outbuffer[2*i] * mp3player.ucVolume /100 + 32768; DAC_buff[1][i+outputSamps/2] = outbuffer[2*i+1] * mp3player.ucVolume /100 + 32768; } } return 1; }//else 解码正常 } //读取一段MP3数据,并把读取的指针赋值read_ptr,长度赋值bytes_left uint8_t read_file(const char *mp3file, uint8_t **read_ptr, int *bytes_left) { result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw); if(result != FR_OK) { printf("读取%s失败 -> %d\r\n", mp3file, result); return 0; } else { *read_ptr = inputbuf; *bytes_left = bw; return 1; } } /** * @brief MP3格式音频播放主程序 * @param mp3file MP3文件路径 * @retval 无 */ void mp3PlayerDemo(const char *mp3file) { uint8_t *read_ptr = inputbuf; int read_offset = 0; /* 读偏移指针 */ int bytes_left = 0; /* 剩余字节数 */ mp3player.ucStatus = STA_IDLE; mp3player.ucVolume = 15; //音量值,100满 //尝试打开MP3文件 result = f_open(&file, mp3file, FA_READ); if(result != FR_OK) { printf("Open mp3file :%s fail!!!->%d\r\n", mp3file, result); result = f_close (&file); return; /* 停止播放 */ } printf("当前播放文件 -> %s\n", mp3file); //初始化MP3解码器 Mp3Decoder = MP3InitDecoder(); if(Mp3Decoder == 0) { printf("初始化helix解码库设备失败!\r\n"); return; /* 停止播放 */ } else { printf("初始化helix解码库完成\r\n"); } //尝试读取一段MP3数据,并把读取的指针赋值read_ptr,长度赋值bytes_left if(!read_file(mp3file, &read_ptr, &bytes_left)) { MP3FreeDecoder(Mp3Decoder); return; /* 停止播放 */ } //尝试解码成功 if(MP3DataDecoder(&read_ptr, &bytes_left)) { //打印MP3信息 printf(" \r\n Bitrate %dKbps", Mp3FrameInfo.bitrate/1000); printf(" \r\n Samprate %dHz", Mp3FrameInfo.samprate); printf(" \r\n BitsPerSample %db", Mp3FrameInfo.bitsPerSample); printf(" \r\n nChans %d", Mp3FrameInfo.nChans); printf(" \r\n Layer %d", Mp3FrameInfo.layer); printf(" \r\n Version %d", Mp3FrameInfo.version); printf(" \r\n OutputSamps %d", Mp3FrameInfo.outputSamps); printf("\r\n"); //启动DAC,开始发声 if (Mp3FrameInfo.nChans == 1) //单声道要将outputSamps*2 { DAC_DMA_Start(Mp3FrameInfo.samprate, 2 * Mp3FrameInfo.outputSamps); } else//双声道直接用Mp3FrameInfo.outputSamps { DAC_DMA_Start(Mp3FrameInfo.samprate, Mp3FrameInfo.outputSamps); } } else //解码失败 { MP3FreeDecoder(Mp3Decoder); return; } /* 放音状态 */ mp3player.ucStatus = STA_PLAYING; /* 进入主程序循环体 */ while(mp3player.ucStatus == STA_PLAYING) { //寻找帧同步,返回第一个同步字的位置 read_offset = MP3FindSyncWord(read_ptr, bytes_left); if(read_offset < 0) //没有找到同步字 { if(!read_file(mp3file, &read_ptr, &bytes_left))//重新读取一次文件再找 { continue;//回到while(mp3player.ucStatus == STA_PLAYING)后面 } } else//找到同步字 { read_ptr += read_offset; //偏移至同步字的位置 bytes_left -= read_offset; //同步字之后的数据大小 if(bytes_left < 1024) //如果剩余的数据小于1024字节,补充数据 { /* 注意这个地方因为采用的是DMA读取,所以一定要4字节对齐 */ u16 i = (uint32_t)(bytes_left)&3; //判断多余的字节 if(i) i=4-i; //需要补充的字节 memcpy(inputbuf+i, read_ptr, bytes_left); //从对齐位置开始复制 read_ptr = inputbuf+i; //指向数据对齐位置 result = f_read(&file, inputbuf+bytes_left+i, INPUTBUF_SIZE-bytes_left-i, &bw);//补充数据 if(result != FR_OK) { printf("读取%s失败 -> %d\r\n",mp3file,result); break; } bytes_left += bw; //有效数据流大小 } } //MP3数据解码并送入DAC缓存 if(!MP3DataDecoder(&read_ptr, &bytes_left)) {//如果播放出错,Isread置1,避免卡住死循环 Isread = 1; } //mp3文件读取完成,退出 if(file.fptr == file.fsize) { printf("单曲播放完毕\r\n"); break; } //等待DAC发送一半或全部中断 while(Isread == 0) { led_delay++; if(led_delay == 0xffffff) { led_delay=0; LED1_TROG; } //Input_scan(); //等待DMA传输完成,此间可以运行按键扫描及处理事件 } Isread = 0; } //运行到此处,说明单曲播放完成,收尾工作 DAC_DMA_Stop();//停止喂DAC数据 mp3player.ucStatus = STA_IDLE; MP3FreeDecoder(Mp3Decoder);//清理缓存 f_close(&file); } void DMA1_Stream6_IRQHandler(void) { if(DMA_GetITStatus(DMA1_Stream6, DMA_IT_HTIF6) != RESET) //半传输 { dac_ht = 1; Isread=1; DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_HTIF6); } if(DMA_GetITStatus(DMA1_Stream6, DMA_IT_TCIF6) != RESET) //全传输 { dac_ht = 0; Isread=1; DMA_ClearITPendingBit(DMA1_Stream6, DMA_IT_TCIF6); } } /***************************** (END OF FILE) *********************************/

源码:main.c

/** ****************************************************************************** * @file ../User/main.c * @author ZL * @version V1.0 * @date 2015-12-26 * @brief Main program body ****************************************************************************** **/ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "hw_includes.h" #include "ff.h" #include "exfuns.h" #include "mp3Player.h" //遍历目录文件并打印输出 u8 scan_files(u8 * path) { FRESULT res; char buf[512] = {0}; char *fn; #if _USE_LFN fileinfo.lfsize = _MAX_LFN * 2 + 1; fileinfo.lfname = buf; #endif res = f_opendir(&dir,(const TCHAR*)path); if (res == FR_OK) { printf("\r\n"); while(1){ res = f_readdir(&dir, &fileinfo); if (res != FR_OK || fileinfo.fname[0] == 0) break; #if _USE_LFN fn = *fileinfo.lfname ? fileinfo.lfname : fileinfo.fname; #else fn = fileinfo.fname; #endif printf("%s/", path); printf("%s\r\n", fn); } } return res; } /** * @brief Main program * @param None * @retval None */ int main(void) { delay_init(168); usart1_Init(115200); LED_Init(); DAC_Config(); if(!SD_Init()) { exfuns_init(); //为fatfs相关变量申请内存 f_mount(fs[0],"0:",1); //挂载SD卡 } //打印SD目录和文件 scan_files("0:"); LED0_ON; while (1) { mp3PlayerDemo("0:/断桥残雪.MP3"); mp3PlayerDemo("0:/张国荣-玻璃之情.MP3"); delay_ms(50); } }

为方便调试测试,使用usart1打印数据。实测效果:

程序源码与原理图,测试音频:

链接:https://pan.baidu.com/s/10hYXkrqnuBQgs0DWKLUUOA?pwd=iatt  提取码:iatt

知道这里下载要积分登录什么的麻烦得很,所以程序放到百度网盘了,假如连接失效,记得在评论区喊我更新!

理论上STM32F1或者其他系列也能用这个方案,要自己改改测试喽,本文把思路分享出来抛砖引玉。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3