Skip to content

UART (Serial / RS-232 / RS-485)

Overview

Universal Asynchronous Receiver/Transmitter (UART) provides serial communication over RS-232/RS-485 physical layers. This implementation uses Linux termios for direct device file configuration with support for all standard baud rates up to 4 Mbaud.

Status: Implemented

Full implementation with configurable baud, parity, flow control, non-blocking I/O, and modem line control.

Protocol Specification

Frame Format

┌───────┬────────────────────────────────┬────────┬──────────┐
│ Start │        Data Bits (5-8)         │ Parity │ Stop Bits│
│  (1)  │  [D0] [D1] [D2] ... [D7]      │ (0-1)  │  (1-2)   │
└───────┴────────────────────────────────┴────────┴──────────┘
  LOW              LSB first                           HIGH

Idle state: HIGH (mark)
Start bit:  LOW (space) signals beginning of frame
Data bits:  5, 6, 7, or 8 bits, LSB first
Parity:     None, Odd, or Even (optional error detection)
Stop bits:  1 or 2 HIGH bits signals end of frame

Timing (115200 baud, 8N1)

                 1 bit = 8.68 µs
         ┌───────────────────────────────┐
         │                               │
    ─────┐   ┌───┐   ┌───┐   ┌───┬───┬─────
         │   │   │   │   │   │   │   │
         └───┘   └───┘   └───┘   │   │
         START  D0  D1  D2 ... D7 STOP IDLE

    Total frame time (10 bits @ 115200): 86.8 µs

API Reference

Configuration Types

typedef enum {
    UART_PARITY_NONE = 0,
    UART_PARITY_ODD  = 1,
    UART_PARITY_EVEN = 2
} uart_parity_t;

typedef enum {
    UART_FLOW_NONE   = 0,
    UART_FLOW_RTSCTS = 1,  /* Hardware flow control */
    UART_FLOW_XONOFF = 2   /* Software flow control */
} uart_flow_t;

typedef struct {
    uint32_t     baud_rate;        /* Baud rate (e.g., 115200) */
    uint8_t      data_bits;        /* 5, 6, 7, or 8 */
    uint8_t      stop_bits;        /* 1 or 2 */
    uart_parity_t parity;
    uart_flow_t   flow_control;
    uint32_t     read_timeout_ms;  /* VTIME-based read timeout */
} uart_config_t;

typedef struct {
    int            fd;              /* File descriptor */
    char           device[64];      /* Device path */
    uart_config_t  config;          /* Active configuration */
    struct termios orig_termios;    /* Original settings (for restore) */
    bool           is_open;
} uart_t;

Supported Baud Rates

Standard High Speed Very High Speed
300 115200 1000000
1200 230400 1500000
2400 460800 2000000
4800 500000 2500000
9600 576000 3000000
19200 921600 3500000
38400 4000000
57600

Lifecycle

uart_default_config

uart_config_t uart_default_config(void);

Returns the standard 115200 8N1 configuration (no flow control, 100ms read timeout).

uart_open

int uart_open(uart_t *uart, const char *device, const uart_config_t *config);

Open a UART device and apply configuration. Sets raw mode (no echo, no canonical processing, no signal generation).

Parameter Type Description
uart uart_t * Handle to initialize
device const char * Device path (e.g., "/dev/ttyS0", "/dev/ttyUSB0")
config const uart_config_t * Baud, parity, flow control settings

Returns: 0 on success, -errno on failure.

uart_close

void uart_close(uart_t *uart);

Drain pending output, restore original terminal settings, and close the device.

Read / Write

uart_write

ssize_t uart_write(uart_t *uart, const void *data, size_t len);

Non-blocking write. Returns bytes written or -errno.

uart_write_all

ssize_t uart_write_all(uart_t *uart, const void *data, size_t len, int timeout_ms);

Blocking write with timeout. Uses poll() internally to wait for TX readiness.

uart_read

ssize_t uart_read(uart_t *uart, void *buf, size_t len);

Non-blocking read. Returns bytes read, 0 if no data available, or -errno.

uart_read_timeout

ssize_t uart_read_timeout(uart_t *uart, void *buf, size_t len, int timeout_ms);

Read with explicit timeout using poll().

Line Control

uart_bytes_available

int uart_bytes_available(uart_t *uart);

Query bytes waiting in the kernel receive buffer (FIONREAD).

uart_flush

int uart_flush(uart_t *uart, bool flush_input, bool flush_output);

Discard pending input/output data.

uart_drain

int uart_drain(uart_t *uart);

Block until all pending output has been physically transmitted.

uart_send_break

int uart_send_break(uart_t *uart, int duration_ms);

Send a break signal (line held LOW for ~250ms).

uart_set_dtr / uart_set_rts

int uart_set_dtr(uart_t *uart, bool active);
int uart_set_rts(uart_t *uart, bool active);

Manually control DTR/RTS modem lines.

Usage Examples

Basic Loopback Test (PTY)

#include "uart.h"
#include <stdio.h>

int main(void)
{
    uart_t uart;
    uart_config_t cfg = uart_default_config();
    cfg.baud_rate = 115200;

    /* Use virtual serial port from socat */
    int rc = uart_open(&uart, "/dev/pts/3", &cfg);
    if (rc < 0) {
        fprintf(stderr, "Open failed: %s\n", strerror(-rc));
        return 1;
    }

    /* Write test data */
    const char *msg = "Hello UART!";
    ssize_t written = uart_write_all(&uart, msg, strlen(msg), 1000);
    printf("Wrote %zd bytes\n", written);

    /* Read back (loopback) */
    char buf[64];
    ssize_t n = uart_read_timeout(&uart, buf, sizeof(buf) - 1, 500);
    if (n > 0) {
        buf[n] = '\0';
        printf("Received: '%s'\n", buf);
    }

    uart_close(&uart);
    return 0;
}

RS-485 Half-Duplex with RTS Direction Control

#include "uart.h"

/* RS-485: Assert RTS before TX, deassert after TX complete */
ssize_t rs485_send(uart_t *uart, const void *data, size_t len)
{
    uart_set_rts(uart, true);       /* Enable TX driver */
    ssize_t n = uart_write_all(uart, data, len, 1000);
    uart_drain(uart);               /* Wait for physical TX */
    uart_set_rts(uart, false);      /* Release bus for RX */
    return n;
}

Virtual Serial Port Setup (Testing)

# Create a virtual serial port pair
socat -d -d pty,raw,echo=0 pty,raw,echo=0

# Output:
# 2024/01/01 12:00:00 socat[1234] N PTY is /dev/pts/3
# 2024/01/01 12:00:00 socat[1234] N PTY is /dev/pts/4

# Connect your application to /dev/pts/3
# Connect a terminal (minicom/picocom) to /dev/pts/4

Build & Run

# Compile
gcc -Wall -Wextra -Werror -O3 -std=c11 -o uart_demo uart.c

# Add user to dialout group (one-time setup)
sudo usermod -aG dialout $USER

# Run
./uart_demo

Prerequisites

Device Access

User must be in the dialout group:

sudo usermod -aG dialout $USER
# Log out and back in for group change to take effect

Flow Control

No flow control. Sender transmits at will. Risk of data loss if receiver can't keep up.

Master                  Slave
  │                       │
  │── RTS (Request) ─────►│
  │◄── CTS (Clear) ──────│
  │── TX Data ───────────►│
  │                       │
Physical handshake lines. Most reliable for high speeds.

Receiver sends:
  XOFF (0x13) → "stop sending"
  XON  (0x11) → "resume sending"
In-band control characters. Cannot send binary data containing 0x11/0x13.

Test Output

$ ./build/uart_demo
[uart] Opened /dev/pts/3 (115200 8N1, no flow control)
[uart] Write: 11 bytes → "Hello UART!"
[uart] Read:  11 bytes ← "Hello UART!" (loopback verified)
[uart] Flush test: input buffer cleared
[uart] Break signal sent
[uart] DTR: asserted, RTS: asserted
[PASS] All uart tests passed