top of page
White Structure

An SD Card MP3 Player

  • Jan 3
  • 16 min read

Updated: Jan 18


For those who want to take their music off-grid, this MP3 player can hold about 35-40 albums and can run off of battery power. It fits into the standard World Board System board stack and uses our old familiar Arduino Uno. Features include pause/play, next album, next/previous track, shuffle mode and 6 equalization settings. Output is to either the internal speaker (behind the display) or there is also a line-out for headphones or powered speakers. You can even plug one of those bluetooth dongles in and broadcast to a bluetooth speaker.


I found a small module containing an SD card reader, a DAC with an I2S interface, and a 3W amplifier capable of driving a small speaker. This module, designed by DFRobot, has a Wiki page with great documentation here: DFPlayer Mini Mp3 Player - DFRobot Wiki and can be purchased from their store here: DFPlayer Mini MP3 Player for Arduino Best-selling Audio Module - DFRobot.



Most of the hard work is done by the module itself. It handles reading the mp3 file from the SD card, decoding the data, and converting it to audio output to drive the speaker. All this is on a single module costing about six bucks.


You will also need a decent speaker. I like this one, which is very small but produces a good output: Amazon.com: DWEII 4 PCS Speaker 3 Watt 8 Ohm Mini Speaker 8ohm 3w Loundspeaker Micro for Arduino with JST-PH2.0 Interface for Small Electronic Projects Advertising Machines LCD TV Monitors : Electronics. Cost is about $10 for four. Of course there are a lot of speakers out there, so you may have a favorite of your own.



As far as the user interface, I used standard-issue components: an OLED display, momentary buttons, and a small variable resistor for volume control.


The rules...


OK, it can't be that easy, right? Actually, there are a few rules to follow due to the hardware design of the DFPlayer module.

SD Card type

The SD card recommended is a 32 GB SDHC Class 10. The class is important -- it refers to the speed that the card can be read and written to. This application requires a fast SD card. Most cheap SD cards are not fast enough and may cause the occasional lockup or failure to auto-advance to the next song. Other labels to look for are "A1", "A2""V10", or even "V30" all of which indicate high speed. The labels "U1" or "U3" are also good because it indicates a newer controller.


A second choice is an SDXC type. These cards are typically larger -- look for the smallest size you can find. I found some that were 64 GB here: Amazon.com: Amazon Basics MicroSDXC UHS-I Class 10 Memory Card with Full Size Adapter, A2, U3, V30, 4K, Read Speed up to 100 MB/s, 64 GB (2pack), for GoPro Cameras Storage, Black : Electronics. These types of cards are typically faster than SDHC because they are meant to store large amounts of video. Note that although I have used this type and size of card successfully, I have not tested it beyond the 32 GB limit that is specified by dfRobot so if you had really large audio files perhaps eventually you might run into problems.

Formatting

The card must be formatted as FAT32 or FAT16. For a 64 GB card, Windows will not default to this format (it will use exFAT, which won't work) and it may be necessary to use a third-party utility to do the formatting. I use EaseUS Partition Master Free: Best FAT32 Formatter Recommend: Choose A Free FAT32 Format Tool 2025. There are lots of other free FAT32 formatters.

File naming

The way files must be named depends on how you want to use the player. In the simplest mode, you can just drop a bunch of mp3 files in the root directory of the SD card and the player will play them in FAT order, which is usually the order they are copied to the card. The actual file name is not used by the player.


Another mode is to use an initial number and then some descriptive text for the file name. Naming files like "004 My Favorite Song .mp3" will work just fine but the player will ignore everything after the number. The advantage of this approach is that you can at least see what the track is when you open the SD card in file explorer. But more importantly you can designate the order of playback by numbering the tracks in this way. You are limited to 255 tracks for this method.


The most flexible mode is to organize tracks using folders. Using this approach, the folders are numbered 00-99 and the tracks within each folder start at 001 and can go as high as 255. Then you can use a command to play track XXX in folder YY.


Even though long file names are tolerated, there are the usual characters that must be avoided in file names and a few more. Do not use: ./\:*?"<>,;[] as these characters are either illegal in filenames or cause dfplayer-specific problems.

Metadata

Things like artist, album name, track name, track duration, and other metadata often stored with mp3 files are simply not available through the player. While the module looks like a standard SD card reader, it is not and does not support most file operations. If you want to display this information we will have to be a little more clever. Basically, we will use a separate utility which is discussed below.

Preparation of dfPlayer mini for WBS system


The dfPlayer mini module has a total of 16 pins, 8 on each side. Sending commands to the player is done using a software implementation of a serial port. We only need six pins to use the player with our Uno. These are:

  • Ground

  • Vcc (5V preferred)

  • Two speaker-out pins

  • Transmit (Uno receive) current sketch uses Uno pin 2)

  • Receive (Uno transmit) current sketch uses Uno pin 3)

  • (Optional -- take the DAC output L & R directly to a 1/8 inch audio jack)


The easiest way to accommodate these six pins is to put the dfplayer module on a small piece of protoboard and route the pins desired to a 6-pin male header that can plug into a river board. You can do this either by soldering wires to the male header pins, or even easier use wire wrap. Here are some pictures of the mod done for this project:


Top view of module on protoboard.  Male header pin are on the top.  Plugging these into the horizontal River board header will allow enough room in the slot to swap the card.
Top view of module on protoboard. Male header pin are on the top. Plugging these into the horizontal River board header will allow enough room in the slot to swap the card.
Bottom side of protoboard showing wire wrap connections.  Male header pins are at the bottom.   Order left to right on male header is speaker, ground, speaker, dfplayer transmit (connect to receive on Uno), dfplayer receive (transmit on Uno), Vcc
Bottom side of protoboard showing wire wrap connections. Male header pins are at the bottom. Order left to right on male header is speaker, ground, speaker, dfplayer transmit (connect to receive on Uno), dfplayer receive (transmit on Uno), Vcc

Pinout for the dfPlayer module used in this project.
Pinout for the dfPlayer module used in this project.

Physical User Interface Placement


You can use your own placement, but this is what made the most sense to me:


Bottom slot: Island board with four buttons (all configured INPUT_PULLUP). Button assignments are at the top of the sketch.

  • Far left: Play/Pause functionality, currently connected to Uno pin 10

  • Medium left: Next album functionality, currently connected to Uno pin 7

  • Medium right: Previous track (if track is currently the first in the folder, this also acts like previous album). Currently connected to Uno pin 13.

  • Far right: Next track. Currently connected to Uno pin 4.


Slot two: River board with a small potentiometer and two buttons. The pot was modded by soldering pins to each end of the pot, and a wire to the tap. The tap then is connected to an analog pin to read the voltage which is mapped to a volume 0-30.

  • The tap is currently connected to Uno pin A2. The ends of the pot are connected to ground and +5V.

    • NOTE: You can also use two buttons for volume up and down. There is a compiler variable, POT_INSTALLED that adapts the compilation to the hardware installed.

  • Medium right button: shuffle mode toggle. This causes shuffle within the folder. An "S" appears next to the volume on the bottom of the display when shuffle mode is on. Currently connected to Uno pin 12.

  • Far right button: Equalizer setting. Six equalizer modes are available: Normal (flat), Pop, Rock, Jazz, Classical, and Bass. If you hear distortion, try a different EQ setting. Some modes may boost bass beyond the distortion limit if the track was mixed with high bass already.


Slot three: River board with the dfplayer module mounted on the protoboard. I made it so that the protoboard six pin header plugs into the horizontal riverboard header so that the SD card slides out to the right and can be replaced without removing the top plate.


Top (display) slot: SH1106 or SSD1306 OLED display goes here. The speaker is mounted just under the display, stuck to the World board with a bit of double sided tape. The wires from the speaker are plugged into the River board in slot three.


Here is a picture with the top plate removed:




Basic dfplayer library commands


The library used is DFRobotDFPlayerMini.h by dfRobot, version 1.0.6. The basic commands are:

  • instantiation: DFRobotDFPlayerMini name;

  • .begin() -- starts the initialization process

  • .readFileCounts() -- returns zero until all files are scanned

  • .play(track#) -- play a specific track numer in the root

  • .playFolder(folder#, track#) -- play a specific track in a specific folder

  • .next() -- play next track

  • .prev() -- play previous track

  • .pause(), .start() -- pause/play functionality

  • .stop() -- completely stops play

  • .volume() -- set volume. 0-30

  • .EQ() -- set equalization, 0-5 accepted (hardware presets)


This sketch primarily uses playFolder to play each track within a folder. It automatically switches to the next folder when all tracks have been played in the current folder.


Quirks of the dfplayer mini (and clones)


We have already talked about file naming, but there is a considerable amount of timing considerations when using the dfplayer mini. In particular, the dfplayer has a two-stage bootup process which takes time. If a command is sent to the player too soon, then unexpected consequences can occur, even with a good fast SD card. Here is the bootup sequence as close as I was able to get from online research:


Stage 0 — Hardware power up

First thing in setup, wait for three seconds to let both the display and player fully power up.

Stage 1 — Serial interface becomes responsive

This happens quickly (≈200–400 ms). At this point, dfplayer.begin() succeeds and you can send commands…

…but the DFPlayer ignores most commands until Stage 2.

Stage 2 — MP3 decoder + SD card fully initialized

This takes 1–12 seconds, depending on:

  • SD card size and speed

  • Number of folders/files

  • Whether the DFPlayer is a clone (most are)

During this stage, the DFPlayer:

  • Resets its internal volume to 20 (default)

  • Ignores volume commands

  • Ignores EQ commands

  • Ignores playFolder commands unless a track is already playing


To ensure correct startup, the sequence would be something like:

  1. Start the softwareSerial serial communication using 9600 baud. Then wait 1 second.

  2. Start the player with .begin() and wait 1 second.

  3. Wait up to 12 seconds or until .readFileCounts() returns nonzero.

  4. Kickstart the module with

    1. .disableDAC(); //mute

    2. .play(1); //play first track

    3. delay(200); //delay

    4. .volume(0); //turn off volume temporarily

    5. .volume(desired volume); //set real volume while playing

    6. .stop(); //stop playing track

    7. delay(200); //wait some more

    8. .enableDAC(); //unmute

    9. delay(3000);


Now it should be ready to start playing a track:

playFolder(folder, track); //play desired track


Why this works

Because it ensures:

  • The DFPlayer has fully scanned the SD card

  • The DFPlayer has completed its internal two‑stage init

  • The DFPlayer is not busy when you send commands

  • SoftwareSerial is not fed garbage bytes

  • playFolder() is only called when safe

This minimizes the problems you might see with slow cards.


This correct timing sequence is essential if you want a player that will continue to run and advance tracks automatically.


Watchdog Timer

Even with the timing under control, the dfPlayer module can sometimes get stuck, especially when it first starts up. Normally lockups are rare, but when they occur, we need a reliable way to automatically get things started again. Unfortunately, there is no reset pin exposed on the dfPlayer module itself (some clones may have one). While there is a built-in watchdog timer on the Uno, this does a "soft" reset (see below). Alternatively, there are two approaches to resolving a lockup using a "harder" reset:


  1. Break the dfPlayer connection to ground so power can't flow. Then reboot the Uno. This will work if the dfPlayer is preventing the Uno from rebooting.

    1. Note: to switch power use a transistor like 2N2222 with emitter connected to Uno ground, collector connected to dfPlayer ground, and base connected to an Uno digital pin through a 1k ohm resistor.

    2. To reboot the Uno after removing dfPlayer power, define this function:

    3. void (*resetFunc)(void) = 0;

      then call resetFunc(); which basically branches to address zero, which triggers the Uno reboot.

  2. Use the Uno reset pin to force a hard reset (like pressing the reset button). This is done similarly to the method for controlling power to the dfPlayer.

    1. Use a 2N2222 transistor with emitter connected to Uno ground, collector connected to the Uno reset pin, and base connected through a 4.7 kOhm resistor to a Uno digital pin. Writing a high to the pin will cause the Uno to reboot.


My approach was to reset the entire Uno using the reset pin to trigger a hard reset which is a bit more extensive than a soft reset. Sometimes it takes several resets to free the stuck dfPlayer but this has always recovered from a lockup. Here is more information about the different types of Uno reset (Full disclosure -- this seems accurate to me, but it was produced by AI prompting).


🔧 Three Types of Reset on the Arduino Uno

🟦 1. Soft Reset (Watchdog Timer Reset)

This is what happens when the built‑in WDT times out.

What it actually does

  • Forces the CPU to jump to the reset vector.

  • Clears most CPU registers.

  • Reinitializes the stack pointer.

  • Sets the WDRF flag in MCUSR.

  • Does NOT toggle the RESET pin.

  • Does NOT reset peripherals that persist across resets, such as:

    • UART hardware state

    • SPI/I²C hardware

    • Timer configuration registers

    • External modules (DFPlayer, sensors, etc.)

Why it feels “partial”

Because the WDT reset is internal, anything outside the CPU core keeps whatever state it was in. If the DFPlayer is wedged, the WDT reset won’t touch it.

Good for

  • Recovering from firmware lockups

  • Restarting your sketch quickly

Not good for

  • Resetting external hardware

  • Clearing peripheral lockups

  • Controlling exactly when watchdog runs and what is done

🟩 2. Branch to Address Zero (Jump to 0x0000)

This is the weakest form of “reset” and is not a real reset at all.

What it actually does

  • Executes a jmp 0x0000 (or equivalent).

  • Starts running from the reset vector without resetting anything.

What does not get reset

  • Stack pointer

  • CPU registers

  • Hardware peripherals

  • Interrupt enable state

  • WDT state

  • MCUSR flags

  • External hardware

Why it’s dangerous

If the system is in a corrupted state (bad stack, interrupts firing, peripherals misconfigured), jumping to 0x0000 simply re‑runs startup code on top of the mess.

Good for

  • Sometimes can resolve specific issues

  • Occasionally useful for bootloader tricks

Not good for

  • Any kind of reliable recovery

🟥 3. Hardware Reset (Pull RESET Pin Low)

This is the gold standard reset — identical to pressing the reset button or toggling DTR from the Serial Monitor.

What it actually does

  • Resets the entire ATmega328P hardware:

    • CPU core

    • All peripherals (UART, SPI, timers, ADC, etc.)

    • Stack pointer

    • Interrupt controller

    • I/O pin states

  • Clears MCUSR except for the EXTRF flag.

  • Forces the bootloader to run (unless disabled).

  • Fully reinitializes the microcontroller’s electrical state.

What it does not do

  • Reset external modules unless you wire them to the Uno’s reset line or give them their own reset transistor.

Why it works best with DFPlayer

The DFPlayer often gets into states where its UART interface or internal decoder locks up. Only a full hardware reset of the Uno (and ideally the DFPlayer too) guarantees a clean restart.

Good for

  • Recovering from any kind of lockup

  • Ensuring peripherals start in a known state

  • Matching the behavior of the reset button


Setting up a custom watchdog timer

While it would seem that the built-in watchdog timer should work, as discussed above it does not reset everything and in this case did not help free the stuck dfPlayer. The branch to address zero method sometimes works but admittedly is only a backup if for some reason the gold standard method does not work. The best approach is to make a custom watchdog timer that will perform a hard reset. This will allow you to control exactly when the routine is enabled and what the reset routine does.


One of the easiest ways to implement a watchdog timer is to use the TimerOne library to set up a separate timer that executes a routine on a periodic basis like once a second. In my case I use a counter to indicate how many times the routine has run and if it runs more than 10 times without the counter being reset then the Uno is rebooted.


In setup, the timer is started early towards the top of the setup function. In both setup and loop, the global counter (called heartbeat) is repeatedly set to zero so that Uno reboot won't occur. It is only when loop() gets locked on a dfPlayer command and cannot update the counter fast enough that a reboot will occur.


Note that in this Uno-reset case, even though the watchdog routine first attempts a hard reset, if this does not work the soft (branch to zero) reset is eventually called. This allows some recovery attempt even when the transistor is not installed.


In summary, you need some type of watchdog timer, but worst case if you don't want to install a transistor to do a hard reset, it may recover (or not). If not, press the reset button manually.


So how do we get the artist, album and track info?


Short answer: we create an include file which maps folder and track numbers to the text information. This file is created automatically by a browser utility using javascript. It also removes illegal characters and Unicode characters which would interfere and replaces them with underscore.


To use the script most efficiently:

  1. organize your tracks in folders on your hard drive.

    1. Top folder: name it anything

    2. Second level: Artist -- make a folder for each artist, podcast, etc.

    3. Third level: Album -- make a folder for each album or logical group of tracks

    4. All tracks go in the third level folder.

    5. At this point the tracks and album titles can be whatever you want. They will be renamed by the utility automatically to be compatible with the dfPlayer module.

  2. Double click the utility file: dfplayer_multi_artist_indexer.html

  3. Click the "Add Artist Folder" button to select an artist. A file requestor will open, and you can select an artist folder and click the "Select Folder" button.

    1. Repeat, selecting as many artists as you want. (On an Uno you'll probably run out of program space at about 40 albums -- an estimate of remaining PROGMEM is provided).

  4. Now click the button to select the destination. The button is called "Select SD Card Root" but it can also be an empty folder on your hard drive.

  5. Click the Run button

    1. The utility copies the folders and files, numbering them appropriately. Note that the artist folder is not copied but the artist's name is saved. Instead, each album folder is copied to the root of the SD card.

    2. After all files have been copied, the dfplayer_map.h file is created and placed in the downloads folder.

    3. If the destination was the SD card, you can now place that in the dfplayer. If the destination was a folder on your hard drive, manually copy everything to the empty SD card and then put it in the dfplayer. (It might make sense to keep the map file in the same folder on your hard drive and then you can have multiple "virtual" sd cards ready to go).

    4. Remember the map file goes with the SD card you just made. It is the "key" that lets us see information for each track as it plays.

  6. Copy the dfplayer_map.h file to your sketch folder, replacing any map file that was already there.

  7. Upload the sketch to the Uno. (This also loads the artists, albums and track names into program memory for our sketch to use).


The Sketch


The sketch uses two compiler variables:

  • Debugging messages are controlled with the compiler variable DEBUG.

  • Volume adjust hardware is controlled with POT_INSTALLED as described above.

Other general comments:

  • The map file can make the sketch too large to load. I have not pushed the limit yet, but it has been tested with 35 albums and 498 total tracks, which used 92% of program space. The utility therefore reports an estimate of how much program space remains after each artist that is added. Of course, the SD card capacity is barely touched, the tracks only take up about 2 GB of the 32 GB capacity.

  • Lots of info in the comment block at the top, and pin definitions and user-changeable globals are near the top also.

  • The display is currently set up for an SSD1306 controller OLED. The instantiation is around line 82 in the sketch. You can substitute SH1106 for SSD1306 if you have a display that uses the SH1106 controller and the rest of the code should work fine.

  • The sketch uses forward declarations. These are not always necessary, but it doesn't hurt and ensures the compiler can recognize functions that come after setup and loop. It also makes it easy to list the functions for the sketch:


void playCurrentTrack(); -- our main function to play a track
void updateDisplay(); -- display handler
void nextTrack(); -- increments track, and possible calls nextAlbum
void prevTrack(); -- decrements track, which may also decrement album
void nextAlbum(); -- increments album and plays first track of album
void prevAlbum(); -- decrements album and plays first track of album
void toggleShuffle(); -- toggles shuffle state variable
void saveBookmark(); -- saves current album, track and EQ in flash memory
void loadBookmark(); -- loads save album, track and EQ info
void volUp(); -- used with buttons only, increments volume
void volDown(); -- used with buttons only, decrements volume
void togglePlayPause(); -- toggles pause mode
void potVol(); -- reads pot and sets volume from mapped voltage
void doUnoReset(); -- Watchdog routine
void (*resetFunc)(void) = 0;  // Declare Uno reset function at address 0

Setup

Setup starts out fairly straightforward with all buttons being set to mode INPUT_PULLUP. As discussed above, it also includes some special timing to accommodate the dfplayer hardware. At the end the volume is set either by reading the pot or if buttons are used the volume is set to the default as defined by DEFAULT_VOLUME.


Loop

Loop starts out by reading all the buttons (and volume pot) and if any are pressed, calls the appropriate function. It then reads the state of the player and if it was playing but is now stopped, it calls the nextTrack function. Finally, it updates the display and then delays for 100ms. Note that we are not concerned about blocking delays too much here because most of the time-critical audio functions are being done in hardware by the dfplayer module itself.


Text display and the dfplayer_map.h file

Most of the functions are pretty straightforward but the map file and syncing text with the track playing deserves some explanation. The basic strategy is to store big text tables in PROGMEM, store compact indexes in structs, and use those indexes to look up human‑readable names while using numeric folder/track IDs to control the DFPlayer.


Think of the SD card as a jukebox that only understands:

Play album 7, track 3.

But the user wants to see:

Artist: Tom Waits Album: The Heart of Saturday Night Track: Fumblin’ With the Blues

The map file is a reverse phone book that translates numbers to names.


What is inside the map file?

  • A giant list of artist names in one long PROGMEM string with null terminations after each entry:

const char artistNames[] PROGMEM =

"Tom Waits\0"

"Nickel Creek\0"

"Enigma\0"

...

  • A giant list of album names, each null-terminated

const char albumNames[] PROGMEM =

"Harvest\0"

"Careless Love\0"

"Heart Shaped World\0"

...

  • A giant list of track names, also each null-terminated

const char trackNames[] PROGMEM =

"01 O Fortuna\0"

"02 Fortune plango vulnera\0"

...

  • Structs that point into those lists

    • Each album has:

struct DFAlbum {

uint8_t folder; // folder number on SD card

uint16_t nameIndex; // index into albumNames[]

const DFTrack* tracks; // pointer to track list

uint8_t count; // number of tracks

};

  • Each track has:

struct DFTrack {

uint16_t trackNum; // actual MP3 number in folder

uint16_t nameIndex; // index into trackNames[]

};

  • Each artist has:

struct DFArtist {

uint16_t nameIndex; // index into artistNames[]

uint8_t firstAlbumFolder; // folder number of first album

uint8_t albumCount; // how many albums belong to this artist

};

How the sketch gets the text to display from the folder and track number

  • The sketch determines which artist owns the current album by checking folder ranges:

for (uint8_t i = 0; i < ARTIST_COUNT; i++) {

uint8_t first = pgm_read_byte(&artists[i].firstAlbumFolder);

uint8_t count = pgm_read_byte(&artists[i].albumCount);


if (albumFolder >= first && albumFolder < (first + count)) {

artistIndex = i;

break;

}

}

  • Then it copies the name from PROGMEM:

strcpy_P(

artistBuf,

artistNames + pgm_read_word(&artists[artistIndex].nameIndex)

);

  • Album name lookup:

strcpy_P(albumName,

albumNames + pgm_read_word(&(albums[currentAlbum].nameIndex)));

  • Track name lookup:

DFTrack track;

memcpy_P(&track, &trackList[currentTrack], sizeof(DFTrack));

strcpy_P(trackBuf,

trackNames + track.nameIndex);


Sketch file

The sketch file is in the zip below. It does not include the map file, that you will create yourself.


The utility you need to make the map file is in this zip:


Have fun and good listening!


Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page