Saturday, January 10, 2026

Building a virtio-serial FPGA device (Part 5): UART receiver and transmitter

This is the fifth post in a series about building a virtio-serial device in Verilog for an FPGA development board. This time we'll look at the UART receiver and transmitter.

Series table of contents

  1. Part 1: Overview
  2. Part 2 - MMIO registers, DMA, and interrupts
  3. Part 3 - virtio-serial device design
  4. Part 4 - Virtqueue processing
  5. Part 5 - UART receiver and transmitter (you are here)
  6. Part 6 - Writing the RISC-V firmware

The code is available at https://gitlab.com/stefanha/virtio-serial-fpga.

How UARTs work

A Universal Asynchronous Receiver-Transmitter (UART) is a simple interface for data transfer that only requires a transmitter (tx) and a receiver (rx) wire. There is no clock wire because both sides of the connection use their own clocks and sample the signal in order to reconstruct the bits being transferred. This agreed-upon data transfer rate (or baud rate) is usually modest and the frame encoding is also not the most efficient way of transferring data, but UARTs get the job done and are commonly used for debug consoles, modems, and other relatively low data rate interfaces.

There is a framing protocol that makes it easier to reconstruct the transferred data. This is important because failure to correctly reconstruct the data results in corrupted data being received on the other side. In this project I used a 9,600 bit/s baud rate and 8 data bits, no parity bit, and 1 stop bit (sometimes written as 8N1). The framing works as follows:

  • When no data is being transferred, the signal is 1.
  • Before the data byte, a start bit is sent with the value 0. This way a receiver can detect the beginning of a frame.
  • The start bit is followed by the 8 data bits in least significant bit order.
  • After the data bits the frame ends with a stop bit with the value 1.

The job of the transmitter is to follow this framing protocol. The job of the receiver is to detect the next frame and to reconstruct the byte being transferred.

Implementation

The uart_reader and uart_writer modules implement the UART receiver and transmitter, respectively. They are designed around the rdwr_stream module's reader and writer interfaces. That means uart_reader receives the next byte from the UART rx pin whenever it is asked to read more data and uart_writer transmits on the UART tx pin whenever it is asked to write more data.

uart_reader follows a trick I learnt from the PicoSoC's simpleuart module: once the rx pin goes from 1 to 0, it waits until half the period (e.g. 9,600 baud @ 12 MHz / 2 = 625 clock cycles) has passed before sampling the rx pin. This works well because the UART only transfers data on the iCESugar PCB and is not exposed to much noise. Fancier approaches involve sampling the pin every clock cycle in order to try to reconstruct the value more accurately, but they don't seem to be necessary for this project.

Here is the core uart_reader code, a state machine that parses the incoming frame:

always @(posedge clk) begin
    ...
    div_counter <= div_counter + 1;
    case (bit_counter)
    0: begin // looking for the start bit
        if (rx == `START_BIT) begin
            div_counter <= 0;
            bit_counter <= 1;
        end
    end
    1: begin
        /* Sample in the middle of the period */
        if (div_counter == clk_div >> 1) begin
            div_counter <= 0;
            bit_counter <= 2;
        end
    end
    10: begin // expecting the stop bit
        if (div_counter == clk_div) begin
            if (rx == `STOP_BIT && !reg_ready) begin
                data <= {24'h0, rx_buf};
                data_len <= 1;
                reg_ready <= 1;
            end
            bit_counter <= 0;
        end
    end
    default: begin // receive the next data bit
        if (div_counter == clk_div) begin
            rx_buf <= {rx, rx_buf[7:1]};
            div_counter <= 0;
            bit_counter <= bit_counter + 1;
        end
    end
    endcase

The uart_writer module is similar, but it has a transmit buffer that it sends over the UART tx pin with the framing that I've described here.

Conclusion

The uart_reader and uart_writer modules are responsible for receiving and transmitting data over the UART rx/tx pins. They implement the framing protocol that UARTs use to protect data. In the next post we will cover the firmware running on the PicoRV32 RISC-V soft-core that drives the I/O.