只用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位的,其输入范围04095,而双通道16位的PCM音频数据是左右声道交替存储,且数据范围-3276832767,因此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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**
******************************************************************************
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/*
******************************************************************************
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
******************************************************************************
* @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或者其他系列也能用这个方案,要自己改改测试喽,本文把思路分享出来抛砖引玉。