只用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 <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 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或者其他系列也能用这个方案,要自己改改测试喽,本文把思路分享出来抛砖引玉。