Mastering UART: From Hardware Basics to Linux Serial Driver Implementation
This article provides a comprehensive guide to UART technology, covering its hardware protocol, baud‑rate calculations, RS232/RS485 differences, flow‑control methods, the Linux TTY serial framework with key data structures, driver registration lifecycle, RS485 debugging modes, and a complete C example for configuring and using a serial port on Linux.
UART Overview
UART (Universal Asynchronous Receiver/Transmitter) converts parallel data from a CPU into a serial stream for transmission and vice‑versa. It is the standard asynchronous serial interface used in embedded systems to communicate with sensors, consoles, other microcontrollers, or a host PC.
Communication Frame
Start bit – logical 0 indicating the beginning of a character.
Data bits – typically 5‑8 bits, transmitted LSB first.
Parity bit – optional even or odd parity for error detection.
Stop bits – 1, 1.5 or 2 high‑level bits marking the end of the frame; more stop bits increase tolerance to clock skew but reduce throughput.
Idle line – logical 1 when no data is being sent.
Baud Rate
The baud rate is the number of symbols transmitted per second. UART hardware usually runs on a clock that is 16× the desired baud rate to allow precise sampling of each bit.
RS232 vs RS485
RS232 uses single‑ended voltage levels (±3‑12 V) and supports full‑duplex with two wires. RS485 uses differential signalling, allowing longer distances and higher noise immunity; full‑duplex requires four wires, while half‑duplex can be achieved with two wires and line‑turn‑around control.
Flow Control
To avoid data loss when the receiver cannot keep up, UART supports:
Hardware flow control : RTS/CTS (Request To Send / Clear To Send) and DTR/DSR signals.
Software flow control : XON (0x11) / XOFF (0x13) characters inserted by the driver when the receive buffer reaches high/low watermarks.
Linux Serial (TTY) Framework
In Linux, serial ports appear as character devices under /dev/ttyS* (hardware ports), /dev/console (system console), and /dev/tty* (virtual terminals). The framework consists of two layers:
Low‑level UART driver layer that interacts directly with the hardware.
TTY core layer that provides a uniform character‑device interface to user space.
Key Data Structures
struct uart_driver {
struct module *owner;
const char *driver_name;
const char *dev_name;
int major;
int minor;
int nr;
struct console *cons;
struct uart_state *state;
struct tty_driver *tty_driver;
/* ... other private fields ... */
};
struct uart_port {
spinlock_t lock;
unsigned long iobase;
unsigned char __iomem *membase;
unsigned int irq;
unsigned int uartclk;
unsigned int fifosize;
const struct uart_ops *ops;
struct uart_state *state;
struct console *cons;
/* ... other fields ... */
};
struct uart_state {
struct tty_port port;
enum uart_pm_state pm_state;
struct circ_buf xmit;
struct uart_port *uart_port;
};
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*throttle)(struct uart_port *);
void (*unthrottle)(struct uart_port *);
void (*send_xchar)(struct uart_port *, char);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*flush_buffer)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *, struct ktermios *);
/* ... many more callbacks ... */
};Driver Lifecycle
uart_register_driver()– registers a uart_driver with the kernel (usually called from module_init). uart_add_one_port() – adds a uart_port to the driver after probing the hardware.
During open, read, write, and close operations the TTY core forwards calls to the driver’s uart_ops callbacks. uart_remove_one_port() – deregisters a port during driver removal. uart_unregister_driver() – removes the driver (usually from module_exit).
RS485 Usage and Debugging
Five practical half‑duplex modes are commonly used:
Two independent GPIOs control DE (driver enable) and RE (receiver enable).
One GPIO controls DE/RE with opposite polarity.
Software half‑duplex using the UART’s internal RS485 registers (de/re timing, turn‑around).
Hardware half‑duplex where the UART automatically toggles DE/RE.
External transceiver circuit with automatic direction control.
For each mode the typical GPIO states for idle, transmit, and post‑transmit periods are listed, and a step‑by‑step validation procedure is recommended:
Verify basic TX/RX functionality.
Check that the DE/RE GPIOs change to the expected levels.
Test isolated TX and RX lines.
Perform full RS485 communication between two nodes.
Example: UART Programming in C
The following program demonstrates opening /dev/ttySLB1, configuring the port for 230400 baud, 8‑N‑1, enabling hardware flow control, sending command frames to a temperature sensor, reading raw bytes, and converting them into floating‑point temperature values.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define UART_DEVICE "/dev/ttySLB1"
struct temp {
float temp_max1;
float temp_max2;
float temp_max3;
float temp_min;
float temp_mean;
float temp_enviromem;
char temp_col[1536];
};
int main(void) {
int fd, i, count;
struct termios oldtio, newtio;
struct temp *t = malloc(sizeof(*t));
char cmd_buf1[9] = {0xAA,0x01,0x04,0x00,0x06,0x10,0x05,0x00,0xBB};
char cmd_buf2[9] = {0xAA,0x01,0x04,0x00,0x00,0xA0,0x00,0x03,0xBB};
char cmd_buf3[9] = {0xAA,0x01,0x04,0x00,0x03,0x10,0x01,0x00,0xBB};
char read_buf[2000];
fd = open(UART_DEVICE, O_RDWR | O_NOCTTY);
if (fd < 0) { perror("open"); return -1; }
tcgetattr(fd, &oldtio);
memset(&newtio, 0, sizeof(newtio));
newtio.c_cflag = B230400 | CS8 | CLOCAL | CREAD | CSTOPB;
newtio.c_iflag = IGNPAR;
tcflush(fd, TCIFLUSH);
tcsetattr(fd, TCSANOW, &newtio);
/* send first command */
write(fd, cmd_buf1, sizeof(cmd_buf1));
usleep(500000);
count = read(fd, read_buf, sizeof(read_buf));
if (count > 0) {
t->temp_max1 = (read_buf[7] << 8) | read_buf[6];
t->temp_max2 = (read_buf[9] << 8) | read_buf[8];
t->temp_max3 = (read_buf[11] << 8) | read_buf[10];
t->temp_min = (read_buf[13] << 8) | read_buf[12];
t->temp_mean = (read_buf[15] << 8) | read_buf[14];
printf("max1=%f max2=%f max3=%f min=%f mean=%f
",
t->temp_max1*0.01, t->temp_max2*0.01, t->temp_max3*0.01,
t->temp_min*0.01, t->temp_mean*0.01);
}
write(fd, cmd_buf3, sizeof(cmd_buf3));
usleep(365);
count = read(fd, read_buf, sizeof(read_buf));
if (count > 0) {
t->temp_enviromem = (read_buf[7] << 8) | read_buf[6];
printf("enviro=%f
", t->temp_enviromem*0.01);
}
write(fd, cmd_buf2, sizeof(cmd_buf2));
usleep(70000);
count = read(fd, read_buf, sizeof(read_buf));
if (count > 0) {
memcpy(t->temp_col, read_buf + 6, 1536);
for (i = 0; i < 1536; ++i) {
if (i % 10 == 0) printf("
");
printf("%#X ", (unsigned char)t->temp_col[i]);
}
printf("
");
}
free(t);
close(fd);
tcsetattr(fd, TCSANOW, &oldtio);
return 0;
}This code shows the complete workflow: open the device, configure termios, transmit command frames, parse raw responses, and clean up.
Typical UART Configuration Steps (C)
Open the device file, e.g. fd = open("/dev/ttySLB0", O_RDWR | O_NOCTTY); Save the current settings with tcgetattr(fd, &oldtio); Zero a new struct termios and set the desired flags: c_cflag = B115200 | CS8 | CLOCAL | CREAD; (baud, 8 data bits, local mode, enable receiver)
Optionally add CSTOPB for 2 stop bits.
Enable hardware flow control with c_cflag |= CRTSCTS; or software flow control with c_iflag |= (IXON|IXOFF|IXANY); Set parity if required: c_cflag |= PARENB; for even, add PARODD for odd.
Flush pending data: tcflush(fd, TCIFLUSH); Apply the new settings immediately: tcsetattr(fd, TCSANOW, &newtio); Use read() and write() as with ordinary files. Restore the original settings before exiting with
tcsetattr(fd, TCSANOW, &oldtio);Key UART Driver API (Kernel)
int uart_register_driver(struct uart_driver *drv);– register a UART driver.
int uart_add_one_port(struct uart_driver *drv, struct uart_port *port);– add a port after probing.
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *port);– remove a port. void uart_unregister_driver(struct uart_driver *drv); – unregister the driver. void uart_write_wakeup(struct uart_port *port); – wake up a blocked writer.
int uart_suspend_port(struct uart_driver *drv, struct uart_port *port);– suspend a port.
int uart_resume_port(struct uart_driver *drv, struct uart_port *port);– resume a suspended port.
unsigned int uart_get_baud_rate(struct uart_port *port, struct ktermios *termios, struct ktermios *old, unsigned int min, unsigned int max); unsigned int uart_get_divisor(struct uart_port *port, unsigned int baud); void uart_update_timeout(struct uart_port *port, unsigned int cflag, unsigned int baud); void uart_insert_char(struct uart_port *port, unsigned int status, unsigned int overrun, unsigned int ch, unsigned int flag); void uart_console_write(struct uart_port *port, const char *s, unsigned int count, void (*putchar)(struct uart_port*, int));RS485 Direction‑Control Modes Summary
Mode 1 : Two independent GPIOs drive DE and RE; UART stays in normal mode.
Mode 2 : One GPIO with opposite polarity controls DE/RE; UART stays in normal mode.
Mode 3 : UART internal RS485 registers control DE/RE (software half‑duplex).
Mode 4 : UART internal RS485 registers control DE/RE (hardware half‑duplex).
Mode 5 : External transceiver with automatic direction control (no UART‑side DE/RE handling).
Each mode requires setting the appropriate GPIO levels (or register bits) for idle (receiver enabled), transmit (driver enabled), and post‑transmit (return to receiver) periods, then verifying the sequence with an oscilloscope or logic analyzer.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
