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.
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:
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:
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 %).
| baud | DIV (1×) | error (1×) | DIV (16×) | error (16×) |
|---|---|---|---|---|
| 9600 | 13021 | −0.001 % | 814 | −0.024 % |
| 57600 | 2170 | +0.006 % | 136 | −0.269 % |
| 115200 | 1085 | +0.006 % | 68 | −0.269 % |
| 460800 | 271 | +0.099 % | 17 | −0.269 % |
| 921600 | 136 | −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.
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 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. Onstart, capturedataand go toALIGN.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; inDATArepeat 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.
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
endmodulebusy 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.
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.
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
endmoduleThe 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.
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.
`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# 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.
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## 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.
- 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.
- 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.
- Build
tick_gen,uart_tx,uart_rxanduart_echowith the XDC above forxc7z020clg400-1, and program the board over JTAG as in Chapter 14. - Plug the adapter into the PC. It appears as
/dev/ttyUSB0(or/dev/ttyACM0) on Linux, aCOMnport on Windows,/dev/tty.usbserial-…on macOS. - Open a terminal at 115200 baud, 8N1, no flow control:
screen /dev/ttyUSB0 115200, orminicom -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). - For an automated test, run this 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.
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.
Click Run to execute the Python code
Code will be executed with Python 3 on the server
Practice Problems
0x41 (ASCII "A") in 8N1 format, from idle to idle.DIV and the rate error. Is it usable? Suggest two ways out.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?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.