USB · Module 28
USB vs UART
UART spends zero wires on synchronisation and pays a tolerance budget that shrinks as the frame grows; USB spends a SYNC field, an encoding rule and a PLL to buy that budget away — measured across 5376 exhaustive points, not quoted.
The first of four comparisons. Each one takes a protocol that is genuinely different from USB, finds the one mechanism the two do not share, and builds that mechanism in hardware so the difference becomes a number instead of an adjective.
1. The Comparison That Is Not A Table
Search for "USB vs UART" and you will get a table: wires, speed, topology, connector, hot-plug. Every row is true. Together they explain nothing, because a table of differences does not tell you which difference caused the others.
That claim is testable, so this chapter tests it. We build the mechanism UART has and USB does not — a receiver that learns where a byte begins from one falling edge and then predicts the next nine sample instants from its own local timer — and we measure what it costs.
The answer is a table of 21 numbers that nobody's datasheet prints, and it is the centre of this chapter.
2. The One Mechanism
A UART receiver is handed a single wire. There is no clock alongside it, no delimiter, no length field and no framing pattern. The only synchronising information it will ever receive for a given byte is the instant the line falls.
From that instant it must place nine more sample points — eight data bits and a stop bit — using nothing but its own oscillator. Each one is an extrapolation, and the error in an extrapolation grows with distance. If the transmitter's bit period differs from the receiver's by Δ, the error at data bit k is roughly k·Δ. The last data bit is always the first to be sampled in the wrong cell.
USB never does this. A USB packet opens with a SYNC field the receiver locks a recovered clock onto; NRZI plus bit stuffing guarantee a transition at least every seven bit times so the lock is continuously refreshed; and the packet ends at an explicit EOP rather than at a length the receiver had to predict. A USB receiver is never extrapolating from a single edge.
Two ways to answer 'where is the next bit?'
3. The Hardware Contract
One module, three languages, one contract. The receiver takes a synchronised serial line and a sample offset — where inside the bit cell to sample — and produces a byte, a framing flag, and four counters that make its internal decisions visible to a testbench.
sample_offset is a port rather than a constant for one reason: the
tolerance budget turns out to be a property of the sample point as much as
of the clock error, and making it an input lets the testbench sweep it
directly instead of re-elaborating the design.
4. The Design (Verilog-2005)
// =====================================================================
// async_rx_framer -- the one mechanism UART has that USB does not.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a complete UART receiver. It has no oversampling
// majority vote, no parity, no break detection, and no framing
// recovery beyond the stop bit.
//
// WHY THIS MECHANISM IS WORTH RTL
// -------------------------------
// A UART receiver has no shared clock and no delimiter. It learns
// where a byte begins from ONE falling edge -- the start bit -- and
// then predicts the centre of all nine following bit cells from its
// OWN local timer. Every subsequent sample instant is an
// extrapolation, so any difference between the transmitter's bit
// period and the receiver's accumulates across the byte.
//
// That accumulation is a hardware budget with a number attached, and
// the number is what this module measures.
//
// USB has no equivalent mechanism and therefore no equivalent budget.
// A USB packet opens with a SYNC pattern the receiver locks its
// recovered clock to, NRZI plus bit stuffing guarantee a transition
// at least every seven bits so the lock is continuously refreshed,
// and the packet ends at an explicit EOP. A USB receiver never has to
// guess where a byte starts from a local timer alone -- so the
// failure mode modelled here does not exist there.
//
// The comparison is therefore not "USB is better timed". It is:
// UART spends ZERO wires and ZERO protocol on synchronisation and
// pays for it with a tolerance budget; USB spends a SYNC field, an
// encoding rule and a PLL and buys freedom from that budget.
// =====================================================================
module async_rx_framer #(
// Nominal receiver clock cycles per bit cell. A real design derives
// this from a baud-rate divisor; here it is a parameter so the
// testbench can sweep the tolerance directly.
parameter integer DIVISOR = 16
) (
input wire clk,
input wire rst_n,
// ---- the serial line, already synchronised to clk ----
//
// RTL simulation cannot model metastability, so this model assumes
// the line has been through a synchroniser. A real receiver needs
// one, and its added latency shifts every sample instant by a fixed
// amount -- which is a constant error, not an accumulating one, and
// therefore not the effect this module is about.
input wire rx,
// Where inside the bit cell to sample. DIVISOR/2 is the centre and
// is what a real design uses; the testbench sweeps it to show that
// the tolerance budget is a property of the SAMPLE POINT and not
// only of the clock error.
input wire [7:0] sample_offset,
// ---- the captured byte ----
output wire byte_valid,
output wire [7:0] byte_data,
output wire framing_error, // the stop bit was not high
// ---- observability ----
output wire [2:0] state,
output wire [31:0] n_bytes,
output wire [31:0] n_framing_err,
output wire [31:0] n_starts,
output wire [31:0] n_false_start // a start edge that did not survive
);
localparam [2:0] S_IDLE = 3'd0,
S_START = 3'd1,
S_DATA = 3'd2,
S_STOP = 3'd3;
reg [2:0] st;
reg [7:0] phase; // cycles into the current bit cell
reg [3:0] bitidx; // which data bit is being received
reg [7:0] shifter;
reg bv_r;
reg [7:0] bd_r;
reg fe_r;
reg [31:0] bytes_c, ferr_c, start_c, false_c;
assign byte_valid = bv_r;
assign byte_data = bd_r;
assign framing_error = fe_r;
assign state = st;
assign n_bytes = bytes_c;
assign n_framing_err = ferr_c;
assign n_starts = start_c;
assign n_false_start = false_c;
// The sample instant for the current bit cell. Note that this is
// computed from `phase`, a LOCAL counter -- there is nothing in this
// expression that refers to the transmitter at all. That is the
// entire point: the receiver is predicting, not observing.
wire at_sample = (phase == sample_offset);
wire at_end = (phase == DIVISOR - 1);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st <= S_IDLE;
phase <= 8'd0;
bitidx <= 4'd0;
shifter <= 8'd0;
bv_r <= 1'b0;
bd_r <= 8'd0;
fe_r <= 1'b0;
bytes_c <= 32'd0;
ferr_c <= 32'd0;
start_c <= 32'd0;
false_c <= 32'd0;
end else begin
bv_r <= 1'b0;
fe_r <= 1'b0;
case (st)
// =============================================================
// IDLE -- wait for the line to fall. That falling edge is the
// ONLY synchronising information the receiver will ever get
// for this byte.
// =============================================================
S_IDLE: begin
if (!rx) begin
st <= S_START;
phase <= 8'd0;
start_c <= start_c + 32'd1;
end
end
// =============================================================
// START -- confirm the line is still low at the sample point.
//
// A glitch that pulls the line low for less than half a bit
// cell is rejected here. This is the cheapest possible noise
// defence and it is all an unoversampled receiver has.
// =============================================================
S_START: begin
if (at_sample) begin
if (rx) begin
// The line came back high: that was not a start bit.
st <= S_IDLE;
phase <= 8'd0;
false_c <= false_c + 32'd1;
end else begin
phase <= phase + 8'd1;
end
end else if (at_end) begin
st <= S_DATA;
phase <= 8'd0;
bitidx <= 4'd0;
end else begin
phase <= phase + 8'd1;
end
end
// =============================================================
// DATA -- eight predicted sample instants.
//
// Each one is DIVISOR cycles after the last, counted on the
// receiver's clock. If the transmitter's bit period differs,
// the error between predicted and actual bit centre grows by
// that difference on EVERY bit -- so the last data bit is
// always the first to be sampled in the wrong cell.
// =============================================================
S_DATA: begin
if (at_sample) begin
// LSB first, which is what every UART does.
shifter <= {rx, shifter[7:1]};
phase <= phase + 8'd1;
end else if (at_end) begin
phase <= 8'd0;
if (bitidx == 4'd7) st <= S_STOP;
else bitidx <= bitidx + 4'd1;
end else begin
phase <= phase + 8'd1;
end
end
// =============================================================
// STOP -- the line must be high.
//
// This is the receiver's only integrity check, and it is a
// weak one: it catches a byte whose framing slipped far
// enough to drag a zero into the stop cell, and it misses
// every slip that happens to land on a one.
// =============================================================
S_STOP: begin
if (at_sample) begin
bv_r <= 1'b1;
bd_r <= shifter;
if (!rx) begin
fe_r <= 1'b1;
ferr_c <= ferr_c + 32'd1;
end
bytes_c <= bytes_c + 32'd1;
phase <= phase + 8'd1;
end else if (at_end) begin
// Return to IDLE only at the END of the stop cell. A start
// edge arriving before that is NOT accepted -- the
// receiver needs the full stop bit as a guard. This is a
// real design decision and it costs one bit time of
// maximum throughput.
st <= S_IDLE;
phase <= 8'd0;
end else begin
phase <= phase + 8'd1;
end
end
default: st <= S_IDLE;
endcase
end
end
endmoduleThree details carry the chapter.
at_sample and at_end are computed from phase alone. Nothing in
either expression refers to the transmitter. That is not an accident of
coding style — it is the mechanism. The receiver is predicting, not
observing, and everything that follows is a consequence.
The start bit is re-checked at the sample point. If the line has gone high again, the edge was noise and the frame is abandoned. This is the cheapest possible noise defence and, without oversampling, it is the only one available.
The receiver returns to IDLE at the end of the stop cell, not at the stop sample. The full stop bit is an inter-frame guard. That decision costs one bit time of peak throughput, and section 11 shows it is invisible to any test that leaves the line idle between frames.
5. The Model Is Geometry, Not A Second State Machine
The obvious way to check this design is to write a more careful version of the same state machine. That is also the way to reproduce its mistakes, because a second FSM written by the same person on the same afternoon tends to misunderstand the same things.
So the model is arithmetic on two periods and mentions no state at all. For the receiver's sample instant of cell k:
sample instant, receiver cell k
S_k = E + k*DIVISOR + sample_offset
transmitter cell occupying that instant
c = floor( (S_k - org) / tx_period )
cell 0 = start bit (0)
cells 1..8 = data bits 0..7 (LSB first)
cell 9 = stop bit (1)
cells >= 10 = idle (1)E is the edge at which the receiver detected the start bit and org is
the tick at which the transmitter's own cell 0 began. For an aligned frame
those are 1 and 0.
The 1 is not a fudge factor. The receiver sees the falling edge on one
clock and starts counting on the next, so its idea of the start cell is one
receiver cycle later than the transmitter's. That offset is constant,
unlike the per-bit error — and section 8 shows it is the reason the
tolerance is asymmetric.
6. The Measurement
The directed phase is exhaustive over three independent dimensions:
- every byte value, 0 through 255;
- seven transmitter bit periods in receiver cycles: 13, 14, 15, 16, 17, 18, 19. 16 is a perfectly matched transmitter; the outer values are ±18.75%, far beyond anything a real UART survives, and they are included so the sweep spans the whole transition rather than stopping at the first failure;
- three sample points: 4, 8 and 12 — quarter, centre and three-quarter of the bit cell.
256 × 7 × 3 = 5376 points, all of them reachable, because the three dimensions are independent inputs with no forbidden combinations. The denominator is honest: there is no state in this design that makes some combination unreachable, so a sweep that reports anything less than 5376 is broken rather than constrained.
Whole-cycle periods are deliberate. A fractional bit period would put the
model's floor() on a boundary where the answer is arguable, and an
arguable measurement is worse than a coarse one.
The tolerance table
Bytes surviving intact, out of 256, per (transmitter period × sample point). This is the chapter's central result and it is measured from the model alone, so it does not depend on the design being correct:
| sample point | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
|---|---|---|---|---|---|---|---|
| 4 (early) | 4 | 16 | 256 | 256 | 32 | 4 | 2 |
| 8 (centre) | 2 | 4 | 32 | 256 | 256 | 16 | 8 |
| 12 (late) | 0 | 1 | 2 | 256 | 256 | 64 | 16 |
Read the centre row. A transmitter 6.25% slow (period 17) is tolerated completely — all 256 byte values survive. A transmitter 6.25% fast (period 15) loses 224 of 256. Same magnitude of error, wildly different outcome.
That asymmetry is not noise and it is not a bug. Section 8 derives it in closed form and the derivation predicts every bold cell in that table.
Where the samples actually land
The table says how many bytes survive. This says why. For each of the receiver's ten sample instants, which transmitter cell was the line actually in at that moment?
A matched transmitter: every sample lands in its own cell
Now the same receiver against a transmitter only 6.25% fast:
A 6.25% fast transmitter: the receiver skips a cell and never notices
7. Three Things The Sweep Found That No Datasheet Prints
The byte count goes wrong, not just the byte
When the transmitter is slower than the receiver expects, the receiver finishes its ten predicted cells before the transmitter has finished sending them. It returns to IDLE in the middle of a frame, sees the remaining data bits as a fresh falling edge, and manufactures a byte that was never sent.
Across the full run: 275 bytes the transmitter never sent.
That is worse than corruption. A corrupted byte is a wrong value in the right slot, and a checksum finds it. A spurious byte shifts every subsequent byte by one position, and every downstream parser that assumed a fixed frame length is now permanently out of step with no mechanism to recover.
This is the failure mode behind "the link works until it doesn't, and then nothing helps until you power-cycle it".
Legitimate start bits get thrown away
The receiver re-checks the line at its start-bit sample point. With a late sample point and a fast transmitter, that instant can land inside data bit 0 — and if that bit happens to be high, a perfectly valid start bit is rejected.
Across the full run: 132 frames rejected at the re-check.
That is not a defect in this design and the testbench does not treat it as one. It is the honest limit of what a single re-sample can tell you, and it is precisely why real receivers sample the centre of the start cell and oversample around it. The model had to be taught to predict rejection: an earlier version assumed every frame was accepted and produced 97 phantom disagreements against a receiver that was entirely correct.
A glitch and a short start bit are the same signal
Phase 2 drives a low pulse of every width from 1 to 16 cycles. Pulses of 9 cycles or fewer are rejected. Pulses of 10 or more are accepted as start bits — and the testbench asserts that they are.
That is deliberate. A pulse longer than the sample point is indistinguishable from a real start bit by any measurement this receiver can make. Asserting that it gets rejected would be asserting something false, and the honest property is the one that says where the line between noise and signal actually falls.
8. The Budget In Closed Form
The measurement stands on its own, but a number you can only measure is a number you cannot design with. So here is the same result derived.
The receiver samples data bit k (k = 0..7) at its own cell k+1:
S = 1 + (k+1)*D + soff D = receiver cycles per cell
soff = sample offsetFor that sample to land in the transmitter's cell k+1, with transmitter period P:
(k+1)*P <= 1 + (k+1)*D + soff < (k+2)*PThe binding case is the last data bit, k = 7, because the error is largest there. Substituting and solving for P:
P <= ( 1 + 8*D + soff ) / 8 slow-transmitter limit
P > ( 1 + 8*D + soff ) / 9 fast-transmitter limitWith D = 16, that gives the whole-cycle periods for which all 256 byte values must survive:
| sample point | fast limit | slow limit | integer periods predicted | measured 256-of-256 at |
|---|---|---|---|---|
| 4 | P > 14.78 | P ≤ 16.63 | 15, 16 | 15, 16 |
| 8 | P > 15.22 | P ≤ 17.13 | 16, 17 | 16, 17 |
| 12 | P > 15.67 | P ≤ 17.63 | 16, 17 | 16, 17 |
The derivation predicts the measurement exactly, in all three rows, with no fitting. That agreement is the point: the table is not an empirical curiosity, it is a budget with an equation behind it.
What this means for frame length
The budget is dominated by the k = 7 term — the last bit of the frame. Redo the derivation for an N-bit frame and the limits converge on D as N grows, roughly as 1/N:
frame length approximate total error budget
------------ ------------------------------
8 data bits +6.9% / -4.9% (measured above)
16 data bits +3.5% / -2.9%
64 data bits +0.9% / -0.8%
1024 data bits +0.05% / -0.05%9. What USB Does Instead
Four mechanisms, none of which UART has, and each one removes a specific term from the budget above.
SYNC. Every packet opens with a known pattern — eight bits at full and
low speed, 32 at high speed. The receiver does not have to guess where the
packet starts from a single edge; it is handed a training sequence and locks
a recovered clock to it. The E term from section 5 is measured rather than
assumed.
NRZI. A 0 is encoded as a transition and a 1 as no transition. Data now carries timing information directly, and the receiver's clock recovery has edges to work with instead of levels to predict.
Bit stuffing. NRZI alone fails on long runs of 1s — no transitions, no timing. So a 0 is inserted after every six consecutive 1s, guaranteeing a transition at least every seven bit times. This is the mechanism that kills the accumulating term: the receiver's error can never grow past seven bits' worth before being corrected.
EOP. The packet ends with an explicit end-of-packet signal, so the receiver never has to predict a length. The stop-bit guess, and the whole class of failures where the receiver desynchronises and manufactures phantom data, does not exist.
The inversion worth knowing
Here is the part that catches people out in interviews. USB's clock accuracy requirements are much tighter than a UART's:
| required accuracy | |
|---|---|
| UART, 8-bit frame, 16× oversampled | ±2–3% typical |
| USB low speed | ±1.5% |
| USB full speed | ±0.25% |
| USB high speed | ±0.05% |
So USB demands a 40× more accurate clock at high speed and is nonetheless the far more robust link. That looks like a contradiction and is not, because the two numbers are budgets for different things:
So when is UART the right answer?
Often, and this chapter would be dishonest if it did not say so. There is no universal winner here.
UART wins whenever the cost of the interface matters more than its capability: two wires, no crystal required at low rates, no enumeration, no driver stack, no host, no protocol engine, and a receiver small enough to fit in a corner of any FPGA. A debug console, a GPS module, a sensor reporting once a second, a bootloader that must work before anything else does — for all of these, USB's mechanisms are pure overhead that buys nothing the application needs.
The trade in one line: UART is the right answer when you can afford a 1/N budget. Short frames, modest rates, and a link where a corrupted byte is recoverable at the application layer. When you cannot afford it — long packets, high rates, or a stream where losing byte alignment is unrecoverable — you need something that pays for synchronisation, and USB is one of the ways to pay.
10. The Testbench (Verilog)
// =====================================================================
// Testbench for async_rx_framer.
//
// THE SHADOW MODEL IS GEOMETRY, NOT A STATE MACHINE.
//
// The design predicts bit centres with an FSM and a phase counter.
// The model computes, for each of the receiver's nine sample instants,
// WHICH TRANSMITTER CELL the line was actually in at that moment:
//
// sample instant of receiver cell k
// S_k = 1 + k*DIVISOR + sample_offset (cycles after the edge)
//
// transmitter cell occupying that instant
// c = floor(S_k / tx_period)
//
// cell 0 = start bit (0)
// cells 1..8 = data bits 0..7
// cell 9 = stop bit (1)
// cells >= 10 = idle (1)
//
// Nothing in that derivation mentions a state, a phase counter or a
// transition. It is arithmetic on two periods, so it cannot repeat a
// mistake the FSM makes -- which is the whole reason for writing it
// this way rather than more carefully.
//
// The `1 +` is not a fudge. The receiver detects the falling edge on
// one clock and begins counting on the next, so its idea of the start
// cell is one receiver cycle later than the transmitter's. That offset
// is CONSTANT, unlike the per-bit error, and it makes the tolerance
// asymmetric -- a transmitter that is slightly slow is tolerated
// further than one that is equally fast.
// =====================================================================
`timescale 1ns/1ps
module tb_ua_v;
localparam integer DIVISOR = 16;
reg clk = 1'b0, rst_n = 1'b0;
reg rx = 1'b1;
reg [7:0] sample_offset = 8'd8;
wire byte_valid, framing_error;
wire [7:0] byte_data;
wire [2:0] state;
wire [31:0] n_bytes, n_framing_err, n_starts, n_false_start;
async_rx_framer #(.DIVISOR(DIVISOR)) dut (
.clk(clk), .rst_n(rst_n), .rx(rx),
.sample_offset(sample_offset),
.byte_valid(byte_valid), .byte_data(byte_data),
.framing_error(framing_error), .state(state),
.n_bytes(n_bytes), .n_framing_err(n_framing_err),
.n_starts(n_starts), .n_false_start(n_false_start)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, bytes_sent = 0;
integer seed;
// $random is SIGNED: mask the sign bit before any modulo.
function [31:0] urand;
input dummy;
begin urand = $random(seed) & 32'h3FFF_FFFF; end
endfunction
// ---- the headline measurement ----
//
// Per (tx_period, sample_offset) cell of the sweep, how many of the
// 256 byte values survived. That table IS the tolerance budget, and
// it is measured rather than quoted.
integer surv [0:6][0:2];
integer tried [0:6][0:2];
integer n_mispredict = 0; // model and DUT disagreed
integer g_correct = 0, g_corrupt = 0, g_ferr = 0;
integer g_bytes = 0;
// ---- a measured consequence, not a testbench inconvenience ----
//
// When the transmitter is SLOWER than the receiver expects, the
// receiver finishes its ten predicted cells before the transmitter
// has finished sending them -- returns to IDLE mid-frame, sees the
// remaining data bits as a fresh falling edge, and manufactures a
// byte that was never sent.
//
// That is not a modelling artefact. It is what a real UART does at
// the edge of its tolerance, and it is worse than a corrupted byte
// because the byte COUNT is now wrong too: every downstream parser
// that assumed a fixed frame length is permanently out of step.
integer g_extra = 0;
integer g_rejected = 0;
integer g_pairs = 0;
// ---- exhaustive reach over (byte, tx_period, sample_offset) ----
reg reach [0:5375];
integer ri, n_reach;
task ck(input cond, input [255:0] what);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 20)
$display(" ERROR @%0t byte#%0d: %0s", $time, bytes_sent, what);
end
end
endtask
// ---------------------------------------------------------------
// The model: what does the line carry in transmitter cell c?
// ---------------------------------------------------------------
function cell_value;
input integer c;
input [7:0] data;
begin
if (c <= 0) cell_value = 1'b0; // start bit
else if (c <= 8) cell_value = data[c-1]; // data bit c-1, LSB first
else cell_value = 1'b1; // stop, then idle
end
endfunction
// ---------------------------------------------------------------
// What byte will the receiver capture, and will it flag framing?
// Pure arithmetic on the two periods.
//
// This also predicts REJECTION. The receiver re-checks the line at
// its start-bit sample point, and with a late sample point and a
// fast transmitter that instant can fall inside DATA BIT 0 -- so a
// perfectly legitimate start bit is thrown away whenever that bit
// happens to be high.
//
// That is not a defect. It is the reason real receivers sample the
// centre of the start cell and oversample around it, and it is why
// this model has to predict rejection rather than assume every frame
// is accepted. Assuming acceptance produced 97 phantom
// "disagreements" against a receiver that was entirely correct.
//
// After a rejected start the receiver resynchronises on whichever
// later bit happens to fall low, and what it captures then is a
// cascade rather than a prediction -- so the byte is deliberately
// NOT predicted in that case.
//
// The geometry is parametrised by the DETECTION EDGE (E) and the
// tick at which the transmitter's cell 0 began (org), because a
// frame the receiver picks up LATE is the same arithmetic with a
// larger E. Hard-coding the aligned case would have made the
// stop-bit-guard test below impossible to write without
// re-implementing the receiver.
// ---------------------------------------------------------------
task predict_at(input [7:0] data, input integer txp, input integer soff,
input integer E, input integer org,
output [7:0] exp_byte, output exp_ferr, output exp_reject);
integer k, s, c;
reg [7:0] b;
begin
// the start-bit re-check instant, receiver cell 0
s = E + soff;
c = (s - org) / txp;
exp_reject = (cell_value(c, data) == 1'b1);
b = 8'd0;
for (k = 1; k <= 8; k = k + 1) begin
s = E + k * DIVISOR + soff;
c = (s - org) / txp;
b[k-1] = cell_value(c, data);
end
exp_byte = b;
// the stop cell, receiver cell 9
s = E + 9 * DIVISOR + soff;
c = (s - org) / txp;
exp_ferr = (cell_value(c, data) == 1'b0);
end
endtask
task predict(input [7:0] data, input integer txp, input integer soff,
output [7:0] exp_byte, output exp_ferr, output exp_reject);
begin
// The aligned case: the receiver sees cell 0 low at edge 1, and the
// transmitter's cell 0 began at tick 0.
predict_at(data, txp, soff, 1, 0, exp_byte, exp_ferr, exp_reject);
end
endtask
// ---------------------------------------------------------------
// Drive one frame and check what came back -- ALL IN ONE CLOCKED
// LOOP, with no fork.
//
// An earlier version drove the line from one process and sampled
// byte_valid from another, joined by fork/join. Both processes woke
// on the same edge, so which of them acted first was undefined --
// and the cell boundaries the checker assumed were not the ones the
// receiver saw. It produced 1772 "disagreements" against a correct
// receiver and a correct model.
//
// Here `rx` is assigned non-blockingly, so it settles before the
// next rising edge and the receiver observes transmitter cell
// floor((M-1)/txp) at edge M. That is exactly the relation the
// model in predict() assumes, and now it is true by construction
// rather than by hope.
//
// txp == DIVISOR is a perfectly matched transmitter. Anything else
// is a clock error, expressed in whole receiver cycles so the sweep
// is exact -- a fractional error would make the model's floor()
// ambiguous at the boundary and the measurement arguable.
// ---------------------------------------------------------------
task send_and_check(input [7:0] data, input integer txp, input integer soff);
reg [7:0] e_byte;
reg e_ferr, e_reject;
reg [7:0] got;
reg got_ferr, got_any;
reg [31:0] fs_before;
integer m, nframe, tick;
begin
sample_offset = soff[7:0];
predict(data, txp, soff, e_byte, e_ferr, e_reject);
fs_before = n_false_start;
got = 8'hXX;
got_ferr = 1'b0;
got_any = 1'b0;
// Ten transmitter cells, then idle long enough for the receiver
// to finish its ten predicted cells AND any extra frame it
// started from the transmitter's trailing bits.
nframe = 10 * txp + 14 * DIVISOR;
tick = 0;
for (m = 0; m < nframe; m = m + 1) begin
rx <= cell_value(tick / txp, data);
@(posedge clk);
#1;
if (byte_valid) begin
if (!got_any) begin
got = byte_data;
got_ferr = framing_error;
got_any = 1'b1;
end else begin
// a byte the transmitter never sent
g_extra = g_extra + 1;
end
g_bytes = g_bytes + 1;
end
tick = tick + 1;
end
rx <= 1'b1;
bytes_sent = bytes_sent + 1;
if (e_reject) begin
// ---- PROPERTY 1a: a predicted rejection IS a rejection ----
//
// The receiver must have thrown the start bit away, and it must
// say so on its false-start counter. What it captures
// afterwards is a resynchronisation cascade and is deliberately
// not predicted.
g_rejected = g_rejected + 1;
ck(n_false_start > fs_before,
"a start bit that lands high at the sample point was not rejected");
end else begin
// ---- PROPERTY 1b: a byte always comes back ----
//
// The receiver must produce exactly one byte per accepted start
// bit, even when the framing has slipped. Silence would be a
// worse failure than a corrupted byte, because the sender
// cannot know.
ck(got_any, "no byte was captured for an accepted frame");
if (got_any) begin
// ---- PROPERTY 2: the byte is what the GEOMETRY says ----
if (got !== e_byte) n_mispredict = n_mispredict + 1;
ck(got === e_byte, "captured byte disagrees with the sample-instant model");
// ---- PROPERTY 3: the framing flag matches the stop cell ----
ck(got_ferr === e_ferr, "framing_error disagrees with the stop-cell model");
if (got === data) g_correct = g_correct + 1;
else g_corrupt = g_corrupt + 1;
if (got_ferr) g_ferr = g_ferr + 1;
end
end
ck(n_mispredict == 0, "the model and the receiver disagree");
end
endtask
// The level actually on the line at tick t, for a stimulus consisting
// of frame A (cells from tick 0) followed by frame B (cells from
// orgB). Frame A's stop cell can therefore be cut short by B, which
// is the whole point of the test below -- and the reason A cannot be
// predicted from dA alone.
function [0:0] line_at(input integer t, input [7:0] dA, input [7:0] dB,
input integer orgB, input integer txp);
begin
if (t < orgB) line_at = cell_value(t / txp, dA);
else if (t < orgB + 10 * txp) line_at = cell_value((t - orgB) / txp, dB);
else line_at = 1'b1;
end
endfunction
// ---------------------------------------------------------------
// A transmitter that CUTS ITS STOP BIT SHORT and starts the next
// frame immediately.
//
// The first version of this test swept an idle GAP between two
// frames and predicted small gaps would be refused. All six of its
// predictions were wrong, and the reason is worth more than the test
// was: at a matched bit rate the transmitter's own stop cell is
// exactly as long as the receiver's, so it pays for the entire guard
// by itself. Back-to-back frames at the nominal rate never touch the
// guard, and no gap sweep can reach it.
//
// The guard is only observable when the transmitter is SHORT of a
// full stop bit. And the cleanest way to observe it is not to
// predict a byte but to time the next START DETECTION, because that
// instant IS the guard:
//
// the receiver may not look for another start bit until its own
// stop cell has finished, at edge 1 + 10*DIVISOR.
//
// That is measured from n_starts and an edge counter -- no model of
// the receiver required, and nothing for a mutation to move.
// ---------------------------------------------------------------
task send_trunc(input [7:0] dA, input [7:0] dB, input integer lead,
input integer soff);
integer m, tick, txp, orgB, eB, ngot, e_restart, edgen, k;
reg [7:0] eByteA, eByteB, gotA, gotB, bb;
reg eFerrA, eFerrB, eRejA, gFerrA, gFerrB;
reg [31:0] st0;
begin
txp = DIVISOR;
sample_offset = soff[7:0];
ngot = 0; gotA = 8'h00; gotB = 8'h00; gFerrA = 1'b0; gFerrB = 1'b0;
orgB = 10 * txp - lead;
e_restart = -1;
st0 = n_starts;
// A with its stop cell cut short by `lead`, then B immediately --
// no idle between them at all -- then a long drain.
tick = 0; edgen = 0;
for (m = 0; m < orgB + 10 * txp + 14 * DIVISOR; m = m + 1) begin
rx <= line_at(tick, dA, dB, orgB, txp);
@(posedge clk); #1;
edgen = edgen + 1;
// the edge at which the receiver went hunting for B's start bit
if (e_restart < 0 && n_starts >= st0 + 32'd2) e_restart = edgen;
if (byte_valid) begin
if (ngot == 0) begin gotA = byte_data; gFerrA = framing_error; end
else if (ngot == 1) begin gotB = byte_data; gFerrB = framing_error; end
ngot = ngot + 1;
end
tick = tick + 1;
end
// ---- PROPERTY 4: the stop-bit guard ----
//
// THE check. The receiver detects A's start at edge 1 and must
// hold S_STOP for the full cell, so the earliest edge at which it
// can detect another start is 2 + 10*DIVISOR. Returning to IDLE at
// the stop SAMPLE instead would make this fire for every lead >= 1.
ck(e_restart >= 2 + 10 * DIVISOR,
"the receiver hunted for a new start bit before its stop cell had finished");
// and it must not give up on B altogether
ck(e_restart > 0, "the receiver never looked for the following frame");
// ---- frame A, from the composed line, not from dA ----
// A's stop sample can land on B's start bit, so a truncation of
// more than (DIVISOR-1 - soff) cycles MUST raise framing_error.
// Predicting this from dA alone would have said "no error".
eRejA = (line_at(1 + soff, dA, dB, orgB, txp) == 1'b1);
bb = 8'd0;
for (k = 1; k <= 8; k = k + 1)
bb[k-1] = line_at(1 + k * DIVISOR + soff, dA, dB, orgB, txp);
eByteA = bb;
eFerrA = (line_at(1 + 9 * DIVISOR + soff, dA, dB, orgB, txp) == 1'b0);
ck(eRejA === 1'b0, "TEST BUG: frame A was rejected at its own re-check");
// ---- frame B, detected late at edge 2 + 10*DIVISOR ----
// dB's bit 0 is required to be 0 by the caller. That keeps B's
// re-check on a LOW cell, so B cannot be rejected and re-detected
// partway through itself. Without that constraint the receiver can
// reject and retry several times inside one frame, and predicting
// the outcome would mean re-implementing the receiver in the
// testbench -- which would check nothing at all.
eB = 2 + 10 * DIVISOR;
ck(dB[0] === 1'b0, "TEST BUG: send_trunc needs dB with bit 0 clear");
bb = 8'd0;
for (k = 1; k <= 8; k = k + 1)
bb[k-1] = line_at(eB + k * DIVISOR + soff, dA, dB, orgB, txp);
eByteB = bb;
eFerrB = (line_at(eB + 9 * DIVISOR + soff, dA, dB, orgB, txp) == 1'b0);
ck(ngot == 2, "a truncated stop bit cost the receiver a whole frame");
if (ngot >= 1) begin
ck(gotA === eByteA, "the frame before the truncation was altered");
ck(gFerrA === eFerrA, "frame A's framing flag disagrees with the line");
end
if (ngot >= 2) begin
ck(gotB === eByteB, "the resynchronised frame does not match the late-detection model");
ck(gFerrB === eFerrB, "frame B's framing flag disagrees with the line");
end
g_pairs = g_pairs + 1;
bytes_sent = bytes_sent + 2;
end
endtask
task reset_dut;
begin
rst_n = 1'b0;
rx <= 1'b1;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
end
endtask
integer bi, pi, si, k, pv, sv;
integer periods [0:6];
integer offsets [0:2];
initial begin
for (ri = 0; ri < 5376; ri = ri + 1) reach[ri] = 1'b0;
for (pi = 0; pi < 7; pi = pi + 1)
for (si = 0; si < 3; si = si + 1) begin
surv[pi][si] = 0;
tried[pi][si] = 0;
end
// Transmitter bit periods in receiver cycles. 16 is matched; the
// outer values are +/- 18.75%, far beyond anything a real UART
// survives, and they are included so the sweep spans the whole
// transition rather than stopping at the first failure.
periods[0] = 13; periods[1] = 14; periods[2] = 15; periods[3] = 16;
periods[4] = 17; periods[5] = 18; periods[6] = 19;
// Quarter, centre and three-quarter of the bit cell.
offsets[0] = 4; offsets[1] = 8; offsets[2] = 12;
seed = 32'd28001;
reset_dut;
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every byte value, at every
// transmitter period, at every sample point.
//
// 256 x 7 x 3 = 5376, and every point is reachable: the three
// dimensions are independent inputs with no forbidden
// combinations.
// =============================================================
for (pi = 0; pi < 7; pi = pi + 1)
for (si = 0; si < 3; si = si + 1)
for (bi = 0; bi < 256; bi = bi + 1) begin
pv = periods[pi];
sv = offsets[si];
send_and_check(bi[7:0], pv, sv);
tried[pi][si] = tried[pi][si] + 1;
ri = (pi * 3 + si) * 256 + bi;
reach[ri] = 1'b1;
end
// recount survivors per cell, from the model alone, so the
// published tolerance table does not depend on the DUT at all
for (pi = 0; pi < 7; pi = pi + 1)
for (si = 0; si < 3; si = si + 1) begin
surv[pi][si] = 0;
for (bi = 0; bi < 256; bi = bi + 1) begin : recount
reg [7:0] eb; reg ef; reg er;
predict(bi[7:0], periods[pi], offsets[si], eb, ef, er);
// A rejected frame is not a surviving byte: the receiver threw
// it away. Counting it as a survivor would overstate the
// tolerance.
if (!er && eb === bi[7:0]) surv[pi][si] = surv[pi][si] + 1;
end
end
// =============================================================
// PHASE 2 (DIRECTED) -- a glitch is not a start bit.
//
// The only noise defence an unoversampled receiver has is
// re-checking the line at the start-bit sample point. A pulse
// shorter than that must be rejected, and one longer must not be.
// =============================================================
reset_dut;
for (k = 1; k <= DIVISOR; k = k + 1) begin : glitch
reg [31:0] fs0, by0;
sample_offset = 8'd8;
fs0 = n_false_start;
by0 = n_bytes;
for (pi = 0; pi < k; pi = pi + 1) begin rx <= 1'b0; @(posedge clk); end
// Idle for a FULL frame time, not a fraction of one. A pulse
// longer than the sample point is accepted as a real start bit,
// and the receiver then needs 1 + 10*DIVISOR cycles to finish --
// waiting only 4 bit cells and then asserting IDLE produced 5
// failures against a receiver that was behaving correctly.
for (pi = 0; pi < 14 * DIVISOR; pi = pi + 1) begin rx <= 1'b1; @(posedge clk); end
#1;
// The start re-check lands at absolute cycle 1 + sample_offset =
// 9, so a pulse of 9 cycles or fewer is still low nowhere near
// that instant and is thrown away.
if (k <= 9) begin
ck(n_false_start > fs0,
"a sub-bit glitch was not rejected at the start-bit re-check");
ck(n_bytes == by0,
"a sub-bit glitch produced a byte");
end else begin
// Longer than the sample point, and therefore INDISTINGUISHABLE
// from a real start bit. The receiver must accept it -- that is
// not a weakness to be checked away, it is the honest limit of
// what one re-sample can tell you, and it is the reason real
// receivers oversample.
ck(n_bytes > by0,
"a pulse longer than the sample point was not accepted as a start bit");
end
ck(state === 3'd0, "the receiver did not return to IDLE after a full frame time");
end
// =============================================================
// PHASE 3 (DIRECTED) -- back-to-back bytes at the nominal rate.
//
// The receiver consumes the whole stop cell as a guard, so the
// tightest legal spacing is exactly one stop bit. This checks
// that a byte starting immediately after that guard is captured.
// =============================================================
reset_dut;
for (k = 0; k < 8; k = k + 1)
send_and_check((8'h5A + k[7:0]), DIVISOR, 8);
// =============================================================
// PHASE 3b (DIRECTED, EXHAUSTIVE) -- the stop-bit guard.
//
// Every truncation from 0 to DIVISOR-2 cycles, at three sample
// points, with two byte pairs. Past DIVISOR-2 the next start bit
// has already come and gone before the receiver is free, which is
// a different property. 15 x 3 x 2 = 90 pairs.
// =============================================================
for (si = 0; si < 3; si = si + 1)
for (k = 0; k <= DIVISOR - 2; k = k + 1) begin
reset_dut;
send_trunc(8'h3C, 8'hA4, k, offsets[si]);
reset_dut;
send_trunc(8'hF0, 8'h80, k, offsets[si]);
end
// =============================================================
// PHASE 4 (RANDOM) -- arbitrary bytes at arbitrary periods.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut;
for (k = 0; k < 600; k = k + 1)
send_and_check((urand(0) & 8'hFF),
13 + (urand(0) % 7),
4 + (urand(0) % 9));
`endif
n_reach = 0;
for (ri = 0; ri < 5376; ri = ri + 1) if (reach[ri]) n_reach = n_reach + 1;
$display("steps=%0d checks=%0d reach=%0d/5376 errors=%0d",
bytes_sent, checks, n_reach, errors);
$display("[rx] frames_sent=%0d bytes_out=%0d correct=%0d corrupted=%0d framing_err=%0d",
bytes_sent, g_bytes, g_correct, g_corrupt, g_ferr);
$display("[rx] frames rejected at the start-bit re-check = %0d", g_rejected);
$display("[rx] back-to-back frame pairs tested = %0d", g_pairs);
$display("[rx] spurious bytes the transmitter never sent = %0d", g_extra);
$display("[the whole point] model/receiver disagreements = %0d", n_mispredict);
$display("--- tolerance: bytes surviving out of 256, by transmitter period ---");
$display(" offset 13 14 15 16 17 18 19");
for (si = 0; si < 3; si = si + 1)
$display(" %6d %6d %6d %6d %6d %6d %6d %6d",
offsets[si], surv[0][si], surv[1][si], surv[2][si],
surv[3][si], surv[4][si], surv[5][si], surv[6][si]);
if (n_reach != 5376) begin
$display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleWhy the stimulus is one clocked loop
An earlier version of this bench drove the line from one process and sampled
byte_valid from another, joined by fork/join. Both processes woke on
the same clock edge, and which of them acted first was undefined. The
cell boundaries the checker assumed were not the boundaries the receiver saw.
That version reported 1772 disagreements against a receiver that was correct and a model that was correct. The bug was entirely in the relationship between two testbench processes.
The fix is structural rather than careful. One loop, one non-blocking
assignment to rx per iteration, so the receiver observes transmitter cell
floor((M-1)/txp) at edge M by construction — which is exactly the
relation the model assumes. The property the model needs is now guaranteed
by the shape of the code instead of by the scheduler.
$random is signed
urand() masks the sign bit before anything else touches the value:
urand = $random(seed) & 32'h3FFF_FFFF;Without the mask, $random % 7 is negative roughly half the time, the
period selection collapses onto a subset of its range, and the random phase
quietly stops sweeping while still reporting that it ran. This is a silent
coverage loss with no symptom — the suite passes, faster than it should,
and nobody looks.
11. The Property That Survived Everything
Seven mutations were injected into this design. Six died immediately. The seventh — K5, which returns the receiver to IDLE at the stop sample instead of the end of the stop cell, destroying the inter-frame guard — scored exactly 0 against a 5376-point exhaustive sweep, a glitch sweep, and a 600-frame random phase.
A mutation that scores zero is either an unreachable statement or a hole in the suite. This one was a hole, and closing it took three attempts that are worth more than the property itself.
Attempt 1: sweep an idle gap between two frames. Predicted that a frame starting inside the guard would be refused, swept gaps of 0 to 16 cycles, and produced six wrong predictions.
The reason is the useful part:
Attempt 2: truncate the stop bit and predict the next byte. Better stimulus, wrong checker: 102 failures. A probe showed why — the receiver can reject the following frame at its re-check, return to IDLE, re-detect a start bit part-way through that same frame, reject again, and finally frame a byte from the tail. Predicting the outcome of that cascade would have meant re-implementing the receiver inside the testbench, which checks nothing.
Attempt 3: measure the guard directly. The guard is not a byte value, it is an instant, so measure the instant:
ck(e_restart >= 2 + 10 * DIVISOR,
"the receiver hunted for a new start bit before its stop cell had finished");e_restart is the edge at which n_starts incremented for the second time.
The receiver detects the first start at edge 1 and must hold S‑STOP for the
whole cell, so the earliest legal edge for the next detection is
2 + 10·DIVISOR. No model of the receiver is required, and there is nothing
for a mutation to move.
The cascade problem is handled by constraining the stimulus rather than
modelling it: send_trunc requires the second byte to have bit 0 clear,
which keeps the re-check on a low cell so the frame cannot be rejected
part-way through itself. That constraint is asserted in the bench — a
TEST BUG check — rather than left as a comment, because an unenforced
precondition is a future silent failure.
K5 now scores 120, entirely from directed stimulus, in all three languages.
12. SystemVerilog
The same contract, with the state as a real enumerated type — a state that does not exist cannot be assigned, and waveforms show names instead of numbers.
// =====================================================================
// async_rx_framer -- SystemVerilog.
//
// CLASSIFICATION: simplified synthesisable teaching RTL.
// This is NOT a complete UART receiver. It has no oversampling
// majority vote, no parity, no break detection, and no framing
// recovery beyond the stop bit.
//
// Same hardware contract as the Verilog module: same ports, same
// widths, same reset values, same cycle-by-cycle behaviour. The
// difference is expression, not function -- the state becomes a real
// enumerated type, so a state that does not exist cannot be assigned
// and the waveform viewer shows names instead of numbers.
//
// WHY THIS MECHANISM IS WORTH RTL
// -------------------------------
// A UART receiver has no shared clock and no delimiter. It learns
// where a byte begins from ONE falling edge -- the start bit -- and
// then predicts the centre of all nine following bit cells from its
// OWN local timer. Every subsequent sample instant is an
// extrapolation, so any difference between the transmitter's bit
// period and the receiver's accumulates across the byte.
//
// USB has no equivalent mechanism and therefore no equivalent budget:
// SYNC gives the receiver a pattern to lock to, NRZI plus bit
// stuffing refresh that lock at least every seven bits, and EOP ends
// the packet explicitly. The comparison is not "USB is better timed" --
// it is that UART spends zero wires on synchronisation and pays a
// tolerance budget, while USB spends a SYNC field, an encoding rule
// and a PLL to buy that budget away.
// =====================================================================
module async_rx_framer #(
parameter int DIVISOR = 16
) (
input logic clk,
input logic rst_n,
// The serial line, ALREADY SYNCHRONISED to clk. RTL simulation
// cannot model metastability; a real receiver needs a synchroniser,
// and its latency shifts every sample instant by a constant -- which
// is not the accumulating error this module is about.
input logic rx,
// Where inside the bit cell to sample. DIVISOR/2 is the centre and is
// what a real design uses; the testbench sweeps it to show the
// tolerance budget is a property of the SAMPLE POINT too.
input logic [7:0] sample_offset,
output logic byte_valid,
output logic [7:0] byte_data,
output logic framing_error,
output logic [2:0] state,
output logic [31:0] n_bytes,
output logic [31:0] n_framing_err,
output logic [31:0] n_starts,
output logic [31:0] n_false_start
);
// The state is a type, not a magic number. `state` is still exported
// as 3 bits so the three languages share one observable contract.
typedef enum logic [2:0] {
S_IDLE = 3'd0,
S_START = 3'd1,
S_DATA = 3'd2,
S_STOP = 3'd3
} rx_state_e;
rx_state_e st;
logic [7:0] phase; // cycles into the current bit cell
logic [3:0] bitidx;
logic [7:0] shifter;
logic bv_r, fe_r;
logic [7:0] bd_r;
logic [31:0] bytes_c, ferr_c, start_c, false_c;
assign byte_valid = bv_r;
assign byte_data = bd_r;
assign framing_error = fe_r;
assign state = st;
assign n_bytes = bytes_c;
assign n_framing_err = ferr_c;
assign n_starts = start_c;
assign n_false_start = false_c;
// Declared and assigned SEPARATELY. `logic at_sample = expr;` is a
// one-shot variable initialiser in SystemVerilog, not a continuous
// assignment -- it would sample x at time zero and never update.
logic at_sample, at_end;
assign at_sample = (phase == sample_offset);
assign at_end = (phase == 8'(DIVISOR - 1));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st <= S_IDLE;
phase <= 8'd0;
bitidx <= 4'd0;
shifter <= 8'd0;
bv_r <= 1'b0;
bd_r <= 8'd0;
fe_r <= 1'b0;
bytes_c <= 32'd0;
ferr_c <= 32'd0;
start_c <= 32'd0;
false_c <= 32'd0;
end else begin
bv_r <= 1'b0;
fe_r <= 1'b0;
unique case (st)
// ---- IDLE: wait for the line to fall. That falling edge is
// the ONLY synchronising information this byte will get.
S_IDLE: begin
if (!rx) begin
st <= S_START;
phase <= 8'd0;
start_c <= start_c + 32'd1;
end
end
// ---- START: confirm the line is still low at the sample
// point. This rejects a glitch shorter than the offset,
// and it is the ONLY noise defence an unoversampled
// receiver has.
S_START: begin
if (at_sample) begin
if (rx) begin
st <= S_IDLE;
phase <= 8'd0;
false_c <= false_c + 32'd1;
end else begin
phase <= phase + 8'd1;
end
end else if (at_end) begin
st <= S_DATA;
phase <= 8'd0;
bitidx <= 4'd0;
end else begin
phase <= phase + 8'd1;
end
end
// ---- DATA: eight PREDICTED sample instants, each DIVISOR
// cycles after the last on the receiver's own clock. A bit
// period mismatch therefore accumulates, and the last data
// bit is always the first to land in the wrong cell.
S_DATA: begin
if (at_sample) begin
shifter <= {rx, shifter[7:1]}; // LSB first
phase <= phase + 8'd1;
end else if (at_end) begin
phase <= 8'd0;
if (bitidx == 4'd7) st <= S_STOP;
else bitidx <= bitidx + 4'd1;
end else begin
phase <= phase + 8'd1;
end
end
// ---- STOP: the line must be high. The receiver's only
// integrity check, and a weak one -- it catches a slip
// that drags a zero into the stop cell and misses every
// slip that happens to land on a one.
S_STOP: begin
if (at_sample) begin
bv_r <= 1'b1;
bd_r <= shifter;
if (!rx) begin
fe_r <= 1'b1;
ferr_c <= ferr_c + 32'd1;
end
bytes_c <= bytes_c + 32'd1;
phase <= phase + 8'd1;
end else if (at_end) begin
// IDLE only at the END of the stop cell. A start edge
// arriving earlier is NOT accepted: the full stop bit is a
// guard. That costs one bit time of peak throughput and is
// a deliberate design decision.
st <= S_IDLE;
phase <= 8'd0;
end else begin
phase <= phase + 8'd1;
end
end
default: st <= S_IDLE;
endcase
end
end
endmoduleThe SystemVerilog testbench
// =====================================================================
// Testbench for async_rx_framer -- SystemVerilog.
//
// THE SHADOW MODEL IS GEOMETRY, NOT A STATE MACHINE.
//
// The design predicts bit centres with an FSM and a phase counter.
// The model computes, for each of the receiver's nine sample instants,
// WHICH TRANSMITTER CELL the line was actually in at that moment:
//
// sample instant of receiver cell k
// S_k = E + k*DIVISOR + sample_offset
//
// transmitter cell occupying that instant
// c = floor((S_k - org) / tx_period)
//
// cell 0 = start bit (0)
// cells 1..8 = data bits 0..7
// cell 9 = stop bit (1)
// cells >= 10 = idle (1)
//
// Nothing in that derivation mentions a state, a phase counter or a
// transition. It is arithmetic on two periods, so it cannot repeat a
// mistake the FSM makes.
//
// SAME STIMULUS AS THE VERILOG BENCH, DELIBERATELY. The same seed and
// the same phase order, so any difference between the Verilog and
// SystemVerilog mutation columns is a real difference between the two
// DESIGNS rather than an artefact of different random streams. The
// independent-stimulus role is played by the VHDL bench.
// =====================================================================
`timescale 1ns/1ps
module tb_ua_sv;
localparam int DIVISOR = 16;
logic clk = 1'b0, rst_n = 1'b0;
logic rx = 1'b1;
logic [7:0] sample_offset = 8'd8;
logic byte_valid, framing_error;
logic [7:0] byte_data;
logic [2:0] state;
logic [31:0] n_bytes, n_framing_err, n_starts, n_false_start;
async_rx_framer #(.DIVISOR(DIVISOR)) dut (
.clk(clk), .rst_n(rst_n), .rx(rx),
.sample_offset(sample_offset),
.byte_valid(byte_valid), .byte_data(byte_data),
.framing_error(framing_error), .state(state),
.n_bytes(n_bytes), .n_framing_err(n_framing_err),
.n_starts(n_starts), .n_false_start(n_false_start)
);
always #5 clk = ~clk;
int errors = 0, checks = 0, bytes_sent = 0;
int seed;
// $random is SIGNED: mask the sign bit before any modulo, or
// `$random % 7` is negative half the time and the sweep silently
// collapses onto one value.
function automatic logic [31:0] urand();
return $random(seed) & 32'h3FFF_FFFF;
endfunction
// ---- the headline measurement ----
//
// Per (tx_period, sample_offset) cell, how many of the 256 byte
// values survived. That table IS the tolerance budget, measured
// rather than quoted.
int surv [0:6][0:2];
int tried [0:6][0:2];
int n_mispredict = 0;
int g_correct = 0, g_corrupt = 0, g_ferr = 0, g_bytes = 0;
// ---- a measured consequence, not a testbench inconvenience ----
//
// When the transmitter is SLOWER than the receiver expects, the
// receiver finishes its ten predicted cells before the transmitter
// has finished sending them, returns to IDLE mid-frame, sees the
// remaining data bits as a fresh falling edge, and manufactures a
// byte that was never sent. The byte COUNT is then wrong too, which
// is worse than corruption: every downstream parser that assumed a
// fixed frame length is permanently out of step.
int g_extra = 0, g_rejected = 0, g_pairs = 0;
bit reach [0:5375];
int ri, n_reach;
task automatic ck(input logic cond, input string what);
checks++;
if (!cond) begin
errors++;
if (errors <= 20)
$display(" ERROR @%0t byte#%0d: %s", $time, bytes_sent, what);
end
endtask
// ---------------------------------------------------------------
// The model: what does the line carry in transmitter cell c?
// ---------------------------------------------------------------
function automatic logic cell_value(input int c, input logic [7:0] data);
if (c <= 0) return 1'b0; // start bit
else if (c <= 8) return data[c-1]; // data bit c-1, LSB first
else return 1'b1; // stop, then idle
endfunction
// ---------------------------------------------------------------
// What byte will the receiver capture, and will it flag framing?
// Pure arithmetic on the two periods.
//
// This also predicts REJECTION. The receiver re-checks the line at
// its start-bit sample point, and with a late sample point and a
// fast transmitter that instant can fall inside DATA BIT 0 -- so a
// perfectly legitimate start bit is thrown away whenever that bit
// happens to be high. That is not a defect; it is the reason real
// receivers sample the centre of the start cell and oversample
// around it.
//
// After a rejected start the receiver resynchronises on whichever
// later bit falls low, and what it captures then is a cascade rather
// than a prediction -- so the byte is deliberately NOT predicted.
//
// Parametrised by the DETECTION EDGE (E) and the tick at which the
// transmitter's cell 0 began (org): a frame the receiver picks up
// LATE is the same arithmetic with a larger E.
// ---------------------------------------------------------------
task automatic predict_at(input logic [7:0] data, input int txp,
input int soff, input int E, input int org,
output logic [7:0] exp_byte,
output logic exp_ferr, output logic exp_reject);
int s, c;
logic [7:0] b;
begin
s = E + soff;
c = (s - org) / txp;
exp_reject = (cell_value(c, data) == 1'b1);
b = 8'd0;
for (int k = 1; k <= 8; k++) begin
s = E + k * DIVISOR + soff;
c = (s - org) / txp;
b[k-1] = cell_value(c, data);
end
exp_byte = b;
s = E + 9 * DIVISOR + soff;
c = (s - org) / txp;
exp_ferr = (cell_value(c, data) == 1'b0);
end
endtask
task automatic predict(input logic [7:0] data, input int txp, input int soff,
output logic [7:0] exp_byte,
output logic exp_ferr, output logic exp_reject);
// The aligned case: cell 0 is seen low at edge 1, and the
// transmitter's cell 0 began at tick 0.
predict_at(data, txp, soff, 1, 0, exp_byte, exp_ferr, exp_reject);
endtask
// ---------------------------------------------------------------
// Drive one frame and check what came back -- ALL IN ONE CLOCKED
// LOOP, with no fork.
//
// An earlier version drove the line from one process and sampled
// byte_valid from another, joined by fork/join. Both woke on the
// same edge, so which acted first was undefined, and the cell
// boundaries the checker assumed were not the ones the receiver saw.
// It produced 1772 "disagreements" against a correct receiver AND a
// correct model.
//
// Here `rx` is assigned non-blockingly, so the receiver observes
// transmitter cell floor((M-1)/txp) at edge M -- exactly the
// relation predict() assumes, now true by construction.
// ---------------------------------------------------------------
task automatic send_and_check(input logic [7:0] data, input int txp,
input int soff);
logic [7:0] e_byte, got;
logic e_ferr, e_reject, got_ferr, got_any;
logic [31:0] fs_before;
int m, nframe, tick;
begin
sample_offset = soff[7:0];
predict(data, txp, soff, e_byte, e_ferr, e_reject);
fs_before = n_false_start;
got = 8'hXX; got_ferr = 1'b0; got_any = 1'b0;
// Ten transmitter cells, then idle long enough for the receiver
// to finish its ten predicted cells AND any extra frame it
// started from the transmitter's trailing bits.
nframe = 10 * txp + 14 * DIVISOR;
tick = 0;
for (m = 0; m < nframe; m++) begin
rx <= cell_value(tick / txp, data);
@(posedge clk);
#1;
if (byte_valid) begin
if (!got_any) begin
got = byte_data; got_ferr = framing_error; got_any = 1'b1;
end else begin
g_extra++; // a byte the transmitter never sent
end
g_bytes++;
end
tick++;
end
rx <= 1'b1;
bytes_sent++;
if (e_reject) begin
// ---- PROPERTY 1a: a predicted rejection IS a rejection ----
g_rejected++;
ck(n_false_start > fs_before,
"a start bit that lands high at the sample point was not rejected");
end else begin
// ---- PROPERTY 1b: a byte always comes back ----
// Exactly one byte per accepted start bit, even when framing
// has slipped. Silence is worse than corruption: the sender
// cannot know.
ck(got_any, "no byte was captured for an accepted frame");
if (got_any) begin
// ---- PROPERTY 2: the byte is what the GEOMETRY says ----
if (got !== e_byte) n_mispredict++;
ck(got === e_byte, "captured byte disagrees with the sample-instant model");
// ---- PROPERTY 3: the framing flag matches the stop cell ----
ck(got_ferr === e_ferr, "framing_error disagrees with the stop-cell model");
if (got === data) g_correct++; else g_corrupt++;
if (got_ferr) g_ferr++;
end
end
ck(n_mispredict == 0, "the model and the receiver disagree");
end
endtask
// The level actually on the line at tick t, for frame A (cells from
// tick 0) followed by frame B (cells from orgB). A's stop cell can
// therefore be cut short by B -- which is the whole point below, and
// the reason A cannot be predicted from dA alone.
function automatic logic line_at(input int t, input logic [7:0] dA,
input logic [7:0] dB, input int orgB,
input int txp);
if (t < orgB) return cell_value(t / txp, dA);
else if (t < orgB + 10 * txp) return cell_value((t - orgB) / txp, dB);
else return 1'b1;
endfunction
// ---------------------------------------------------------------
// A transmitter that CUTS ITS STOP BIT SHORT and starts the next
// frame immediately.
//
// The first version of this test swept an idle GAP between two
// frames and predicted small gaps would be refused. All six of its
// predictions were wrong, and the reason is worth more than the test
// was: at a matched bit rate the transmitter's own stop cell is
// exactly as long as the receiver's, so it pays for the entire guard
// by itself. Back-to-back frames at the nominal rate never touch the
// guard, and no gap sweep can reach it.
//
// The guard is only observable when the transmitter is SHORT of a
// full stop bit. And the cleanest way to observe it is not to
// predict a byte but to TIME THE NEXT START DETECTION, because that
// instant IS the guard:
//
// the receiver may not look for another start bit until its own
// stop cell has finished, at edge 1 + 10*DIVISOR.
//
// Measured from n_starts and an edge counter -- no model of the
// receiver required, and nothing for a mutation to move.
// ---------------------------------------------------------------
task automatic send_trunc(input logic [7:0] dA, input logic [7:0] dB,
input int lead, input int soff);
int m, tick, txp, orgB, eB, ngot, e_restart, edgen;
logic [7:0] eByteA, eByteB, gotA, gotB, bb;
logic eFerrA, eFerrB, eRejA, gFerrA, gFerrB;
logic [31:0] st0;
begin
txp = DIVISOR;
sample_offset = soff[7:0];
ngot = 0; gotA = 8'h00; gotB = 8'h00; gFerrA = 1'b0; gFerrB = 1'b0;
orgB = 10 * txp - lead;
e_restart = -1;
st0 = n_starts;
tick = 0; edgen = 0;
for (m = 0; m < orgB + 10 * txp + 14 * DIVISOR; m++) begin
rx <= line_at(tick, dA, dB, orgB, txp);
@(posedge clk); #1;
edgen++;
// the edge at which the receiver went hunting for B's start bit
if (e_restart < 0 && n_starts >= st0 + 32'd2) e_restart = edgen;
if (byte_valid) begin
if (ngot == 0) begin gotA = byte_data; gFerrA = framing_error; end
else if (ngot == 1) begin gotB = byte_data; gFerrB = framing_error; end
ngot++;
end
tick++;
end
// ---- PROPERTY 4: the stop-bit guard ----
//
// THE check. The receiver detects A's start at edge 1 and must
// hold S_STOP for the full cell, so the earliest edge at which it
// can detect another start is 2 + 10*DIVISOR. Returning to IDLE
// at the stop SAMPLE instead makes this fire for every lead >= 1.
ck(e_restart >= 2 + 10 * DIVISOR,
"the receiver hunted for a new start bit before its stop cell had finished");
ck(e_restart > 0, "the receiver never looked for the following frame");
// ---- frame A, from the COMPOSED line, not from dA ----
// A's stop sample can land on B's start bit, so a truncation of
// more than (DIVISOR-1 - soff) cycles MUST raise framing_error.
// Predicting this from dA alone would have said "no error".
eRejA = (line_at(1 + soff, dA, dB, orgB, txp) == 1'b1);
bb = 8'd0;
for (int k = 1; k <= 8; k++)
bb[k-1] = line_at(1 + k * DIVISOR + soff, dA, dB, orgB, txp);
eByteA = bb;
eFerrA = (line_at(1 + 9 * DIVISOR + soff, dA, dB, orgB, txp) == 1'b0);
ck(eRejA === 1'b0, "TEST BUG: frame A was rejected at its own re-check");
// ---- frame B, detected late at edge 2 + 10*DIVISOR ----
// dB's bit 0 must be 0. That keeps B's re-check on a LOW cell, so
// B cannot be rejected and re-detected partway through itself.
// Without that constraint the receiver can reject and retry
// several times inside one frame, and predicting the outcome
// would mean re-implementing the receiver in the testbench --
// which would check nothing at all.
eB = 2 + 10 * DIVISOR;
ck(dB[0] === 1'b0, "TEST BUG: send_trunc needs dB with bit 0 clear");
bb = 8'd0;
for (int k = 1; k <= 8; k++)
bb[k-1] = line_at(eB + k * DIVISOR + soff, dA, dB, orgB, txp);
eByteB = bb;
eFerrB = (line_at(eB + 9 * DIVISOR + soff, dA, dB, orgB, txp) == 1'b0);
ck(ngot == 2, "a truncated stop bit cost the receiver a whole frame");
if (ngot >= 1) begin
ck(gotA === eByteA, "the frame before the truncation was altered");
ck(gFerrA === eFerrA, "frame A's framing flag disagrees with the line");
end
if (ngot >= 2) begin
ck(gotB === eByteB, "the resynchronised frame does not match the late-detection model");
ck(gFerrB === eFerrB, "frame B's framing flag disagrees with the line");
end
g_pairs++;
bytes_sent += 2;
end
endtask
task automatic reset_dut();
rst_n = 1'b0;
rx <= 1'b1;
@(posedge clk); @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
endtask
int bi, pi, si, k, pv, sv;
int periods [0:6];
int offsets [0:2];
initial begin
for (ri = 0; ri < 5376; ri++) reach[ri] = 1'b0;
for (pi = 0; pi < 7; pi++)
for (si = 0; si < 3; si++) begin surv[pi][si] = 0; tried[pi][si] = 0; end
// Transmitter bit periods in receiver cycles. 16 is matched; the
// outer values are +/- 18.75%, far beyond anything a real UART
// survives, and they are included so the sweep spans the whole
// transition rather than stopping at the first failure.
periods[0]=13; periods[1]=14; periods[2]=15; periods[3]=16;
periods[4]=17; periods[5]=18; periods[6]=19;
// Quarter, centre and three-quarter of the bit cell.
offsets[0]=4; offsets[1]=8; offsets[2]=12;
seed = 32'd28001;
reset_dut();
// =============================================================
// PHASE 1 (DIRECTED, EXHAUSTIVE) -- every byte value, at every
// transmitter period, at every sample point. 256 x 7 x 3 = 5376,
// and every point is reachable: the three dimensions are
// independent inputs with no forbidden combinations.
// =============================================================
for (pi = 0; pi < 7; pi++)
for (si = 0; si < 3; si++)
for (bi = 0; bi < 256; bi++) begin
pv = periods[pi];
sv = offsets[si];
send_and_check(bi[7:0], pv, sv);
tried[pi][si]++;
ri = (pi * 3 + si) * 256 + bi;
reach[ri] = 1'b1;
end
// Recount survivors per cell FROM THE MODEL ALONE, so the published
// tolerance table does not depend on the DUT at all.
for (pi = 0; pi < 7; pi++)
for (si = 0; si < 3; si++) begin
surv[pi][si] = 0;
for (bi = 0; bi < 256; bi++) begin
logic [7:0] eb; logic ef, er;
predict(bi[7:0], periods[pi], offsets[si], eb, ef, er);
// A rejected frame is not a surviving byte -- the receiver threw
// it away. Counting it would overstate the tolerance.
if (!er && eb === bi[7:0]) surv[pi][si]++;
end
end
// =============================================================
// PHASE 2 (DIRECTED) -- a glitch is not a start bit.
// =============================================================
reset_dut();
for (k = 1; k <= DIVISOR; k++) begin
logic [31:0] fs0, by0;
sample_offset = 8'd8;
fs0 = n_false_start;
by0 = n_bytes;
for (pi = 0; pi < k; pi++) begin rx <= 1'b0; @(posedge clk); end
// Idle for a FULL frame time, not a fraction of one. A pulse
// longer than the sample point is accepted as a real start bit
// and the receiver then needs 1 + 10*DIVISOR cycles to finish;
// waiting only 4 bit cells produced 5 failures against a
// receiver that was behaving correctly.
for (pi = 0; pi < 14 * DIVISOR; pi++) begin rx <= 1'b1; @(posedge clk); end
#1;
if (k <= 9) begin
// The re-check lands at absolute cycle 1 + sample_offset = 9,
// so a pulse of 9 cycles or fewer is thrown away.
ck(n_false_start > fs0, "a sub-bit glitch was not rejected at the start-bit re-check");
ck(n_bytes == by0, "a sub-bit glitch produced a byte");
end else begin
// Longer than the sample point, and therefore
// INDISTINGUISHABLE from a real start bit. The receiver must
// accept it. That is not a weakness to be checked away, it is
// the honest limit of what one re-sample can tell you -- and it
// is why real receivers oversample.
ck(n_bytes > by0, "a pulse longer than the sample point was not accepted as a start bit");
end
ck(state === 3'd0, "the receiver did not return to IDLE after a full frame time");
end
// =============================================================
// PHASE 3 (DIRECTED) -- back-to-back bytes at the nominal rate.
// =============================================================
reset_dut();
for (k = 0; k < 8; k++)
send_and_check(8'h5A + k[7:0], DIVISOR, 8);
// =============================================================
// PHASE 3b (DIRECTED, EXHAUSTIVE) -- the stop-bit guard.
//
// Every truncation from 0 to DIVISOR-2 cycles, at three sample
// points, with two byte pairs. Past DIVISOR-2 the next start bit
// has come and gone before the receiver is free, which is a
// different property. 15 x 3 x 2 = 90 pairs.
// =============================================================
for (si = 0; si < 3; si++)
for (k = 0; k <= DIVISOR - 2; k++) begin
reset_dut();
send_trunc(8'h3C, 8'hA4, k, offsets[si]);
reset_dut();
send_trunc(8'hF0, 8'h80, k, offsets[si]);
end
// =============================================================
// PHASE 4 (RANDOM) -- arbitrary bytes at arbitrary periods.
// =============================================================
`ifndef DIRECTED_ONLY
reset_dut();
for (k = 0; k < 600; k++)
send_and_check(urand() & 8'hFF, 13 + (urand() % 7), 4 + (urand() % 9));
`endif
n_reach = 0;
for (ri = 0; ri < 5376; ri++) if (reach[ri]) n_reach++;
$display("steps=%0d checks=%0d reach=%0d/5376 errors=%0d",
bytes_sent, checks, n_reach, errors);
$display("[rx] frames_sent=%0d bytes_out=%0d correct=%0d corrupted=%0d framing_err=%0d",
bytes_sent, g_bytes, g_correct, g_corrupt, g_ferr);
$display("[rx] frames rejected at the start-bit re-check = %0d", g_rejected);
$display("[rx] back-to-back frame pairs tested = %0d", g_pairs);
$display("[rx] spurious bytes the transmitter never sent = %0d", g_extra);
$display("[the whole point] model/receiver disagreements = %0d", n_mispredict);
$display("--- tolerance: bytes surviving out of 256, by transmitter period ---");
$display(" offset 13 14 15 16 17 18 19");
for (si = 0; si < 3; si++)
$display(" %6d %6d %6d %6d %6d %6d %6d %6d",
offsets[si], surv[0][si], surv[1][si], surv[2][si],
surv[3][si], surv[4][si], surv[5][si], surv[6][si]);
if (n_reach != 5376) begin
$display("FAIL: exhaustive sweep incomplete"); errors++;
end
if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
else $display("FAIL: %0d errors in %0d checks", errors, checks);
$finish;
end
endmoduleThis bench uses the same seed and the same phase order as the Verilog
one, deliberately. Icarus seeds $random and $urandom identically, so the
two benches drive identical stimulus — which means any difference between
the Verilog and SystemVerilog mutation columns is a real difference between
the two designs rather than an artefact of two random streams. The
independent-stimulus role is played by VHDL.
13. VHDL-2008
-- =====================================================================
-- async_rx_framer -- VHDL-2008.
--
-- CLASSIFICATION: simplified synthesisable teaching RTL.
-- This is NOT a complete UART receiver. It has no oversampling
-- majority vote, no parity, no break detection, and no framing
-- recovery beyond the stop bit.
--
-- Same hardware contract as the Verilog and SystemVerilog modules:
-- same ports, same widths, same reset values, same cycle-by-cycle
-- behaviour. The state is a real enumeration and is exported as a
-- 3-bit code so all three languages share one observable interface.
--
-- WHY THIS MECHANISM IS WORTH RTL
-- -------------------------------
-- A UART receiver has no shared clock and no delimiter. It learns
-- where a byte begins from ONE falling edge -- the start bit -- and
-- then predicts the centre of all nine following bit cells from its
-- OWN local timer. Every subsequent sample instant is an
-- extrapolation, so any difference between the transmitter's bit
-- period and the receiver's accumulates across the byte.
--
-- USB has no equivalent mechanism and therefore no equivalent budget:
-- SYNC gives the receiver a pattern to lock to, NRZI plus bit stuffing
-- refresh that lock at least every seven bits, and EOP ends the packet
-- explicitly. The comparison is not "USB is better timed" -- it is
-- that UART spends zero wires on synchronisation and pays a tolerance
-- budget, while USB spends a SYNC field, an encoding rule and a PLL to
-- buy that budget away.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity async_rx_framer is
generic (
DIVISOR : natural := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- The serial line, ALREADY SYNCHRONISED to clk. RTL simulation
-- cannot model metastability; a real receiver needs a synchroniser,
-- and its latency shifts every sample instant by a constant, which
-- is not the accumulating error this module is about.
rx : in std_logic;
-- Where inside the bit cell to sample. DIVISOR/2 is the centre and
-- is what a real design uses; the testbench sweeps it to show the
-- tolerance budget is a property of the SAMPLE POINT too.
sample_offset : in std_logic_vector(7 downto 0);
byte_valid : out std_logic;
byte_data : out std_logic_vector(7 downto 0);
framing_error : out std_logic;
state : out std_logic_vector(2 downto 0);
n_bytes : out std_logic_vector(31 downto 0);
n_framing_err : out std_logic_vector(31 downto 0);
n_starts : out std_logic_vector(31 downto 0);
n_false_start : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of async_rx_framer is
type rx_state_t is (S_IDLE, S_START, S_DATA, S_STOP);
-- Exported as a 3-bit code so the Verilog, SystemVerilog and VHDL
-- modules present one identical observable contract to their benches.
function st_code (s : rx_state_t) return std_logic_vector is
begin
case s is
when S_IDLE => return "000";
when S_START => return "001";
when S_DATA => return "010";
when S_STOP => return "011";
end case;
end function;
signal st : rx_state_t := S_IDLE;
signal phase : unsigned(7 downto 0) := (others => '0');
signal bitidx : unsigned(3 downto 0) := (others => '0');
signal shifter : std_logic_vector(7 downto 0) := (others => '0');
signal bv_r : std_logic := '0';
signal fe_r : std_logic := '0';
signal bd_r : std_logic_vector(7 downto 0) := (others => '0');
signal bytes_c : unsigned(31 downto 0) := (others => '0');
signal ferr_c : unsigned(31 downto 0) := (others => '0');
signal start_c : unsigned(31 downto 0) := (others => '0');
signal false_c : unsigned(31 downto 0) := (others => '0');
-- The sample instant for the current bit cell, computed from `phase`,
-- a LOCAL counter. Nothing in these expressions refers to the
-- transmitter at all -- which is the entire point: the receiver is
-- predicting, not observing.
signal at_sample : std_logic;
signal at_end : std_logic;
begin
byte_valid <= bv_r;
byte_data <= bd_r;
framing_error <= fe_r;
state <= st_code(st);
n_bytes <= std_logic_vector(bytes_c);
n_framing_err <= std_logic_vector(ferr_c);
n_starts <= std_logic_vector(start_c);
n_false_start <= std_logic_vector(false_c);
at_sample <= '1' when phase = unsigned(sample_offset) else '0';
at_end <= '1' when phase = to_unsigned(DIVISOR - 1, 8) else '0';
process (clk, rst_n)
begin
if rst_n = '0' then
st <= S_IDLE;
phase <= (others => '0');
bitidx <= (others => '0');
shifter <= (others => '0');
bv_r <= '0';
bd_r <= (others => '0');
fe_r <= '0';
bytes_c <= (others => '0');
ferr_c <= (others => '0');
start_c <= (others => '0');
false_c <= (others => '0');
elsif rising_edge(clk) then
bv_r <= '0';
fe_r <= '0';
case st is
-- ---- IDLE: wait for the line to fall. That falling edge is
-- the ONLY synchronising information this byte will get.
when S_IDLE =>
if rx = '0' then
st <= S_START;
phase <= (others => '0');
start_c <= start_c + 1;
end if;
-- ---- START: confirm the line is still low at the sample
-- point. This rejects a glitch shorter than the offset and
-- is the ONLY noise defence an unoversampled receiver has.
when S_START =>
if at_sample = '1' then
if rx = '1' then
-- The line came back high: that was not a start bit.
st <= S_IDLE;
phase <= (others => '0');
false_c <= false_c + 1;
else
phase <= phase + 1;
end if;
elsif at_end = '1' then
st <= S_DATA;
phase <= (others => '0');
bitidx <= (others => '0');
else
phase <= phase + 1;
end if;
-- ---- DATA: eight PREDICTED sample instants, each DIVISOR
-- cycles after the last on the receiver's own clock. A bit
-- period mismatch accumulates, so the last data bit is
-- always the first to land in the wrong cell.
when S_DATA =>
if at_sample = '1' then
shifter <= rx & shifter(7 downto 1); -- LSB first
phase <= phase + 1;
elsif at_end = '1' then
phase <= (others => '0');
if bitidx = 7 then
st <= S_STOP;
else
bitidx <= bitidx + 1;
end if;
else
phase <= phase + 1;
end if;
-- ---- STOP: the line must be high. The receiver's only
-- integrity check, and a weak one -- it catches a slip
-- that drags a zero into the stop cell and misses every
-- slip that happens to land on a one.
when S_STOP =>
if at_sample = '1' then
bv_r <= '1';
bd_r <= shifter;
if rx = '0' then
fe_r <= '1';
ferr_c <= ferr_c + 1;
end if;
bytes_c <= bytes_c + 1;
phase <= phase + 1;
elsif at_end = '1' then
-- IDLE only at the END of the stop cell. A start edge
-- arriving earlier is NOT accepted: the full stop bit is a
-- guard. That costs one bit time of peak throughput and is
-- a deliberate design decision.
st <= S_IDLE;
phase <= (others => '0');
else
phase <= phase + 1;
end if;
end case;
end if;
end process;
end architecture;The VHDL testbench
This is the independent bench. The directed phases are structurally identical, so the directed mutation columns must agree exactly across all three languages and any disagreement is a real finding. The random phase uses a VHDL-native linear congruential generator and therefore a genuinely different stream.
-- =====================================================================
-- Testbench for async_rx_framer -- VHDL-2008.
--
-- THE SHADOW MODEL IS GEOMETRY, NOT A STATE MACHINE.
--
-- The design predicts bit centres with an FSM and a phase counter.
-- The model computes, for each of the receiver's nine sample instants,
-- WHICH TRANSMITTER CELL the line was actually in at that moment:
--
-- sample instant of receiver cell k
-- S_k = E + k*DIVISOR + sample_offset
--
-- transmitter cell occupying that instant
-- c = (S_k - org) / tx_period (floor)
--
-- cell 0 = start bit (0)
-- cells 1..8 = data bits 0..7
-- cell 9 = stop bit (1)
-- cells >= 10 = idle (1)
--
-- Nothing in that derivation mentions a state, a phase counter or a
-- transition. It is arithmetic on two periods, so it cannot repeat a
-- mistake the FSM makes.
--
-- THIS IS THE INDEPENDENT BENCH. The directed phases are deliberately
-- identical in structure to the Verilog and SystemVerilog benches, so
-- the DIRECTED mutation columns must agree EXACTLY across all three
-- languages and any disagreement is a real finding. The random phase
-- uses a VHDL-native generator and therefore a different stream, so
-- the ALL columns are expected to differ -- that difference is the
-- measure of how much the random phase contributes.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
entity tb_ua_vhdl is
generic (
-- Set true (nvc -e -gDIRECTED_ONLY=true) to run the directed phases
-- alone. The directed phases must pass and must kill every mutation
-- by themselves; a suite that needs its random phase to find a
-- defect has not characterised the defect.
DIRECTED_ONLY : boolean := false
);
end entity;
architecture sim of tb_ua_vhdl is
constant DIVISOR : natural := 16;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal rx : std_logic := '1';
signal sample_offset : std_logic_vector(7 downto 0) := x"08";
signal byte_valid : std_logic;
signal byte_data : std_logic_vector(7 downto 0);
signal framing_error : std_logic;
signal state : std_logic_vector(2 downto 0);
signal n_bytes : std_logic_vector(31 downto 0);
signal n_framing_err : std_logic_vector(31 downto 0);
signal n_starts : std_logic_vector(31 downto 0);
signal n_false_start : std_logic_vector(31 downto 0);
signal done : boolean := false;
begin
dut : entity work.async_rx_framer
generic map (DIVISOR => DIVISOR)
port map (
clk => clk, rst_n => rst_n, rx => rx,
sample_offset => sample_offset,
byte_valid => byte_valid, byte_data => byte_data,
framing_error => framing_error, state => state,
n_bytes => n_bytes, n_framing_err => n_framing_err,
n_starts => n_starts, n_false_start => n_false_start
);
clkgen : process
begin
while not done loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
main : process
variable errors : integer := 0;
variable checks : integer := 0;
variable bytes_sent : integer := 0;
-- ---- the headline measurement ----
--
-- Per (tx_period, sample_offset) cell, how many of the 256 byte
-- values survived. That table IS the tolerance budget, measured
-- rather than quoted.
type tbl_t is array (0 to 6, 0 to 2) of integer;
variable surv : tbl_t := (others => (others => 0));
variable tried : tbl_t := (others => (others => 0));
variable n_mispredict : integer := 0;
variable g_correct : integer := 0;
variable g_corrupt : integer := 0;
variable g_ferr : integer := 0;
variable g_bytes : integer := 0;
-- ---- a measured consequence, not a testbench inconvenience ----
--
-- When the transmitter is SLOWER than the receiver expects, the
-- receiver finishes its ten predicted cells before the transmitter
-- has finished sending them, returns to IDLE mid-frame, sees the
-- remaining data bits as a fresh falling edge, and manufactures a
-- byte that was never sent. The byte COUNT is then wrong too, which
-- is worse than corruption: every downstream parser that assumed a
-- fixed frame length is permanently out of step.
variable g_extra : integer := 0;
variable g_rejected : integer := 0;
variable g_pairs : integer := 0;
type reach_t is array (0 to 5375) of boolean;
variable reach : reach_t := (others => false);
variable n_reach : integer := 0;
type iarr7 is array (0 to 6) of integer;
type iarr3 is array (0 to 2) of integer;
-- 16 is a matched transmitter; the outer values are +/- 18.75%, far
-- beyond anything a real UART survives, and they are here so the
-- sweep spans the whole transition rather than stopping at the
-- first failure.
variable periods : iarr7 := (13, 14, 15, 16, 17, 18, 19);
-- Quarter, centre and three-quarter of the bit cell.
variable offsets : iarr3 := (4, 8, 12);
variable lo : line;
-- A VHDL-native LCG. Deliberately NOT the same stream as the
-- Verilog bench, so the random phases are genuinely independent.
variable rnd_state : unsigned(31 downto 0) := x"0006D6D1";
impure function urand return integer is
begin
-- resize() back to 32 bits: `unsigned * unsigned` widens to 64 in
-- VHDL, and truncating to 32 is exactly the LCG's mod 2**32.
rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
+ to_unsigned(12345, 32);
-- Return the HIGH bits, not the low ones. In an LCG with a
-- power-of-two modulus, bit i has period 2**(i+1): bit 0 alternates,
-- bit 1 cycles in four. So `urand mod 4` taken from the low bits
-- returns 3,0,1,2,3,0,1,2,... in perfect lockstep -- while its
-- histogram is exactly uniform, which is why the defect survives
-- every distribution check anyone would think to run.
return to_integer(rnd_state(30 downto 15));
end function;
procedure ck (cond : boolean; what : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 20 then
write(lo, string'(" ERROR @"));
write(lo, time'image(now));
write(lo, string'(" byte#"));
write(lo, integer'image(bytes_sent));
write(lo, string'(": "));
write(lo, what);
writeline(output, lo);
end if;
end if;
end procedure;
-- -----------------------------------------------------------------
-- The model: what does the line carry in transmitter cell c?
-- -----------------------------------------------------------------
function cell_value (c : integer; data : std_logic_vector(7 downto 0))
return std_logic is
begin
if c <= 0 then
return '0'; -- start bit
elsif c <= 8 then
return data(c - 1); -- data bit c-1, LSB first
else
return '1'; -- stop, then idle
end if;
end function;
-- -----------------------------------------------------------------
-- What byte will the receiver capture, and will it flag framing?
-- Pure arithmetic on the two periods.
--
-- This also predicts REJECTION. The receiver re-checks the line at
-- its start-bit sample point, and with a late sample point and a
-- fast transmitter that instant can fall inside DATA BIT 0 -- so a
-- perfectly legitimate start bit is thrown away whenever that bit
-- happens to be high. That is not a defect; it is the reason real
-- receivers sample the centre of the start cell and oversample
-- around it.
--
-- After a rejected start the receiver resynchronises on whichever
-- later bit falls low, and what it captures then is a cascade
-- rather than a prediction -- so the byte is deliberately NOT
-- predicted in that case.
--
-- Parametrised by the DETECTION EDGE (E) and the tick at which the
-- transmitter's cell 0 began (org): a frame the receiver picks up
-- LATE is the same arithmetic with a larger E.
-- -----------------------------------------------------------------
procedure predict_at (data : std_logic_vector(7 downto 0);
txp, soff, E, org : integer;
exp_byte : out std_logic_vector(7 downto 0);
exp_ferr : out std_logic;
exp_reject : out std_logic) is
variable s, c : integer;
variable b : std_logic_vector(7 downto 0) := (others => '0');
begin
s := E + soff;
c := (s - org) / txp;
if cell_value(c, data) = '1' then exp_reject := '1';
else exp_reject := '0'; end if;
b := (others => '0');
for k in 1 to 8 loop
s := E + k * DIVISOR + soff;
c := (s - org) / txp;
b(k - 1) := cell_value(c, data);
end loop;
exp_byte := b;
s := E + 9 * DIVISOR + soff;
c := (s - org) / txp;
if cell_value(c, data) = '0' then exp_ferr := '1';
else exp_ferr := '0'; end if;
end procedure;
procedure predict (data : std_logic_vector(7 downto 0);
txp, soff : integer;
exp_byte : out std_logic_vector(7 downto 0);
exp_ferr : out std_logic;
exp_reject : out std_logic) is
begin
-- The aligned case: cell 0 is seen low at edge 1, and the
-- transmitter's cell 0 began at tick 0.
predict_at(data, txp, soff, 1, 0, exp_byte, exp_ferr, exp_reject);
end procedure;
procedure reset_dut is
begin
rst_n <= '0';
rx <= '1';
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
-- -----------------------------------------------------------------
-- Drive one frame and check what came back -- ALL IN ONE CLOCKED
-- LOOP.
--
-- An earlier Verilog version of this drove the line from one
-- process and sampled byte_valid from another. Both woke on the
-- same edge, so which acted first was undefined, and the cell
-- boundaries the checker assumed were not the ones the receiver
-- saw. It produced 1772 "disagreements" against a correct receiver
-- AND a correct model. Here `rx` is assigned once per iteration
-- before the edge, so the receiver observes transmitter cell
-- (M-1)/txp at edge M -- exactly the relation predict() assumes.
-- -----------------------------------------------------------------
procedure send_and_check (data : std_logic_vector(7 downto 0);
txp, soff : integer) is
variable e_byte : std_logic_vector(7 downto 0);
variable e_ferr : std_logic;
variable e_reject : std_logic;
variable got : std_logic_vector(7 downto 0) := (others => 'X');
variable got_ferr : std_logic := '0';
variable got_any : boolean := false;
variable fs_before: integer;
variable nframe, tick : integer;
begin
sample_offset <= std_logic_vector(to_unsigned(soff, 8));
predict(data, txp, soff, e_byte, e_ferr, e_reject);
fs_before := to_integer(unsigned(n_false_start));
got := (others => 'X'); got_ferr := '0'; got_any := false;
-- Ten transmitter cells, then idle long enough for the receiver
-- to finish its ten predicted cells AND any extra frame it
-- started from the transmitter's trailing bits.
nframe := 10 * txp + 14 * DIVISOR;
tick := 0;
for m in 0 to nframe - 1 loop
rx <= cell_value(tick / txp, data);
wait until rising_edge(clk);
wait for 1 ns;
if byte_valid = '1' then
if not got_any then
got := byte_data; got_ferr := framing_error; got_any := true;
else
g_extra := g_extra + 1; -- a byte the transmitter never sent
end if;
g_bytes := g_bytes + 1;
end if;
tick := tick + 1;
end loop;
rx <= '1';
bytes_sent := bytes_sent + 1;
if e_reject = '1' then
-- ---- PROPERTY 1a: a predicted rejection IS a rejection ----
g_rejected := g_rejected + 1;
ck(to_integer(unsigned(n_false_start)) > fs_before,
"a start bit that lands high at the sample point was not rejected");
else
-- ---- PROPERTY 1b: a byte always comes back ----
-- Exactly one byte per accepted start bit, even when framing has
-- slipped. Silence is worse than corruption: the sender cannot
-- know.
ck(got_any, "no byte was captured for an accepted frame");
if got_any then
-- ---- PROPERTY 2: the byte is what the GEOMETRY says ----
if got /= e_byte then n_mispredict := n_mispredict + 1; end if;
ck(got = e_byte, "captured byte disagrees with the sample-instant model");
-- ---- PROPERTY 3: the framing flag matches the stop cell ----
ck(got_ferr = e_ferr, "framing_error disagrees with the stop-cell model");
if got = data then g_correct := g_correct + 1;
else g_corrupt := g_corrupt + 1; end if;
if got_ferr = '1' then g_ferr := g_ferr + 1; end if;
end if;
end if;
ck(n_mispredict = 0, "the model and the receiver disagree");
end procedure;
-- The level actually on the line at tick t, for frame A (cells from
-- tick 0) followed by frame B (cells from orgB). A's stop cell can
-- therefore be cut short by B -- which is the whole point below, and
-- the reason A cannot be predicted from dA alone.
function line_at (t : integer; dA, dB : std_logic_vector(7 downto 0);
orgB, txp : integer) return std_logic is
begin
if t < orgB then
return cell_value(t / txp, dA);
elsif t < orgB + 10 * txp then
return cell_value((t - orgB) / txp, dB);
else
return '1';
end if;
end function;
-- -----------------------------------------------------------------
-- A transmitter that CUTS ITS STOP BIT SHORT and starts the next
-- frame immediately.
--
-- The first version of this test swept an idle GAP between two
-- frames and predicted small gaps would be refused. All six of its
-- predictions were wrong, and the reason is worth more than the
-- test was: at a matched bit rate the transmitter's own stop cell
-- is exactly as long as the receiver's, so it pays for the entire
-- guard by itself. Back-to-back frames at the nominal rate never
-- touch the guard, and no gap sweep can reach it.
--
-- The guard is only observable when the transmitter is SHORT of a
-- full stop bit. And the cleanest way to observe it is not to
-- predict a byte but to TIME THE NEXT START DETECTION, because
-- that instant IS the guard:
--
-- the receiver may not look for another start bit until its own
-- stop cell has finished, at edge 1 + 10*DIVISOR.
--
-- Measured from n_starts and an edge counter -- no model of the
-- receiver required, and nothing for a mutation to move.
-- -----------------------------------------------------------------
procedure send_trunc (dA, dB : std_logic_vector(7 downto 0);
lead, soff : integer) is
variable tick, txp, orgB, eB, ngot, e_restart, edgen : integer;
variable eByteA, eByteB, gotA, gotB : std_logic_vector(7 downto 0);
variable bb : std_logic_vector(7 downto 0);
variable eFerrA, eFerrB, eRejA, gFerrA, gFerrB : std_logic;
variable st0 : integer;
begin
txp := DIVISOR;
sample_offset <= std_logic_vector(to_unsigned(soff, 8));
ngot := 0; gotA := x"00"; gotB := x"00"; gFerrA := '0'; gFerrB := '0';
orgB := 10 * txp - lead;
e_restart := -1;
st0 := to_integer(unsigned(n_starts));
tick := 0; edgen := 0;
for m in 0 to orgB + 10 * txp + 14 * DIVISOR - 1 loop
rx <= line_at(tick, dA, dB, orgB, txp);
wait until rising_edge(clk);
wait for 1 ns;
edgen := edgen + 1;
-- the edge at which the receiver went hunting for B's start bit
if e_restart < 0 and to_integer(unsigned(n_starts)) >= st0 + 2 then
e_restart := edgen;
end if;
if byte_valid = '1' then
if ngot = 0 then
gotA := byte_data; gFerrA := framing_error;
elsif ngot = 1 then
gotB := byte_data; gFerrB := framing_error;
end if;
ngot := ngot + 1;
end if;
tick := tick + 1;
end loop;
-- ---- PROPERTY 4: the stop-bit guard ----
--
-- THE check. The receiver detects A's start at edge 1 and must
-- hold S_STOP for the full cell, so the earliest edge at which it
-- can detect another start is 2 + 10*DIVISOR. Returning to IDLE
-- at the stop SAMPLE instead makes this fire for every lead >= 1.
ck(e_restart >= 2 + 10 * DIVISOR,
"the receiver hunted for a new start bit before its stop cell had finished");
ck(e_restart > 0, "the receiver never looked for the following frame");
-- ---- frame A, from the COMPOSED line, not from dA ----
-- A's stop sample can land on B's start bit, so a truncation of
-- more than (DIVISOR-1 - soff) cycles MUST raise framing_error.
-- Predicting this from dA alone would have said "no error".
if line_at(1 + soff, dA, dB, orgB, txp) = '1' then eRejA := '1';
else eRejA := '0'; end if;
bb := (others => '0');
for k in 1 to 8 loop
bb(k - 1) := line_at(1 + k * DIVISOR + soff, dA, dB, orgB, txp);
end loop;
eByteA := bb;
if line_at(1 + 9 * DIVISOR + soff, dA, dB, orgB, txp) = '0' then eFerrA := '1';
else eFerrA := '0'; end if;
ck(eRejA = '0', "TEST BUG: frame A was rejected at its own re-check");
-- ---- frame B, detected late at edge 2 + 10*DIVISOR ----
-- dB's bit 0 must be 0. That keeps B's re-check on a LOW cell, so
-- B cannot be rejected and re-detected partway through itself.
-- Without that constraint the receiver can reject and retry
-- several times inside one frame, and predicting the outcome would
-- mean re-implementing the receiver in the testbench -- which
-- would check nothing at all.
eB := 2 + 10 * DIVISOR;
ck(dB(0) = '0', "TEST BUG: send_trunc needs dB with bit 0 clear");
bb := (others => '0');
for k in 1 to 8 loop
bb(k - 1) := line_at(eB + k * DIVISOR + soff, dA, dB, orgB, txp);
end loop;
eByteB := bb;
if line_at(eB + 9 * DIVISOR + soff, dA, dB, orgB, txp) = '0' then eFerrB := '1';
else eFerrB := '0'; end if;
ck(ngot = 2, "a truncated stop bit cost the receiver a whole frame");
if ngot >= 1 then
ck(gotA = eByteA, "the frame before the truncation was altered");
ck(gFerrA = eFerrA, "frame A's framing flag disagrees with the line");
end if;
if ngot >= 2 then
ck(gotB = eByteB, "the resynchronised frame does not match the late-detection model");
ck(gFerrB = eFerrB, "frame B's framing flag disagrees with the line");
end if;
g_pairs := g_pairs + 1;
bytes_sent := bytes_sent + 2;
end procedure;
variable pv, sv, ri : integer;
variable fs0, by0 : integer;
variable eb : std_logic_vector(7 downto 0);
variable ef, er : std_logic;
begin
reset_dut;
-- ===============================================================
-- PHASE 1 (DIRECTED, EXHAUSTIVE) -- every byte value, at every
-- transmitter period, at every sample point. 256 x 7 x 3 = 5376,
-- and every point is reachable: the three dimensions are
-- independent inputs with no forbidden combinations.
-- ===============================================================
for pi in 0 to 6 loop
for si in 0 to 2 loop
for bi in 0 to 255 loop
pv := periods(pi);
sv := offsets(si);
send_and_check(std_logic_vector(to_unsigned(bi, 8)), pv, sv);
tried(pi, si) := tried(pi, si) + 1;
ri := (pi * 3 + si) * 256 + bi;
reach(ri) := true;
end loop;
end loop;
end loop;
-- Recount survivors per cell FROM THE MODEL ALONE, so the published
-- tolerance table does not depend on the DUT at all.
for pi in 0 to 6 loop
for si in 0 to 2 loop
surv(pi, si) := 0;
for bi in 0 to 255 loop
predict(std_logic_vector(to_unsigned(bi, 8)),
periods(pi), offsets(si), eb, ef, er);
-- A rejected frame is not a surviving byte -- the receiver
-- threw it away. Counting it would overstate the tolerance.
if er = '0' and eb = std_logic_vector(to_unsigned(bi, 8)) then
surv(pi, si) := surv(pi, si) + 1;
end if;
end loop;
end loop;
end loop;
-- ===============================================================
-- PHASE 2 (DIRECTED) -- a glitch is not a start bit.
-- ===============================================================
reset_dut;
for k in 1 to DIVISOR loop
sample_offset <= x"08";
fs0 := to_integer(unsigned(n_false_start));
by0 := to_integer(unsigned(n_bytes));
for pi in 0 to k - 1 loop
rx <= '0'; wait until rising_edge(clk);
end loop;
-- Idle for a FULL frame time, not a fraction of one. A pulse
-- longer than the sample point is accepted as a real start bit and
-- the receiver then needs 1 + 10*DIVISOR cycles to finish;
-- waiting only 4 bit cells produced 5 failures against a receiver
-- that was behaving correctly.
for pi in 0 to 14 * DIVISOR - 1 loop
rx <= '1'; wait until rising_edge(clk);
end loop;
wait for 1 ns;
if k <= 9 then
-- The re-check lands at absolute cycle 1 + sample_offset = 9, so
-- a pulse of 9 cycles or fewer is thrown away.
ck(to_integer(unsigned(n_false_start)) > fs0,
"a sub-bit glitch was not rejected at the start-bit re-check");
ck(to_integer(unsigned(n_bytes)) = by0, "a sub-bit glitch produced a byte");
else
-- Longer than the sample point, and therefore INDISTINGUISHABLE
-- from a real start bit. The receiver must accept it. That is
-- not a weakness to be checked away, it is the honest limit of
-- what one re-sample can tell you -- and it is why real
-- receivers oversample.
ck(to_integer(unsigned(n_bytes)) > by0,
"a pulse longer than the sample point was not accepted as a start bit");
end if;
ck(state = "000", "the receiver did not return to IDLE after a full frame time");
end loop;
-- ===============================================================
-- PHASE 3 (DIRECTED) -- back-to-back bytes at the nominal rate.
-- ===============================================================
reset_dut;
for k in 0 to 7 loop
send_and_check(std_logic_vector(to_unsigned(16#5A# + k, 8)), DIVISOR, 8);
end loop;
-- ===============================================================
-- PHASE 3b (DIRECTED, EXHAUSTIVE) -- the stop-bit guard.
--
-- Every truncation from 0 to DIVISOR-2 cycles, at three sample
-- points, with two byte pairs. Past DIVISOR-2 the next start bit
-- has come and gone before the receiver is free, which is a
-- different property. 15 x 3 x 2 = 90 pairs.
-- ===============================================================
for si in 0 to 2 loop
for k in 0 to DIVISOR - 2 loop
reset_dut;
send_trunc(x"3C", x"A4", k, offsets(si));
reset_dut;
send_trunc(x"F0", x"80", k, offsets(si));
end loop;
end loop;
-- ===============================================================
-- PHASE 4 (RANDOM) -- arbitrary bytes at arbitrary periods.
-- ===============================================================
if not DIRECTED_ONLY then
reset_dut;
for k in 0 to 599 loop
send_and_check(std_logic_vector(to_unsigned(urand mod 256, 8)),
13 + (urand mod 7),
4 + (urand mod 9));
end loop;
end if;
n_reach := 0;
for i in 0 to 5375 loop
if reach(i) then n_reach := n_reach + 1; end if;
end loop;
write(lo, string'("steps=") & integer'image(bytes_sent) &
string'(" checks=") & integer'image(checks) &
string'(" reach=") & integer'image(n_reach) &
string'("/5376 errors=") & integer'image(errors));
writeline(output, lo);
write(lo, string'("[rx] frames_sent=") & integer'image(bytes_sent) &
string'(" bytes_out=") & integer'image(g_bytes) &
string'(" correct=") & integer'image(g_correct) &
string'(" corrupted=") & integer'image(g_corrupt) &
string'(" framing_err=") & integer'image(g_ferr));
writeline(output, lo);
write(lo, string'("[rx] frames rejected at the start-bit re-check = ") &
integer'image(g_rejected));
writeline(output, lo);
write(lo, string'("[rx] back-to-back frame pairs tested = ") &
integer'image(g_pairs));
writeline(output, lo);
write(lo, string'("[rx] spurious bytes the transmitter never sent = ") &
integer'image(g_extra));
writeline(output, lo);
write(lo, string'("[the whole point] model/receiver disagreements = ") &
integer'image(n_mispredict));
writeline(output, lo);
write(lo, string'("--- tolerance: bytes surviving out of 256, by transmitter period ---"));
writeline(output, lo);
write(lo, string'(" offset 13 14 15 16 17 18 19"));
writeline(output, lo);
for si in 0 to 2 loop
write(lo, string'(" "));
write(lo, offsets(si), right, 6);
for pi in 0 to 6 loop
write(lo, string'(" "));
write(lo, surv(pi, si), right, 6);
end loop;
writeline(output, lo);
end loop;
if n_reach /= 5376 then
write(lo, string'("FAIL: exhaustive sweep incomplete")); writeline(output, lo);
errors := errors + 1;
end if;
if errors = 0 then
write(lo, string'("PASS: 0 errors in ") & integer'image(checks) & string'(" checks"));
else
write(lo, string'("FAIL: ") & integer'image(errors) &
string'(" errors in ") & integer'image(checks) & string'(" checks"));
end if;
writeline(output, lo);
done <= true;
wait;
end process;
end architecture;What the third language found
A defect in its own random generator — and not on this chapter. The same LCG was reused in the next chapter, where it made the random phase produce zero address matches out of 400 tokens. Tracing that back condemned the generator here too.
What the VHDL port also provides, on every run, is a cross-check on the check count. The full runs report 24,523 checks in Verilog and SystemVerilog and 24,517 in VHDL. That difference of −6 is fully accounted for: VHDL's independent random stream rejected 135 frames at the start-bit re-check where the Verilog stream rejected 132, and a rejected frame runs 2 checks where an accepted frame runs 4, so 3 × (2 − 4) = −6.
A check-count difference that reconciles exactly against an independently reported counter is evidence the two benches are doing the same work. A difference that does not reconcile is the first sign that one of them is quietly skipping something — and it is how the generator defect above was caught rather than shipped.
14. Assertions
The properties this design must satisfy, written as SVA. These express the local obligations — the ones that are true cycle by cycle without needing the geometric model:
// ---------------------------------------------------------------------
// Properties for async_rx_framer.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus Verilog does not support
// concurrent assertions, so every number published here comes from the
// procedural checks in the testbenches instead. These are the same
// obligations in the form a commercial simulator or a formal tool would
// take, and they are included because the translation is where most of
// the value of writing properties down actually is.
// ---------------------------------------------------------------------
module async_rx_framer_sva #(parameter integer DIVISOR = 16) (
input logic clk,
input logic rst_n,
input logic rx,
input logic [7:0] sample_offset,
input logic byte_valid,
input logic framing_error,
input logic [2:0] state,
input logic [31:0] n_bytes,
input logic [31:0] n_starts,
input logic [31:0] n_false_start
);
localparam logic [2:0] S_IDLE = 3'd0, S_START = 3'd1,
S_DATA = 3'd2, S_STOP = 3'd3;
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// ---- 1. byte_valid is a single-cycle pulse ----
// A byte that is announced for two cycles is counted twice by any
// consumer that simply samples the flag.
property p_bv_pulse;
byte_valid |=> !byte_valid;
endproperty
a_bv_pulse : assert property (p_bv_pulse);
// ---- 2. framing_error only ever accompanies a byte ----
// The flag qualifies a byte. Asserting it on its own would make it
// unattributable, and a consumer could not tell which byte was bad.
property p_fe_with_bv;
framing_error |-> byte_valid;
endproperty
a_fe_with_bv : assert property (p_fe_with_bv);
// ---- 3. a byte is only ever produced from S_STOP ----
// This is the structural statement of "the receiver counted ten cells
// before delivering anything".
property p_bv_from_stop;
byte_valid |-> $past(state) == S_STOP;
endproperty
a_bv_from_stop : assert property (p_bv_from_stop);
// ---- 4. every start detection is accounted for exactly once ----
// n_starts increments only on the IDLE -> START transition. A design
// that double-counts here would overstate its own activity, and the
// false-start rate computed from these counters would be wrong.
property p_start_counted;
(state == S_IDLE && !rx) |=> (n_starts == $past(n_starts) + 32'd1);
endproperty
a_start_counted : assert property (p_start_counted);
// ---- 5. a rejected start bit produces no byte ----
// The liveness/safety pair for the re-check: rejecting must be silent,
// not merely different.
property p_reject_silent;
(n_false_start != $past(n_false_start)) |-> !byte_valid;
endproperty
a_reject_silent : assert property (p_reject_silent);
// ---- 6. THE GUARD (the property that took three attempts) ----
// Once a byte has been delivered, the receiver must not detect another
// start bit until its stop cell has finished. Expressed locally: from
// byte_valid, the state may not reach S_START again before the
// remaining stop cycles have elapsed.
//
// This is the assertion form of section 11's e_restart check, and it
// is strictly stronger -- the procedural version samples a counter
// once per frame, while this holds at every cycle.
property p_stop_guard;
byte_valid |=> (state != S_START) until (state == S_IDLE);
endproperty
a_stop_guard : assert property (p_stop_guard);
// ---- 7. the byte counter moves ONLY when a byte is announced ----
// Stated in this direction on purpose. "byte_valid implies the counter
// incremented" is the easy half and is not the half that matters: a
// counter which also advances on some other condition would satisfy it
// while making every measurement in the testbench unfalsifiable,
// because the published totals are read from these counters.
property p_bytes_track;
(n_bytes != $past(n_bytes)) |-> byte_valid;
endproperty
a_bytes_track : assert property (p_bytes_track);
// ---- COVER: the interesting states are actually reached ----
// A suite of passing assertions over stimulus that never enters S_DATA
// proves nothing. These covers are the denominator.
c_reject : cover property ((state == S_START) ##1 (state == S_IDLE));
c_framing : cover property (byte_valid && framing_error);
c_clean : cover property (byte_valid && !framing_error);
endmodule15. Where UVM Fits
This design is small enough that the procedural benches above are the right tool, and pretending otherwise would be dishonest. But the shape of the verification problem is exactly what UVM is for, and it is worth seeing the mapping because the shape recurs.
The problem has three properties that make it a UVM problem at scale:
- the stimulus is a cross-product sweep over independent dimensions;
- the checker is a model, not a set of expected values;
- the interesting configurations are parameters of the environment (bit period, sample point), not of the transaction.
// ---------------------------------------------------------------------
// UVM structure for async_rx_framer.
//
// NOT SIMULATED IN THIS CHAPTER. Icarus Verilog cannot compile UVM --
// it breaks on virtual method dispatch -- so every number published in
// this chapter comes from the procedural benches. This is the structure
// a production environment would use, and the mapping from the
// procedural bench is exact, which is the point of showing it.
// ---------------------------------------------------------------------
// ---- the transaction is a FRAME, not a bit ----
//
// This is the first real design decision. Modelling bits would make the
// sequence library a timing description and move the geometry into the
// driver, where the scoreboard cannot see it. Modelling frames keeps the
// geometry in one place.
class uart_frame_item extends uvm_sequence_item;
`uvm_object_utils(uart_frame_item)
rand bit [7:0] data;
rand int tx_period; // transmitter cycles per bit cell
rand int sample_offset; // where the receiver samples
// The truncation from section 11. Zero for a well-formed frame; a
// malformed frame is a first-class transaction rather than a special
// case bolted onto the driver, because the guard property can only be
// reached by malformed stimulus.
rand int stop_truncate;
constraint c_reasonable {
tx_period inside {[13:19]};
sample_offset inside {[4:12]};
stop_truncate inside {[0:14]};
}
// The nominal case must stay reachable under randomisation, or the
// regression never tests the configuration that actually ships.
constraint c_nominal_reachable {
soft tx_period == 16;
soft sample_offset == 8;
soft stop_truncate == 0;
}
function new(string name = "uart_frame_item");
super.new(name);
endfunction
endclass
// ---- the driver owns the CELL GEOMETRY and nothing else ----
//
// It converts a frame into levels on a wire. It does not know what the
// receiver is supposed to capture -- that belongs to the scoreboard, and
// keeping it out of the driver is what stops the environment from
// checking the driver against itself.
class uart_driver extends uvm_driver #(uart_frame_item);
`uvm_component_utils(uart_driver)
virtual uart_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
uart_frame_item tr;
seq_item_port.get_next_item(tr);
vif.sample_offset <= tr.sample_offset;
drive_frame(tr);
seq_item_port.item_done();
end
endtask
task drive_frame(uart_frame_item tr);
int cells = 10 * tr.tx_period - tr.stop_truncate;
for (int t = 0; t < cells; t++) begin
vif.rx <= cell_value(t / tr.tx_period, tr.data);
@(posedge vif.clk);
end
endtask
function bit cell_value(int c, bit [7:0] d);
if (c <= 0) return 1'b0;
else if (c <= 8) return d[c-1];
else return 1'b1;
endfunction
endclass
// ---- the scoreboard IS the geometric model ----
//
// Identical arithmetic to predict_at() in the procedural benches. The
// value of the UVM form is not a better model; it is that this model can
// be reused unchanged across a whole regression of configurations.
class uart_scoreboard extends uvm_scoreboard;
`uvm_component_utils(uart_scoreboard)
uvm_analysis_imp #(uart_frame_item, uart_scoreboard) frame_ap;
uvm_analysis_imp #(byte_item, uart_scoreboard) byte_ap;
int unsigned n_checked, n_rejected, n_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
frame_ap = new("frame_ap", this);
byte_ap = new("byte_ap", this);
endfunction
// Pure arithmetic on two periods, parametrised by the detection edge
// and the transmitter's cell origin -- see section 5 for why those two
// are parameters and not constants.
function void predict(uart_frame_item tr, int E, int org,
output bit [7:0] exp_byte,
output bit exp_ferr, output bit exp_reject);
int s, c;
exp_byte = 8'h00;
s = E + tr.sample_offset;
c = (s - org) / tr.tx_period;
exp_reject = (cell_value(c, tr.data) == 1'b1);
for (int k = 1; k <= 8; k++) begin
s = E + k * 16 + tr.sample_offset;
c = (s - org) / tr.tx_period;
exp_byte[k-1] = cell_value(c, tr.data);
end
s = E + 9 * 16 + tr.sample_offset;
c = (s - org) / tr.tx_period;
exp_ferr = (cell_value(c, tr.data) == 1'b0);
endfunction
// ... write_frame / write_byte pair the two streams and compare.
endclass
// ---- coverage is the SWEEP, expressed once ----
//
// The procedural bench encodes the 5376-point cross-product as three
// nested for-loops. Here it is a covergroup, which is the same
// information in a form the regression can report against.
class uart_coverage extends uvm_subscriber #(uart_frame_item);
`uvm_component_utils(uart_coverage)
covergroup cg with function sample(uart_frame_item tr);
// Every byte value. Not a bucketed range -- section 6 showed that
// the surviving byte values are a structured subset, so bucketing
// would hide exactly the effect being measured.
cp_data : coverpoint tr.data { bins b[256] = {[0:255]}; }
cp_per : coverpoint tr.tx_period { bins p[] = {13,14,15,16,17,18,19}; }
cp_soff : coverpoint tr.sample_offset { bins s[] = {4,8,12}; }
// The cross IS the measurement. A regression that covers the three
// dimensions separately and not their cross has not measured a
// tolerance budget at all -- it has measured three margins.
x_budget : cross cp_per, cp_soff;
// The guard from section 11 needs malformed frames, so the
// truncation is a covered dimension rather than an error injection.
cp_trunc : coverpoint tr.stop_truncate { bins t[] = {[0:14]}; }
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg = new();
endfunction
function void write(uart_frame_item t);
cg.sample(t);
endfunction
endclass16. Mutation Testing
Seven defects, injected one at a time into all three languages, each one a mistake a competent engineer could make. Every replacement is asserted by the generator: a mutation that fails to apply silently produces a score of zero that is indistinguishable from an uncaught defect.
The scores are decomposed into directed and random. That decomposition is the real test of the suite: a mutation only the random phase finds has not been characterised, and a directed column that disagrees across languages means the mutation covers a different statement set in one of them.
| # | the injected defect | V-all | V-dir | SV-all | SV-dir | VHDL-all | VHDL-dir |
|---|---|---|---|---|---|---|---|
| BASE | unmodified design | 0 | 0 | 0 | 0 | 0 | 0 |
| K1 | sample at the cell end instead of the offset | 6129 | 5533 | 6129 | 5533 | 6126 | 5533 |
| K2 | the start bit is never re-checked | 150 | 146 | 150 | 146 | 153 | 146 |
| K3 | the shifter fills from the wrong end | 11560 | 10414 | 11560 | 10414 | 11564 | 10414 |
| K4 | the stop bit is never checked | 1046 | 944 | 1046 | 944 | 1033 | 944 |
| K5 | IDLE at the stop sample, losing the guard | 120 | 120 | 120 | 120 | 120 | 120 |
| K6 | nine data bits shifted instead of eight | 12864 | 11588 | 12864 | 11588 | 12860 | 11588 |
| K7 | phase not cleared entering S_DATA | 13789 | 12458 | 13789 | 12458 | 13806 | 12458 |
Every mutation is killed, and every one is killed by directed stimulus alone. The directed column is identical in all three languages at all seven rows — 5533, 146, 10414, 944, 120, 11588, 12458 — which is what "the same defect, covering the same statements" looks like when it is true.
Reading the table
BASE reads 0 in all six columns. Including the three directed-only columns, which is a separate check from the full run and has caught problems in this series three times: a suite whose "exhaustive" claim silently depends on its random phase passes the full run and fails directed-only.
K2 scores 146 and that is not weak. Removing the start re-check does not corrupt data — it makes the receiver accept noise. The only stimulus that can see it is the glitch phase, which is 16 pulses, and 146 failures from 16 pulses means every single one of them is detected several times over. A low score is only a problem when the mechanism had many chances to be caught.
K1 scores 5533 directed and has the most instructive shape. Sampling at the end of the bit cell still works perfectly against a matched transmitter. It fails across almost the whole rest of the sweep, because the end of the cell is exactly where the accumulated error is largest. A suite that tested only the nominal rate would score zero on K1 — and the nominal rate is what most bring-up tests use.
K5's 120 is entirely directed and entirely from one phase. The random phase contributes nothing, and it never could: the guard is unreachable without a malformed transmitter, and a random generator producing well-formed frames will never produce one. This is the clearest case in the module of random stimulus being structurally incapable of reaching a property, rather than merely unlucky.
The VHDL columns differ and should
VHDL's all column differs from the other two at six of seven rows (6126 vs 6129, 153 vs 150, 11564 vs 11560, 1033 vs 1046, 12860 vs 12864, 13806 vs 13789) while its directed column matches exactly. That is the signature of an independent random stream and it is the intended design of the experiment.
Run totals
| steps | checks | exhaustive reach | errors | |
|---|---|---|---|---|
| Verilog, full | 6164 | 24,523 | 5376 / 5376 | 0 |
| Verilog, directed only | 5564 | 22,131 | 5376 / 5376 | 0 |
| SystemVerilog, full | 6164 | 24,523 | 5376 / 5376 | 0 |
| SystemVerilog, directed only | 5564 | 22,131 | 5376 / 5376 | 0 |
| VHDL, full | 6164 | 24,517 | 5376 / 5376 | 0 |
| VHDL, directed only | 5564 | 22,131 | 5376 / 5376 | 0 |
The directed-only rows are identical across all three languages in every column. The full rows differ only in VHDL's check count, reconciled in section 13.
17. What This Model Does Not Cover
Stated plainly, because a measurement whose limits are not stated will be quoted outside them.
No metastability. rx is assumed already synchronised. RTL simulation
cannot model metastability — a simulator resolves a setup violation to a
definite value and a real flip-flop does not. A production receiver needs a
synchroniser, and its latency adds a constant to every sample instant. A
constant shifts the tolerance window; it does not change its width, which is
why it is outside this model without invalidating it.
No oversampling and no majority vote. A real 16× UART samples three times around the centre and votes. That widens the effective window against noise, and does nothing at all about frequency error — the budget in section 8 is unchanged by voting, because it is a geometry result. Conflating the two is the most common error in reasoning about UART robustness.
No parity and no break detection. Both are real UART features and both are orthogonal to the mechanism being measured.
Whole-cycle bit periods only. Real clock error is fractional. Whole
cycles keep the model's floor() off the boundary so the measurement is
exact rather than arguable; the closed form in section 8 is continuous and
covers the fractional cases.
One stop bit. Two stop bits widen the guard and slightly relax the slow-transmitter limit. The derivation extends directly; the measurement here does not cover it.
The USB side is described, not built. SYNC, NRZI, bit stuffing and EOP are discussed and not implemented in this chapter. The claim being measured here is about UART's budget; the claim about USB is that the mechanisms listed remove the accumulating term, which is a structural argument rather than a measurement. The USB RTL in this track lives in modules 12 through 22.
18. The Interview Answer
If you are asked to compare USB and UART, the table is the wrong answer even when every row is right. Three sentences, in this order:
1. Name the mechanism, not the feature. "UART learns bit timing from a single falling edge and then extrapolates every remaining sample instant from a local timer. USB locks a recovered clock to a SYNC field and keeps it locked with bit stuffing."
2. Name the consequence with a number. "That means UART's timing budget is per-frame and shrinks as roughly 1/N — about ±5% for an 8-bit frame, under ±1% by 64 bits. USB's budget is per-transition and does not shrink with packet length at all."
3. Draw the conclusion the interviewer was actually looking for. "Which is why a UART frame is 8 bits and a USB packet can be 1024 bytes. The frame size is not a convention, it is the largest frame a free-running receiver can extrapolate across."
If there is time, the best follow-up detail is the inversion from section 9: USB requires a 40× more accurate clock at high speed and is far more robust, because the two accuracy budgets are spent on entirely different problems. Knowing why that is not a contradiction is a good signal that you understand both links rather than having memorised a comparison.
19. What Carries Forward
The method of this chapter is the method of the next three: find the one mechanism the two protocols do not share, build it, and measure what it costs.
Here that mechanism was synchronisation from a single edge, and the cost was a tolerance budget that shrinks as 1/N. The next chapter takes SPI, whose missing mechanism is not timing at all — SPI has a clock wire, so it has no tolerance budget worth measuring. What SPI lacks is any way to ask a device who it is, and the cost of that shows up as a wire per peripheral and a board that cannot change after it is laid out.
Continue learning
Related tutorials
- Related topic
USB vs SPI
SPI selects a peripheral with a wire routed at layout time and USB with an address the host assigned — so a chip-select contention is invisible to every slave (0 of 11) while a duplicate USB address is detected every time (274 of 274).
- Related topic
USB vs Ethernet
USB has one authority that assigns every address; Ethernet has none, so a switch infers the topology from traffic — and an inferred table is wrong 294 times out of 1065 where an assigned one is wrong 0 times out of 130.
- Related topic
USB vs PCIe
USB holds one transaction outstanding per endpoint so its throughput is exactly 1/(latency+1) whatever the wire carries; PCIe tags many at once and needs exactly latency+1 tags to saturate — both measured as closed forms over 64 points.
- Related topic
USB Flash Drives
Every flash drive speaks Bulk-Only Transport — CBW out, data, CSW in. The spec enumerates thirteen cases of host-versus-device disagreement, six of them fatal, and the two rarest are the ones that ship broken.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
