UART What You Read
Computers are not much use unless you can interact with them. This means that the user must be able to input data into the system’s memory, and that data can be retrieved and displayed (or output) for the user. The most common way to interact with computers (i.e. smartphones) is via a touch screen which provides both the input and output modality. However, this is a complex mechanism for communicating with the user that requires a non-trivial software stack (i.e. layers of different software which handle the details of drawing on the screen and recognizing touch input).
A more basic form of input and output can be carried out via the device’s serial port. As the name implies, when using this mechanism, data is exchanged one byte at a time serially. The most common type of serial controller is the Universal Asynchronous Receiver/Transmitter (UART). The UART controller is not an intrinsic part of the CPU core, however, it is a peripheral device with its own specification. The CPU will access the serial controller using memory-mapped addresses.
Memory-mapped addresses are setup when the chip is designed. A particular range of physical addresses are reserved to access peripherals such as serial controllers. Individual addresses will map to registers in the peripheral device. Since the addresses used to exchange data with the UART controller may vary from one machine to another, the software used to carry out input and output via the UART controller will be specific to a particular chip.
History of the UART
Serial communication was introduced with the INS8250 UART device in the IBM PC/XT computer1. The INS8250 uses eight I/O ports with a one-byte send buffer, and a one-byte receive buffer. This device was upgraded in the AT series to the NS16450 device which improves the design of the INS8250 allowing it to be used with faster bus speeds. the NS16450 series is capable of handling communication speeds of up to 38.4 KB/s.
To respond to the demands for even higher data rates, a new series of UART devices was developed: the 16550A1 which makes use of two 16-bit FIFO buffers to queue input and output data. This device allows for serial communication speeds of up to 115.2 KB/s. This device and its successors have become the most popular UART design and is the device emulated by the QEMU virt machine.
The RISC-V virtual machine defined by the QEMU emulator uses the address range 0x10000000
–0x10000100
to access the UART controller. Specific addresses in this range map to the controller’s registers. A program may read and write byte values to the registers using these addresses.
Although the address range mapped to UART I/O ports spans 256 bytes (0x100
), only eight bytes are needed to access the controller’s registers. This implies that there can be as many as 16 serial I/O devices available on the given machine; however, only one UART is actually used. The relevant registers are described in table 12:
Register | Offset | Direction | Purpose |
---|---|---|---|
RBR | 0 | Read-Only | receive buffer |
THR | 0 | Write-Only | transmit holding |
DLL | 0 | Read/Write | Divisor Latch LSB |
DLH | 0 | Read/Write | Divisor Latch MSB |
IER | 1 | Read/Write | interrupt enable |
IIR | 2 | Read-Only | interrupt identification |
FCR | 2 | Write-Only | FIFO control |
LCR | 3 | Read/Write | Line control |
MCR | 4 | Read/Write | modem control |
LSR | 5 | Read-Only | line status |
MSR | 6 | Read-Only | modem status |
SCR | 7 | Read/Write | scratch |
Note that several of the registers map to the same offset in memory. This is possible because some of them are read-only while others are write-only. Therefore the offsets are re-used; the mapping will change based on the value of the line control register (LCR
) at offset 3. If bit 7 of the LCR
register is asserted, the THR
(transmit holding), RBR
(receive buffer), and IER
(interrupt enable) registers are disabled; reads and writes to these offset will be redirected to the DLL
(divisor latch LSB) and DLH
(divisor latch MSB) which are used to set the baud rate.
Output
The assembly function to write a single byte to the serial controller is illustrated in the following listing:
__uart_putc:
function_prologue 0
1: lbu t0, LSR(a0) # Get the line status
andi t0, t0, LSR_RE # Mask all but the status bit
beqz t0, 1b # Loop until THR is ready
sb a1, THR(a0) # Write character
function_epilogue 0
ret
The inputs of this function are the base address of the UART controller (in register a0
), and the byte to write the the device (in register a1
).
This code assumes that bit 7 of the LCR
register is clear (i.e. 0). The function uses two constants corresponding with the offsets of the THR
and LSR
registers (offset 0 and 5 respectively). Moreover, the LSR_RE
constant represents a bit mask (0x20) to isolate bit 5 of the LSR
status register. If this bit is asserted, then the transmit holding register is ready to receive a byte of data.
The __uart_putc
function first checks the line status register to determine if the THR
register is ready to receive data. This check is performed by verifying bit 5 of the LSR
status register (using the LSR_RE
constant). If this bit is asserted, the byte from register a1
is stored at offset THR
from the base address stored in register a0
. Otherwise, the function loops to verify the line status register again.
Input
The assembly code to read a single byte from the UART controller is illustrated in the following listing:
__uart_getc:
li a1, -1
lbu t0, LSR(a0) # Get the line status (get LSR)
andi t0, t0, LSR_DA
beqz t0, .L__uart_getc_exit
lbu a1, RBR(a0)
.L__uart_getc_exit:
ret
Much like the output function, the __uart_getc
function takes the base address of the UART controller in register a0
. The character code is returned in register a1
.
The LSR
register is queried to determine whether or not there is unread data available in the receive buffer (offset RBR
). If bit 0 is asserted (masked using the LSR_DA
) the function loads the contents of the read buffer into register a1
. Unlike the input function, the __uart_getc
function does not loop until data arrives. If no unread data is available in the receive buffer, the function simply returns -1.
On a successful read, the LSR_DA
bit of the line-status register is automatically cleared. It is asserted again once new data becomes available for reading. The mechanism by which this is carried out is an implementation detail of the controller. However, the upshot of this is that it’s fairly easy to determine if there is new data available for reading in the UART’s receive queue.
Can I Finish?
The function for reading from the serial device polls the line status register to determine when unread data is available. Even though a program may have well defined points in its execution where input is required, the arrival of the input will nonetheless be asynchronous. The program may call the __uart_getc
function in a loop until all of the required data is received. However, this prevents the processor from being used to carry out other computations while waiting for user input.
To work around this, the UART controller can be configured to trigger an interrupt whenever there is new data available for reading via the IER
register (interrupt enable). The read can be carried out by an interrupt handler. This allows processing to continue until new data arrives. At which point execution will trap to the function to carry out the read.
In Summary
The NS16550A UART controller is a basic mechanism to support I/O. Data is read and/or written to the serial ports one byte at a time and transmitted to the opposite end. This provides a simple mechanism for programs to exchange data with their users. The serial ports are mapped to specific addresses in the system’s main memory. Reads and/or writes to those addresses allow programs to interact with the controller as a peripheral device.
Output is the simpler case from the program’s perspective when using the UART since this action is effectively synchronous; the program knows when output is needed.
Input, is inherently asynchronous; it is not possible to predict when the user will provide input. Polling the line status register will prevent the CPU from doing other useful work.
Although the UART controller can be configured to trigger an interrupt when the line status changes, this is beyond the scope of the current article. Handling interrupts is complex enough that the subject should be treated on its own. In the meantime, the poll design pattern is good enough to get an idea of how to interact with the UART controller and other on-chip peripherals in general (via memory mapped addresses).
Footnotes
1 https://www.freebsd.org/doc/en_US.ISO8859-1/articles/serial-uart/index.html
2 https://www.lammertbies.nl/comm/info/serial-uart
Filed under: RISC V - @ 2023-08-10 22:45