Projects Coming Soon
Project instructions will be available soon. Check back for updates!
Back to HomeProject instructions will be available soon. Check back for updates!
Back to HomeMicrocontrollers often communicate with other devices, like sensors, actuators, or maybe... an MP3 player module. In this project, we will explore how embedded systems transmit and receive data through the UART communication protocol.
Small checkpoints will prepare you for the final task: You will create an MP3 player with a "Play/Pause" toggle button and a "Skip/Next Song" button, using the DFPlayer Mini module. Songs are pre-loaded onto a microSD card which you will insert into the module. The system will output sound through a small speaker.
Embedded within the ESP32 is a device called the Universal Asynchronous Receiver-Transmitter (UART). It may communicate with another UART by transmitting or receiving messages known as packets. This is done in serial, meaning packets are sent one bit of data at a time. UARTs also communicate asynchronously, so there is no clock to time when packets are transmitted or received.
UART is a hardware component (either a circuit on a microcontroller or an Integrated Circuit) that converts parallel data to serial data for transmission, and converts a received signal from serial data back to parallel data.
Because Uart communication is asynchronous, the protocol for serial data communication is to transmit the data segments as packets. Each packet contains:
Why do we care? Beyond our project, this is a fairly common protocol that is well documented, has a built-in error check, uses only two wires, needs no clock, and the structure of the packet can be changed to meet the project needs. This is still a commonly used protocol, and you may get interviewed on UART. While you don’t need to be an expert on this or future protocols we review, it is worthwhile familiarizing yourself with the protocol for possible internship and job opportunities!
We can use the UART in our ESP32 to transmit data to another UART device. To do so, we will enlist the help of the
Serial Library, which is already installed in the Arduino IDE and includes functions you are already familiar with (ex. Serial.begin(115200), Serial.print(val), etc).
The data within each packet sent with UART is one byte in size, meaning if the data contained an int, its value could only be in the range -128 to 127 (signed) or 0 to 255 (unsigned). We’ll see how this comes into play later.
Every UART can both transmit and receive packets, which is referred to as duplex communication. They communicate over their TX and RX pins; recall that the TX pin on one device connects to the RX pin on the other.
Remember how we set the baud rate of our ESP32 using Serial.begin(115200)? When a UART receives a transmission, it reads each bit at the speed of the baud rate (measured in bits per second). Both the transmitting and receiving UART must have the same baud rate for proper communication.
There are three core functions of the Serial library:
Serial.write(val) sends data from the ESP32 board to another UART device. You will use it to transmit one byte at a time, meaning the int value passed as an argument must be in the range -128 to 127 (signed) or 0 to 255 (unsigned).Serial.available() will return an int, the number of bytes currently in the UART buffer. If the number is zero, nothing has been received.Serial.read(), which returns an int.
USB is a serial communication protocol used widely by modern computers. It defines the standards by which devices transmit data over USB cables. When you plug the ESP32 into your computer, upload a sketch, and execute a program with Serial.print(), data transmits between the ESP32 and your computer using the USB protocol.
Hidden behind the scenes is the UART. Data is transmitted between the computer and ESP32 through a chip that converts USB signals to UART signals. Those converted signals are then transmitted to and from the UART within the ESP32's microcontroller.
With this in mind, remember that the ESP32's USB port is hardwired to the TX and RX pins of the board. Do not upload a sketch to an ESP32 while the board is connected via the TX/RX pins to another ESP32. Do not attempt to use Serial.print() under these conditions either.
The HardwareSerial library enables UART communication over the predefined RX and TX other pins of the ESP32 board. Why do we need it? Hardware Serial provides a built-in UART connection through the ESP32 and creates a fast, reliable (meaning error correction and buffering), and efficient. This lightens the load on the microcontroller CPU allowing more processing power for other tasks. HardwareSerial’s reliability is important for sensors that want accurate information. ESP32’s UART is able to be full-duplex meaning that it can send and receive at the same time using its TX and RX pins respectively.
#include <HardwareSerial.h>
const int rxPin = 20;
const int txPin = 21;
HardwareSerial mySerial1(1);
The code above creates an object of the HardwareSerial class that manages a new UART connection over TX and RX pins passed to the constructor. Then, you just use the pins like you would normally!
There are several limitations of HardwareSerial: not all microcontrollers support multiple RX and TX pins, which makes multiple simultaneous serial connections not possible. Additionally, not all digital pins may support HardwareSerial. If you're curious, you find out more Find out more here.
Suppose you use a digital input pin on the ESP32 board, but it’s not connected to anything. Is the state of the pin HIGH or LOW? For it to be HIGH, the pin would need to be connected to the VCC power source. For the state to be LOW, the pin would need to be connected to GND. When the state is indeterminate like this, the pin is said to have a floating signal, which we should avoid.
The solution is what we call a pull-up resistor. The input pin is connected to a high-value resistor R1 whose other terminal is attached to VCC (as depicted below).
The intent is to set the input pin to the HIGH voltage. We often use a 10kΩ resistor to do so. Now, the input pin has an effective resistance in the range of 100 MΩ. That’s right: mega-ohms.
The resistance of the pull-up resistor R1 and effective resistance of the input pin divide the voltage VCC such that the voltage at the input pin is equal to VCC (HIGH).
Now, let’s introduce the button: one terminal attached to the input pin and the other GND.
When the button is pressed, the voltage at the input pin is shorted to GND (LOW).
With this circuit, the input pin will either be VCC or GND (HIGH or LOW). Let’s see how it would look on a breadboard:
In the layout above, the pushbutton is connected to an external pullup resistor. Its state (HIGH or LOW) is read from pin 10 of the ESP32.
When you use a button, you should combine it with a pull-up resistor. Instead of building an external circuit, you can use an internal pull-up resistor in the ESP32 board. You may do so by setting the pin mode as follows:
pinMode(pin, INPUT_PULLUP);
The layout above is for a pushbutton that uses the ESP32’s internal pullup resistor.
The ESP32 has too little memory to load and play audio files. Instead, we use a separate device to store and manage the audio media: The DFPlayer Mini is a small MP3 module that connects to an ESP32 (or other microcontrollers) over UART.
We will use the module's serial mode. In serial mode, we need only connect the module's TX/RX, Power, Ground, and speaker pins. Using UART, the ESP32 will send commands to the DFPlayer Mini to playback songs, pause, change volume, etc. This is facilitated by a custom Arduino library.
Explore more about the DFplayer Mini at its wiki page.
As an optional exercise of technical documentation literacy, we will not provide the pinout diagram. Instead, you can access the datasheet here.
Remember that you are looking for the following in the datasheet’s pinout diagram: TX, RX, Power (VCC), Ground (GND), Speaker +, and Speaker -. (Hint: Look at Section 2 of the datasheet).
Read Section 1.2, 1.4 for information about the supported file types, file naming conventions, and folder (a.k.a. directory) structure for the MicroSD card.
You can download the DFPlayer Mini library v1.0.5 here. We recommend this version as newer ones have produced errors in our testing. It can be used in your code with the following include:
#include <DFRobotDFPlayerMini.h>
Below is sample code that demonstrates the module in serial mode. The program plays an audio file named “0001.mp3” for five seconds, then pauses it for five seconds, then resumes. Then, in the loop, it cycles through songs every 5 seconds. Use this as a reference for the commands to send to the MP3 module.
Do not blindly copy-paste the sample code to your project deliverables. Read it, understand it, then adapt it to your own code.
#include <HardwareSerial.h>
#include <DFRobotDFPlayerMini.h>
// Use pins 20 and 21 with HardwareSerial to communicate with DFPlayer Mini
const int PIN_MP3_RX = 20; // Connects to module's RX
const int PIN_MP3_TX = 21; // Connects to module's TX
// Define serial port objects
HardwareSerial mySerial1(1); //use UART1
// Create the player object which will serve as an interface for our DFPlayer commands
DFRobotDFPlayerMini player;
void setup() {
// Initialize USB serial port for debugging
Serial.begin(115200);
// Initialize the pins for HardwareSerial. No pinMode calls needed.
mySerial1.begin(9600, SERIAL_8N1, PIN_MP3_RX, PIN_MP3_TX); // RX=20, TX=21
delay(1000); //DO NOT REMOVE THIS DELAY. It is needed to properly initialize HardwareSerial.
// Start communication with DFPlayer Mini using player.begin
// player.begin returns true if the connection was a success
// and false if it failed
if (!player.begin(mySerial1)){
Serial.println("Connecting to DFPlayer Mini failed!");
}
// Set the player volume to maximum (0 to 30).
player.volume(30);
// Play the "0001.mp3" file in the "mp3" folder on the microSD card.
player.play(1);
// Delay the next command by 5 seconds.
delay(5000);
// Pause the playback.
player.pause();
// Delay the next command by 2 seconds.
delay(2000);
// Resume the playback from pause.
player.start();
// Here are some useful debugging functions
// Sometimes they don't return valid values. In that case, ignore them.
Serial.println(player.readFileCounts()); // Returns number of files in microSD
Serial.println(player.readCurrentFileNumber()); // Returns current file number being played
}
// Change songs every 5 seconds.
void loop() {
// Delay the next command by 5 seconds.
delay(5000);
// Skip to the next song.
player.next();
}
Here is a quick recap on how you can install Arduino libraries in the Arduino IDE 2.0.0+
You're done! Just make sure to #include the library header file so that your
code compiles.
| DFRobot DFPlayerMini | Download |
Thinking about using a pin but you don't know what it does? Refer to the ESP32 Board pinout diagram.
One of the player’s buttons must toggle the music between “Play” mode and “Pause” mode.
The second button should be the “Skip/Next Song” button, which interrupts the current song and plays a new song.
| Part Name | Qty |
|---|---|
| Jumper Wire | ? |
| Breadboard | 1 |
| ESP32 | 1 |
| MicroSD Card | 1 |
| Pushbutton Switch | 2 |
| Speaker, 0.5W, 8Ω | 1 |
| Resistor, 1kΩ | 1 |
| Resistor, 10kΩ | 1 |
| DFPlayer Mini Module | 1 |
| Mini USB Cable | 1 |
Students enrolled in the course must submit the deliverables below to the corresponding Canvas course assignment.
Place the following files in a single folder:
Compress the folder to a zip file and rename the file using the format “ops_project5_lastname_firstname.zip” Then, submit the zip file to the Project 5 Canvas assignment.