Serial Communication: UART

The UART frame, baud-rate generation, a transmitter and a 16× oversampling receiver as state machines, a loopback testbench and an echo test with a PC.

1. Talking to a PC with one wire

Your designs so far spoke through four LEDs. To exchange real data with a computer you need a communication link, and the simplest one in existence is the UART — Universal Asynchronous Receiver/Transmitter. It sends bytes one bit at a time over a single wire in each direction, with no clock wire at all. “Asynchronous” means exactly that: the two sides never share a clock. They only agree in advance on how long one bit lasts.

That agreed speed is the baud rate: the number of bit periods (symbols) per second. At 115 200 baud one bit lasts \(T_\text{bit} = 1/115\,200 \approx 8.68\ \mu\text{s}\), which is about 1085 cycles of our 125 MHz clock. Every byte travels in a frame:

  • Idle: when nothing is sent the line rests at logic 1 (high).
  • Start bit: the line goes to 0 for one bit period. This falling edge is the only timing reference the receiver gets.
  • Data bits: usually 8, least significant bit first.
  • Parity bit (optional): an extra bit making the number of 1s even (or odd), so a single flipped bit is detected. We will not use it; the common setting is “8N1” — 8 data bits, No parity, 1 stop bit.
  • Stop bit: the line returns to 1 for at least one bit period, so the next start bit has a falling edge to show.
startD0D1D2D3D4D5D6D7stop0110100101idle = 1T_bit = 1/baud10 bit periods per byte
One 8N1 frame carrying 0x4B = 0100_1011. Bits leave LSB first, so the line shows 1,1,0,1,0,0,1,0 after the start bit. The receiver samples each bit in its middle (dots), measured from the falling edge of the start bit.

2. How precise must the baud rate be?

Because there is no shared clock, the receiver times everything from the falling edge of the start bit using its own clock. If its idea of \(T_\text{bit}\) differs a little from the transmitter's, the error accumulates bit after bit. Sampling in the middle of each bit gives half a bit of slack in each direction. The last sample in a frame is the stop bit, taken \(9.5\) receiver bit periods after the start edge. It must still land inside the transmitter's stop bit, which spans \(9\) to \(10\) transmitter bit periods:

\[ 9\,T_\text{tx} < 9.5\,T_\text{rx} < 10\,T_\text{tx} \quad\Longrightarrow\quad \left|\frac{T_\text{rx}}{T_\text{tx}} - 1\right| < \frac{0.5}{9.5} \approx 5.3\% \]

That is the total mismatch the two ends may have together, in the ideal case. In practice the receiver detects the start edge only to within one oversampling tick (1/16 of a bit, see Section 5) plus a couple of synchroniser cycles, which eats into the half-bit margin: \((0.5 - 1/16)/9.5 \approx 4.6\%\). Real lines also have slow edges. A sensible design rule is therefore about ±2 % per side.

Now the FPGA side. A baud generator divides the 125 MHz clock by an integer, so the rate is rarely exact. For one tick per bit at 115 200 baud:

\[ \frac{125\times10^6}{115\,200} = 1085.07 \;\to\; \text{DIV}=1085, \qquad \frac{125\times10^6}{1085} = 115\,207\ \text{baud} \;\;(+0.006\%) \]

Our receiver needs 16 ticks per bit (Section 5), so the division is coarser:\(125\times10^6/(16 \times 115\,200) = 67.82 \to 68\), giving 114 890 baud, an error of\(-0.27\%\). That is far inside the budget. The Python program in Section 8 tabulates all common rates; only 921 600 baud with 16× oversampling breaks down (+6 %).

baudDIV (1×)error (1×)DIV (16×)error (16×)
960013021−0.001 %814−0.024 %
576002170+0.006 %136−0.269 %
1152001085+0.006 %68−0.269 %
460800271+0.099 %17−0.269 %
921600136−0.269 %8+5.964 %

3. The baud tick: reusing tick_gen

In Chapter 14 we built tick_gen, which outputs a one-cycle pulse every DIV clock cycles. That is exactly a baud generator. With DIV = 68 it produces\(16 \times 115\,200\) ticks per second, which both the transmitter and the receiver use as a clock enable — everything still runs on the single 125 MHz clock.

tick_gen (from Chapter 14) — used here with DIV = 68
module tick_gen #(
    parameter DIV = 125_000_000   // one tick per DIV clock cycles
) (
    input  wire clk,
    input  wire rst,
    output reg  tick = 1'b0       // high for exactly one cycle
);
    localparam W = $clog2(DIV);
    reg [W-1:0] count = 0;

    always @(posedge clk) begin
        if (rst) begin
            count <= 0;
            tick  <= 1'b0;
        end else if (count == DIV - 1) begin
            count <= 0;
            tick  <= 1'b1;
        end else begin
            count <= count + 1'b1;
            tick  <= 1'b0;
        end
    end
endmodule
Key idea: one tick stream, two users
The transmitter does not need oversampling: it simply holds each bit for 16 ticks. Sharing the same tick16 between TX and RX saves a counter and guarantees that both run at the same rate.

4. The transmitter: a five-state machine

A transmitter is a finite state machine (Chapter 12) that walks through the frame. It also contains a shift register: the byte is copied in when sending starts, and shifted right after each data bit so that the next bit to send is always in position 1, then 0 — LSB first, as required.

  • IDLE: line high. On start, capture data and go to ALIGN.
  • ALIGN: wait for the next tick, then drive the start bit. Starting on a tick makes the start bit exactly 16 ticks long instead of 16 ticks plus a random fraction.
  • START, DATA, STOP: count 16 ticks per bit; in DATA repeat 8 times.

The output tx is a register, never a combinational function of the state. That matters for a line that leaves the chip: a combinational output can glitch for a few nanoseconds when the state changes, and a receiver could mistake a glitch on an idle line for a start bit.

uart_tx — 8N1 transmitter driven by a 16× tick
module uart_tx (
    input  wire       clk,
    input  wire       rst,
    input  wire       tick16,     // 16 pulses per bit period (from tick_gen)
    input  wire [7:0] data,       // byte to send
    input  wire       start,      // one-cycle request, ignored while busy
    output wire       busy,
    output reg        tx = 1'b1   // serial line, idle high
);
    localparam IDLE = 3'd0, ALIGN = 3'd1, START = 3'd2, DATA = 3'd3, STOP = 3'd4;
    reg [2:0] state   = IDLE;
    reg [3:0] ticks   = 4'd0;     // 16 ticks = one bit period
    reg [2:0] bit_idx = 3'd0;     // which data bit is on the line
    reg [7:0] shreg   = 8'd0;     // shifts right: LSB goes out first

    assign busy = (state != IDLE);

    always @(posedge clk) begin
        if (rst) begin
            state <= IDLE;
            ticks <= 4'd0;
            tx    <= 1'b1;
        end else begin
            case (state)
                IDLE: begin
                    tx <= 1'b1;
                    if (start) begin
                        shreg <= data;             // capture the byte now
                        state <= ALIGN;
                    end
                end
                ALIGN: if (tick16) begin           // begin on a tick, so the
                    ticks <= 4'd0;                 // start bit is exactly
                    tx    <= 1'b0;                 // 16 ticks long
                    state <= START;
                end
                START: if (tick16) begin
                    if (ticks == 4'd15) begin
                        ticks   <= 4'd0;
                        bit_idx <= 3'd0;
                        tx      <= shreg[0];       // data bit 0
                        state   <= DATA;
                    end else
                        ticks <= ticks + 1'b1;
                end
                DATA: if (tick16) begin
                    if (ticks == 4'd15) begin
                        ticks <= 4'd0;
                        shreg <= {1'b0, shreg[7:1]};
                        if (bit_idx == 3'd7) begin
                            tx    <= 1'b1;         // stop bit
                            state <= STOP;
                        end else begin
                            bit_idx <= bit_idx + 1'b1;
                            tx      <= shreg[1];   // next data bit
                        end
                    end else
                        ticks <= ticks + 1'b1;
                end
                default: if (tick16) begin         // STOP
                    if (ticks == 4'd15) begin
                        ticks <= 4'd0;
                        state <= IDLE;
                    end else
                        ticks <= ticks + 1'b1;
                end
            endcase
        end
    end
endmodule

busy is high from the cycle after start until the stop bit has ended. A user must wait for busy = 0 before requesting the next byte; a start pulse during a frame is ignored. The VHDL keeps the line in an internal signal tx_r for the same VHDL-93 reason as in Chapter 14, and calls its states START_B etc. because start is already a port name (VHDL is not case sensitive).

5. The receiver: oversampling and mid-bit sampling

The receiver has the harder job. It does not know when a frame begins, so it watches the line at 16 times the baud rate (oversampling). When it sees the line low, it waits 8 ticks — half a bit — and looks again. If the line is still low, this is a genuine start bit and the receiver is now aligned to the middle of it. From then on it samples every 16 ticks, which lands in the middle of each data bit and finally the middle of the stop bit.

rxstart bitD0 = 1D1 = 0tick16edge seen8 ticks16 ticks16 ticksstill 0?
16× oversampling. The receiver detects the falling edge to within one tick, confirms the start bit 8 ticks later (half a bit), then samples every 16 ticks, i.e. in the middle of each following bit, where the signal is most stable.

The rx pin is driven by a different device with its own clock, so it is asynchronous to our 125 MHz clock. As with the buttons in Chapter 14, it goes through a two-flip-flop synchroniser first. Its flip-flops start at 1 so that the receiver does not see a phantom start bit at power-up.

uart_rx — 16× oversampling receiver
module uart_rx (
    input  wire       clk,
    input  wire       rst,
    input  wire       tick16,         // 16 pulses per bit period
    input  wire       rx,             // asynchronous serial input
    output reg  [7:0] data = 8'd0,    // last byte received
    output reg        valid = 1'b0,   // one-cycle pulse: data is new
    output reg        frame_err = 1'b0 // one-cycle pulse: stop bit was 0
);
    localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;
    reg       rx_s0 = 1'b1, rx_s1 = 1'b1;   // synchroniser, idle = 1
    reg [1:0] state   = IDLE;
    reg [3:0] ticks   = 4'd0;
    reg [2:0] bit_idx = 3'd0;
    reg [7:0] shreg   = 8'd0;

    always @(posedge clk) begin
        rx_s0 <= rx;                  // two flip-flops against metastability
        rx_s1 <= rx_s0;
    end

    always @(posedge clk) begin
        valid     <= 1'b0;
        frame_err <= 1'b0;
        if (rst) begin
            state <= IDLE;
            ticks <= 4'd0;
        end else begin
            case (state)
                IDLE: if (rx_s1 == 1'b0) begin      // falling edge: start bit?
                    ticks <= 4'd0;
                    state <= START;
                end
                START: if (tick16) begin
                    if (ticks == 4'd7) begin        // middle of the start bit
                        ticks <= 4'd0;
                        bit_idx <= 3'd0;
                        if (rx_s1 == 1'b0) state <= DATA;  // still low: real start
                        else               state <= IDLE;  // glitch: ignore
                    end else
                        ticks <= ticks + 1'b1;
                end
                DATA: if (tick16) begin
                    if (ticks == 4'd15) begin       // middle of a data bit
                        ticks <= 4'd0;
                        shreg <= {rx_s1, shreg[7:1]};   // LSB arrives first
                        if (bit_idx == 3'd7) state <= STOP;
                        else bit_idx <= bit_idx + 1'b1;
                    end else
                        ticks <= ticks + 1'b1;
                end
                default: if (tick16) begin          // STOP
                    if (ticks == 4'd15) begin       // middle of the stop bit
                        ticks <= 4'd0;
                        state <= IDLE;
                        data  <= shreg;
                        if (rx_s1) valid     <= 1'b1;
                        else       frame_err <= 1'b1;
                    end else
                        ticks <= ticks + 1'b1;
                end
            endcase
        end
    end
endmodule

The receiver leaves the frame in the middle of the stop bit, not at its end. That gives it half a bit of extra time to be back in IDLE before the next start edge, which absorbs a transmitter that runs a little fast. If the stop bit reads 0, the frame is broken (wrong baud rate, or a break condition), and frame_err pulses instead of valid.

Common mistake: sampling at the bit edges
A receiver that samples at tick 0 or 15 of each bit has almost no margin: the smallest rate error or edge delay puts the sample on the wrong side of a transition. Mid-bit sampling is what turns the 5 % budget of Section 2 into reality.

6. A loopback testbench

The most convincing test of a transmitter and a receiver is to connect them: whatever TX sends, RX must get back. The testbench uses DIV = 4 so that a bit lasts only 64 clock cycles, sends five bytes back to back, including the edge cases 0x00 and 0xFF (no transitions inside the data), and checks every received byte in a separate process. Both simulators print the same five bytes and PASS at 27 368 ns.

uart_loopback_tb — TX → RX, five bytes, self-checking
`timescale 1ns/1ps
module uart_loopback_tb;
    reg  clk = 0, rst = 0, start = 0;
    reg  [7:0] din = 8'd0;
    wire tick16, line, busy, valid, ferr;
    wire [7:0] dout;
    reg  [7:0] msg [0:4];
    integer i, errors = 0, received = 0;

    // DIV = 4: one bit = 16 ticks x 4 clocks = 64 clocks (fast simulation)
    tick_gen #(.DIV(4)) baud (.clk(clk), .rst(rst), .tick(tick16));
    uart_tx tx_i (.clk(clk), .rst(rst), .tick16(tick16), .data(din),
                  .start(start), .busy(busy), .tx(line));
    uart_rx rx_i (.clk(clk), .rst(rst), .tick16(tick16), .rx(line),
                  .data(dout), .valid(valid), .frame_err(ferr));

    always #4 clk = ~clk;

    // Checker: compare every received byte with what was sent
    always @(posedge clk) begin
        if (valid) begin
            if (dout !== msg[received]) begin
                $display("FAIL: byte %0d got %h expected %h", received, dout, msg[received]);
                errors = errors + 1;
            end else
                $display("rx byte %0d = %h", received, dout);
            received = received + 1;
        end
        if (ferr) begin
            $display("FAIL: framing error");
            errors = errors + 1;
        end
    end

    initial begin
        msg[0] = 8'h55; msg[1] = 8'hA3; msg[2] = 8'h00; msg[3] = 8'hFF; msg[4] = 8'h0F;
        repeat (3) @(negedge clk);
        for (i = 0; i < 5; i = i + 1) begin
            din = msg[i]; start = 1;
            @(negedge clk) start = 0;
            @(negedge clk);
            while (busy) @(negedge clk);       // send the next byte at once
        end
        repeat (200) @(negedge clk);           // let the last byte arrive
        if (errors == 0 && received == 5)
            $display("PASS: 5 bytes looped back correctly at %0d ns", $time);
        else
            $display("FAIL: %0d errors, %0d bytes received", errors, received);
        $finish;
    end
endmodule
Run it
# Verilog with Icarus
iverilog -g2012 -o tb tick_gen.v uart_tx.v uart_rx.v uart_loopback_tb.v && vvp tb

# VHDL with GHDL
ghdl -a --std=08 tick_gen.vhd uart_tx.vhd uart_rx.vhd uart_loopback_tb.vhd
ghdl -e --std=08 uart_loopback_tb && ghdl -r --std=08 uart_loopback_tb

# Expected (both):
#   rx byte 0 = 55 ... rx byte 4 = 0F
#   PASS: 5 bytes looped back correctly at 27368 ns

7. On the board: an echo server on PMOD A

The PYNQ-Z2 has a USB-UART chip on board, but it is wired to the processing system (the ARM cores), not to the programmable logic. Linux uses it as its console. To reach a PC from our PL design we therefore add a cheap 3.3 V USB–serial adapter (FTDI, CP2102, CH340 and similar) on the PMOD A connector. The design below echoes every byte back and shows the low four bits of the last byte on the LEDs.

uart_echo — top level for the board
module uart_echo #(
    parameter DIV = 68                // 125 MHz / (16 x 115200) = 67.8 -> 68
) (
    input  wire       clk,            // 125 MHz
    input  wire       uart_rxd,       // from the adapter's TXD (PMOD JA2)
    output wire       uart_txd,       // to the adapter's RXD (PMOD JA1)
    output reg  [3:0] led = 4'd0      // low nibble of the last byte
);
    wire       tick16, valid, ferr, busy;
    wire [7:0] byte_rx;

    tick_gen #(.DIV(DIV)) baud (.clk(clk), .rst(1'b0), .tick(tick16));

    uart_rx rx_i (.clk(clk), .rst(1'b0), .tick16(tick16), .rx(uart_rxd),
                  .data(byte_rx), .valid(valid), .frame_err(ferr));

    // Send every received byte straight back
    uart_tx tx_i (.clk(clk), .rst(1'b0), .tick16(tick16), .data(byte_rx),
                  .start(valid), .busy(busy), .tx(uart_txd));

    always @(posedge clk)
        if (valid) led <= byte_rx[3:0];
endmodule
PCterminal orpyserialUSBUSB–serial adapterset to 3.3 V logicTXDRXDGNDPYNQ-Z2 PMOD AJA2 (Y19) uart_rxdJA1 (Y18) uart_txdGND pinPC → FPGAFPGA → PC
Wiring: TX and RX cross over. The adapter's TXD drives the FPGA input on JA2; the FPGA output on JA1 drives the adapter's RXD. A common ground is essential; the adapter's VCC pin stays unconnected.
uart_echo.xdc
## PYNQ-Z2 constraints for uart_echo (3.3 V USB-serial adapter on PMOD A)

## 125 MHz system clock
set_property -dict { PACKAGE_PIN H16 IOSTANDARD LVCMOS33 } [get_ports clk]
create_clock -name sys_clk -period 8.000 -waveform {0 4} [get_ports clk]

## PMOD A: JA1 = FPGA TX -> adapter RXD, JA2 = adapter TXD -> FPGA RX
set_property -dict { PACKAGE_PIN Y18 IOSTANDARD LVCMOS33 } [get_ports uart_txd]
set_property -dict { PACKAGE_PIN Y19 IOSTANDARD LVCMOS33 PULLUP true } [get_ports uart_rxd]

## Green LEDs LD0-LD3: low nibble of the last byte received
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports {led[0]}]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports {led[1]}]
set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports {led[2]}]
set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports {led[3]}]

The PULLUP on the input keeps the line at the idle level if the adapter is unplugged, so the receiver does not decode noise from a floating pin.

On the board: echoing bytes through PMOD A
  1. Check that your adapter uses 3.3 V logic levels (many have a 3.3 V/5 V jumper). The PMOD pins are LVCMOS33 and must never see 5 V.
  2. With the board off, connect adapter GND to a PMOD A ground pin, adapter TXD to JA2, adapter RXD to JA1. Leave the adapter's VCC pin unconnected.
  3. Build tick_gen, uart_tx, uart_rx and uart_echo with the XDC above for xc7z020clg400-1, and program the board over JTAG as in Chapter 14.
  4. Plug the adapter into the PC. It appears as /dev/ttyUSB0 (or /dev/ttyACM0) on Linux, a COMn port on Windows, /dev/tty.usbserial-… on macOS.
  5. Open a terminal at 115200 baud, 8N1, no flow control: screen /dev/ttyUSB0 115200, or minicom -D /dev/ttyUSB0 -b 115200, or PuTTY in “Serial” mode on Windows. Type: every character comes back, and the LEDs show its low nibble ("a" = 0x61 lights LD0 only).
  6. For an automated test, run this on the PC:
pyserial_test.py (on the PC)
# pip install pyserial   -- run this on the PC, not on the board
import serial

# Linux: /dev/ttyUSB0 or /dev/ttyACM0; Windows: 'COM5'; macOS: /dev/tty.usbserial-...
with serial.Serial('/dev/ttyUSB0', 115200, bytesize=8, parity='N',
                   stopbits=1, timeout=0.5) as port:
    port.reset_input_buffer()
    bad = 0
    for value in range(256):                 # every possible byte value
        port.write(bytes([value]))
        echo = port.read(1)                  # wait for it to come back
        if echo != bytes([value]):
            bad += 1
            print(f"0x{value:02X}: got {echo!r}")
    print(f"256 bytes echoed, {bad} errors")

If nothing comes back, swap TXD and RXD first — it is the most common wiring error. Garbage characters usually mean a baud-rate mismatch between the terminal and the design.

Back-to-back bytes and a slow transmitter
The echo design is fine for typing and for the byte-by-byte pyserial test, but if the PC sends a continuous stream it loses every second byte: our transmitter (DIV = 68, 0.27 % slow) needs slightly longer for a frame than the PC takes to send one, so it is still busy when the next byte arrives. Problem 5 works out the numbers; Chapter 17 adds the FIFO that real designs put between a receiver and a transmitter.

8. Python: a frame, its samples, and the baud error

The program draws the frame for 0x4B with the ideal mid-bit sampling points, and the samples of a receiver whose clock is 6 % slow — beyond the 5.3 % limit derived in Section 2. Watch the red crosses drift to the right: the D7 sample lands in the stop bit and the byte is read as 0xCB. The second panel shows the rate error from integer division at 125 MHz for common baud rates.

Python
script.py68 lines

Click Run to execute the Python code

Code will be executed with Python 3 on the server

Practice Problems

Progress: 0/5 (0%)
1
⭐ Easy
At 115 200 baud with 8 data bits, no parity and one stop bit (8N1), how many bytes per second can a UART carry at most? What fraction of the line time is payload?
2
⭐ Easy
Sketch the line level for the byte 0x41 (ASCII "A") in 8N1 format, from idle to idle.
3
⭐⭐ Medium
You want 921 600 baud with 16× oversampling from the 125 MHz clock. Compute the integer DIV and the rate error. Is it usable? Suggest two ways out.
4
⭐⭐ Medium
Why does uart_rx check the line again in the middle of the start bit, and what would happen without that check if a 50 ns noise spike hit an idle line?
5
⭐⭐⭐ Hard
With DIV = 68 the echo design works when you type by hand but loses every second byte when the PC sends a continuous stream. Explain this precisely with numbers, and propose two fixes.

Problem Set Statistics

Total Problems
5
Attempted
0
Completion
0%
Share:XRedditLinkedIn
Rate this chapter: