Chapter 33: UART Driver
- How the PL011 UART works
- UART registers: DR, FR, IBRD, FBRD, LCR_H, ICR, IMSC
- How to initialize the UART at a specific baud rate
- Polled and interrupt-driven UART I/O
- Ring buffer for buffered serial output
- Our complete PL011 driver implementation
33.1 The PL011 UART
The PL011 is a UART (Universal Asynchronous Receiver/Transmitter) IP block from ARM. It converts parallel data from the CPU to serial data for transmission over a serial line (and vice versa). On QEMU virt, the PL011 is at MMIO address 0x09000000 and uses SPI interrupt 33 (GIC interrupt ID 33).
33.2 UART Registers
/* PL011 register offsets (each 32-bit wide) */
#define UART_DR 0x000 /* Data Register (read/write) */
#define UART_RSRECR 0x004 /* Receive Status Register */
#define UART_FR 0x018 /* Flag Register (read-only) */
#define UART_IBRD 0x024 /* Integer Baud Rate Divisor */
#define UART_FBRD 0x028 /* Fractional Baud Rate Divisor */
#define UART_LCR_H 0x02C /* Line Control Register */
#define UART_CR 0x030 /* Control Register */
#define UART_IFLS 0x034 /* Interrupt FIFO Level Select */
#define UART_IMSC 0x038 /* Interrupt Mask Set/Clear */
#define UART_RIS 0x03C /* Raw Interrupt Status */
#define UART_MIS 0x040 /* Masked Interrupt Status */
#define UART_ICR 0x044 /* Interrupt Clear Register */
/* Flag register bits */
#define UART_FR_TXFF (1 << 5) /* Transmit FIFO full */
#define UART_FR_RXFE (1 << 4) /* Receive FIFO empty */
#define UART_FR_BUSY (1 << 3) /* UART busy transmitting */
/* Line control bits */
#define UART_LCR_H_FEN (1 << 4) /* Enable FIFOs */
#define UART_LCR_H_WLEN_8 (3 << 5) /* 8-bit word length */
/* Control register bits */
#define UART_CR_UARTEN (1 << 0) /* UART enable */
#define UART_CR_TXE (1 << 8) /* Transmit enable */
#define UART_CR_RXE (1 << 9) /* Receive enable */
/* Interrupt mask bits */
#define UART_IMSC_RXIM (1 << 4) /* Receive interrupt mask */
#define UART_IMSC_TXIM (1 << 5) /* Transmit interrupt mask */
/* Helper: read/write MMIO registers */
#define readl(base, off) (*(volatile uint32_t *)((base) + (off)))
#define writel(base, off, val) (*(volatile uint32_t *)((base) + (off)) = (val))
33.3 UART Initialization
/* Initialize PL011 UART at a given baud rate */
void pl011_init(uint64_t base, int baud_rate) {
/* Disable UART while configuring */
writel(base, UART_CR, 0);
/* Wait for any pending transmission to complete */
while (readl(base, UART_FR) & UART_FR_BUSY);
/* Set baud rate: IBRD = UART_CLK / (16 * baud)
QEMU virt uses 24 MHz UART clock for PL011 */
int uart_clk = 24000000;
int ibrd = uart_clk / (16 * baud_rate);
int fbrd = (uart_clk * 4 / baud_rate + 1) / 2 - ibrd * 32;
writel(base, UART_IBRD, ibrd);
writel(base, UART_FBRD, fbrd);
/* Configure line: 8 data bits, 1 stop bit, no parity, enable FIFO */
writel(base, UART_LCR_H, UART_LCR_H_FEN | UART_LCR_H_WLEN_8);
/* Enable UART, transmit, and receive */
writel(base, UART_CR, UART_CR_UARTEN | UART_CR_TXE | UART_CR_RXE);
/* Clear any pending interrupts */
writel(base, UART_ICR, 0x7FF);
}
33.4 Polled Transmit and Receive
/* Polled: send a single character (busy-wait until FIFO has space) */
void pl011_putchar(uint64_t base, char c) {
/* Wait until transmit FIFO is not full */
while (readl(base, UART_FR) & UART_FR_TXFF);
writel(base, UART_DR, c);
}
/* Polled: receive a single character (busy-wait until data available) */
char pl011_getchar(uint64_t base) {
/* Wait until receive FIFO is not empty */
while (readl(base, UART_FR) & UART_FR_RXFE);
return readl(base, UART_DR) & 0xFF;
}
/* Print a string (used for kernel debug output) */
void pl011_puts(uint64_t base, const char *s) {
while (*s) pl011_putchar(base, *s++);
}
33.5 Interrupt-Driven UART
Polled UART wastes CPU cycles spinning while waiting for data. For better efficiency, especially when the kernel has multiple processes to run, the UART should use interrupts:
/* UART device structure (per-instance) */
struct pl011_device {
uint64_t base;
char tx_ring[TX_RING_SIZE]; /* Transmit ring buffer */
int tx_head, tx_tail;
char rx_ring[RX_RING_SIZE]; /* Receive ring buffer */
int rx_head, rx_tail;
struct spinlock lock;
struct semaphore tx_empty;
};
/* Interrupt handler for PL011 */
void pl011_irq_handler(struct device *dev) {
struct pl011_device *pl011 = dev->priv_data;
uint64_t base = pl011->base;
uint32_t mis = readl(base, UART_MIS);
/* Receive interrupt: data available */
if (mis & UART_IMSC_RXIM) {
while (!(readl(base, UART_FR) & UART_FR_RXFE)) {
pl011->rx_ring[pl011->rx_head] = readl(base, UART_DR) & 0xFF;
pl011->rx_head = (pl011->rx_head + 1) % RX_RING_SIZE;
}
}
/* Transmit interrupt: FIFO has space for more data */
if (mis & UART_IMSC_TXIM) {
spinlock_lock(&pl011->lock);
while (!(readl(base, UART_FR) & UART_FR_TXFF) &&
pl011->tx_tail != pl011->tx_head) {
writel(base, UART_DR, pl011->tx_ring[pl011->tx_tail]);
pl011->tx_tail = (pl011->tx_tail + 1) % TX_RING_SIZE;
}
/* If no more data to send, disable TX interrupt */
if (pl011->tx_tail == pl011->tx_head) {
writel(base, UART_IMSC, readl(base, UART_IMSC) & ~UART_IMSC_TXIM);
sem_signal(&pl011->tx_empty);
}
spinlock_unlock(&pl011->lock);
}
/* Clear all pending interrupts */
writel(base, UART_ICR, mis);
}
/* Non-blocking putchar (adds to ring buffer, enables TX interrupt) */
void pl011_putchar_irq(struct pl011_device *pl011, char c) {
spinlock_lock(&pl011->lock);
pl011->tx_ring[pl011->tx_head] = c;
pl011->tx_head = (pl011->tx_head + 1) % TX_RING_SIZE;
/* Enable transmit interrupt if it was disabled */
writel(pl011->base, UART_IMSC,
readl(pl011->base, UART_IMSC) | UART_IMSC_TXIM);
spinlock_unlock(&pl011->lock);
}
33.6 Our Implementation
Our kernel's UART driver (drivers/serial/pl011.c) provides:
- Early debug output: polled putchar before interrupts are enabled
- Interrupt-driven I/O: ring buffer for TX and RX with semaphore synchronization
- printf support: a kernel
printf()function that writes to the UART - devfs integration: the UART appears as
/dev/uart0for user-space programs - Baud rate configuration: default 115200, configurable per device
The UART is the kernel's primary console device. All kernel log messages (using printk()) go through the UART. The shell (Chapter 39) reads from and writes to the UART for interactive use.
33.7 Exercises
Exercise 1: Baud Rate Calculator
Calculate IBRD and FBRD for 115200 baud with a 24 MHz clock. Verify: IBRD = 13, FBRD = 1 (24000000 / (16 * 115200) = 13.02).
Exercise 2: Serial Echo
Write a simple kernel thread that reads characters from the UART and echoes them back with interrupts. Test by typing into the QEMU serial console.
33.8 Summary
The PL011 UART is the primary serial communication interface for our kernel. It uses MMIO registers at 0x09000000 on QEMU virt. Polled mode is simple but wastes CPU; interrupt-driven mode uses a ring buffer for efficient, non-blocking I/O. The driver initializes the UART at 115200 baud, 8 data bits, 1 stop bit. It integrates with the kernel console system and provides devfs access for user-space programs.