The UART (Universal Asynchronous Receiver-Transmitter) is an asynchronous serial protocol for sending bytes between two devices. Despite being an aging technology, it remains ubiquitous.
Want to connect a GPS module? UART. A Bluetooth HC-05 module? UART. Communicate two boards with each other? UART. Even when you connect your board to the computer via USB for programming, you are actually using a USB-Serial converter that emulates a UART.
In this article, we will learn how to use the machine.UART class to master this fundamental protocol.
What is UART?
Unlike other protocols we will see (like I2C or SPI), UART is asynchronous. This means that there is no shared clock signal to set the pace.
For two devices to understand each other, both must agree beforehand on the transmission speed, known as the Baud Rate (typically 9600 or 115200 baud).
Basic Wiring: TX with RX
UART communication uses two data lines:
- TX (Transmit): Where data is sent out.
- RX (Receive): Where data comes in.
The Crossing Rule This is the cause of 90% of failures in beginners: The TX of one device goes to the RX of the other, and vice versa.
- TX → RX
- RX → TX
- GND → GND (Never forget to share the ground!)
The machine.UART class
In MicroPython, handling it is very simple. First, we need to know which hardware UARTs our board has available.
- ESP32: Has 3 UARTs (0, 1, 2). UART0 is usually occupied by the USB/REPL. We will generally use UART1 or UART2. The great thing about the ESP32 is that we can assign the TX/RX pins to almost any GPIO.
- Raspberry Pi Pico: Has 2 UARTs (0 and 1) with predefined pins, but flexible within certain groups.
- ESP8266: More limited, it has UART0 (USB) and UART1 (TX only).
Basic Initialization
Let’s configure a communication at 9600 baud.
from machine import UART, Pin
# Example for ESP32 using UART2
# tx=17, rx=16 are typical pins for UART2 on many ESP32 boards
uart = UART(2, baudrate=9600, tx=17, rx=16)
# Example for Raspberry Pi Pico (UART0)
# tx=Pin(0), rx=Pin(1)
# uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))
print(uart)
Printing the uart object will show us the current configuration, allowing us to verify that we have assigned the parameters correctly.
Sending Data
To send information, we use the .write() method. It is important to remember that UART sends bytes, not Unicode text strings.
# Send raw bytes
uart.write(b'Hello World')
# If we have a string, it must be encoded
message = "Temperature: 25C\n"
uart.write(message.encode('utf-8'))
Adding \n (newline) at the end is good practice to signal to the receiver that the message has ended.
Receiving Data
Reading is a bit trickier because we don’t know when data will arrive. We have several methods:
read(n): Readsnbytes. If you don’t providen, it reads everything available.readline(): Reads until a newline character (\n) is found.any(): Returns a positive value if there are characters waiting in the buffer. This one is very important. Note, it is not always an exact counter of all available bytes.
Non-blocking Read
The correct way to read a sensor or serial device without blocking our program is to first check if there is anything to read with .any().
import time
while True:
# Ask: Are there data in the mailbox?
if uart.any() > 0:
# Read everything available
data = uart.read()
# Process the data (decode from bytes to string)
try:
text = data.decode('utf-8')
print("Received:", text)
except:
print("Binary data received (not text)")
# Do other things...
time.sleep(0.1)
Practical Loopback Example
How do we test if this works without having a GPS or Bluetooth module handy? By doing a Loopback.
Connect a wire from the TX pin of your board directly to its own RX pin. That way, everything you “say” should be “heard” immediately.
from machine import UART
import time
# Configure the pins according to your board (e.g., ESP32 UART2)
uart = UART(2, baudrate=9600, tx=17, rx=16)
uart.write(b'Loopback test')
time.sleep(0.1) # Give a moment for the signal to travel
if uart.any():
received = uart.read()
print(received) # Should print b'Loopback test'
else:
print("I didn't hear anything. Did you check the wire?")
Advanced Configuration
Although 99% of the time we use the standard 8N1 configuration (8 data bits, No parity, 1 stop bit), sometimes we encounter strange industrial equipment. We can configure everything:
# Explicit 8N1 configuration (default)
uart.init(bits=8, parity=None, stop=1)
# Configuration with Even Parity - Rare but exists
uart.init(parity=0)
Common Mistakes
- Line Noise: If you receive strange characters or odd symbols, it’s almost certainly a Baud Rate mismatch. Ensure both the sender and receiver have the same speed.
- Voltage Levels: Remember that the ESP32/Pico works at 3.3V. If you connect an older 5V UART device (like some old Arduinos or older GPS modules), you could burn the RX pin. Use a voltage divider or a logic level converter.
- Buffer Overflow: If the device sends data very fast and you don’t read it with
uart.read(), the internal buffer will fill up and you will start losing information.
UART is the gateway to the world of external modules. Once you master write, any, and read, you can connect practically anything.