Beginner

UART Serial Communication

Understand Universal Asynchronous Receiver-Transmitter (UART) communication for serial data transfer and debugging. Master baud rate configuration, RS-232/RS-485 standards, hardware flow control, and essential debugging techniques.

1. Introduction to UART

Universal Asynchronous Receiver-Transmitter (UART) is one of the most widely used serial communication protocols in embedded systems. Unlike synchronous protocols like SPI and I2C, UART is asynchronous, meaning it does not require a shared clock signal between devices. Instead, both devices must agree on a common data rate (baud rate) beforehand.

UART is commonly used for point-to-point communication between microcontrollers and peripheral devices, as well as for debugging and data logging through serial terminals. Its simplicity and widespread support make it an essential protocol for embedded developers.

Key Advantage: UART is asynchronous and requires only two wires (TX and RX) for bidirectional communication, making it simple to implement and highly reliable for point-to-point connections.
Common Applications
  • Serial console and debugging output
  • GPS module communication
  • Bluetooth and WiFi module interfacing
  • GSM/4G modem AT command interface
  • PC-to-microcontroller communication
  • Sensor data transmission over long distances (RS-485)
  • Industrial automation and SCADA systems

2. UART Basics and Architecture

Two-Wire Interface

UART uses two unidirectional data lines for communication:

  • TX (Transmit): Data output line from the device
  • RX (Receive): Data input line to the device

For bidirectional communication, the TX of one device connects to the RX of the other device and vice versa. A common ground connection is also required between devices.

[UART Connection Diagram]
Device 1: TX → RX :Device 2
Device 1: RX ← TX :Device 2
Common Ground (GND) connection
Asynchronous Communication

UART is asynchronous, which means:

  • No shared clock signal between devices
  • Both devices must use the same baud rate
  • Data framing includes start and stop bits for synchronization
  • Each device uses its own internal clock
  • Clock tolerance is critical (typically ±2-5%)
Voltage Levels

UART signals can use different voltage standards:

  • TTL (3.3V/5V): Logic HIGH = VCC, Logic LOW = 0V (most microcontrollers)
  • RS-232: Logic HIGH = -3V to -15V, Logic LOW = +3V to +15V (inverted)
  • RS-485: Differential signaling for noise immunity and long distances

Level shifters or converters (like MAX232) are needed when interfacing different voltage standards.

3. Data Frame Format

A UART data frame consists of several components that define how data is packaged and transmitted:

[UART Data Frame]
IDLE (HIGH) → START BIT (LOW) → DATA BITS (5-9) → PARITY BIT (optional) → STOP BIT(S) (HIGH) → IDLE
Frame Components

1. Idle State

  • Line is held HIGH when no data is being transmitted
  • Indicates the bus is ready for transmission

2. Start Bit

  • Single bit pulled LOW to signal start of transmission
  • Allows receiver to synchronize with transmitter
  • Always 0 (LOW)

3. Data Bits (5, 6, 7, 8, or 9 bits)

  • Actual data being transmitted
  • Most common: 8 data bits
  • Transmitted LSB (Least Significant Bit) first
  • 5-7 bits used for ASCII text (older systems)
  • 9 bits used for address/data multiplexing in some protocols

4. Parity Bit (Optional)

  • Used for basic error detection
  • Even Parity: Bit set to make total number of 1s even
  • Odd Parity: Bit set to make total number of 1s odd
  • None: No parity bit (most common)

5. Stop Bits (1, 1.5, or 2 bits)

  • Line pulled HIGH to signal end of transmission
  • Provides time for receiver to process data
  • Most common: 1 stop bit
  • 2 stop bits provide extra stability for noisy environments
Common Frame Configurations
Configuration Description Notation Typical Use
8N1 8 data bits, No parity, 1 stop bit 8-N-1 Most common (Arduino default)
8E1 8 data bits, Even parity, 1 stop bit 8-E-1 Industrial protocols (Modbus)
8O1 8 data bits, Odd parity, 1 stop bit 8-O-1 Some legacy systems
7E1 7 data bits, Even parity, 1 stop bit 7-E-1 ASCII communication
Standard Configuration: Unless otherwise specified, most modern systems use 8N1 (8 data bits, no parity, 1 stop bit), which is the default for Arduino, Raspberry Pi, and most embedded systems.

4. Baud Rate and Timing

Baud rate is the speed at which data is transmitted over UART, measured in bits per second (bps). Both transmitting and receiving devices must use the same baud rate for successful communication.

Common Baud Rates
Baud Rate Bit Period Typical Application
9600 104.2 μs Standard debugging, GPS modules
19200 52.1 μs Faster debugging
38400 26.0 μs Bluetooth modules
57600 17.4 μs High-speed data transfer
115200 8.7 μs Common default (Arduino, ESP32)
230400 4.3 μs Very high-speed applications
921600 1.1 μs Maximum for many microcontrollers
Calculating Baud Rate

For microcontrollers, baud rate is typically generated by dividing the system clock:

Baud Rate = Clock Frequency / (16 × (BRR + 1))

Where:
- Clock Frequency = System or peripheral clock (e.g., 16 MHz)
- BRR = Baud Rate Register value
- 16 = Oversampling factor (typical for UART)

Example for 9600 baud at 16 MHz:
9600 = 16,000,000 / (16 × (BRR + 1))
BRR = 103.17 ≈ 103

Actual baud rate = 16,000,000 / (16 × 104) = 9615 baud
Error = (9615 - 9600) / 9600 × 100% = 0.16% ✓ Acceptable
Baud Rate Error Tolerance

Clock accuracy is critical in UART communication:

  • Acceptable Error: Typically ±2% for reliable communication
  • Marginal Error: ±2% to ±5% may work but is unreliable
  • Unacceptable Error: Greater than ±5% will likely fail
  • Error accumulates over the entire frame (start + data + parity + stop)
  • Longer frames (9 data bits + parity) are more sensitive to error
Important: When using internal RC oscillators instead of crystal oscillators, verify baud rate accuracy. Internal oscillators can have ±10% tolerance, which may cause UART errors, especially at high baud rates.
Bit Timing Example

For 115200 baud (8N1 configuration):

  • Bit period: 1 / 115200 = 8.68 μs per bit
  • Start bit: 8.68 μs
  • 8 data bits: 8 × 8.68 = 69.44 μs
  • Stop bit: 8.68 μs
  • Total frame time: 87.1 μs
  • Maximum throughput: 11,520 bytes/second

5. RS-232 and RS-485 Standards

UART can be implemented using different physical layer standards, each with specific voltage levels and characteristics:

RS-232 (Recommended Standard 232)

Characteristics:

  • Point-to-point communication only
  • Single-ended signaling
  • Logic 1 (Mark): -3V to -15V
  • Logic 0 (Space): +3V to +15V
  • Maximum distance: ~15 meters (50 feet)
  • Maximum speed: 115200 baud (standard), up to 1 Mbps (some implementations)
  • Used in: PC COM ports, industrial equipment, older peripherals

Common Connectors:

  • DB9 (9-pin connector) - Most common
  • DB25 (25-pin connector) - Legacy systems
TTL to RS-232 Conversion: Use MAX232, MAX3232, or similar level shifter ICs to interface microcontroller (TTL 0-5V) with RS-232 devices (±12V).
RS-485 (Recommended Standard 485)

Characteristics:

  • Multi-drop capability (up to 32-256 devices on one bus)
  • Differential signaling (A and B lines)
  • Differential voltage: ±1.5V to ±6V
  • Maximum distance: 1200 meters (4000 feet)
  • Maximum speed: 10 Mbps (short distance) to 100 kbps (long distance)
  • Excellent noise immunity due to differential signaling
  • Used in: Industrial automation, Modbus RTU, DMX512, building automation

Bus Configuration:

  • Requires 120Ω termination resistors at both ends of the bus
  • Half-duplex: requires Direction Enable (DE) and Receiver Enable (RE) control
  • Master-slave or multi-master with collision detection
Important: RS-485 requires proper termination and biasing. Without termination resistors, reflections will cause data corruption. Biasing resistors keep the bus in a known state when idle.
Comparison Table
Feature TTL UART RS-232 RS-485
Signaling Single-ended Single-ended Differential
Voltage Levels 0V / 3.3V or 5V ±3V to ±15V Differential ±1.5V
Max Distance ~1 meter ~15 meters ~1200 meters
Max Speed Up to 5 Mbps Up to 115 kbps Up to 10 Mbps
Devices 1 to 1 1 to 1 Up to 32-256
Noise Immunity Low Moderate Excellent

6. Hardware Flow Control

Flow control prevents data loss when one device cannot keep up with the data rate. Hardware flow control uses additional signal lines for handshaking.

RTS/CTS Flow Control

RTS (Request To Send) and CTS (Clear To Send) are the most common hardware flow control signals:

  • RTS: Output from device indicating it's ready to receive data
  • CTS: Input to device indicating the other side is ready to receive
  • Device asserts RTS (LOW) when ready to receive
  • Device monitors CTS before transmitting
  • If CTS is deasserted (HIGH), transmission is paused
[RTS/CTS Flow Control]
Device 1: TX → RX :Device 2
Device 1: RX ← TX :Device 2
Device 1: RTS → CTS :Device 2
Device 1: CTS ← RTS :Device 2
DTR/DSR Flow Control

DTR (Data Terminal Ready) and DSR (Data Set Ready):

  • DTR: Output indicating device is powered and ready
  • DSR: Input from modem/device indicating it's ready
  • Used primarily in modem communications
  • Less common in modern embedded systems
Software Flow Control (XON/XOFF)

Alternative to hardware flow control using special control characters:

  • XON (0x11): Resume transmission
  • XOFF (0x13): Pause transmission
  • No additional wires needed
  • Cannot transmit binary data containing 0x11 or 0x13
  • Not recommended for binary protocols
When to Use Flow Control
Scenario Flow Control Needed? Recommendation
Simple debugging output No 2-wire (TX/RX only)
High-speed data transfer Yes Hardware flow control (RTS/CTS)
Slow processing on receiver Yes Hardware or software flow control
GPS or sensor data Usually No 2-wire, ensure sufficient buffer size
File transfer Recommended Hardware flow control or protocol-level ACK
Best Practice: For most embedded applications, ensure your receive buffer is large enough to handle incoming data bursts rather than implementing flow control. Use hardware flow control only when buffer overrun is unavoidable.

7. Code Examples

Arduino - Basic UART Communication
Arduino Serial Transmission and Reception
void setup() {
// Initialize serial communication at 9600 baud
Serial.begin(9600);

// Wait for serial port to connect (for USB serial)
while (!Serial) {
; // Wait
}

Serial.println("Arduino UART Example");
Serial.println("Type something and press Enter");
}

void loop() {
// Check if data is available to read
if (Serial.available() > 0) {
// Read incoming byte
char incomingByte = Serial.read();

// Echo back
Serial.print("Received: ");
Serial.println(incomingByte);

// Read entire string
String incomingString = Serial.readStringUntil('\n');
Serial.print("String: ");
Serial.println(incomingString);
}

// Send periodic message
static unsigned long lastSend = 0;
if (millis() - lastSend > 5000) {
Serial.println("Heartbeat message");
lastSend = millis();
}
}
Arduino - Multiple Serial Ports
Using Hardware Serial Ports
// For Arduino Mega, Due, or boards with multiple UARTs

void setup() {
Serial.begin(9600); // USB Serial (for debugging)
Serial1.begin(9600); // Hardware UART1 (pins 18/19)
Serial2.begin(115200); // Hardware UART2 (pins 16/17)

Serial.println("Multiple UART Example");
}

void loop() {
// Read from Serial1 and echo to Serial (debug)
if (Serial1.available()) {
char data = Serial1.read();
Serial.print("From Serial1: ");
Serial.println(data);
}

// Read from Serial and send to Serial2
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
Serial2.println(command);
Serial.print("Sent to Serial2: ");
Serial.println(command);
}

// Read from Serial2
if (Serial2.available()) {
String response = Serial2.readStringUntil('\n');
Serial.print("From Serial2: ");
Serial.println(response);
}
}
Arduino - Software Serial
Using Software Serial for Additional UARTs
#include <SoftwareSerial.h>

// Define RX and TX pins for software serial
SoftwareSerial mySerial(10, 11); // RX, TX

void setup() {
Serial.begin(9600); // Hardware serial
mySerial.begin(9600); // Software serial

Serial.println("Software Serial Example");
}

void loop() {
// Forward data from software serial to hardware serial
if (mySerial.available()) {
char c = mySerial.read();
Serial.write(c);
}

// Forward data from hardware serial to software serial
if (Serial.available()) {
char c = Serial.read();
mySerial.write(c);
}
}

// Note: SoftwareSerial has limitations:
// - Maximum ~115200 baud (lower for reliable operation)
// - Cannot transmit and receive simultaneously
// - May miss data if not read frequently
STM32 - HAL UART Implementation
STM32 HAL UART Configuration and Usage
UART_HandleTypeDef huart2;

/* UART2 initialization */
void UART_Init(void) {
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;

if (HAL_UART_Init(&huart2) != HAL_OK) {
Error_Handler();
}
}

/* Blocking transmit */
void UART_SendString(char* str) {
HAL_UART_Transmit(&huart2, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);
}

/* Blocking receive */
void UART_ReceiveByte(uint8_t* data) {
HAL_UART_Receive(&huart2, data, 1, HAL_MAX_DELAY);
}

/* Non-blocking interrupt-based transmit */
void UART_SendString_IT(char* str) {
HAL_UART_Transmit_IT(&huart2, (uint8_t*)str, strlen(str));
}

/* Non-blocking interrupt-based receive */
uint8_t rxBuffer[100];
void UART_ReceiveData_IT(void) {
HAL_UART_Receive_IT(&huart2, rxBuffer, 100);
}

/* UART interrupt callback */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART2) {
// Process received data
// Restart reception
HAL_UART_Receive_IT(&huart2, rxBuffer, 100);
}
}
GPS Module Example (NMEA Parsing)
Reading GPS Data via UART
#include <SoftwareSerial.h>

SoftwareSerial gpsSerial(4, 3); // RX, TX

void setup() {
Serial.begin(9600);
gpsSerial.begin(9600);

Serial.println("GPS UART Example");
}

void loop() {
// Read NMEA sentences from GPS
if (gpsSerial.available()) {
String nmeaSentence = gpsSerial.readStringUntil('\n');

// Check for GPGGA sentence (Global Positioning System Fix Data)
if (nmeaSentence.startsWith("$GPGGA")) {
Serial.print("GPS Data: ");
Serial.println(nmeaSentence);

// Parse NMEA sentence (simplified example)
parseGPGGA(nmeaSentence);
}
}
}

void parseGPGGA(String sentence) {
// GPGGA format: $GPGGA,time,lat,N/S,lon,E/W,quality,numSV,HDOP,alt,M,...

int commaIndex[15];
int index = 0;

// Find comma positions
for (int i = 0; i < sentence.length() && index < 15; i++) {
if (sentence.charAt(i) == ',') {
commaIndex[index++] = i;
}
}

if (index >= 9) {
String latitude = sentence.substring(commaIndex[1] + 1, commaIndex[2]);
String longitude = sentence.substring(commaIndex[3] + 1, commaIndex[4]);
String altitude = sentence.substring(commaIndex[8] + 1, commaIndex[9]);

Serial.print("Lat: "); Serial.print(latitude);
Serial.print(" Lon: "); Serial.print(longitude);
Serial.print(" Alt: "); Serial.println(altitude);
}
}

8. Serial Debugging Techniques

UART is the primary debugging interface for embedded systems. Mastering serial debugging techniques is essential for efficient development.

Basic Debug Output
Effective Debug Printing
// Define debug levels
#define DEBUG_LEVEL 2 // 0=none, 1=errors, 2=warnings, 3=info, 4=debug

#if DEBUG_LEVEL >= 1
#define DEBUG_ERROR(x) Serial.print("[ERROR] "); Serial.println(x)
#else
#define DEBUG_ERROR(x)
#endif

#if DEBUG_LEVEL >= 2
#define DEBUG_WARN(x) Serial.print("[WARN] "); Serial.println(x)
#else
#define DEBUG_WARN(x)
#endif

#if DEBUG_LEVEL >= 3
#define DEBUG_INFO(x) Serial.print("[INFO] "); Serial.println(x)
#else
#define DEBUG_INFO(x)
#endif

#if DEBUG_LEVEL >= 4
#define DEBUG(x) Serial.print("[DEBUG] "); Serial.println(x)
#else
#define DEBUG(x)
#endif

void setup() {
Serial.begin(115200);
DEBUG_INFO("System initialized");
}

void loop() {
int sensorValue = analogRead(A0);

if (sensorValue > 1000) {
DEBUG_ERROR("Sensor value out of range!");
} else if (sensorValue > 800) {
DEBUG_WARN("Sensor value approaching limit");
} else {
DEBUG("Sensor reading normal");
}

delay(1000);
}
Formatted Output
Using sprintf for Formatted Debugging
void printFormattedData() {
float temperature = 25.47;
int humidity = 65;
unsigned long timestamp = millis();

char buffer[100];

// Format: [timestamp] Temp: XX.XX°C, Humidity: XX%
sprintf(buffer, "[%lu] Temp: %.2f°C, Humidity: %d%%",
timestamp, temperature, humidity);

Serial.println(buffer);
}

// Output example: [12345] Temp: 25.47°C, Humidity: 65%
Binary Data Debugging
Hex Dump Function
void hexDump(uint8_t* data, size_t length) {
Serial.print("Hex Dump (");
Serial.print(length);
Serial.println(" bytes):");

for (size_t i = 0; i < length; i++) {
if (i % 16 == 0) {
Serial.printf("\n%04X: ", i);
}
Serial.printf("%02X ", data[i]);
}
Serial.println();
}

// Example usage
uint8_t buffer[] = {0x48, 0x65, 0x6C, 0x6C, 0x6F};
hexDump(buffer, sizeof(buffer));

// Output:
// Hex Dump (5 bytes):
// 0000: 48 65 6C 6C 6F
Performance Monitoring
Timing and Performance Analysis
// Measure execution time
unsigned long startTime, endTime;

void performanceTest() {
startTime = micros();

// Code to measure
complexCalculation();

endTime = micros();

Serial.print("Execution time: ");
Serial.print(endTime - startTime);
Serial.println(" μs");
}

// Memory usage reporting
void printMemoryUsage() {
Serial.print("Free RAM: ");
Serial.print(freeMemory());
Serial.println(" bytes");
}

// For AVR boards
int freeMemory() {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
Serial Monitor Tools and Settings

Arduino Serial Monitor:

  • Set correct baud rate in bottom-right corner
  • Choose line ending: "Newline" for most applications
  • Enable timestamps for time-based debugging
  • Use "Autoscroll" for continuous monitoring

Alternative Terminal Programs:

  • PuTTY: Windows/Linux, supports logging
  • Tera Term: Windows, macro support
  • CoolTerm: Cross-platform, hex display
  • screen: Linux/Mac command-line tool
  • minicom: Linux terminal emulator
Common Debugging Issues
Problem Possible Cause Solution
Garbage characters Wrong baud rate Match baud rate on both sides
Missing characters Buffer overflow Increase buffer size or read more frequently
No output TX/RX swapped Verify TX connects to RX
Intermittent errors Poor connection Check wiring and ground connection
Extra line breaks Line ending mismatch Set correct line ending in terminal
Pro Tip: Use different baud rates for different purposes - lower rates (9600) for debugging humans read, higher rates (115200+) for high-volume data logging.

9. Advantages and Disadvantages

Advantages
  • Simple hardware implementation (only 2 wires needed)
  • No clock signal required (asynchronous)
  • Universal support across all microcontroller platforms
  • Full-duplex communication capability
  • Well-established and mature protocol
  • Easy to debug and troubleshoot
  • Built-in error detection with parity bit
  • Can work over long distances with RS-485
  • Low cost and minimal external components
Disadvantages
  • Requires precise baud rate agreement between devices
  • Sensitive to clock accuracy (±2-5% tolerance)
  • Point-to-point only (without RS-485)
  • No built-in addressing (single slave only)
  • Lower speed compared to SPI
  • Start/stop bits add overhead (~20% for 8N1)
  • No acknowledgment mechanism
  • Limited distance without level shifters (TTL)
  • Vulnerable to noise on long cables
When to Choose UART

Use UART when:

  • Simple point-to-point communication is needed
  • Debugging output and serial console required
  • Interfacing with GPS, GSM, or Bluetooth modules
  • Long-distance communication needed (with RS-485)
  • Full-duplex communication is required
  • Maximum simplicity is priority

Consider alternatives when:

  • High-speed data transfer needed → Use SPI
  • Multiple devices on same bus → Use I2C or CAN
  • Synchronized data required → Use SPI or I2C
  • Very short distances and high speed → Use SPI

10. Summary and Next Steps

You've learned the fundamentals of UART serial communication including:

  • Asynchronous communication with TX and RX lines
  • Data frame format with start, data, parity, and stop bits
  • Baud rate configuration and timing calculations
  • RS-232 and RS-485 physical layer standards
  • Hardware flow control with RTS/CTS signals
  • Practical serial debugging techniques
  • Code implementation on Arduino and STM32
Next Steps:
  • Build a simple chat program between two microcontrollers
  • Interface with a GPS module and parse NMEA sentences
  • Implement a command-line interface (CLI) over UART
  • Create a data logger that sends sensor data via serial
  • Experiment with different baud rates and measure error rates
  • Build an RS-485 multi-drop network with multiple nodes
  • Implement a simple protocol with checksums and error handling
Recommended Projects
  1. Bluetooth Terminal: Interface HC-05 Bluetooth module and create wireless serial communication
  2. GPS Tracker: Parse GPS NMEA data and display location on OLED
  3. Modbus RTU Slave: Implement Modbus protocol over RS-485
  4. Data Logger: Stream sensor data to PC and save to CSV file
  5. Remote Control: Create AT command interface for controlling hardware
  6. Multi-Device Network: Build RS-485 network with master-slave architecture
Further Learning Resources
  • Study Modbus RTU protocol for industrial applications
  • Learn about DMA (Direct Memory Access) for efficient UART transfers
  • Explore circular buffers for interrupt-driven UART
  • Implement CRC or checksum-based error detection
  • Study NMEA protocol for GPS applications
  • Learn AT command set for GSM/WiFi modules