Chapter 135: UART Communication Basics with ESP-IDF
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the fundamental principles of UART communication.
- Explain key UART parameters: baud rate, data bits, parity, and stop bits.
- Configure and initialize a UART peripheral on an ESP32 using ESP-IDF.
- Send and receive data over UART.
- Understand differences in UART capabilities across various ESP32 variants.
- Troubleshoot common UART communication issues.
- Implement basic UART-based applications.
Introduction
Universal Asynchronous Receiver/Transmitter (UART) is one ofthe oldest and most widely used serial communication protocols in embedded systems. Its simplicity and effectiveness make it an indispensable tool for a variety of tasks, including debugging (e.g., printf
style logging), inter-device communication (e.g., with GPS modules, sensors, or other microcontrollers), and interfacing with computers via serial-to-USB converters.
In the ESP32 ecosystem, UART peripherals are robust and highly configurable, managed by the ESP-IDF’s UART driver. This chapter will guide you through the essentials of UART communication, enabling you to leverage this fundamental interface in your ESP32 projects. We will start with the theory behind UART, then move to practical examples using ESP-IDF v5.x.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans, sans-serif'}}}%% graph TD subgraph "ESP32 with UART" direction LR ESP32[/"ESP32<br>Microcontroller"/] end style ESP32 fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 ESP32 -- "UART" --> Debugging(("Debugging<br>(printf logs via<br>Serial Monitor)")); ESP32 -- "UART" --> InterDeviceCom{{"Inter-Device<br>Communication"}}; ESP32 -- "UART" --> PCInterface(("Interfacing with PC<br>(via USB-to-Serial<br>Adapter)")); InterDeviceCom --> GPS([GPS Module]); InterDeviceCom --> Sensors([Sensors with<br>Serial Output]); InterDeviceCom --> OtherMCU([Other MCUs]); style Debugging fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style InterDeviceCom fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style PCInterface fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style GPS fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E style Sensors fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E style OtherMCU fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E classDef default fill:#FFFFFF,stroke:#333,stroke-width:1px,color:#333,font-family:'Open Sans';
Theory
What is UART?
UART stands for Universal Asynchronous Receiver/Transmitter. Let’s break down these terms:
- Universal: It’s widely adopted and can be configured to interface with a vast range of devices.
- Asynchronous: Unlike synchronous protocols (like SPI or I2C), UART does not use a shared clock signal between the transmitter and receiver to synchronize data bits. Instead, synchronization is achieved through a pre-agreed timing mechanism (baud rate) and special start/stop bits embedded in the data stream.
- Receiver/Transmitter: A UART peripheral typically contains circuitry for both sending (transmitting) and receiving data, often simultaneously (full-duplex communication).
Communication typically involves two wires:
- TX (Transmit): Carries data from the transmitting UART to the receiving UART.
- RX (Receive): Carries data from the transmitting UART of the other device to the receiving UART.
A common ground (GND) connection is also essential between the communicating devices.
How UART Works: The Data Frame
Data is transmitted byte by byte. Each byte is framed with synchronization bits:
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans, sans-serif'}}}%% stateDiagram-v2 direction TB [*] --> Idle : Line High Idle --> StartBit : Detects Falling Edge (Start Condition) StartBit : Start Bit (Logic 0) StartBit --> DataBits : Timed Sampling Begins DataBits : Data Bits ("5-8 bits, LSB first") DataBits --> HasParity HasParity : Has Parity? HasParity --> ParityBit : Yes HasParity --> StopBits : No ParityBit : Parity Bit (Even, Odd, Mark, Space) ParityBit --> StopBits : End of Data + Parity StopBits : Stop Bit(s) (1, 1.5, or 2 bits, Logic 1) StopBits --> Idle : Line Returns High, Ready for Next Frame Idle --> [*] : Communication Ends or Pauses classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E class Idle,StartBit,DataBits,ParityBit,StopBits process class HasParity decision
- Idle State: When no data is being transmitted, the TX line is held at a high voltage level (logic ‘1’).
- Start Bit: To signal the beginning of a data frame, the transmitter pulls the TX line low (logic ‘0’) for one bit duration. The receiver detects this falling edge and starts its internal clock to sample the subsequent bits.
- Data Bits: Following the start bit, the actual data bits are transmitted, typically 5 to 8 bits, with the Least Significant Bit (LSB) sent first by default. The number of data bits is configurable and must be the same on both communicating devices.
- Parity Bit (Optional): After the data bits, an optional parity bit can be sent for basic error checking.
- Even Parity: The parity bit is set such that the total number of ‘1’s in the data bits plus the parity bit is even.
- Odd Parity: The parity bit is set such that the total number of ‘1’s in the data bits plus the parity bit is odd.
- None: No parity bit is used.If used, both devices must agree on the parity scheme.
- Stop Bit(s): To mark the end of the data frame, the transmitter pulls the TX line high (logic ‘1’) for one, one and a half, or two bit durations. This ensures the line returns to the idle state, allowing the receiver to prepare for the next start bit.
Key UART Parameters
For successful communication, both the transmitter and receiver must be configured with the same parameters:
- Baud Rate: This is the rate at which bits are transmitted, measured in bits per second (bps). Common baud rates include 9600, 19200, 38400, 57600, 115200, etc. The baud rate determines the duration of each bit. For example, at 9600 baud, each bit lasts for 1/9600 seconds (approximately 104 microseconds).
- Data Bits: The number of actual data bits in each frame (e.g., 7 or 8 bits). 8 data bits is the most common setting.
- Parity: The type of parity checking used (None, Even, or Odd). ‘None’ is very common.
- Stop Bits: The number of stop bits used to signal the end of a frame (typically 1 or 2). 1 stop bit is standard.
Parameter | Description | Common Values / Settings | ESP-IDF Configuration |
---|---|---|---|
Baud Rate | The speed of data transmission in bits per second (bps). Determines bit duration. Both devices must use the same baud rate. | 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600 bps. | uart_config_t.baud_rate |
Data Bits | The number of actual data bits in each frame (character). | 5, 6, 7, or 8 bits. 8 bits is most common. | uart_config_t.data_bits (e.g., UART_DATA_8_BITS) |
Parity | An optional bit used for basic error checking. |
None: No parity bit. (Most common) Even: Parity bit makes total ‘1’s even. Odd: Parity bit makes total ‘1’s odd. (Mark/Space also possible but less common) |
uart_config_t.parity (e.g., UART_PARITY_DISABLE, UART_PARITY_EVEN, UART_PARITY_ODD) |
Stop Bits | One or more bits sent after data (and parity, if used) to signal the end of the frame and allow the receiver to resynchronize. Always logic ‘1’. | 1, 1.5, or 2 bits. 1 stop bit is standard. | uart_config_t.stop_bits (e.g., UART_STOP_BITS_1, UART_STOP_BITS_2) |
Common Shorthand | A common way to express configuration is “Data Bits, Parity, Stop Bits”. For example, 8N1 means 8 data bits, No parity, 1 stop bit. |
A common configuration is often referred to as “8N1”: 8 data bits, No parity, 1 stop bit.
Voltage Levels
ESP32 UARTs operate at TTL (Transistor-Transistor Logic) or CMOS (Complementary Metal-Oxide-Semiconductor) logic levels, typically 3.3V for a logic ‘1’ and 0V for a logic ‘0’. This is different from the +/-12V levels used by traditional RS-232 serial ports found on older PCs. To connect an ESP32 UART to an RS-232 device, a level shifter IC (like a MAX3232) is required. However, most modern USB-to-Serial adapters operate at 3.3V TTL levels, making direct connection to ESP32 pins possible.
Flow Control
In situations where a transmitter might send data faster than a receiver can process it, flow control mechanisms can be used to prevent data loss (buffer overflows).
- Hardware Flow Control (RTS/CTS): Uses two additional lines:
- RTS (Request To Send): Asserted by a device ready to send data.
- CTS (Clear To Send): Asserted by the receiving device when it’s ready to receive data. If the receiver’s buffer is full, it de-asserts CTS, signaling the transmitter to pause.
- Software Flow Control (XON/XOFF): Uses special characters (XON and XOFF) transmitted in-band (over the regular TX/RX lines) to control data flow. This is less common in simple embedded applications.
For basic applications, flow control is often disabled (UART_HW_FLOWCTRL_DISABLE
).
Feature | Hardware Flow Control (RTS/CTS) | Software Flow Control (XON/XOFF) | No Flow Control |
---|---|---|---|
Mechanism | Uses dedicated signal lines: RTS (Request To Send) and CTS (Clear To Send). | Uses special control characters (XON, XOFF) sent in-band over the data lines (TX/RX). | No mechanism to prevent buffer overflows; relies on receiver being fast enough or data being infrequent. |
Signal Lines | Requires 2 additional wires (RTS, CTS) besides TX, RX, GND. | Uses existing TX/RX lines. No extra wires. | Only TX, RX, GND needed. |
ESP-IDF Config | uart_config_t.flow_ctrl set to UART_HW_FLOWCTRL_CTS_RTS, UART_HW_FLOWCTRL_CTS, or UART_HW_FLOWCTRL_RTS. Pins set via uart_set_pin(). |
Not directly supported by ESP-IDF UART driver for automatic handling. Must be implemented in application logic if needed. (Less common in ESP32 projects). | uart_config_t.flow_ctrl set to UART_HW_FLOWCTRL_DISABLE. |
Pros |
|
|
|
Cons |
|
|
|
Typical Use | High-speed data transfer, devices with limited buffering, modem communication. | Older systems, situations where extra pins are unavailable. | Many basic embedded applications, debugging, low-speed sensor data where receiver can keep up. Often the default for simple ESP32 examples. |
ESP-IDF UART Driver
The ESP-IDF provides a comprehensive UART driver API (found in driver/uart.h
) to manage the UART peripherals. Key functions include:
uart_driver_install()
: Installs the UART driver and allocates resources like RX/TX buffers and, optionally, an event queue.uart_param_config()
: Configures the UART parameters (baud rate, data bits, parity, stop bits, flow control).uart_set_pin()
: Assigns specific GPIO pins for TX, RX, RTS, and CTS signals. Thanks to the ESP32’s GPIO matrix, most digital pins can be configured for UART functions.uart_write_bytes()
: Transmits data from a buffer over UART.uart_read_bytes()
: Reads data received by the UART into a buffer.uart_driver_delete()
: Uninstalls the UART driver and frees resources.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans, sans-serif'}}}%% graph TD A[Start: Configure UART] --> B(Define uart_config_t struct<br>e.g., baud_rate, data_bits, parity, stop_bits, flow_ctrl); B --> C{"Install UART Driver<br><br><b>uart_driver_install()</b><br><i>(UART_PORT, rx_buf_size, tx_buf_size, queue_size, *queue_handle, intr_alloc_flags)</i>"}; C -- Success --> D{"Configure UART Parameters<br><br><b>uart_param_config()</b><br><i>(UART_PORT, &uart_config)</i>"}; D -- Success --> E{"Set UART Pins<br><br><b>uart_set_pin()</b><br><i>(UART_PORT, txd_pin, rxd_pin, rts_pin, cts_pin)</i>"}; E -- Success --> F{UART Ready for Communication}; subgraph "Data Transmission / Reception Loop" direction LR F --> G{Need to Send Data?}; G -- Yes --> H[Prepare Data Buffer]; H --> I{"Write Bytes<br><br><b>uart_write_bytes()</b><br><i>(UART_PORT, *data, length)</i>"}; I --> LoopS{More to Send/Receive?}; G -- No --> J{Need to Receive Data?}; J -- Yes --> K[Prepare RX Buffer]; K --> L{"Read Bytes<br><br><b>uart_read_bytes()</b><br><i>(UART_PORT, *buffer, expected_len, timeout_ticks)</i>"}; L --> M[Process Received Data]; M --> LoopS; J -- No --> LoopS; end LoopS -- Yes --> G; LoopS -- No --> N{Application Done with UART?}; N -- Yes --> O{"Delete UART Driver<br><br><b>uart_driver_delete()</b><br><i>(UART_PORT)</i>"}; O -- Success --> P[End: UART Resources Freed]; N -- No --> F; C -- Failure --> X1[Error Handling: Driver Install Failed]; D -- Failure --> X2[Error Handling: Param Config Failed]; E -- Failure --> X3[Error Handling: Set Pins Failed]; I -- Error/Timeout --> X4[Error Handling: Write Failed/Partial]; L -- Error/Timeout --> X5[Error Handling: Read Failed/Timeout]; O -- Failure --> X6[Error Handling: Driver Delete Failed]; classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E classDef ioNode fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0369A1 classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B classDef readyNode fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 class A,P startNode; class B,H,K,M processNode; class C,D,E,I,L,O ioNode; class G,J,LoopS,N decisionNode; class F readyNode; class X1,X2,X3,X4,X5,X6 errorNode;
The driver handles the low-level details of bit timing, buffering, and interrupt management, simplifying UART communication for the application developer.
Practical Examples
Let’s dive into some practical examples. Ensure you have your ESP-IDF v5.x environment set up in VS Code.
Example 1: Basic UART Echo
This example demonstrates how to initialize a UART port, read incoming data, and echo it back to the sender. This is a fundamental test to verify your UART setup.
Project Setup:
- Create a new ESP-IDF project in VS Code or use an existing one.
- Open the
main.c
file (or create it if it doesn’t exist inmain/
).
Code (main/main.c
):
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h" // For GPIO_NUM_NC if not using certain pins
#include "esp_log.h"
#define UART_PORT_NUM UART_NUM_1 // UART port number
#define UART_RX_BUF_SIZE (1024) // RX buffer size
#define UART_TX_BUF_SIZE (0) // TX buffer size (0 = default/no buffer, direct write)
// Define UART pins (Change these to your desired pins)
// For ESP32-C3, ESP32-S3, etc., ensure these pins are valid for your specific board.
// UART0 is often used for console output. Using UART1 or UART2 is safer for general applications.
#define UART_TXD_PIN (GPIO_NUM_17)
#define UART_RXD_PIN (GPIO_NUM_16)
#define UART_RTS_PIN (UART_PIN_NO_CHANGE) // Or GPIO_NUM_NC if not used
#define UART_CTS_PIN (UART_PIN_NO_CHANGE) // Or GPIO_NUM_NC if not used
static const char *TAG = "UART_ECHO_EXAMPLE";
static void uart_echo_task(void *arg) {
// Configure a temporary buffer for the incoming data
uint8_t *data = (uint8_t *) malloc(UART_RX_BUF_SIZE);
if (data == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for UART data buffer");
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "UART echo task started. Waiting for data...");
while (1) {
// Read data from the UART
int len = uart_read_bytes(UART_PORT_NUM, data, (UART_RX_BUF_SIZE - 1), 20 / portTICK_PERIOD_MS);
// Write data back to the UART
if (len > 0) {
data[len] = '\0'; // Null-terminate whatever we received
ESP_LOGI(TAG, "Received %d bytes: '%s'", len, (char*)data);
uart_write_bytes(UART_PORT_NUM, (const char *)data, len);
ESP_LOGI(TAG, "Echoed %d bytes.", len);
}
// Small delay to yield to other tasks if no data
// vTaskDelay(pdMS_TO_TICKS(10));
}
free(data);
vTaskDelete(NULL);
}
void app_main(void) {
// Configure UART parameters
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT, // Or UART_SCLK_APB
};
ESP_LOGI(TAG, "Configuring UART%d...", UART_PORT_NUM);
// Install UART driver, and get the queue.
// We don't use the event queue here, so we pass 0 for the queue size and NULL for the queue handle.
ESP_ERROR_CHECK(uart_driver_install(UART_PORT_NUM, UART_RX_BUF_SIZE * 2, UART_TX_BUF_SIZE, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config(UART_PORT_NUM, &uart_config));
// Set UART pins (TX, RX, RTS, CTS)
// Note: In ESP-IDF v5.x, UART_PIN_NO_CHANGE means "don't change the current pin,"
// which is useful if pins are already set by bootloader or for partial reconfig.
// For initial setup, explicitly define pins. If a pin is not used, use GPIO_NUM_NC.
ESP_LOGI(TAG, "Setting UART%d pins: TXD=%d, RXD=%d", UART_PORT_NUM, UART_TXD_PIN, UART_RXD_PIN);
ESP_ERROR_CHECK(uart_set_pin(UART_PORT_NUM, UART_TXD_PIN, UART_RXD_PIN, UART_RTS_PIN, UART_CTS_PIN));
ESP_LOGI(TAG, "UART driver installed and configured.");
// Create a task to handle UART echo
xTaskCreate(uart_echo_task, "uart_echo_task", 2048, NULL, 10, NULL);
ESP_LOGI(TAG, "UART echo task created.");
}
Build Instructions:
- Save the
main.c
file. - Open the ESP-IDF terminal in VS Code (Ctrl+
or Cmd+
). - Ensure your ESP32 target is selected (
idf.py set-target esp32
or your specific variant). - Build the project:
idf.py build
Run/Flash/Observe Steps:
- Hardware Connection:
- Connect your ESP32 board to your computer via USB.
- If you are using UART1 or UART2 (as in the example with
UART_PORT_NUM = UART_NUM_1
), you’ll need a separate USB-to-Serial (TTL level, 3.3V) adapter. - Connect the adapter’s TX pin to the ESP32’s
UART_RXD_PIN
(GPIO16 in the example). - Connect the adapter’s RX pin to the ESP32’s
UART_TXD_PIN
(GPIO17 in the example). - Connect the adapter’s GND pin to one of the ESP32’s GND pins.
- Tip: If you use
UART_NUM_0
, it’s usually connected to the on-board USB-to-Serial chip used for programming and the default console. You might see bootloader messages and ESP_LOG output on the same terminal. This can be convenient for simple tests but might interfere with dedicated UART applications. For this example, usingUART_NUM_1
orUART_NUM_2
with an external adapter is cleaner.
- Flashing:
- Flash the firmware to your ESP32:
idf.py -p <YOUR_SERIAL_PORT> flash monitor
- Replace
<YOUR_SERIAL_PORT>
with the actual serial port of your ESP32 (e.g.,/dev/ttyUSB0
on Linux,COM3
on Windows). - The
monitor
command will automatically open the ESP-IDF serial monitor forUART0
(console output).
- Replace
- Flash the firmware to your ESP32:
- Observing with a Serial Terminal:
- If you used
UART_NUM_1
orUART_NUM_2
, you need to open a separate serial terminal program (e.g., PuTTY, Tera Term, minicom, or the VS Code Serial Monitor extension) connected to the USB-to-Serial adapter you wired up. - Configure this terminal with the same settings:
- Port: The COM port of your USB-to-Serial adapter.
- Baud rate: 115200
- Data bits: 8
- Parity: None
- Stop bits: 1
- Flow control: None
- Once connected, type some characters into this serial terminal and press Enter (or send). You should see the ESP32 receive the characters (logged in the ESP-IDF monitor if
UART_PORT_NUM
isUART_NUM_0
, or just processed silently if another UART) and then echo them back to your serial terminal window. - ESP-IDF Monitor Output (for
UART0
logging):
- If you used
I (XXX) UART_ECHO_EXAMPLE: Configuring UART1...
I (XXX) UART_ECHO_EXAMPLE: Setting UART1 pins: TXD=17, RXD=16
I (XXX) UART_ECHO_EXAMPLE: UART driver installed and configured.
I (XXX) UART_ECHO_EXAMPLE: UART echo task created.
I (XXX) UART_ECHO_EXAMPLE: UART echo task started. Waiting for data...
I (XXX) UART_ECHO_EXAMPLE: Received 5 bytes: 'Hello'
I (XXX) UART_ECHO_EXAMPLE: Echoed 5 bytes.
- Your Serial Terminal (connected to UART1):If you type “Test” and send, you should see “Test” appear back in the terminal.
Example 2: Sending Periodic Messages
This example shows how to configure UART to send a message from the ESP32 to a connected device (e.g., a PC) periodically.
Code (main/main.c
):
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"
#define UART_PORT_NUM UART_NUM_1
#define UART_TX_BUF_SIZE (0) // No TX buffer, direct write
#define UART_TXD_PIN (GPIO_NUM_17)
#define UART_RXD_PIN (GPIO_NUM_16) // Technically not needed for TX only, but good practice to define
#define UART_RTS_PIN (UART_PIN_NO_CHANGE)
#define UART_CTS_PIN (UART_PIN_NO_CHANGE)
static const char *TAG = "UART_TX_EXAMPLE";
static void uart_tx_task(void *arg) {
char message[64];
int count = 0;
ESP_LOGI(TAG, "UART TX task started. Sending messages periodically...");
while (1) {
snprintf(message, sizeof(message), "Hello from ESP32! Count: %d\r\n", count++);
int len_written = uart_write_bytes(UART_PORT_NUM, message, strlen(message));
if (len_written > 0) {
ESP_LOGI(TAG, "Sent %d bytes: %s", len_written, message);
} else {
ESP_LOGE(TAG, "Error sending UART message");
}
vTaskDelay(pdMS_TO_TICKS(2000)); // Send every 2 seconds
}
vTaskDelete(NULL);
}
void app_main(void) {
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
ESP_LOGI(TAG, "Configuring UART%d...", UART_PORT_NUM);
ESP_ERROR_CHECK(uart_driver_install(UART_PORT_NUM, 256, UART_TX_BUF_SIZE, 0, NULL, 0)); // RX buffer size not critical for TX only
ESP_ERROR_CHECK(uart_param_config(UART_PORT_NUM, &uart_config));
ESP_LOGI(TAG, "Setting UART%d pins: TXD=%d, RXD=%d", UART_PORT_NUM, UART_TXD_PIN, UART_RXD_PIN);
ESP_ERROR_CHECK(uart_set_pin(UART_PORT_NUM, UART_TXD_PIN, UART_RXD_PIN, UART_RTS_PIN, UART_CTS_PIN));
ESP_LOGI(TAG, "UART driver installed and configured for TX.");
xTaskCreate(uart_tx_task, "uart_tx_task", 2048, NULL, 10, NULL);
ESP_LOGI(TAG, "UART TX task created.");
}
Build Instructions:
- Save the
main.c
file. - Build the project:
idf.py build
Run/Flash/Observe Steps:
- Hardware Connection:
- Connect your ESP32 board to your computer.
- Connect a USB-to-Serial adapter: Adapter’s RX pin to ESP32’s
UART_TXD_PIN
(GPIO17). Adapter’s GND to ESP32’s GND. The ESP32’s RXD pin (GPIO16) is not strictly needed for this TX-only example but is configured.
- Flashing:
idf.py -p <YOUR_SERIAL_PORT> flash monitor
- Observing:
- Open your serial terminal program (PuTTY, etc.) connected to the USB-to-Serial adapter.
- Configure it for 115200 baud, 8N1.
- You should see messages like “Hello from ESP32! Count: X” appearing every 2 seconds.
- The ESP-IDF monitor (connected to UART0) will show log messages like “Sent X bytes…”.
Variant Notes
The number of available UART controllers and their specific capabilities can vary between ESP32 variants.
- ESP32 (Original): Features 3 UART controllers (UART0, UART1, UART2).
UART0
: Typically used for console logging and flashing. Its TXD0 is GPIO1, RXD0 is GPIO3.UART1
: TXD1 is GPIO10, RXD1 is GPIO9 by default (often used for SPI flash, so care must be taken if reconfiguring). Can be routed to other pins.UART2
: TXD2 is GPIO17, RXD2 is GPIO16 by default. Can be routed to other pins.
- ESP32-S2: Features 2 UART controllers (UART0, UART1).
UART0
: Default console.UART1
: General purpose.- All variants support routing UART signals to most GPIO pins via the GPIO Matrix, configured using
uart_set_pin()
.
- ESP32-S3: Features 3 UART controllers (UART0, UART1, UART2). Similar to ESP32.
UART0
: Default console.UART1
,UART2
: General purpose.
- ESP32-C3 (RISC-V): Features 2 UART controllers (UART0, UART1).
UART0
: Default console.UART1
: General purpose.
- ESP32-C6: Features 2 UART controllers (UART0, UART1).
UART0
: Default console.UART1
: General purpose.
- ESP32-H2: Features 2 UART controllers (UART0, UART1).
UART0
: Default console.UART1
: General purpose.
Key Considerations:
- Console UART (UART0): By default,
ESP_LOGx
messages and bootloader output go to UART0. If you plan to use UART0 for your application’s serial data, be aware that this console output might interfere. You can:- Reconfigure logging to use a different UART port or disable it.
- Parse the incoming data carefully to distinguish application data from log messages.
- Use a different UART port (UART1 or UART2) for your application, which is generally recommended for dedicated serial communication.
- Pin Multiplexing: Always consult the datasheet for your specific ESP32 variant to confirm default pin assignments and ensure the pins you choose for UART are not conflicting with other peripherals (e.g., JTAG, SPI flash). The GPIO matrix offers great flexibility but requires careful planning.
UART_SCLK_DEFAULT
vsUART_SCLK_APB
vsUART_SCLK_XTAL
etc.: Thesource_clk
field inuart_config_t
allows selecting the clock source for the UART peripheral.UART_SCLK_DEFAULT
(orUART_SCLK_REF_TICK
in older versions,UART_SCLK_APB
in newer versions for many chips) is usually a safe choice. APB clock is common. Some chips might offer XTAL or RTC clock sources for specific low-power scenarios, but these might have limitations on achievable baud rates. Refer to the ESP-IDF documentation and chip-specific TRM (Technical Reference Manual) for details. For ESP-IDF v5.x,UART_SCLK_DEFAULT
is often an alias to the recommended clock source like APB.
Tip: When defining pins, use
GPIO_NUM_NC
(No Connect) for RTS and CTS if you are not using hardware flow control. This is clearer thanUART_PIN_NO_CHANGE
for initial setup if you intend for a pin not to be used by UART.
// Example for no hardware flow control
ESP_ERROR_CHECK(uart_set_pin(UART_PORT_NUM, UART_TXD_PIN, UART_RXD_PIN, GPIO_NUM_NC, GPIO_NUM_NC));
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Mismatched Baud Rates/Parameters | Garbled characters, no data received, sporadic incorrect data, or gibberish on serial terminal. |
Ensure all UART parameters match on both devices:
|
TX/RX Pins Swapped | No communication at all. Data sent from ESP32 not seen on PC, and vice-versa. |
Verify connections:
|
Driver Not Installed or Incorrectly Configured | ESP-IDF functions (uart_write_bytes, uart_read_bytes) return errors. Device may crash or behave unexpectedly. ESP_LOG errors related to UART. |
Follow correct initialization sequence:
|
Insufficient RX Buffer or Delayed Reading | Lost incoming data. UART RX buffer overflow errors (e.g., uart_event_type_t == UART_BUFFER_FULL if using event queue, or driver logs). Partial messages. |
Increase RX buffer size: In uart_driver_install(), increase rx_buffer_size. Process data promptly: Ensure your task calls uart_read_bytes() frequently enough. Consider using a UART event queue for more responsive handling (advanced topic). |
Pin Conflicts or Incorrect Pin Assignment | UART not working. Other peripherals (e.g., SPI flash, JTAG debugger) malfunctioning. Device instability or boot loops. |
Consult ESP32 variant’s datasheet for valid GPIOs for UART and potential conflicts. Use GPIO_NUM_NC in uart_set_pin() for unused signals (e.g., RTS/CTS if flow control is disabled). Be cautious when re-assigning default pins of UART0, especially if console output is needed. |
Incorrect Voltage Levels | No communication or damage to ESP32 if connecting directly to RS-232 (+/-12V) without a level shifter. Unreliable communication if levels are marginal. |
ESP32 UARTs are typically 3.3V TTL. Use a level shifter (e.g., MAX3232) when connecting to true RS-232 devices. Ensure USB-to-Serial adapters are 3.3V TTL compatible. |
Forgetting to Free UART Driver | Resource leaks if UART is re-initialized multiple times without freeing. Potential instability over long runtimes or multiple reconfigurations. | If UART is no longer needed or being reconfigured, call uart_driver_delete(uart_port_num) to free resources. |
No ESP_LOGx Output or Boot Messages | When using UART0 for application data, default console output might be missing or garbled if settings are changed. |
If UART0 is used for custom communication, ESP-IDF logging might be affected. Consider using UART1 or UART2 for dedicated application communication to keep UART0 free for console. Alternatively, reconfigure logging (esp_log_set_vprintf) or be prepared to parse mixed data. |
Warning: Always check the return values of ESP-IDF functions (e.g.,
uart_driver_install
,uart_write_bytes
). They often provide valuable information about errors (ESP_OK
on success).
esp_err_t err = uart_driver_install(UART_PORT_NUM, UART_RX_BUF_SIZE * 2, 0, 0, NULL, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to install UART driver: %s", esp_err_to_name(err));
// Handle error appropriately
}
Exercises
- Case Changer Echo: Modify the “Basic UART Echo” example (Example 1). When data is received, convert all lowercase letters to uppercase and all uppercase letters to lowercase before echoing the modified data back. Non-alphabetic characters should be echoed unchanged.
- UART Controlled LED: Connect an LED to a GPIO pin of your ESP32. Write a program that listens for specific commands via UART (e.g., “LED_ON”, “LED_OFF”, “LED_TOGGLE”). When a valid command is received, control the LED accordingly. Send an acknowledgment message back via UART (e.g., “LED is ON”).
- Simple UART Calculator:
- The ESP32 should expect a string in the format “NUM1 OP NUM2” (e.g., “10 + 5”, “100 * 3”).
- Parse the received string to extract the two numbers and the operator (+, -, *, /).
- Perform the calculation.
- Send the result back via UART (e.g., “Result: 15”).
- Include basic error handling for invalid formats or division by zero.
- UART Bridge:
- Configure two UART ports on your ESP32 (e.g., UART1 and UART2), each with different TX/RX pins.
- Write an application that acts as a bridge: any data received on UART1 should be immediately transmitted on UART2. Similarly, any data received on UART2 should be transmitted on UART1.
- You’ll need two USB-to-Serial adapters connected to your PC to test this, or loop back one UART’s TX to the other’s RX if they are on the same ESP32 (careful with this).
- Periodic Sensor Data Sender:
- Simulate reading a sensor value (e.g., generate a random number, or read from an ADC if you’re familiar with it from Chapter 126).
- Format this sensor data as a Comma Separated Value (CSV) string, including a timestamp or a sequence number (e.g., “1,25.5”, “2,25.8”, where the first number is a sequence and the second is the sensor value).
- Transmit this CSV string over UART periodically (e.g., every 1 second).
- Bonus: Allow the PC to send a command (e.g., “START”, “STOP”) to control the periodic transmission.
Summary
- UART is a fundamental asynchronous serial communication protocol using TX and RX lines.
- Key parameters – baud rate, data bits, parity, stop bits – must match between communicating devices.
- The ESP-IDF provides a UART driver (
driver/uart.h
) for easy configuration and use. - Core functions include
uart_driver_install
,uart_param_config
,uart_set_pin
,uart_write_bytes
, anduart_read_bytes
. - ESP32 variants differ in the number of UART controllers (typically 2 or 3), but all offer flexible pin assignment via the GPIO Matrix.
- UART0 is commonly used for console output; using other UARTs (UART1, UART2) is often preferred for dedicated application communication.
- Common issues involve mismatched parameters, swapped pins, driver setup errors, and buffer handling.
- UART is versatile, used for debugging, sensor interfacing, GPS modules, inter-MCU communication, and more.
Further Reading
- ESP-IDF UART Documentation:
- ESP-IDF UART API Reference (Replace
esp32
with your target variant likeesp32s3
,esp32c3
if needed for variant-specific details).
- ESP-IDF UART API Reference (Replace
- General Serial Communication Tutorials: