Digital Design & Verification (Verilog, SystemVerilog, VLSI) Module 4 – Finite-state machines in RTL
FSM case study: UART and SPI controllers
Build a UART transmitter, a receiver that samples 16 times a bit and an SPI controller for all four modes as state machines, then verify them on models.
What you will learn
- Translate a serial protocol's timing description into state machines and counters
- Implement a UART transmitter and a receiver that samples the line 16 times a bit
- Implement an SPI controller with selectable clock polarity and phase
- Verify serial links with loopback testbenches and with reference models
Before you start
What you’ll learn · Before you start
What you will learn
- Translate a serial protocol's timing description into state machines and counters
- Implement a UART transmitter and a receiver that samples the line 16 times a bit
- Implement an SPI controller with selectable clock polarity and phase
- Verify serial links with loopback testbenches and with reference models
Before you start
On this page
At 115,200 baud a bit lasts 8.68 microseconds, and a 12 MHz clock cannot count that out exactly: the nearest whole divider runs 7 % slow. Of 1,000 bytes from a UART built on it, a receiver at the right rate read 373 wrong and flagged 489 more as framing errors, while the UART’s own loopback test passed all 1,000. Serial controllers are state machines with counters, and they are where a plain timing description becomes RTL, and where a test that checks a design against itself can hide a mistake.
You will turn the UART’s frame into a transmitter with four states, generate its ticks with a divider and with a phase accumulator, build a receiver that samples the middle of each bit, verify both with models at the exact rate, and write an SPI controller for all four clock modes. Icarus Verilog 13.0 and Verilator 5.052 ran every simulation, and the rate calculations run in your browser.
A frame is a start bit, eight data bits and a stop bit, timed by a counter
A UART (universal asynchronous receiver-transmitter): A serial link with no shared clock: each byte travels as a frame of a start bit of 0, the data bits least significant first, an optional parity bit and a stop bit of 1, on a line that idles at 1. line idles at 1. Texas Instruments’ data sheet of its TL16C550D describes the frame such a device sends: a start bit of 0, five to eight data bits with bit 0, the least significant, sent first, an optional parity bit, and one, one and a half or two stop bits of 1. This lesson’s frame is 8N1: 8 data bits, no parity, one stop bit, ten bits in all. At a baud rate: The number of symbols a line carries per second; on a UART each symbol is one bit, so at 115,200 baud a bit lasts 1/115,200 s, about 8.68 microseconds. of 115,200, one bit lasts 1 ÷ 115,200 s, 8.68 microseconds.
The description turns into RTL in two parts. Every stretch of the frame that does something different becomes a state: IDLE, START, DATA and STOP. Every count inside a state becomes a counter: uart_tx.sv’s ticks_q counts the 16 ticks of a bit, and bit_q the data bit on the line. The machine decides what the line does and the counters decide when. The line itself comes from a flip-flop, tx_q, so it never glitches, the lesson before this one’s rule for an output that leaves the chip. tb_frame.sv sends 0x4B, the letter K, and prints every change of state or bit:
hdl/uart_tx.sv
`timescale 1ns / 1ps
// A UART transmitter for 8N1 frames: the line idles at 1; a frame is a start bit of 0, the 8 data bits least
// significant first, and a stop bit of 1, each exactly 16 ticks of tick_i long. A byte is taken in a cycle where
// valid_i and ready_o are both 1. The line comes from a flip-flop, so it never glitches.
module uart_tx (
input logic clk_i,
input logic rst_ni,
input logic tick_i, // 16 per bit, from baud_gen
input logic valid_i,
input logic [7:0] data_i,
output logic ready_o,
output logic tx_o
);
typedef enum logic [1:0] {IDLE, START, DATA, STOP} state_t;
state_t state_q, state_d;
logic [3:0] ticks_q, ticks_d; // ticks into the current bit
logic [2:0] bit_q, bit_d; // the data bit on the line
logic [7:0] shift_q, shift_d; // the byte, shifted right as it goes out
logic tx_q, tx_d;
logic bit_end; // the 16th tick of a bit
assign bit_end = tick_i && (ticks_q == 4'd15);
always_comb begin
state_d = state_q;
ticks_d = tick_i ? ticks_q + 4'd1 : ticks_q;
bit_d = bit_q;
shift_d = shift_q;
tx_d = tx_q;
unique case (state_q)
IDLE: begin
ticks_d = '0;
if (valid_i) begin
shift_d = data_i;
state_d = START;
end
end
START: begin
if (tx_q) begin // the start bit begins on a tick, so it lasts 16 of them
ticks_d = '0;
if (tick_i) tx_d = 1'b0;
end else if (bit_end) begin
state_d = DATA;
bit_d = '0;
tx_d = shift_q[0];
end
end
DATA: if (bit_end) begin
if (bit_q == 3'd7) begin
state_d = STOP;
tx_d = 1'b1;
end else begin
bit_d = bit_q + 3'd1;
shift_d = shift_q >> 1;
tx_d = shift_q[1];
end
end
STOP: if (bit_end) state_d = IDLE;
default: begin
state_d = IDLE;
tx_d = 1'b1;
end
endcase
end
always_ff @(posedge clk_i or negedge rst_ni) begin
if (!rst_ni) begin
state_q <= IDLE;
ticks_q <= '0;
bit_q <= '0;
shift_q <= '0;
tx_q <= 1'b1; // the idle line is high
end else begin
state_q <= state_d;
ticks_q <= ticks_d;
bit_q <= bit_d;
shift_q <= shift_d;
tx_q <= tx_d;
end
end
assign ready_o = (state_q == IDLE);
assign tx_o = tx_q;
endmodule hdl/baud_gen.sv
`timescale 1ns / 1ps
// A clock enable 16 times per bit for a UART: tick_o is 1 for one clk_i cycle at a time. With Divisor > 0 it counts
// clock cycles and ticks on every Divisor-th; with Divisor = 0 it adds Step to a 20-bit phase accumulator every cycle
// and ticks on each carry out, so on average the ticks come at Step / 2^20 of the clock rate.
module baud_gen #(
parameter int unsigned Divisor = 0, // 7 for 12 MHz and 115,200 baud: 12e6 / 7 / 16 = 107,143 baud
parameter int unsigned Step = 161061 // round(2^20 * 16 * 115200 / 12e6) = round(161061.27)
) (
input logic clk_i,
input logic rst_ni,
output logic tick_o
);
if (Divisor > 0) begin : g_divider
localparam logic [7:0] Last = 8'(Divisor - 1);
logic [7:0] count_q;
always_ff @(posedge clk_i or negedge rst_ni) begin
if (!rst_ni) count_q <= '0;
else if (count_q == Last) count_q <= '0;
else count_q <= count_q + 8'd1;
end
assign tick_o = (count_q == Last);
end else begin : g_accumulator
logic [20:0] acc_q; // one more bit than the accumulator: the carry
always_ff @(posedge clk_i or negedge rst_ni) begin
if (!rst_ni) acc_q <= '0;
else acc_q <= {1'b0, acc_q[19:0]} + 21'(Step);
end
assign tick_o = acc_q[20];
end
endmodule hdl/tb_frame.sv
`timescale 1ns / 1ps
// One frame of 0x4B, the letter K, from the transmitter on a 12 MHz clock with the phase-accumulator ticks. Just
// after every clock edge the testbench looks at the line and at the transmitter's state and bit index, and prints a
// line when the start bit falls and whenever the state or the bit changes: the time since the start bit fell, how
// long the bit before lasted, and the line. One bit at 115,200 baud lasts 8.68 microseconds.
module tb_frame;
logic clk = 1'b0, rst_n = 1'b0, tick, tx, ready, valid = 1'b0;
always #(1000.0 / 24.0) clk = ~clk; // 12 MHz
baud_gen u_baud (.clk_i(clk), .rst_ni(rst_n), .tick_o(tick));
uart_tx u_tx (.clk_i(clk), .rst_ni(rst_n), .tick_i(tick), .valid_i(valid), .data_i(8'h4B), .ready_o(ready),
.tx_o(tx));
realtime fell = -1, last = 0;
string was = "IDLE", now;
int bit_was = 0;
task automatic show(input string what);
$display("t = %6.2f us (+%4.2f) %s, line %b", ($realtime - fell) / 1000.0, ($realtime - last) / 1000.0, what,
tx);
last = $realtime;
endtask
always @(posedge clk) begin
#1;
now = u_tx.state_q.name();
if (rst_n && fell < 0 && tx === 1'b0) begin
fell = $realtime;
last = $realtime;
show("START, the start bit");
end else if (now != was || (now == "DATA" && u_tx.bit_q != bit_was)) begin
if (now == "DATA") show($sformatf("DATA, bit %0d", u_tx.bit_q));
else if (now == "STOP") show("STOP, the stop bit");
else if (now == "IDLE" && fell >= 0) show("IDLE again");
end
was = now;
bit_was = u_tx.bit_q;
end
initial begin
#1000 rst_n = 1'b1;
@(posedge clk) valid <= 1'b1;
@(posedge clk) valid <= 1'b0;
wait (tx === 1'b0);
wait (ready);
#100 $finish;
end
endmodule Commands · hdl/frame_session.py
from session import run
# One frame of 0x4B from the transmitter, on Icarus Verilog and on Verilator.
run("""
iverilog -g2012 -Wall -o frame.vvp tb_frame.sv baud_gen.sv uart_tx.sv
vvp -n frame.vvp
""") Output
# Icarus Verilog 13.0 (stable) $ iverilog -g2012 -Wall -o frame.vvp tb_frame.sv baud_gen.sv uart_tx.sv uart_tx.sv:46: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_tx.sv:56: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_tx.sv:31: vvp.tgt sorry: Case unique/unique0 qualities are ignored. $ vvp -n frame.vvp t = 0.00 us (+0.00) START, the start bit, line 0 t = 8.67 us (+8.67) DATA, bit 0, line 1 t = 17.33 us (+8.67) DATA, bit 1, line 1 t = 26.08 us (+8.75) DATA, bit 2, line 0 t = 34.75 us (+8.67) DATA, bit 3, line 1 t = 43.42 us (+8.67) DATA, bit 4, line 0 t = 52.08 us (+8.67) DATA, bit 5, line 0 t = 60.75 us (+8.67) DATA, bit 6, line 1 t = 69.42 us (+8.67) DATA, bit 7, line 0 t = 78.17 us (+8.75) STOP, the stop bit, line 1 t = 86.83 us (+8.67) IDLE again, line 1 tb_frame.sv:46: $finish called at 88559041 (1ps)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 frame_session.py
The sorry lines are Icarus Verilog’s notes on its own limits, not errors: a block that reads one bit of shift_q wakes on a change of any of its bits, and the checks of the unique qualifier are skipped, which changes none of the values the blocks compute. The data bits went out as 1, 1, 0, 1, 0, 0, 1, 0, which is 4b read from the least significant bit up, and the frame ended at 86.83 microseconds against 86.81 for ten bits at the exact rate. Step through it:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- A byte is taken. The line idles at 1. The byte 4b is taken in a cycle where valid_i and ready_o are both 1, and the state becomes START, with the byte in the shift register.
- The start bit. At the next tick the transmitter drives the line to 0, the start bit, at t = 0. From now on ticks_q counts the 16 ticks of each bit.
- Bit 0 first. On the 16th tick the state becomes DATA and the line takes bit 0 of 4b, which is 1, at 8.67 microseconds. The least significant bit goes first.
- The next bits. Every 16 ticks the shift register moves one place right and the line takes the next bit. Bit 1 = 1 at 17.33, bit 2 = 0 at 26.08, bit 3 = 1 at 34.75 microseconds.
- Bits 4 to 7. Bits 4 to 7 follow, 0, 0, 1 and 0, read in that order. bit_q reaches 7 at 69.42 microseconds.
- The stop bit. After bit 7 the state becomes STOP and the line returns to 1 for a whole bit, at 78.17 microseconds. A receiver checks that it is there.
- Ready again. Sixteen ticks later, at 86.83 microseconds, the frame is over and the state is IDLE, so ready_o is 1. Ten bits at 8.68 microseconds would be 86.81.
- Why 8.67 or 8.75. Sixteen ticks are 104 or 105 cycles of the 12 MHz clock, because the accumulator's ticks come 6 or 7 cycles apart. Each bit is a little short or long, and the frame does not drift.
START waits for a tick before it drops the line, so the start bit lasts 16 ticks like every other bit; dropping it at once would make it up to a tick short. The bits lasted 8.67 or 8.75 microseconds, 104 or 105 clock cycles, never 8.68, which is the next section’s subject.
Checkpoint
Check: A UART frame as states and counters
Sixteen ticks a bit need a divider that fits, or an accumulator
The same data sheet gives the rule for its baud generator: its output is 16 times the baud rate, and the divisor is the input clock divided by 16 times the baud rate. For 12 MHz and 115,200 baud that is 12,000,000 ÷ 1,843,200, about 6.51. A counter can divide only by a whole number, and the nearest, 7, ticks at 1.714 MHz, which makes 107,143 baud, 6.99 % slow.
A phase accumulator: A register that adds a fixed step every clock cycle and gives a tick on each carry out of its top bit, so ticks come on average at step / 2^n of the clock rate, a fraction no whole-number divider can reach. divides by a fraction instead. baud_gen.sv adds a step of 161,061 to a 20-bit register every cycle and ticks on each carry out of it, so on average it ticks 161,061 ÷ 2²⁰ of the clock rate, 1,843,197 times a second, 0.0002 % slow. Its ticks come 6 or 7 cycles apart, which is why the bits above lasted 104 or 105 cycles, but they never drift. baud_error.py works out both methods for a few clocks and rates, then what a 7 % error does to a frame. Add a pair of your own to PAIRS and run it:
"""How far a UART's bit rate lands from the one asked for, with 16 ticks per bit, for a few clocks and rates: an
integer divisor, round(clock / (16 * baud)), against a 20-bit phase accumulator that adds
round(2**20 * 16 * baud / clock) every cycle. Then where a receiver that runs about 7 % slow samples each bit of a
frame sent at the right rate.
"""
ACC_BITS = 20
PAIRS = [(12_000_000, 9_600), (12_000_000, 115_200), (25_000_000, 115_200), (50_000_000, 115_200),
(100_000_000, 921_600)]
def rate_error(clock, baud):
"""(divisor, its error in %, accumulator step, its error in %)."""
divisor = round(clock / (16 * baud))
by_divisor = clock / (16 * divisor)
step = round(2**ACC_BITS * 16 * baud / clock)
by_accumulator = clock * step / 2**ACC_BITS / 16
return divisor, 100 * (by_divisor / baud - 1), step, 100 * (by_accumulator / baud - 1)
print(" clock baud | divisor error | accumulator step error")
for clock, baud in PAIRS:
divisor, d_err, step, a_err = rate_error(clock, baud)
print(f"{clock / 1e6:5.0f} MHz {baud:9,} | {divisor:7} {d_err:+7.3f} % | {step:16,} {a_err:+8.4f} %")
# The stop bit's middle is 9.5 bits after the start edge, and a receiver at 16 ticks a bit may see that edge up to
# one tick, 1/16 of a bit, late. Drifting half a bit by then means sampling the wrong bit.
print(f"\nmismatch a frame survives: about {100 * (0.5 - 1 / 16) / 9.5:.1f} % (half a bit, less a tick, over 9.5 bits)")
# A receiver at 12 MHz / 7 / 16 = 107,143 baud reading a frame sent at 115,200: where its samples land, in the
# sender's bits (the start bit is 0 to 1, data bit k is k + 1 to k + 2, the stop bit is 9 to 10).
slow = 115_200 / (12_000_000 / 7 / 16)
names = ["start"] + [f"bit {k}" for k in range(8)] + ["stop"]
print(f"\na receiver 7 % slow samples each bit {slow:.4f} of the sender's bits apart:")
for i, name in enumerate(names):
at = (i + 0.5) * slow
landed = names[int(at)] if int(at) < len(names) else "after the frame"
note = "" if landed == name else f" <- reads {landed}"
print(f" {name:>5}: sampled at {at:5.2f}{note}") Output
clock baud | divisor error | accumulator step error 12 MHz 9,600 | 78 +0.160 % | 13,422 +0.0017 % 12 MHz 115,200 | 7 -6.994 % | 161,061 -0.0002 % 25 MHz 115,200 | 14 -3.119 % | 77,309 -0.0005 % 50 MHz 115,200 | 27 +0.469 % | 38,655 +0.0008 % 100 MHz 921,600 | 7 -3.119 % | 154,619 +0.0001 % mismatch a frame survives: about 4.6 % (half a bit, less a tick, over 9.5 bits) a receiver 7 % slow samples each bit 1.0752 of the sender's bits apart: start: sampled at 0.54 bit 0: sampled at 1.61 bit 1: sampled at 2.69 bit 2: sampled at 3.76 bit 3: sampled at 4.84 bit 4: sampled at 5.91 bit 5: sampled at 6.99 bit 6: sampled at 8.06 <- reads bit 7 bit 7: sampled at 9.14 <- reads stop stop: sampled at 10.21 <- reads after the frame
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 baud_error.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
How much error a frame survives
The middle of the stop bit is 9.5 bits after the start edge, and a receiver that samples 16 times a bit may see that edge up to a sixteenth of a bit late. Drifting half a bit by then means sampling the wrong bit, so the two ends together can be about (0.5 − 1/16) ÷ 9.5 = 4.6 % apart.
Every rate in the table was within 3.2 % with a divider except 12 MHz at 115,200, at 7 %. The last lines show what 7 % does to a receiver: each of its samples lands 1.0752 of the sender’s bits after the one before, so bit 6 is read from bit 7, bit 7 from the stop bit, and the stop bit after the frame has ended. An accumulator of 20 bits put every pair within 0.002 %.
Checkpoint
Check: Dividers and phase accumulators
The receiver samples each bit in its middle
uart_rx.sv reads the line 16 times a bit, which is called oversampling: Sampling a signal several times per bit, such as 16 times for a UART receiver, so that the receiver can find an edge and then the middle of each bit without the sender's clock.. The line comes from another device, so it can change at any moment, and two flip-flops in a row first bring it into this clock’s time, a two-flip-flop synchronizer: Two flip-flops in a row in the receiving clock domain. The first may go metastable when its input changes near an edge; the second samples it a clock period later, once it has almost surely settled. More in “Metastability, MTBF and two-flop synchronizers”, which the clock-domain module of this track explains in full. Then the machine runs on the ticks:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Into this clock. The line changes whenever the far end likes. Two flip-flops bring it into this clock's time first, so the receiver sees every change two cycles late, and never half-way.
- The line falls. At a tick the receiver sees 0 for the first time. It may be a start bit or a short glitch, so the state becomes START and ticks_q starts at 0.
- Half a bit later. Eight ticks on, in the middle of the start bit, the line is still 0, so it is a real start bit. A pulse shorter than half a bit would be 1 again here and go back to IDLE.
- Bit 0. Sixteen ticks after the middle of the start bit comes the middle of bit 0. The receiver samples 1 and shifts it in from the left.
- Seven more bits. Every 16 ticks after that it samples the middle of the next bit, 1, 0, 1, 0, 0, 1 and 0. After bit 7 the shift register holds 4b.
- The stop bit. Sixteen ticks after bit 7, in the middle of the stop bit, the line is 1, so valid_o is 1 for one cycle with data_o = 4b. A 0 here would set frame_err_o instead.
- Ready early. The receiver is back in IDLE in the middle of the stop bit, half a bit before the frame ends, so a start bit that follows at once is never missed.
Each choice has its reason. Looking 8 ticks after the line falls puts the check in the middle of the start bit, where a pulse shorter than half a bit has already gone, so a glitch on the line sends the receiver back to IDLE instead of into a byte; the TL16C550D lists the same false-start detection among its features. Sampling every 16 ticks from there lands in the middle of each later bit, as far as possible from both of its edges, which is what lets the two ends differ by a few per cent. A stop bit of 0 is a framing error: A UART frame whose stop bit was received as 0 instead of 1, usually because the two ends run at rates too far apart or the receiver lost the frame's start., the data sheet’s “did not have a valid (set) stop bit”, and the receiver reports it on frame_err_o instead of valid_o.
Sampling where the line changes
Wrong: count 16 ticks from the falling edge for bit 0, which samples at the very edge between the start bit and bit 0, where a small rate error picks either. Right: check the start bit 8 ticks after the fall, then count 16 ticks from there, so every sample is half a bit from both edges.
Checkpoint
Check: Sampling in the middle of each bit
A loopback test shares the mistake; a model does not
tb_uart.sv checks the link three ways at once, with 1,000 bytes from a 32-bit xorshift generator that gives the same bytes on every simulator. A model receiver samples the transmitter’s line in real time at exactly 115,200 baud, a model transmitter drives a receiver at that rate, and a second receiver reads the transmitter’s own line, a loopback test: A test that connects a design's transmitter to its own receiver. It shows that the two halves agree with each other, but not that either meets the specification, since a mistake both share cancels out.. The models know nothing of the design’s ticks:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The design sends. The transmitter sends 1,000 seeded bytes as fast as it can, timed by the design's ticks.
- A model reads it. A model receiver samples that line in real time at exactly 115,200 baud. It knows nothing of the ticks, so it checks the transmitter's rate.
- The loopback. A copy of the receiver reads the same line through the same ticks. It checks that the two halves of the design agree with each other.
- A model sends. A model transmitter drives a third line at exactly 115,200 baud into another receiver, which checks the receiver's rate.
- The divider of 7. With ticks from the divider of 7, 7 % slow, the model receiver found 373 wrong bytes and 489 framing errors, and the receiver fed by the model 316 and 575.
- The loopback passes. The loopback decoded all 1,000 bytes without an error. Both of its ends run 7 % slow together, so it cannot see a wrong rate.
The session builds the testbench twice on Icarus Verilog, once with each tick generator, and then once on Verilator with the accumulator, after a lint of the design files:
hdl/uart_rx.sv
`timescale 1ns / 1ps
// A UART receiver for 8N1 frames that looks at the line 16 times per bit. Two flip-flops first bring the line,
// which changes whenever the far end likes, into this clock's time (the clock-domain module explains why). The receiver
// waits for the line to fall, checks the start bit again 8 ticks later, in its middle, then samples each data bit and
// the stop bit in the middle, every 16 ticks. A stop bit of 0 is a framing error.
module uart_rx (
input logic clk_i,
input logic rst_ni,
input logic tick_i, // 16 per bit, from baud_gen
input logic rx_i, // the line, from outside this clock's time
output logic valid_o, // one cycle: data_o holds a byte
output logic [7:0] data_o,
output logic frame_err_o // one cycle: the stop bit was 0
);
typedef enum logic [1:0] {IDLE, START, DATA, STOP} state_t;
state_t state_q, state_d;
logic [1:0] sync_q; // the synchroniser: rx_i two clock edges late
logic [3:0] ticks_q, ticks_d;
logic [2:0] bit_q, bit_d;
logic [7:0] shift_q, shift_d; // bits come in least significant first, from the left
logic valid_d, err_d;
logic rx;
assign rx = sync_q[1];
always_comb begin
state_d = state_q;
ticks_d = tick_i ? ticks_q + 4'd1 : ticks_q;
bit_d = bit_q;
shift_d = shift_q;
valid_d = 1'b0;
err_d = 1'b0;
unique case (state_q)
IDLE: begin
ticks_d = '0;
if (tick_i && !rx) state_d = START; // the line fell: maybe a start bit
end
START: if (tick_i && ticks_q == 4'd7) begin // 8 ticks on: the middle of the start bit
ticks_d = '0;
if (!rx) begin
state_d = DATA;
bit_d = '0;
end else begin
state_d = IDLE; // only a glitch: back to waiting
end
end
DATA: if (tick_i && ticks_q == 4'd15) begin // 16 ticks on: the middle of the next bit
shift_d = {rx, shift_q[7:1]};
if (bit_q == 3'd7) state_d = STOP;
else bit_d = bit_q + 3'd1;
end
STOP: if (tick_i && ticks_q == 4'd15) begin
state_d = IDLE;
if (rx) valid_d = 1'b1;
else err_d = 1'b1;
end
default: state_d = IDLE;
endcase
end
always_ff @(posedge clk_i or negedge rst_ni) begin
if (!rst_ni) begin
sync_q <= 2'b11; // the idle line is high
state_q <= IDLE;
ticks_q <= '0;
bit_q <= '0;
shift_q <= '0;
valid_o <= 1'b0;
frame_err_o <= 1'b0;
end else begin
sync_q <= {sync_q[0], rx_i};
state_q <= state_d;
ticks_q <= ticks_d;
bit_q <= bit_d;
shift_q <= shift_d;
valid_o <= valid_d;
frame_err_o <= err_d;
end
end
assign data_o = shift_q;
endmodule hdl/tb_uart.sv
// 1,000 seeded bytes three ways at once, on a 12 MHz clock: the transmitter's line read by a model receiver that
// samples at exactly 115,200 baud; a model transmitter at exactly 115,200 baud read by the receiver; and the
// transmitter's line looped back into a second receiver. DIVISOR picks the tick generator: 0 is the phase
// accumulator, 7 the integer divisor. The models know nothing of the design's ticks: they use real time.
`timescale 1ns / 1ps
module tb_uart;
localparam int Bytes = 1000;
localparam real Bit = 1.0e9 / 115200.0; // 8680.556 ns
parameter int DIVISOR = 0;
logic clk = 1'b0, rst_n = 1'b0;
always #(1000.0 / 24.0) clk = ~clk; // 12 MHz
logic tick, tx, tx_valid = 1'b0, tx_ready, model_line = 1'b1;
logic [7:0] tx_data = '0;
logic loop_valid, loop_err, rx_valid, rx_err;
logic [7:0] loop_data, rx_data;
baud_gen #(.Divisor(DIVISOR)) u_baud (.clk_i(clk), .rst_ni(rst_n), .tick_o(tick));
uart_tx u_tx (.clk_i(clk), .rst_ni(rst_n), .tick_i(tick), .valid_i(tx_valid), .data_i(tx_data),
.ready_o(tx_ready), .tx_o(tx));
uart_rx u_loop (.clk_i(clk), .rst_ni(rst_n), .tick_i(tick), .rx_i(tx),
.valid_o(loop_valid), .data_o(loop_data), .frame_err_o(loop_err));
uart_rx u_rx (.clk_i(clk), .rst_ni(rst_n), .tick_i(tick), .rx_i(model_line),
.valid_o(rx_valid), .data_o(rx_data), .frame_err_o(rx_err));
// The same bytes on every simulator: a 32-bit xorshift from seed 1, its low 8 bits.
function automatic logic [7:0] byte_at(input int n);
logic [31:0] x = 32'd1;
for (int i = 0; i <= n; i++) begin
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
end
return x[7:0];
endfunction
logic [7:0] sent [Bytes];
initial for (int i = 0; i < Bytes; i++) sent[i] = byte_at(i);
int model_n = 0, model_bad = 0, model_ferr = 0; // the transmitter, read by the model
int rx_n = 0, rx_bad = 0, rx_ferr = 0; // the model transmitter, read by the receiver
int loop_n = 0, loop_bad = 0, loop_ferr = 0; // the loopback
// The design's transmitter sends every byte as soon as it is ready.
initial begin
#1000 rst_n = 1'b1;
for (int i = 0; i < Bytes; i++) begin
@(posedge clk);
while (!tx_ready) @(posedge clk);
tx_valid <= 1'b1;
tx_data <= sent[i];
@(posedge clk);
tx_valid <= 1'b0;
@(posedge clk); // ready falls after the byte is taken
end
end
// The model receiver: on a falling edge of the line, it waits one and a half bits to the middle of bit 0.
initial begin
logic [7:0] b;
@(posedge rst_n);
for (int k = 0; k < Bytes; k++) begin
@(negedge tx);
#(1.5 * Bit);
for (int i = 0; i < 8; i++) begin
b[i] = tx;
if (i < 7) #(Bit);
end
#(Bit);
if (!tx) model_ferr++;
else if (b != sent[k]) model_bad++;
model_n++;
end
end
// The model transmitter: start bit, 8 data bits least significant first, stop bit, at exactly 115,200 baud.
initial begin
#5000;
for (int k = 0; k < Bytes; k++) begin
model_line = 1'b0;
#(Bit);
for (int i = 0; i < 8; i++) begin
model_line = sent[k][i];
#(Bit);
end
model_line = 1'b1;
#(Bit);
end
end
always @(posedge clk) begin
if (rx_valid) begin
if (rx_n >= Bytes || rx_data != sent[rx_n]) rx_bad++;
rx_n++;
end
if (rx_err) begin
rx_ferr++;
rx_n++;
end
if (loop_valid) begin
if (loop_n >= Bytes || loop_data != sent[loop_n]) loop_bad++;
loop_n++;
end
if (loop_err) begin
loop_ferr++;
loop_n++;
end
end
initial begin
#(1.2 * Bytes * 10 * Bit); // long enough for every frame, 20 % to spare
if (DIVISOR == 0) $display("tick generator: phase accumulator");
else $display("tick generator: divide by %0d", DIVISOR);
$display(" transmitter -> model at 115,200: %4d frames, %4d wrong bytes, %4d framing errors", model_n, model_bad, model_ferr);
$display(" model at 115,200 -> receiver: %4d frames, %4d wrong bytes, %4d framing errors", rx_n, rx_bad, rx_ferr);
$display(" transmitter -> receiver (loop): %4d frames, %4d wrong bytes, %4d framing errors", loop_n, loop_bad, loop_ferr);
$finish;
end
endmodule Commands · hdl/uart_session.py
from session import run
# 1,000 seeded bytes three ways, on Icarus Verilog: first with the phase-accumulator ticks, then with the integer
# divisor of 7 (-P sets the testbench's parameter).
run("""
iverilog -g2012 -Wall -o acc.vvp tb_uart.sv baud_gen.sv uart_tx.sv uart_rx.sv
vvp -n acc.vvp
iverilog -g2012 -Wall -Ptb_uart.DIVISOR=7 -o div.vvp tb_uart.sv baud_gen.sv uart_tx.sv uart_rx.sv
vvp -n div.vvp
""") Output
# Icarus Verilog 13.0 (stable) $ iverilog -g2012 -Wall -o acc.vvp tb_uart.sv baud_gen.sv uart_tx.sv uart_rx.sv uart_tx.sv:46: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_tx.sv:56: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_rx.sv:49: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_rx.sv:49: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_tx.sv:31: vvp.tgt sorry: Case unique/unique0 qualities are ignored. uart_rx.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored. uart_rx.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored. $ vvp -n acc.vvp tick generator: phase accumulator transmitter -> model at 115,200: 1000 frames, 0 wrong bytes, 0 framing errors model at 115,200 -> receiver: 1000 frames, 0 wrong bytes, 0 framing errors transmitter -> receiver (loop): 1000 frames, 0 wrong bytes, 0 framing errors tb_uart.sv:118: $finish called at 104166666667 (1ps) $ iverilog -g2012 -Wall -Ptb_uart.DIVISOR=7 -o div.vvp tb_uart.sv baud_gen.sv uart_tx.sv uart_rx.sv uart_tx.sv:46: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_tx.sv:56: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_rx.sv:49: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_rx.sv:49: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'shift_q[7:0]'). uart_tx.sv:31: vvp.tgt sorry: Case unique/unique0 qualities are ignored. uart_rx.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored. uart_rx.sv:34: vvp.tgt sorry: Case unique/unique0 qualities are ignored. $ vvp -n div.vvp tick generator: divide by 7 transmitter -> model at 115,200: 1000 frames, 373 wrong bytes, 489 framing errors model at 115,200 -> receiver: 892 frames, 316 wrong bytes, 575 framing errors transmitter -> receiver (loop): 1000 frames, 0 wrong bytes, 0 framing errors tb_uart.sv:118: $finish called at 104166666667 (1ps)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 uart_session.py
from session import run
# A lint of each design file on its own, then the same 1,000 bytes with the phase-accumulator ticks, on Verilator.
run("""
verilator --lint-only -Wall --quiet baud_gen.sv
verilator --lint-only -Wall --quiet uart_tx.sv
verilator --lint-only -Wall --quiet uart_rx.sv
verilator --lint-only -Wall --quiet spi_ctrl.sv
verilator --binary -j 0 --quiet --top-module tb_uart tb_uart.sv baud_gen.sv uart_tx.sv uart_rx.sv
obj_dir/Vtb_uart +verilator+quiet
""") Output
# Verilator 5.052 $ verilator --lint-only -Wall --quiet baud_gen.sv $ verilator --lint-only -Wall --quiet uart_tx.sv $ verilator --lint-only -Wall --quiet uart_rx.sv $ verilator --lint-only -Wall --quiet spi_ctrl.sv $ verilator --binary -j 0 --quiet --top-module tb_uart tb_uart.sv baud_gen.sv uart_tx.sv uart_rx.sv $ obj_dir/Vtb_uart +verilator+quiet tick generator: phase accumulator transmitter -> model at 115,200: 1000 frames, 0 wrong bytes, 0 framing errors model at 115,200 -> receiver: 1000 frames, 0 wrong bytes, 0 framing errors transmitter -> receiver (loop): 1000 frames, 0 wrong bytes, 0 framing errors - tb_uart.sv:118: Verilog $finish
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 uart_verilator_session.py
| Path | Phase accumulator | Divider of 7 |
|---|---|---|
| transmitter to the model receiver | 1,000 bytes, no error | 373 wrong bytes, 489 framing errors |
| the model transmitter to the receiver | 1,000 bytes, no error | 892 frames: 316 wrong, 575 framing errors |
| transmitter to receiver, loopback | 1,000 bytes, no error | 1,000 bytes, no error |
With the accumulator every path decoded every byte on both simulators. With the divider of 7, both paths through a model failed, and the loopback passed all 1,000, because both of its ends were 7 % slow together and agreed with each other perfectly. The receiver fed by the model even lost 108 frames, 892 of 1,000: after a framing error it took the middle of a frame for a new start bit.
Key idea
A loopback test shows that the two halves of a design agree with each other; only a model that runs at the specified rate shows that they agree with the rest of the world.
Checkpoint
Check: Loopback tests and models
SPI: the clock’s polarity and phase decide which edge samples
SPI (serial peripheral interface): A synchronous serial bus with no formal standard: a controller drives a clock and a chip select, and on every clock period one bit goes each way, on MOSI to the peripheral and on MISO back. has no formal specification, as the Linux kernel’s documentation of it says. A controller drives the clock, sclk, and a chip select: The signal that picks one device on a shared bus, usually active low: an SPI peripheral listens and drives MISO only while its chip select is asserted., usually active low, and the two sides exchange one bit each way on every clock period, MOSI towards the peripheral and MISO back, most often most significant bit first. Two mode bits fix the timing, in the kernel’s words: CPOL is “the initial clock polarity”, and CPHA picks the edge that samples, the leading one for 0 and the trailing one for 1. The SPI mode: One of four timings of an SPI bus, numbered with CPOL as the high bit and CPHA as the low bit: CPOL is the level the clock rests at, CPHA chooses whether data is sampled on the leading or the trailing edge of each bit. number has CPOL as its high bit:
| Mode | CPOL, CPHA | Clock rests at | Data sampled on | Data changes on |
|---|---|---|---|---|
| 0 | 0, 0 | 0 | rising, the leading edge | falling |
| 1 | 0, 1 | 0 | falling, the trailing edge | rising |
| 2 | 1, 0 | 1 | falling, the leading edge | rising |
| 3 | 1, 1 | 1 | rising, the trailing edge | falling |
spi_ctrl.sv is a machine of three states, IDLE, SHIFT and HOLD, with a counter of half periods and one of the 16 edges of a transfer; the mode bits decide which edges sample and which change MOSI, and with CPHA 0 bit 7 is on the line before the first edge. tb_spi.sv runs it against a model peripheral written from the table alone, which answers transfer n with the byte 3c + n and whose outputs, like the controller’s, change 2 ns after the edge that moves them:
hdl/spi_ctrl.sv
`timescale 1ns / 1ps
// An SPI controller, the side that drives the clock, for 8-bit transfers, most significant bit first, in any of the
// four modes. sclk_o rests at cpol_i between transfers. With cpha_i = 0 both sides sample on the first edge of each
// bit and change their data on the second; with cpha_i = 1 they change on the first edge and sample on the second.
// cs_no is low for the whole transfer, and every output comes from a flip-flop.
module spi_ctrl #(
parameter int unsigned Half = 3 // clk_i cycles per half period of sclk_o
) (
input logic clk_i,
input logic rst_ni,
input logic cpol_i,
input logic cpha_i,
input logic start_i, // taken when busy_o is 0
input logic [7:0] tx_i,
output logic busy_o,
output logic done_o, // one cycle: rx_o holds the byte read
output logic [7:0] rx_o,
output logic cs_no,
output logic sclk_o,
output logic mosi_o,
input logic miso_i
);
typedef enum logic [1:0] {IDLE, SHIFT, HOLD} state_t;
state_t state_q, state_d;
logic [3:0] wait_q, wait_d; // cycles left in this half period
logic [4:0] edges_q, edges_d; // sclk edges so far: 16 in a transfer
logic [7:0] tx_q, tx_d, rx_q, rx_d;
logic cs_q, cs_d, sclk_q, sclk_d, mosi_q, mosi_d, done_d;
logic step; // this cycle makes the next edge of sclk
logic odd; // that edge is the first edge of a bit (edges 1, 3 ... 15)
assign odd = !edges_q[0];
always_comb begin
state_d = state_q;
wait_d = (wait_q != '0) ? wait_q - 4'd1 : wait_q;
edges_d = edges_q;
tx_d = tx_q;
rx_d = rx_q;
cs_d = cs_q;
sclk_d = sclk_q;
mosi_d = mosi_q;
done_d = 1'b0;
step = 1'b0;
unique case (state_q)
IDLE: begin
sclk_d = cpol_i; // the clock rests at its polarity
if (start_i) begin
state_d = SHIFT;
cs_d = 1'b0;
tx_d = tx_i;
edges_d = '0;
wait_d = 4'(Half - 1); // half a period from cs_no falling to edge 1
if (!cpha_i) mosi_d = tx_i[7]; // CPHA 0: the first bit is on the line before edge 1
end
end
SHIFT: if (wait_q == '0) begin
if (edges_q == 5'd16) begin
state_d = HOLD;
wait_d = 4'(Half - 1);
end else begin
step = 1'b1;
end
end
HOLD: if (wait_q == '0) begin
state_d = IDLE;
cs_d = 1'b1;
done_d = 1'b1;
end
default: begin
state_d = IDLE;
cs_d = 1'b1;
end
endcase
if (step) begin
wait_d = 4'(Half - 1);
sclk_d = !sclk_q;
edges_d = edges_q + 5'd1;
if (odd != cpha_i) rx_d = {rx_q[6:0], miso_i}; // sample: odd edges for CPHA 0, even for CPHA 1
if (!cpha_i && !odd) begin // CPHA 0 changes on even edges
mosi_d = tx_q[6];
tx_d = {tx_q[6:0], 1'b0};
end
if (cpha_i && odd) mosi_d = tx_q[7]; // CPHA 1 changes on odd edges
if (cpha_i && !odd) tx_d = {tx_q[6:0], 1'b0}; // and readies the next bit after sampling
end
end
always_ff @(posedge clk_i or negedge rst_ni) begin
if (!rst_ni) begin
state_q <= IDLE;
wait_q <= '0;
edges_q <= '0;
tx_q <= '0;
rx_q <= '0;
cs_q <= 1'b1;
sclk_q <= 1'b0;
mosi_q <= 1'b0;
done_o <= 1'b0;
end else begin
state_q <= state_d;
wait_q <= wait_d;
edges_q <= edges_d;
tx_q <= tx_d;
rx_q <= rx_d;
cs_q <= cs_d;
sclk_q <= sclk_d;
mosi_q <= mosi_d;
done_o <= done_d;
end
end
assign busy_o = (state_q != IDLE);
assign rx_o = rx_q;
assign cs_no = cs_q;
assign sclk_o = sclk_q;
assign mosi_o = mosi_q;
endmodule hdl/tb_spi.sv
// The SPI controller against a model of a peripheral, written from the mode rules alone. The peripheral answers
// transfer n with the byte 3c + n and keeps every byte it receives. Each side's data reaches the other 2 ns after the
// clock edge that changes it, as a real output does. 256 transfers in each of the four modes with the peripheral in the
// same mode, the first transfer of modes 0 and 3 drawn as text (one column per half period of sclk), and then a
// controller in mode 0 against a peripheral in mode 1.
`timescale 1ns / 1ps
module tb_spi;
logic clk = 1'b0, rst_n = 1'b0;
always #10 clk = ~clk; // 50 MHz; sclk is 50 / (2 * 3) = 8.33 MHz
logic cpol, cpha, start = 1'b0, busy, done, cs_n, sclk, mosi_q, mosi, miso = 1'b0;
logic [7:0] tx = '0, rx;
spi_ctrl #(.Half(3)) u_ctrl (.clk_i(clk), .rst_ni(rst_n), .cpol_i(cpol), .cpha_i(cpha), .start_i(start),
.tx_i(tx), .busy_o(busy), .done_o(done), .rx_o(rx), .cs_no(cs_n), .sclk_o(sclk),
.mosi_o(mosi_q), .miso_i(miso));
assign #2 mosi = mosi_q; // the controller's data, 2 ns after its clock edge
// ---- the peripheral model ----
logic p_cpol, p_cpha; // the peripheral's own mode
logic [7:0] p_in, p_out, p_got [256];
int p_n = 0; // transfers so far
always @(negedge cs_n) begin
p_out = 8'h3c + 8'(p_n);
if (!p_cpha) miso <= #2 p_out[7]; // CPHA 0: the first bit as soon as it is selected
end
always @(sclk) if (!cs_n) begin
bit lead;
lead = (sclk != p_cpol); // a leading edge moves sclk away from where it rests
if (lead != p_cpha) begin // its sampling edge: leading for CPHA 0, trailing for CPHA 1
p_in = {p_in[6:0], mosi};
if (p_cpha) p_out = {p_out[6:0], 1'b0}; // CPHA 1: the next bit goes out on the next leading edge
end else if (p_cpha) begin
miso <= #2 p_out[7]; // CPHA 1: change on the leading edge
end else begin
p_out = {p_out[6:0], 1'b0}; // CPHA 0: change on the trailing edge
miso <= #2 p_out[7];
end
end
always @(posedge cs_n) if (rst_n) begin
p_got[p_n % 256] = p_in;
p_n++;
end
// ---- one transfer, and its waveform as text, one column per half period of sclk ----
string w_sclk, w_mosi, w_miso;
bit draw = 1'b0;
always @(posedge clk) if (draw && !cs_n && u_ctrl.wait_q == 4'd1) begin
w_sclk = {w_sclk, sclk ? "1" : "0"};
w_mosi = {w_mosi, mosi ? "1" : "0"};
w_miso = {w_miso, miso ? "1" : "0"};
end
task automatic transfer(input logic [7:0] b, output logic [7:0] got);
@(posedge clk);
while (busy) @(posedge clk);
tx <= b;
start <= 1'b1;
@(posedge clk);
start <= 1'b0;
@(posedge done);
got = rx;
endtask
task automatic run_mode(input int c_mode, input int p_mode, input bit show);
logic [7:0] got, sent [256];
int read_wrong = 0, got_wrong = 0;
{cpol, cpha} = 2'(c_mode);
{p_cpol, p_cpha} = 2'(p_mode);
p_n = 0;
repeat (4) @(posedge clk);
for (int i = 0; i < 256; i++) begin
sent[i] = 8'(i * 37 + 75); // 4b first, then every byte once
draw = show && i == 0;
if (draw) begin
w_sclk = ""; w_mosi = ""; w_miso = "";
end
transfer(sent[i], got);
if (draw) begin
$display("mode %0d: sent %h, read %h", c_mode, sent[i], got);
$display(" sclk %s", w_sclk);
$display(" mosi %s", w_mosi);
$display(" miso %s", w_miso);
end
if (got !== 8'h3c + 8'(i)) begin
if (read_wrong == 0) $display(" first byte read wrong: %h instead of %h", got, 8'h3c + 8'(i));
read_wrong++;
end
end
for (int i = 0; i < 256; i++) if (p_got[i] !== sent[i]) begin
if (got_wrong == 0) $display(" first byte received wrong: %h instead of %h", p_got[i], sent[i]);
got_wrong++;
end
$display("controller mode %0d, peripheral mode %0d: 256 transfers, %0d read wrong, %0d received wrong",
c_mode, p_mode, read_wrong, got_wrong);
endtask
initial begin
{cpol, cpha} = 2'b00;
#100 rst_n = 1'b1;
run_mode(0, 0, 1);
run_mode(1, 1, 0);
run_mode(2, 2, 0);
run_mode(3, 3, 1);
run_mode(0, 1, 0);
$finish;
end
endmodule Commands · hdl/spi_session.py
from session import run
# 256 transfers in each of the four modes against the peripheral model, two of them drawn, then a mode mismatch.
run("""
iverilog -g2012 -Wall -o spi.vvp tb_spi.sv spi_ctrl.sv
vvp -n spi.vvp
""") Output
# Icarus Verilog 13.0 (stable) $ iverilog -g2012 -Wall -o spi.vvp tb_spi.sv spi_ctrl.sv spi_ctrl.sv:55: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'tx_i[7:0]'). spi_ctrl.sv:80: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'rx_q[7:0]'). spi_ctrl.sv:82: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'tx_q[7:0]'). spi_ctrl.sv:83: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'tx_q[7:0]'). spi_ctrl.sv:85: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'tx_q[7:0]'). spi_ctrl.sv:86: sorry: constant selects in always_* processes are not fully supported (the process will be sensitive to all bits in 'tx_q[7:0]'). spi_ctrl.sv:46: vvp.tgt sorry: Case unique/unique0 qualities are ignored. $ vvp -n spi.vvp mode 0: sent 4b, read 3c sclk 010101010101010100 mosi 001100001100111100 miso 000011111111000000 controller mode 0, peripheral mode 0: 256 transfers, 0 read wrong, 0 received wrong controller mode 1, peripheral mode 1: 256 transfers, 0 read wrong, 0 received wrong controller mode 2, peripheral mode 2: 256 transfers, 0 read wrong, 0 received wrong mode 3: sent 4b, read 3c sclk 101010101010101011 mosi 000110000110011111 miso 000001111111100000 controller mode 3, peripheral mode 3: 256 transfers, 0 read wrong, 0 received wrong first byte read wrong: 9e instead of 3c controller mode 0, peripheral mode 1: 256 transfers, 256 read wrong, 0 received wrong tb_spi.sv:105: $finish called at 1434090000 (1ps)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 spi_session.py
In every mode the controller read 3c + n in all 256 transfers, and the peripheral received every byte. The drawings have one column per half period of sclk, from the half period after cs_no falls to the one after the last edge. In mode 0 the clock rests at 0 and MOSI already holds 0, bit 7 of 4b, before the first rising edge; in mode 3 the clock rests at 1 and each bit appears one column later, on a falling edge, the leading one, and is sampled on the next rising edge. With the controller in mode 0 and the peripheral in mode 1, all 256 reads were wrong, the first 9e instead of 3c. The peripheral changed MISO on the rising edges, the very edges at which the controller sampled, so the controller read every bit one edge late, and the line’s stale 1 as bit 7. Step through a transfer in mode 0:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Selected. cs_no falls. In mode 0 the clock rests at 0, and both sides put bit 7 on their data line at once, MOSI 0 for 4b and MISO 0 for 3c.
- Edge 1 rises. The leading edge, a rising one in mode 0, is the sampling edge. The peripheral reads MOSI, 0, and the controller reads MISO, 0.
- Edge 2 falls. The trailing edge changes the data. Both sides put out bit 6, MOSI 1 and MISO 0, and have half a period to settle before the next rising edge.
- Edges 3 to 16. The same pattern seven more times, sampling on every rising edge and changing on every falling one, most significant bit first. Sixteen edges move eight bits each way.
- Deselected. Half a period after edge 16 the controller raises cs_no. It read 3c and the peripheral received 4b; every one of the 256 transfers in each mode came out right.
- A mode 1 peripheral. A peripheral in mode 1 changes MISO on the rising edges, the very edges at which the mode 0 controller samples, and its new bit comes 2 ns after the edge.
- One edge late. So the controller reads each bit one edge late, with the line's stale 1 in front of them. 3c, 00111100, arrives as 9e, 10011110, and all 256 reads were wrong.
A mismatch that works in one direction
The peripheral in mode 1 received every byte correctly, only because the controller’s MOSI changed 2 ns after the edge at which the peripheral sampled it. In silicon that margin is a hold time, and it may be gone on another board. Match the mode, and read the peripheral’s data sheet for it.
Checkpoint
Check: SPI clock polarity and phase
Practise: parity and breaks in the receiver’s model
The exercise models the receiver in Python, line samples in and frames out, and adds two checks the RTL lacks: a parity bit, even or odd, and a line held low for a whole frame, a break (UART): A UART line held at 0 for longer than a whole frame, which a receiver reports as a break rather than as a byte with a framing error., which the TL16C550D reports when the input stays low for longer than a full word. Its sample tests include frames sent 2 % fast and 2 % slow.
Exercise · Medium · Python
The UART receiver as a model, with parity and breaks
Model the receiver of the case study in Python, and add the two checks it lacks: parity and line breaks.
receive(samples, parity) takes samples, the line as the receiver sees it 16 times a bit (a list of 0 and 1, where 1 is the idle level), and parity, one of "none", "even" and "odd". It returns the frames it found, in order, as a list of events:
("byte", value)for a good frame;("parity", value)when the parity bit is wrong: with even parity the data bits and the parity bit hold an even number of 1s, with odd parity an odd number;("frame", value)when the stop bit is 0, a framing error;("break",)when every bit of the frame, data, parity and stop, was 0: the line was held low.
Sample the way the hardware does. A frame starts at the first 0 after a 1, at sample s. Sample s + 8, the middle of the start bit, must still be 0, or the 0 was a glitch and the search goes on from s + 1. Data bit k (least significant first) is sample s + 8 + 16 * (k + 1), then come the parity bit, if any, and the stop bit, 16 samples apart. After a frame, wait for the line to be 1 again before looking for the next start. A frame cut off by the end of the list is not reported.
Starter code · uart_rx.py
def receive(samples, parity="none"):
"""The frames in a list of line samples taken 16 times a bit, as a list of events."""
events = []
return events The sample tests · test_uart_rx.py
from uart_rx import receive
def frame(byte, parity="none", stop=1, per_bit=16.0):
"""Samples of one frame, least significant bit first, with per_bit samples a bit (not always a whole number)."""
bits = [0] + [(byte >> k) & 1 for k in range(8)]
if parity != "none":
ones = sum(bits[1:])
bits.append(ones % 2 if parity == "even" else 1 - ones % 2)
bits.append(stop)
count = round(per_bit * len(bits))
return [bits[min(int(n / per_bit), len(bits) - 1)] for n in range(count)]
IDLE = [1] * 20
def test_one_byte():
"""decodes one 8N1 frame of 0x4B"""
assert receive(IDLE + frame(0x4B) + IDLE) == [("byte", 0x4B)]
def test_back_to_back():
"""decodes frames that follow each other with no idle time between them"""
line = IDLE + frame(0x00) + frame(0xFF) + frame(0x81) + IDLE
assert receive(line) == [("byte", 0x00), ("byte", 0xFF), ("byte", 0x81)]
def test_parity():
"""checks even and odd parity, and reports a byte whose parity bit is wrong"""
assert receive(IDLE + frame(0x4B, "even") + IDLE, "even") == [("byte", 0x4B)]
assert receive(IDLE + frame(0x4B, "odd") + IDLE, "odd") == [("byte", 0x4B)]
assert receive(IDLE + frame(0x4B, "even") + IDLE, "odd") == [("parity", 0x4B)]
assert receive(IDLE + frame(0x07, "odd") + IDLE, "even") == [("parity", 0x07)]
def test_framing_error():
"""reports a stop bit of 0 as a framing error, then finds the next frame"""
line = IDLE + frame(0x55, stop=0) + IDLE + frame(0x2A) + IDLE
assert receive(line) == [("frame", 0x55), ("byte", 0x2A)]
def test_break():
"""reports a line held at 0 for a whole frame as a break, once, and waits for the line to rise"""
line = IDLE + [0] * 400 + IDLE + frame(0x31) + IDLE
assert receive(line) == [("break",), ("byte", 0x31)]
assert receive(IDLE + [0] * 400 + IDLE, "even") == [("break",)]
def test_glitch():
"""ignores a 0 that has gone again by the middle of the start bit"""
line = IDLE + [0, 0, 0] + IDLE + frame(0x5A) + IDLE
assert receive(line) == [("byte", 0x5A)]
def test_start_bit_check():
"""checks the start bit at sample s + 8: a 0 still there starts a frame, a 0 gone by then was a glitch"""
assert receive(IDLE + [0] * 8 + [1] * 200) == []
assert receive(IDLE + [0] * 9 + [1] * 200) == [("byte", 0xFF)]
def test_cut_off():
"""does not report a frame that the end of the samples cuts off, wherever the cut falls"""
assert receive(IDLE + frame(0x4B) + IDLE + [0] * 4) == [("byte", 0x4B)]
assert receive(IDLE + frame(0x4B) + IDLE + frame(0x2A)[:100]) == [("byte", 0x4B)]
def test_rate_off_by_two_percent():
"""still decodes back-to-back frames sent 2 % fast or 2 % slow"""
for per_bit in (15.68, 16.32):
line = IDLE + frame(0xC3, per_bit=per_bit) + frame(0x3C, per_bit=per_bit) + IDLE
assert receive(line) == [("byte", 0xC3), ("byte", 0x3C)]
assert receive(IDLE + frame(0x3C, "even", per_bit=per_bit) + IDLE, "even") == [("byte", 0x3C)] A hint
Find the start, check s + 8, then read every bit of the frame into a list at its sample, s + 8 + 16 * (k + 1) for k = 0, 1, 2 and so on. Decide in this order: all bits 0 is a break, a stop bit of 0 is a framing error, then parity.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The self-check covers frames, tick generators, the receiver, the three checks of a link and SPI modes, starting with the idea you have answered least well:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): why does a UART receiver sample the line 16 times a bit instead of once?
Because the receiver does not know the sender’s clock. All it has is the falling edge of the start bit, so it needs a finer clock to find that edge and then the middle of every bit after it. With 16 ticks a bit, it sees the edge within a sixteenth of a bit, checks the start bit 8 ticks later, in its middle, to reject a short glitch, and samples each later bit 16 ticks after the one before, half a bit from both of its edges. That margin is what lets the two ends run at slightly different rates: about 4.6 % between them for an 8N1 frame. Sampling once a bit would put the sample wherever the two clocks happened to line up, often right at an edge.
The harder questions are about a divider that passes its loopback test, an SPI sensor whose bytes come back shifted, and a fast link between two clocks. Decide your first step, then compare it with the one given for each:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A UART on a 12 MHz FPGA clock must run at 115,200 baud. A colleague divides the clock by 7 for the 16 ticks a bit and shows a loopback test that passed 10,000 bytes. Is the design right? How would you test it, and how would you fix it?
-
Mid to senior
Bytes read back from an SPI sensor look shifted right by one bit, with a stray bit on top, while the sensor accepts the commands written to it. What do you suspect, how do you confirm it, and how do you fix it?
-
Senior
Two boards talk over a UART at 921,600 baud. One makes its 16 ticks a bit from 100 MHz with a whole-number divider; the other runs from a crystal 1 % fast. Will frames get through, with how much margin, and what would you change?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does an 8N1 UART frame contain?
A start bit of 0, eight data bits with the least significant first, no parity and one stop bit of 1: ten bits in all, on a line that idles at 1.
-
How long does one bit last at 115,200 baud?
1 / 115,200 of a second, about 8.68 microseconds, so an 8N1 frame takes about 86.8 microseconds.
-
Why can't a counter on a 12 MHz clock make 16 ticks a bit at 115,200 baud exactly?
The divisor is 12,000,000 / (16 x 115,200), about 6.51, and a counter divides only by a whole number. The nearest, 7, gives 107,143 baud, 6.99 % slow.
-
How does a phase accumulator make ticks at a fractional rate?
It adds a fixed step to a register every cycle and ticks on each carry out. A step of 161,061 in a 20-bit register gives 1,843,197 ticks a second from 12 MHz, within 0.0002 %; ticks come 6 or 7 clock cycles apart, and their average holds.
-
Why does the UART receiver check the start bit 8 ticks after the line falls?
That puts the check in the middle of the start bit, so a pulse shorter than half a bit sends it back to IDLE; sampling every 16 ticks from there lands half a bit from both edges of every later bit.
-
How far apart may the two ends of an 8N1 link run?
About 4.6 % in all. The stop bit's middle lies 9.5 bits past the start edge, and with 16 samples a bit the receiver may find that edge up to 1/16 of a bit late: (0.5 - 1/16) / 9.5.
-
Why did the UART's loopback test pass with a tick generator 7 % slow?
Both ends of the loopback were slow together, so they agreed with each other. Only the model sender and receiver at the exact rate showed hundreds of wrong bytes and framing errors.
-
What do CPOL and CPHA set in SPI?
CPOL is the level at which the clock rests, and CPHA picks the edge that samples: the leading edge for 0, the trailing edge for 1. The mode number has CPOL as its high bit, so mode 3 rests at 1 and samples on rising edges.
Key takeaways
- A UART frame becomes states for each stretch of the line, IDLE, START, DATA and STOP, and counters for the ticks of a bit and the bits of a byte; 0x4B left the transmitter as 1, 1, 0, 1, 0, 0, 1, 0, least significant bit first.
- Sixteen ticks a bit at 115,200 baud from 12 MHz need a divisor of 6.51; the whole number 7 runs 6.99 % slow, while a 20-bit phase accumulator came within 0.0002 %.
- The receiver checks the start bit 8 ticks after the fall and samples every 16 ticks from there, half a bit from the edges, which lets two ends differ by about 4.6 % in all.
- With the divider of 7 the loopback decoded all 1,000 bytes while both paths through a model failed: a loopback cannot see a mistake that both halves share.
- SPI’s CPOL sets the clock’s resting level and CPHA the edge that samples: the controller passed 256 transfers in each of the four modes, and a peripheral in mode 1 made every read of the mode 0 controller wrong, 9e for 3c.
Terms in this lesson
- baud rate
- The number of symbols a line carries per second; on a UART each symbol is one bit, so at 115,200 baud a bit lasts 1/115,200 s, about 8.68 microseconds.
- break (UART)
- A UART line held at 0 for longer than a whole frame, which a receiver reports as a break rather than as a byte with a framing error.
- chip select
- The signal that picks one device on a shared bus, usually active low: an SPI peripheral listens and drives MISO only while its chip select is asserted.
- framing error
- A UART frame whose stop bit was received as 0 instead of 1, usually because the two ends run at rates too far apart or the receiver lost the frame's start.
- loopback test
- A test that connects a design's transmitter to its own receiver. It shows that the two halves agree with each other, but not that either meets the specification, since a mistake both share cancels out.
- oversampling
- Sampling a signal several times per bit, such as 16 times for a UART receiver, so that the receiver can find an edge and then the middle of each bit without the sender's clock.
- phase accumulator
- A register that adds a fixed step every clock cycle and gives a tick on each carry out of its top bit, so ticks come on average at step / 2^n of the clock rate, a fraction no whole-number divider can reach.
- SPI (serial peripheral interface)
- A synchronous serial bus with no formal standard: a controller drives a clock and a chip select, and on every clock period one bit goes each way, on MOSI to the peripheral and on MISO back.
- SPI mode
- One of four timings of an SPI bus, numbered with CPOL as the high bit and CPHA as the low bit: CPOL is the level the clock rests at, CPHA chooses whether data is sampled on the leading or the trailing edge of each bit.
- UART (universal asynchronous receiver-transmitter)
- A serial link with no shared clock: each byte travels as a frame of a start bit of 0, the data bits least significant first, an optional parity bit and a stop bit of 1, on a line that idles at 1.
References
- TL16C550D asynchronous communications element: data sheet (SLLS597E) (Texas Instruments)
- Overview of Linux kernel SPI support: the four SPI clock modes (The Linux Kernel documentation)
- IEEE 1800-2023: IEEE Standard for SystemVerilog, Unified Hardware Design, Specification, and Verification Language (IEEE Standards Association)
- Icarus Verilog documentation: iverilog command line flags (-P for parameters) (Icarus Verilog)
- Verilator documentation: verilator arguments (--binary, --lint-only) (Verilator)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress