Understanding UART and Linux Serial Driver Architecture: From Basics to Implementation
This article provides a comprehensive guide to UART fundamentals, communication protocols, baud‑rate calculations, RS232/RS485 differences, flow‑control mechanisms, and the Linux TTY serial framework, including driver registration, data flow, key kernel structures, and practical C code examples for embedded development.
UART basics
UART (Universal Asynchronous Receiver‑Transmitter) converts parallel data to a serial bit stream for asynchronous communication between a host processor and peripheral devices (sensors, consoles, other MCUs, etc.).
Communication frame
Start bit – logical 0, marks the beginning of a character.
Data bits – usually 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; they also give the receiver time to resynchronise.
Idle – line stays high (logic 1) when no data are sent.
Baud rate
The baud rate is the number of symbols per second. UART hardware samples the incoming line at 16 × the baud rate, so the divisor is calculated from the crystal frequency to obtain the desired baud.
Transmission process
When idle the line is high. To send a character the start bit pulls the line low, followed by the data bits (LSB first), optional parity, and stop bits. Reception detects the falling edge, samples each bit at the 16× clock, and checks parity if enabled.
RS‑232 vs RS‑485
UART defines the logical protocol; RS‑232 and RS‑485 define the electrical characteristics. RS‑232 uses single‑ended voltage levels (±3‑15 V) and supports point‑to‑point full‑duplex. RS‑485 uses differential signalling, allowing longer distances (up to 1.2 km at 9600 bps) and multi‑drop networks; it can operate in half‑ or full‑duplex modes with four wires.
Flow control
Hardware flow control (RTS/CTS, DTR/DSR) and software flow control (XON/XOFF) pause transmission when the receiver’s buffers are full, preventing data loss.
Linux serial (TTY) framework
TTY driver overview
Serial ports appear as character devices (e.g. /dev/ttyS0). The TTY layer sits above the low‑level UART driver and provides a uniform interface to user space.
Key data structures
struct uart_driver– registers the driver, holds driver/device names, major/minor numbers and a pointer to the associated struct tty_driver. struct uart_port – describes I/O base address, IRQ, clock, FIFO size, and links to struct uart_state. struct uart_state – contains a struct tty_port, a circular transmit buffer and a pointer back to the uart_port. struct uart_ops – callbacks for start/stop TX/RX, set termios, configure the port, handle interrupts, power‑management, etc. struct console – optional console support when the UART is used as a kernel console.
Registration flow
Allocate an array of uart_state objects for the maximum number of ports.
Create a tty_driver and assign it to uart_driver->tty_driver.
Set default baud rate, parity, and register a tty_operations structure that bridges the TTY core and the UART driver.
Initialise each uart_state ’s embedded tty_port.
Register the tty_driver (which implicitly registers the UART driver).
Typical driver‑side API
int uart_register_driver(struct uart_driver *drv);
void uart_unregister_driver(struct uart_driver *drv);
int uart_add_one_port(struct uart_driver *drv, struct uart_port *port);
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *port);
int uart_suspend_port(struct uart_driver *drv, struct uart_port *port);
int uart_resume_port(struct uart_driver *drv, struct uart_port *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));RS‑485 usage modes
Four typical configurations are used to drive the DE (driver enable) and RE (receiver enable) lines:
Mode 1 – Two independent GPIOs control DE and RE for half‑duplex operation; the UART stays in normal mode.
Mode 2 – A single GPIO toggles DE/RE when they have opposite polarity (one active‑high, the other active‑low).
Mode 3 – The UART’s internal RS‑485 engine is enabled; DE/RE are controlled by software registers for half‑duplex.
Mode 4 – Hardware‑assisted half‑duplex using the UART’s RS‑485 engine (DE and RE are driven automatically by the hardware).
Each mode requires setting the appropriate GPIO states before transmission, toggling DE during TX, and restoring RE after TX completes.
Example driver and user‑space program
The sample kernel module registers a uart_driver, adds a single uart_port, and implements minimal open/write/read/close callbacks. The user‑space demo opens /dev/ttySLB1, configures the port with termios, sends command frames to a temperature sensor, parses the multi‑byte response, and prints calibrated values.
/* User‑space example – compile with: gcc -Wall -o temp_demo temp_demo.c */
#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 *temp = malloc(sizeof(*temp));
if (!temp) return -1;
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; /* 230400 bps, 8N2 */
newtio.c_iflag = IGNPAR; /* ignore parity errors */
tcflush(fd, TCIFLUSH);
tcsetattr(fd, TCSANOW, &newtio);
/* --- send a command frame (example) --- */
unsigned char cmd_buf1[9] = {0xAA,0x01,0x04,0x00,0x06,0x10,0x05,0x00,0xBB};
count = write(fd, cmd_buf1, sizeof(cmd_buf1));
if (count != sizeof(cmd_buf1)) { printf("send failed
"); return -1; }
usleep(500000); /* wait for sensor to respond */
unsigned char read_buf[2000];
memset(read_buf, 0, sizeof(read_buf));
count = read(fd, read_buf, sizeof(read_buf));
if (count > 0) {
temp->temp_max1 = (read_buf[7] << 8) | read_buf[6];
temp->temp_max2 = (read_buf[9] << 8) | read_buf[8];
temp->temp_max3 = (read_buf[11] << 8) | read_buf[10];
temp->temp_min = (read_buf[13] << 8) | read_buf[12];
temp->temp_mean = (read_buf[15] << 8) | read_buf[14];
printf("max1=%f max2=%f max3=%f min=%f mean=%f
",
temp->temp_max1*0.01, temp->temp_max2*0.01,
temp->temp_max3*0.01, temp->temp_min*0.01,
temp->temp_mean*0.01);
} else {
printf("read failed
");
return -1;
}
/* Additional command frames (cmd_buf2, cmd_buf3) and colour data handling
are omitted for brevity but follow the same pattern: write → usleep → read. */
free(temp);
close(fd);
tcsetattr(fd, TCSANOW, &oldtio); /* restore original termios */
return 0;
}This example demonstrates how to open a UART device, configure baud rate, data bits, stop bits and flow control, transmit raw command packets, receive the binary response, reconstruct multi‑byte sensor values, and clean up resources.
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.
