1. Introduction
Serial communication is one of the very first skills you need on the ESP32. It lets your board talk to your computer (or another device) over a simple two-wire protocol — one wire to transmit (TX) and one to receive (RX).
In this guide we build a small program step by step: the ESP32 will listen for a character sent from the computer, store it, and echo it back after a short delay. Along the way we explain why each function works the way it does — not just how to use it.
(Serial Monitor)
RX Buffer
(loop function)
(TX)
2. Getting Started: Serial.begin()
Before any serial communication can happen, you must "open" the communication channel and tell the ESP32 how fast to talk. This is done inside setup() with a baud rate — the number of bits transmitted per second.
void setup() {
Serial.begin(9600); // open serial port at 9600 baud
}
A short delay is often added right after Serial.begin() so the port has time to fully initialize before you start sending or receiving data — especially useful on boards where the USB-to-serial chip needs a moment to enumerate.
3. Understanding the Serial Buffer
Whenever a byte is sent to the ESP32 over serial, it doesn't go straight into your variables. It is first stored in a small area of memory called the serial (RX) buffer — a temporary holding area, like a mailbox, that queues up incoming bytes until your program reads them.
Incoming bytes fill the RX buffer left to right. Your program later drains it using Serial.read().
The size of this buffer depends on the microcontroller/board you are using:
| Board / Platform | Default RX Buffer Size |
|---|---|
| Classic Arduino (Uno / ATmega328P) | 64 characters (63 usable) |
| ESP32 (Arduino core) | 256 bytes |
If you don't read from the buffer, the data just sits there. If you try to read when the buffer is empty, you'll get nothing useful back — which is exactly the problem the next function solves.
4. Checking for Data: Serial.available()
Since we can't blindly read from an empty buffer, we need a way to ask: "Has anything arrived yet?" That is exactly what Serial.available() does.
Serial.available() returns the number of bytes available in the RX buffer, or 0 if no data is available.
if (Serial.available() > 0) {
// at least one byte has been received — safe to read
}
The logic is simple: if the returned value is greater than zero, it means at least one character has been received and is waiting in the buffer, so the code inside the if block is safe to execute.
5. Reading the Data: Serial.read()
Once we know data is available, we pull it out of the buffer with Serial.read(). This reads a single byte from the Serial port and returns the byte read (0–255), or -1 if no data is available.
char incomingByte;
void loop() {
if (Serial.available() > 0) {
incomingByte = Serial.read(); // pull one byte out of the buffer
}
}
Serial.available() before calling Serial.read(). Reading an empty buffer wastes a read cycle and returns a meaningless value (-1).
6. Why Everything is a Number: ASCII
Here's a detail that trips up many beginners: when you type a character into the Serial Monitor and hit send, the ESP32 does not receive the character itself — it receives its ASCII code, a number that represents that character.
| You Type | ASCII Value Actually Sent |
|---|---|
| '0' | 48 |
| 'A' | 65 |
| 'a' | 97 |
This matters a lot when you decide how to send data back to the computer, which is exactly what the next section covers.
7. Sending Data Back: print() vs. write()
The ESP32's Serial object gives you two very different ways to send bytes back out, and mixing them up is a common source of confusion.
Serial.print() — human-readable output
Use this when you want the Serial Monitor to display the character exactly as you'd expect a human to read it. If you received the ASCII value for the letter "E" and want to echo back the letter "E" (not the raw number), use:
Serial.print(incomingByte); // prints the character itself, e.g. "E" Serial.println(); // moves to a new line
Serial.write() — raw byte output
This sends the raw numeric byte value instead of a formatted, human-readable string. For example, if you write the number 65 with Serial.write(), the Serial Monitor will display the letter "A" (its ASCII character), rather than the digits "65".
Serial.write(65); // sends the raw byte 65 → appears as 'A' on the monitor
Serial.print() for readable text/numbers in the monitor. Use Serial.write() when you need to send raw binary/byte values to another device or protocol.
8. Putting It All Together: A Simple Echo Program
Now let's combine everything above into one working sketch. The ESP32 waits for a character, waits 3 seconds (to make the behavior visible), then echoes the character back to the Serial Monitor.
char incomingByte;
void setup() {
Serial.begin(9600);
delay(500);
Serial.println("ESP32 Serial Echo — Ready!");
}
void loop() {
if (Serial.available() > 0) {
incomingByte = Serial.read(); // grab the byte from the buffer
delay(3000); // simulate some processing time
Serial.print(incomingByte); // echo the character back
Serial.println(); // newline for a clean display
}
}
Try sending the capital letter E from the Serial Monitor — after about 3 seconds, you should see E echoed back. Send 0 and you'll get 0 back, because Serial.print() is printing the character representation, not the raw ASCII number.
9. Using the Serial Monitor
Follow these steps to test the program above:
Newline Behavior Matters
By default, the Serial Monitor automatically appends a newline character every time you click "Send." This means an extra byte is transmitted along with your intended character. If you don't want this, change the line-ending dropdown (often labeled "No line ending") in the monitor settings.
10. Serial as a Debugging Tool
Beyond talking to sensors or other boards, one of the biggest practical benefits of serial communication is debugging. As your embedded programs grow more advanced, it becomes hard to know:
- Whether a particular section of code is actually being executed
- Whether a command sent to a peripheral was received correctly
- What value a variable holds at a specific moment
The simplest fix is to sprinkle Serial.print() statements ("print traces") at key points in your code:
void loop() {
Serial.println("Entering loop...");
if (Serial.available() > 0) {
Serial.println("Data detected in buffer!");
char c = Serial.read();
Serial.print("Received: ");
Serial.println(c);
}
}
11. Quick Reference: Core Serial Functions
| Function | Purpose |
|---|---|
Serial.begin(baud) | Opens the serial port at the given baud rate (must be called first, in setup()) |
Serial.available() | Returns the number of bytes available in the RX buffer, or 0 if no data is available |
Serial.read() | Reads a single byte from the Serial port; returns the byte read (0–255), or -1 if no data is available |
Serial.print(x) | Sends data as human-readable text/characters |
Serial.println(x) | Same as print(), but adds a newline afterward |
Serial.write(x) | Sends a raw byte value directly |
Serial.flush() | Waits for outgoing data to finish transmitting |
Serial.setRxBufferSize(size) | Must be called before begin() to take effect; default RX buffer size is 256 bytes on ESP32 |