本文将详细介绍如何使用ESP32 S3开发板结合DFPlayer Mini MP3模块制作一个简易音乐播放器,支持通过编码器选择歌曲、按键播放,OLED屏幕显示歌曲信息。教程包含元器件清单、硬件接线、软件配置、程序解析及调试步骤,代码可直接复用。

一、元器件清单

制作该音乐播放器需要以下元器件,清单已标注关键参数和用途:

  • 启明云端WT9932S3:作为主控单元,负责逻辑控制、OLED显示和编码器交互

  • DFPlayer Mini MP3模块:音频播放核心,支持SD卡读取MP3文件

  • OLED屏幕(128x64 I2C接口):用于显示歌曲总数、当前选择歌曲等信息

  • 旋转编码器(带按键):实现歌曲选择(顺时针/逆时针旋转)和播放控制(按下按键)

  • Micro SD卡(≤32GB):存储MP3格式音乐文件,需提前格式化为FAT32格式

  • 扬声器:连接DFPlayer Mini的音频输出接口

  • 杜邦线(公对母/公对公):用于硬件之间的接线

  • 5V电源模块:为ESP32 S3和DFPlayer Mini供电(可选,也可通过开发板USB供电)

二、硬件接线图与说明

以下是各模块与ESP32 S3的接线对应关系,严格按照程序中的引脚定义接线,避免因引脚错误导致功能异常:

模块

引脚

ESP32 S3 引脚

功能说明

OLED屏幕(I2C)

SDA

11

I2C数据线

SCL

12

I2C时钟线

旋转编码器

CLK

38

编码器时钟引脚(上拉输入)

DT

21

编码器数据引脚(上拉输入)

SW

47

编码器按键引脚(上拉输入)

DFPlayer Mini

VCC

5V

模块供电(需5V电压)

GND

GND

接地(与开发板共地)

TX

17(MP3_RX_PIN)

模块串口发送→开发板串口接收(已对调)

RX

18(MP3_TX_PIN)

模块串口接收→开发板串口发送(已对调)

注意:DFPlayer Mini的TX/RX引脚与ESP32 S3的串口引脚已在程序中对调,接线时需严格对应表格,否则无法通信。编码器引脚需启用开发板内部上拉电阻(程序中已配置)。

三、软件准备

3.1 安装Arduino IDE与ESP32开发环境

  1. 下载并安装最新版Arduino IDE

  2. 打开Arduino IDE,依次点击「文件」→「首选项」→「附加开发板管理器网址」,输入ESP32开发板地址:https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

  3. 点击「工具」→「开发板」→「开发板管理器」,搜索「ESP32」并安装最新版「esp32 by Espressif Systems」;

  4. 安装完成后,在「工具」→「开发板」中选择「ESP32 S3 Dev Module」。

3.2 安装所需库文件

程序依赖以下3个库,通过Arduino IDE的库管理器安装:

  • U8g2库:OLED屏幕驱动库,支持中文显示。搜索「U8g2」安装(作者:Oliver Kraus);

  • DFRobotDFPlayerMini库:DFPlayer Mini模块控制库。搜索「DFRobotDFPlayerMini」安装(作者:DFRobot);

  • Wire库:I2C通信库,一般随Arduino IDE默认安装,无需额外下载。

四、程序解析与修改

以下是对核心代码的解析,关键部分已标注注释,可根据实际硬件调整引脚定义(若接线与本文一致,无需修改)。

4.1 硬件配置宏定义


4.2 编码器与按键处理逻辑

程序采用纯轮询+硬锁定方式处理编码器,避免抖动和重复触发:

  • 旋转检测:通过CLK引脚状态变化判断旋转方向,顺时针增加歌曲号,逆时针减少,超出范围时循环;

  • 按键检测:按下编码器按键时,播放当前选中的歌曲号,带200ms防抖处理;

  • 锁定机制:旋转时临时锁定编码器,确保单次旋转仅触发一次状态变化。

4.3 OLED显示功能

OLED屏幕支持两种显示模式:

  • 无歌曲时:显示「未检测到SD卡歌曲」提示,提醒检查SD卡;

  • 有歌曲时:显示标题「MP3播放器」、当前选择歌曲号、总歌曲数和操作提示「按编码器键播放」。

4.4 DFPlayer初始化与歌曲读取

`initDFPlayer()`函数实现MP3模块初始化,关键步骤包括:

  1. 初始化串口通信(波特率9600);

  2. 配置音量(默认30,范围0-30)和输出设备(SD卡);

  3. 自动读取SD卡中MP3文件夹的歌曲数量,失败时重试一次;

  4. 初始化失败时,OLED显示错误提示并进入死循环。

五、SD卡准备与调试运行

5.1 SD卡文件准备

  1. 将Micro SD卡格式化为FAT32格式(右键SD卡→格式化→选择FAT32);

  2. 在SD卡根目录创建名为「MP3」的文件夹(必须大写);

  3. 将MP3格式的音乐文件放入「MP3」文件夹,文件命名建议按顺序(如001.mp3、002.mp3...),方便对应歌曲号。

5.2 程序下载与调试

  1. 将ESP32 S3开发板通过USB线连接电脑;

  2. 在Arduino IDE中选择正确的「端口」(工具→端口);

  3. 复制完整代码到Arduino IDE,点击「上传」按钮(右箭头图标);

  4. 上传完成后,打开「串口监视器」(波特率设为115200),查看初始化信息: 成功时显示「自动检测到歌曲数:X」和「系统初始化完成!」;

  5. 失败时显示「DFPlayer初始化失败!」或「无法读取SD卡歌曲数!」,需检查接线和SD卡。

六、功能测试与常见问题

6.1 功能测试

  • 旋转编码器:观察OLED屏幕,当前歌曲号应随旋转方向变化;

  • 按下编码器按键:播放器应播放当前选中的歌曲;

  • 歌曲切换:播放时旋转编码器,下次按键会播放新选中的歌曲。

6.2 常见问题解决

  • OLED不显示:检查I2C引脚接线(SCL=12,SDA=11),确认屏幕供电正常,尝试重新安装U8g2库;

  • 编码器无响应:检查CLK/DT/SW引脚接线,确保程序中启用了INPUT_PULLUP模式;

  • DFPlayer不工作:确认TX/RX引脚对调接线,检查SD卡格式和MP3文件夹路径,尝试更换SD卡或模块;

  • 无声音输出:检查扬声器与DFPlayer的音频接口连接,增大程序中的音量值(myPlayer.volume(30))。

七、完整代码

以下是可直接复用的完整代码,已包含所有功能实现:

#include <U8g2lib.h>
#include <Wire.h>
#include "DFRobotDFPlayerMini.h"
#include <HardwareSerial.h>

// ==================== 硬件配置 ====================
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, 12, 11, U8X8_PIN_NONE);

// 编码器配置(纯轮询,无中断/无库)
#define ENCODER_CLK 38    
#define ENCODER_DT 21     
#define ENCODER_SW 47     
// 编码器防抖变量
byte lastCLK = HIGH;
byte lastDT = HIGH;
unsigned long encoderDebounce = 0;
const int encoderDebounceTime = 20; // 防抖时间
bool encoderLocked = false;         // 旋转锁定(防止重复触发)

// MP3模块配置(RX/TX已对调)
#define MP3_RX_PIN 17     
#define MP3_TX_PIN 18     
HardwareSerial mp3Serial(1);  
DFRobotDFPlayerMini myPlayer;

// ==================== 全局变量 ====================
int totalSongs = 0;            // 自动读取总歌曲数
int currentSong = 1;           // 当前选中的歌曲号(硬锁定)

// 按键消抖
bool btnPressed = false;
unsigned long btnPressTime = 0;
const unsigned long btnDebounce = 200;

// ==================== 编码器纯轮询检测(核心:硬锁定) ====================
void checkEncoder() {
  // 无歌曲时不处理
  if (totalSongs == 0) return;
  
  // 防抖+锁定:仅当无操作且超时后才检测
  if (encoderLocked || millis() - encoderDebounce < encoderDebounceTime) return;
  
  // 读取当前引脚状态(上拉输入,默认HIGH)
  byte currCLK = digitalRead(ENCODER_CLK);
  byte currDT = digitalRead(ENCODER_DT);
  
  // 仅当CLK引脚状态变化时处理(核心:只认CLK的有效跳变)
  if (currCLK != lastCLK && currCLK == LOW) {
    // 锁定编码器:处理完成前不再响应
    encoderLocked = true;
    encoderDebounce = millis();
    
    // 判断旋转方向(仅一次有效切换)
    if (currDT != currCLK) {
      // 顺时针:下一首(循环)
      currentSong = (currentSong >= totalSongs) ? 1 : currentSong + 1;
    } else {
      // 逆时针:上一首(循环)
      currentSong = (currentSong <= 1) ? totalSongs : currentSong - 1;
    }
    
    // 强制刷新显示(确保数值正确)
    updateOLEDDisplay();
    Serial.print("当前选中:");
    Serial.println(currentSong);
    
    // 解锁延迟:确保单次旋转仅触发一次
    delay(100);
    encoderLocked = false;
  }
  
  // 更新最后状态
  lastCLK = currCLK;
  lastDT = currDT;
  encoderDebounce = millis();
}

// ==================== 编码器按键检测(纯轮询) ====================
void checkEncoderButton() {
  // 无歌曲时不处理
  if (totalSongs == 0) return;
  
  if (digitalRead(ENCODER_SW) == LOW && !btnPressed && millis() - btnPressTime > btnDebounce) {
    btnPressed = true;
    btnPressTime = millis();
    // 强制播放当前锁定的歌曲号,绝不跳变
    myPlayer.play(currentSong);        
    Serial.print("播放歌曲:");
    Serial.println(currentSong);
    updateOLEDDisplay();
  } else if (digitalRead(ENCODER_SW) == HIGH && btnPressed) {
    btnPressed = false;
    btnPressTime = millis();
  }
}

// ==================== OLED显示(支持自动读取歌曲数) ====================
void updateOLEDDisplay() {
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_unifont_t_chinese1);  // 中文支持字体
    u8g2.setFontPosTop();
    u8g2.enableUTF8Print();
    
    // 标题
    u8g2.setCursor(0, 0);
    u8g2.print("MP3播放器");
    
    // 分割线
    u8g2.drawHLine(0, 15, 128);
    
    // 无歌曲时提示
    if (totalSongs == 0) {
      u8g2.setCursor(0, 20);
      u8g2.print("未检测到SD卡歌曲");
      u8g2.setCursor(0, 35);
      u8g2.print("请检查SD卡!");
    } else {
      // 有歌曲时显示正常信息
      u8g2.setCursor(0, 20);
      u8g2.print("当前选择:");
      u8g2.print(currentSong);
      u8g2.print(" 号歌曲");
      
      u8g2.setCursor(0, 35);
      u8g2.print("总歌曲数:");
      u8g2.print(totalSongs);
      
      u8g2.setCursor(0, 50);
      u8g2.print("按编码器键播放");
    }
    
  } while (u8g2.nextPage());
}

// ==================== 初始化DFPlayer(自动读取歌曲数) ====================
bool initDFPlayer() {
  mp3Serial.begin(9600, SERIAL_8N1, MP3_RX_PIN, MP3_TX_PIN);
  delay(2000); // 超长初始化延时,确保SD卡识别
  
  // 初始化播放器
  if (!myPlayer.begin(mp3Serial)) {
    Serial.println("DFPlayer初始化失败!");
    return false;
  }
  
  // 基础配置
  myPlayer.setTimeOut(1000);  // 延长超时时间
  myPlayer.volume(30);
  myPlayer.outputDevice(DFPLAYER_DEVICE_SD);
  delay(1000); // 等待SD卡挂载
  
  // 自动读取SD卡中MP3文件夹的歌曲数量(核心)
  totalSongs = myPlayer.readFileCounts(DFPLAYER_DEVICE_SD);
  
  // 容错处理:读取失败时重试一次
  if (totalSongs <= 0) {
    Serial.println("首次读取歌曲数失败,重试...");
    delay(1000);
    totalSongs = myPlayer.readFileCounts(DFPLAYER_DEVICE_SD);
  }
  
  // 最终容错:仍失败则提示
  if (totalSongs <= 0) {
    Serial.println("无法读取SD卡歌曲数!");
    totalSongs = 0;
  } else {
    Serial.print("自动检测到歌曲数:");
    Serial.println(totalSongs);
    // 初始化当前歌曲号(确保在有效范围)
    currentSong = (totalSongs >= 1) ? 1 : 0;
  }
  
  Serial.println("MP3模块初始化完成");
  return true;
}

// ==================== 初始化函数 ====================
void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);
  
  // OLED初始化(强制清屏)
  u8g2.setI2CAddress(0x3C*2);
  u8g2.begin();
  u8g2.enableUTF8Print();
  u8g2.clearBuffer();
  u8g2.sendBuffer();
  
  // 编码器引脚初始化(强制上拉,硬件防抖)
  pinMode(ENCODER_CLK, INPUT_PULLUP);
  pinMode(ENCODER_DT, INPUT_PULLUP);
  pinMode(ENCODER_SW, INPUT_PULLUP);
  
  // 初始读取编码器状态
  lastCLK = digitalRead(ENCODER_CLK);
  lastDT = digitalRead(ENCODER_DT);
  encoderLocked = false;
  
  // MP3初始化(自动读取歌曲数)
  if (!initDFPlayer()) {
    // 初始化失败时显示错误
    u8g2.firstPage();
    do {
      u8g2.setFont(u8g2_font_unifont_t_chinese1);
      u8g2.setCursor(0, 20);
      u8g2.print("MP3模块初始化失败!");
      u8g2.setCursor(0, 35);
      u8g2.print("请检查接线!");
    } while (u8g2.nextPage());
    while (1) delay(1000);
  }
  
  // 强制初始化显示
  updateOLEDDisplay();
  Serial.println("系统初始化完成!");
}

// ==================== 主循环(极简,仅轮询) ====================
void loop() {
  // 仅轮询编码器,无任何自动修改歌曲号的逻辑
  checkEncoder();          
  // 仅轮询按键,播放当前锁定的歌曲号
  checkEncoderButton();   
  // 极低延时,保证响应但不占用过多资源
  delay(5);               
}

更多推荐