✔零知开源(零知IDE)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 "配置环境" 转移到 "创意实现",极大降低了技术门槛。零知IDE编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知开源(零知IDE)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
目录
[1.1 硬件清单](#1.1 硬件清单)
[1.2 接线方案表](#1.2 接线方案表)
[1.3 具体接线图](#1.3 具体接线图)
[1.4 接线实物图](#1.4 接线实物图)
[3.1 代码架构](#3.1 代码架构)
[3.2 网络连接模式(STA+AP)](#3.2 网络连接模式(STA+AP))
[3.3 按键处理状态](#3.3 按键处理状态)
[3.4 Web服务器](#3.4 Web服务器)
[3.5 LVGL界面](#3.5 LVGL界面)
[3.6 MP3模块初始化](#3.6 MP3模块初始化)
[4.1 系统启动](#4.1 系统启动)
[4.2 多控制方式](#4.2 多控制方式)
[4.3 视频演示](#4.3 视频演示)
[五、DFPlayer MP3模块工作原理](#五、DFPlayer MP3模块工作原理)
[5.1 硬件架构图](#5.1 硬件架构图)
[5.2 通信协议详解](#5.2 通信协议详解)
项目概述
本项目基于零知ESP32-WROOM-32开发板,结合DFPlayer Mini MP3模块和ST7789 TFT彩屏,打造了一款集本地硬件控制、Web远程控制和现代化LVGL界面于一体的智能音乐播放系统。支持 MP3 格式音频文件的播放、暂停、上一首、下一首等基本操作
项目难点及解决方案
问题描述:音频文件无法播放和格式兼容性
**解决方案:**必须将 SD 卡格式化为 FAT32 文件系统,并且簇大小设置为 4096 字节,使用 128-320kbps 比特率的 MP3 文件。歌曲文件使用数字编号前缀命名,如"1.mp3"、"2.mp3" 直接存放在 SD 卡根目录
一、系统接线部分
1.1 硬件清单
| 组件名称 | 型号规格 | 数量 | 备注 |
|---|---|---|---|
| ESP32开发板 | 零知ESP32-WROOM-32 | 1 | 主控制器 |
| MP3解码模块 | DFPlayer Mini | 1 | 音频解码 |
| TFT显示屏 | ST7789 240x320 | 1 | 界面显示 |
| SD卡模块 | 不超过 32GB,FAT32 格式 | 1 | SD卡模块,带卡槽 |
| 扬声器/耳机 | 8Ω 3W | 1 | 音频输出 |
| 按键 | 轻触按键 | 3 | 控制按键 |
| 杜邦线 | 公对公、公对母 | 若干 | 系统连接 |
1.2 接线方案表
根据代码中的引脚定义,硬件接线方案如下:
| ESP32引脚 | 连接模块 | 功能描述 | 代码定义 |
|---|---|---|---|
| 19 | DFPlayer TX | MP3模块发送 | PIN_MP3_TX |
| 21 | DFPlayer RX | MP3模块接收 | PIN_MP3_RX |
| 5 | 按键1 | 上一首控制 | PIN_BUTTON_PREV |
| 16 | 按键2 | 播放/暂停 | PIN_BUTTON_PLAY |
| 14 | 按键3 | 下一首控制 | PIN_BUTTON_NEXT |
| 18 | ST7789 SCL | 屏幕时钟 | TFT_eSPI配置 |
| 23 | ST7789 SDA | 屏幕数据 | TFT_eSPI配置 |
| 15 | ST7789 CS | 片选信号 | TFT_eSPI配置 |
| 13 | ST7789 DC | 数据/命令 | TFT_eSPI配置 |
| 4 | ST7789 RST | 复位信号 | TFT_eSPI配置 |
1.3 具体接线图

请注意:DFPlayer MP3模块需要 5V 供电,而 TFT 屏幕使用 3.3V 供电,注意不要接错电压,以免损坏元件
1.4 接线实物图

二、安装与使用部分
2.1 开源平台-输入DFPlayer 并搜索-代码下载自动打开

2.2 连接-验证-上传

2.3 调试-串口监视器

三、代码讲解部分
3.1 代码架构
bash
mp3_player_demo/
├── mp3_player_demo.ino # 主程序(网络+控制逻辑)
├── lvgl_ui.h # LVGL界面头文件
├── lvgl_ui.cpp # LVGL界面实现
├── web_interface.h # Web界面HTML/CSS/JS
├── button_handler.h # 优化的按键处理
├── lv_conf.h # LVGL 9.2.2配置
└── libraries/
├── TFT_eSPI/ # ST7789驱动
├── DFRobotDFPlayerMini/ # MP3控制库
├── WiFi/ # ESP32 WiFi
├── WebServer/ # HTTP服务器
└── lvgl/ # LVGL图形库
3.2 网络连接模式(STA+AP)
cpp
void setupWiFi() {
Serial.println("→ 初始化WiFi双模式...");
// 优先尝试连接现有WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
// 显示连接状态到屏幕
tft.fillRect(0, 160, 240, 30, TFT_BLACK);
tft.setCursor(20, 170);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.println("Connecting WiFi...");
// 连接超时处理(10秒)
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
// STA模式成功
wifiConnected = true;
IPAddress ip = WiFi.localIP();
Serial.printf("\n✓ WiFi已连接\n IP: %s\n", ip.toString().c_str());
// 屏幕显示IP地址
showIPOnScreen(ip, true);
} else {
// STA模式失败,启动AP模式
Serial.println("\n→ 启动AP模式...");
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASSWORD);
IPAddress apIP = WiFi.softAPIP();
Serial.printf("✓ AP模式已启动\n SSID: %s\n IP: %s\n",
AP_SSID, apIP.toString().c_str());
// 屏幕显示AP信息
showAPOnScreen(AP_SSID, apIP);
}
delay(2000); // 显示信息2秒
}
WiFi连接的STA / AP模式选择后并打印出IP 地址
3.3 按键处理状态
cpp
// 按键事件状态机
ButtonEvent Button::update() {
bool currentRaw = digitalRead(pin);
unsigned long now = millis();
ButtonEvent event = BTN_NONE;
// 状态变化检测
if (currentRaw != lastRawState) {
lastStateChangeTime = now;
lastRawState = currentRaw;
}
// 消抖处理(50ms稳定期)
if ((now - lastStateChangeTime) >= DEBOUNCE_TIME) {
// 按键按下检测(下降沿)
if (currentRaw == LOW && stableState == HIGH) {
stableState = LOW;
isPressed = true;
pressStartTime = now;
longPressTriggered = false;
lastRepeatTime = now;
}
// 按键释放检测(上升沿)
else if (currentRaw == HIGH && stableState == LOW) {
stableState = HIGH;
// 短按判定:按下时间 < 长按阈值 且 未触发长按
if ((now - pressStartTime) < LONG_PRESS_TIME && !longPressTriggered) {
event = BTN_SHORT_PRESS; // 触发短按事件
}
isPressed = false;
longPressTriggered = false;
}
// 持续按下状态处理
else if (isPressed && stableState == LOW) {
unsigned long pressDuration = now - pressStartTime;
// 首次长按触发
if (pressDuration >= LONG_PRESS_TIME && !longPressTriggered) {
longPressTriggered = true;
event = BTN_LONG_PRESS; // 触发长按事件
lastRepeatTime = now;
}
// 长按重复触发
else if (longPressTriggered && (now - lastRepeatTime) >= REPEAT_INTERVAL) {
event = BTN_LONG_REPEAT; // 触发重复事件
lastRepeatTime = now;
}
}
}
return event;
}
3.4 Web服务器
cpp
// Web服务器路由配置
void setupWebServer() {
// 页面请求
server.on("/", HTTP_GET, handleRoot); // 主页面
server.on("/style.css", HTTP_GET, handleCSS); // 样式表
server.on("/script.js", HTTP_GET, handleJS); // JavaScript
// API接口
server.on("/api/status", HTTP_GET, handleStatus); // 获取状态
server.on("/api/play", HTTP_POST, handlePlay); // 播放控制
server.on("/api/pause", HTTP_POST, handlePause); // 暂停
server.on("/api/next", HTTP_POST, handleNext); // 下一首
server.on("/api/prev", HTTP_POST, handlePrev); // 上一首
server.on("/api/volume", HTTP_POST, handleVolume); // 音量控制
server.on("/api/songs", HTTP_GET, handleSongList); // 歌曲列表
// 错误处理
server.onNotFound(handleNotFound);
server.begin();
Serial.println("✓ Web服务器已启动");
}
Web服务器打开,引用Web API处理函数
3.5 LVGL界面
cpp
void createUI() {
// 1. 创建主屏幕
scr = lv_obj_create(NULL);
lv_obj_set_size(scr, SCREEN_WIDTH, SCREEN_HEIGHT);
lv_obj_set_style_bg_color(scr, COLOR_BG_MAIN, 0);
lv_scr_load(scr);
// 2. 创建标题栏
header = lv_obj_create(scr);
lv_obj_set_size(header, SCREEN_WIDTH, HEADER_HEIGHT);
lv_obj_add_style(header, &style_header, 0);
lblTitle = lv_label_create(header);
lv_label_set_text(lblTitle, LV_SYMBOL_AUDIO " MP3 Player");
lv_obj_add_style(lblTitle, &style_label_title, 0);
lv_obj_center(lblTitle);
// 3. 创建歌曲信息卡片
songCard = lv_obj_create(scr);
lv_obj_set_size(songCard, CARD_WIDTH, CARD_HEIGHT);
lv_obj_set_pos(songCard, CARD_MARGIN, HEADER_HEIGHT + 15);
lv_obj_add_style(songCard, &style_card, 0);
// 4. 创建控制按钮区域
createControlButtons();
// 5. 创建音量显示
createVolumeDisplay();
}
DFPlayer音乐播放器UI组件创建流程
3.6 MP3模块初始化
cpp
bool setupMP3() {
Serial.println("→ 初始化DFPlayer...");
mp3Serial.begin(9600, SERIAL_8N1, PIN_MP3_RX, PIN_MP3_TX);
delay(500);
if (!myDFPlayer.begin(mp3Serial, true, false)) {
Serial.println("✗ DFPlayer通信失败");
return false;
}
Serial.println("✓ DFPlayer通信成功");
myDFPlayer.setTimeOut(500);
myDFPlayer.volume(currentVolume);
myDFPlayer.outputDevice(DFPLAYER_DEVICE_SD);
myDFPlayer.EQ(DFPLAYER_EQ_NORMAL);
delay(300);
// 读取歌曲数量
for (int i = 0; i < 3; i++) {
int count = myDFPlayer.readFileCounts();
if (count > 0 && count <= 255) {
totalSongs = count;
Serial.printf("✓ 检测到 %d 首歌曲\n", totalSongs);
return true;
}
delay(200);
}
Serial.println("⚠ 未检测到MP3文件");
return false;
}
四、项目结果演示
4.1 系统启动
屏幕显示启动画面,检测各模块;读取SD卡歌曲列表;尝试连接WiFi,失败则启动AP模式;LVGL界面和Web服务器启动;显示IP地址和控制方式
4.2 多控制方式
1)物理按键操作
短按播放键:播放/暂停切换;短按上一首/下一首:切换歌曲;长按上一首/下一首:音量调节;所有操作实时同步到Web和LVGL界面
2)Web界面操作
使用电脑/手机浏览器,访问显示屏或者串口打印出的IP

访问IP地址后,点击播放列表中的歌曲直接播放;滑动条或按钮调节音量,实时显示当前播放状态和歌曲信息

3)LVGL界面操作
实时显示歌曲数量和播放状态
4.3 视频演示
零知ESP32+DFPlayer全功能音乐系统演示
音乐播放器系统物理按键操作/多控制方式状态同步+Web界面和远程控制演示+LVGL GUI图形界面
五、DFPlayer MP3模块工作原理
5.1 硬件架构图
比特流解析(读取MP3文件头)→霍夫曼解码(解压缩频域数据)→反量化(恢复MDCT系数)→IMDCT变换(时域到频域转换)→子带合成(合成PCM音频数据)→PCM输出(16位44.1kHz立体声输出)

5.2 通信协议详解
串口模式
串口指令格式:模块支持异步串口通讯模式,通过串口接受控制命令
| 指令名称 | 对应功能 | 功能描述 |
|---|---|---|
| $S | 起始位0x7E | 每条命令反馈均以$开头,即0x7E |
| VER | 版本 | 版本信息[目前默认为0xFF] |
| Len | len后字节个数 | 校验和不计算在内 |
| CMD | 命令字 | 表示具体的操作,比如播放/暂停等等 |
| Feedback | 命令反馈 | 是否需要反馈信息,1反馈,0不反馈 |
| para1 | 参数1 | 查询的数据高字节(比如歌曲序号) |
| para2 | 参数2 | 查询的数据低字节 |
| checksum | 校验和[占两个字节] | 累加和校验[不计起始位$] |
| $0 | 结束位 | 结束位0xEF |
校验和计算
cpp
uint16_t calculateChecksum(uint8_t *cmd, uint8_t len) {
uint16_t sum = 0;
for (uint8_t i = 1; i < 7; i++) { // 字节1-6
sum += cmd[i];
}
return -sum; // 取反
}
// 示例:播放第5首歌曲
uint8_t playSong5[] = {
0x7E, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x05, 0xFE, 0xF9, 0xEF
// ↑ ↑ ↑
// 固定长度 播放指令 歌曲编号5
};
串口控制指令示例
| 指令码 | 功能 | 参数说明 | 响应格式 |
|---|---|---|---|
| 0x01 | 下一首 | 无参数 | 0x3F + 0x01 |
| 0x02 | 上一首 | 无参数 | 0x3F + 0x02 |
| 0x03 | 播放指定文件 | 文件编号1-255 | 0x3F + 文件编号 |
| 0x04 | 音量调整 | 音量值0-30 | 0x3F + 音量值 |
| 0x05 | 设置EQ | 0-5模式 | 0x3F + EQ值 |
串口发送7E FF 06 0E 00 00 00 FE ED EF暂停播放器
六、常见问题解答
Q1:Web界面无法访问怎么办?
*A:排查步骤:*检查网络连接,命令行输入ping 192.168.x.xxx;观察串口输出的ESP32 IP地址;检查防火墙设置,确保防火墙允许80端口;AP模式连接,STA模式失败自动切换
Q2:SD卡无法识别或读取?
*A:排查流程:*格式化SD卡FAT32,4096簇格式,检查文件命名,按顺序复制到SD卡根目录下,确保mp3格式正确
Q3:系统内存不足或重启?
A:内存优化策略:
cpp
// 1. 减少LVGL缓冲区
static lv_color_t buf[SCREEN_WIDTH * 8]; // 从10行减少到8行
// 2. 优化Web界面
const char HTML_PAGE[] PROGMEM; // 使用PROGMEM存储
// 3. 减少串口缓冲区
mp3Serial.setRxBufferSize(128); // 默认256
mp3Serial.setTxBufferSize(128);
// 4. 监控内存使用
void checkMemory() {
Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
Serial.printf("Min free: %d bytes\n", ESP.getMinFreeHeap());
}
项目资源整合
DFPlayer MP3模块: DFPlayer Mini Manual
DFPlayer 库文件: DFRobot/DFRobotDFPlayerMini