只用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);//配置DMADMA_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];//DAC1DMA_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];//DAC2DMA_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缓冲长度需要停止DMADAC_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 <stdio.h>
#include <string.h>
#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 3000static 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_leftif(!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_LFNfileinfo.lfsize = _MAX_LFN * 2 + 1;fileinfo.lfname = buf;
#endifres = 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_LFNfn = *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或者其他系列也能用这个方案,要自己改改测试喽,本文把思路分享出来抛砖引玉。