UART · Module 14
Building a UART Verification Plan
Deriving checkable requirements from framing, timing, error and status behaviour, and turning the configuration space into a coverage model that reports what testing actually reached.
Thirteen modules produced a UART that works. Every result in them came from a testbench written by the same person who wrote the RTL, checking what that person thought to check — and the most interesting defects were found not by testing harder but by a new consumer arriving with a new access pattern (Chapter 13.4 §5 being the clearest case).
That is the problem a verification plan exists to solve. Not more tests — a stated, checkable set of requirements, and an honest measurement of which ones have actually been exercised.
1. Verify the Specification, Not the Implementation
The distinction sounds pedantic and is the single most consequential decision in a verification plan.
A test written from the RTL asks: does the design do what it does? It will pass. It will keep passing after a refactor that preserves the bug. It encodes the implementation's assumptions, including the wrong ones.
A test written from the specification asks: does the design do what it must? It can fail, which is the only property that makes a passing test informative.
The practical test of which kind you have written: could this check have been written before the RTL existed? If reading the RTL was necessary to write it, it is at best a regression lock and at worst a tautology.
2. From Specification to Checkable Requirements
A requirement is checkable when it names an observable and a condition. "The receiver shall be robust" is not a requirement. Working through the specification the previous modules built:
| # | Requirement | Observable | Source |
|---|---|---|---|
| R1 | A frame is start (space), DATA_W data bits LSB-first, optional parity, stop (mark) | the line | 3.1, 3.2 |
| R2 | Parity is computed over the data bits only, per the configured mode | the line | 3.3 |
| R3 | A received byte is delivered with its own parity and framing verdict | read port | 6.4 |
| R4 | A framing error is reported when the stop interval is not at mark | read port | 9.1 |
| R5 | A parity error is reported and the byte is still delivered | read port | 6.4 |
| R6 | A byte lost to a full queue is reported as overrun | status | 9.3 |
| R7 | A line held low past a frame time is reported as a break | status | 9.4 |
| R8 | The receiver recovers on the next frame after any error | read port | 6.2 |
| R9 | Transmit order is preserved; no byte is duplicated or dropped | the line | 7.4 |
| R10 | A configuration change does not reformat a frame in flight | the line | 11.3 |
Every one names something visible at a boundary. Not one requires looking inside the design, which is what makes them independently checkable.
R5 is the one people get wrong. The intuitive requirement is "a parity error is reported", and the actual specification also says the byte is still delivered — because the receiver cannot know whether the consumer wants it. A plan that omits the second half will happily pass a design that discards the byte.
3. The Configuration Space Is the Coverage Model
A UART's behaviour depends on more than its data. The plan has to enumerate what varies:
| Axis | Bins | Why it matters |
|---|---|---|
| parity mode | none, even, odd, mark | changes frame length and the parity rule |
| data pattern | all-zero, all-one, mixed, single-one, single-zero | 11.4 §2's false break needed all-zero |
| corruption | none, parity, stop, short stop, glitch, break | each maps to a requirement |
| baud offset | slow, nominal, fast | 4.5's budget |
The cross of parity mode against corruption is the one that earns its keep — 24 combinations, and it is where the surprises live. Chapter 14.6 runs all 24 and finds one.
// COVERAGE — written by hand, because Icarus Verilog supports no covergroups.
// The bins are the same ones a covergroup would declare; what is lost is the
// tool's bookkeeping, not the thinking. Chapter 14.1 is about choosing them.
localparam int N_PAR = 4; // none, even, odd, mark
localparam int N_PAT = 5; // all-zero, all-one, alternating, single-1, single-0
localparam int N_ERR = 6; // ERR_NONE .. ERR_BREAK
localparam int N_BAUD = 3; // slow, nominal, fast
int cp_parity [0:N_PAR-1];
int cp_pattern[0:N_PAT-1];
int cp_err [0:N_ERR-1];
int cp_baud [0:N_BAUD-1];
int cross_pe [0:N_PAR-1][0:N_ERR-1]; // parity mode x error kindData patterns are binned by shape, not by value. Binning all 256 byte values would report 256 bins and tell you nothing about which kinds were tried; binning by weight — all-zero, all-one, single-one, single-zero, mixed — names the shapes that break things. All-zero is what produced the false break in Chapter 11.4; single-one is what Chapter 7.5 needed to catch a truncated parity mode.
4. What Coverage Actually Told Us
Here is the result that justifies the whole apparatus. The assembled environment was run against the IP with a reasonable-looking set of directed tests: eight data patterns in 8N1, then three corruption cases in 8E1, then eight transmitted frames checked by an independent monitor.
Everything passed. Nineteen scoreboard comparisons, zero mismatches.
scoreboard: 19 matches, 0 mismatches
---- functional coverage ----
parity mode 2/4 bins
data pattern 5/5 bins
error injection 3/6 bins
baud offset 1/3 bins
parity x error 4/24 cross binsClosing the holes is Chapter 14.6's work. Running the full cross plus the baud corners:
---- functional coverage ----
parity mode 4/4 bins
error injection 6/6 bins
baud offset 3/3 bins
parity x error 24/24 cross binsAnd that run found a real behaviour nothing else had — one combination out of twenty-four behaving differently from its twenty-three neighbours. Chapter 14.6 §3.
5. What Coverage Does Not Tell You
It does not mean the checks were good. A run with 100% coverage and no scoreboard reaches every bin and verifies nothing. Coverage measures stimulus; checking is a separate axis, and a plan needs both.
It does not mean the model was right. The bins are a human's guess about what matters. A missing axis is invisible — and the DMA access pattern that found Chapter 13.4's defect was not in any coverage model, because nobody had thought of "how fast is the queue drained" as an axis until a DMA engine existed.
It does not weight bins by risk. A cross bin exercised once counts the same as one exercised ten thousand times, and "parity mode × corruption" bins are not equally likely to break.
100% is not the goal; it is the floor of the space you thought of. The useful question is not whether coverage closed, but which axes are missing — and the honest answer is discovered when something new is attached, which is the pattern every module since 11 has repeated.
6. The Plan Itself
A usable plan is short. For this UART:
| Section | Content |
|---|---|
| Requirements | R1–R10 of §2, each with its observable and its source chapter |
| Coverage model | the four axes and the one cross of §3 |
| Stimulus | a serial BFM that can drive any baud, any format, and six corruptions — 14.2 |
| Checking | an independent monitor, predictor and scoreboard — 14.5 |
| Corner list | the combinations deliberately targeted — 14.6 |
| Exit criteria | all requirements checked, coverage model closed, zero mismatches |
The exit criteria are three conditions and not one. A plan whose exit criterion is "all tests pass" has no way to notice §4's result.
7. Closing the Loop: Which Check Covers Which Requirement
A plan that lists requirements and a suite that lists checks are two documents until something connects them. This is that connection, for the components this module publishes.
| # | Requirement | Covered by | Where |
|---|---|---|---|
| R1 | frame is start / LSB-first data / parity / stop | the exhaustive predictor walk — 30,720 frames, every slot | 14.5 |
| R1 | …and the driver actually emits that | 480 format combinations compared slot-by-slot against the model | 14.2 |
| R2 | parity over the data bits, per mode | the walk's population-count check, all 4 modes × 5 widths | 14.5 |
| R4 | framing error when the stop is not MARK | ERR_STOP injected; monitor reports framing, not parity | 14.4 |
| R5 | parity error reported and the byte still delivered | ERR_PARITY injected; both halves asserted separately | 14.4 |
| R7 | a line held low past a frame is a break | ERR_BREAK injected; exactly one start edge, one framing error | 14.4 |
| — | the environment's own timing tolerance | 21-point baud sweep, measured against 0.5/8.5 | 14.4 |
Coverage, written the way this toolchain allows. Icarus Verilog 13.0 has no covergroups — confirmed by a minimal test, not assumed — so the model is arrays of counters. The shape matters more than the syntax:
// The configuration cross of section 3, as counters. Every bin is a
// REQUIREMENT-bearing axis, not a convenient one: widths and parity modes
// because R1 and R2 are defined per format, error kinds because R4, R5 and
// R7 each need their own injection, and baud offset because the tolerance
// window is the one continuous axis in the design.
integer cov_width [5:9];
integer cov_parity [0:3];
integer cov_stop [2:4];
integer cov_err [0:5];
integer cov_baud [0:20]; // -10% .. +10% in 1% steps
// Crosses are the point; the marginals are nearly free and nearly useless.
integer cov_fmt_x [5:9][0:3][2:4];
task cov_sample;
input integer nb, pm, sh, er, baud_idx;
begin
cov_width[nb]++; cov_parity[pm]++; cov_stop[sh]++;
cov_err[er]++; cov_baud[baud_idx]++;
cov_fmt_x[nb][pm][sh]++;
end
endtask
// And the check that makes coverage mean something: every bin non-zero.
task cov_report;
integer nb, pm, sh, holes;
begin
holes = 0;
for (nb = 5; nb <= 9; nb = nb + 1)
for (pm = 0; pm <= 3; pm = pm + 1)
for (sh = 2; sh <= 4; sh = sh + 1)
if (cov_fmt_x[nb][pm][sh] == 0) begin
holes = holes + 1;
$display(" HOLE: width=%0d parity=%0d stop=%0d never driven",
nb, pm, sh);
end
$display(" format cross: %0d of 60 bins hit, %0d holes", 60-holes, holes);
end
endtask8. Verification of the Verification
Test the testbench before trusting it. The BFM of Chapter 14.2 is checked against itself — driver into monitor, no DUT — before either is pointed at the design. If the driver and monitor disagree about a frame, the problem is in the environment, and finding that out while debugging the DUT is expensive.
pass 8N1 0x55 recovered
pass 8E1 0xA5 with flipped parity -> parity error
pass stop held at SPACE -> framing error
pass 7O1 0x2A recovered
== 8 checks, 0 failures ==Inject a known defect and confirm the environment catches it. An environment that has never failed has not been shown to be able to fail. The error-injection modes exist partly for this: ERR_PARITY must produce a reported parity error, and if it does not, the checker is broken rather than the design.
Check that the predictor is not the design. Grep the testbench for imports of the design's packages and for hierarchical references into the DUT. Both are mechanical checks and both catch §1's leaks.
9. Understanding Check
10. Summary
Verify the specification, not the implementation. The practical test is whether the check could have been written before the RTL — and if not, it is a regression lock rather than verification.
Three leaks to guard against: peeking at internal state, sharing the design's own definitions, and borrowing its timing. Each looks like reuse and each removes the independence that makes a mismatch mean something.
A requirement is checkable when it names an observable and a condition. Ten of them come straight out of Modules 3 through 11, and the one most often stated wrongly is that a parity error must be reported and the byte still delivered.
The configuration space is the coverage model: parity mode, data pattern shape, corruption kind and baud offset — with the parity-by-corruption cross as the axis that earns its keep.
The result that justifies all of it: a competent set of directed tests produced 19 matches and 0 mismatches while reaching 4 of 24 cross bins and 1 of 3 baud bins. The tests were not bad; a green run simply is not evidence of completeness.
Coverage measures stimulus, not checking, not quality, and not the axes nobody thought of. 100% is the floor of the space you imagined — and the defects that mattered most in this curriculum were each found when something new was attached and brought an axis with it.
11. What Comes Next
The plan names a stimulus generator it does not yet have. Chapter 14.2 builds it: a serial bus-functional model that can drive a UART line at an arbitrary bit period — including deliberately wrong ones — in any frame format, with six kinds of corruption available on demand.
It is the piece that makes the baud axis of §3 reachable at all, and the chapter ends by measuring something the design has never been asked: exactly how much baud error this receiver tolerates.
Browse the full path on the UART tutorials index. For the timing budget the baud axis exercises, read back to Chapter 4.5.
Continue learning
Related tutorials
- Related topic
Case Study: Verifying and Debugging a Production UART IP
A UART taken to sign-off with full functional and code coverage, the silicon escape that followed, the malformed frame measured off the wire in three HDLs, and the missing coverage axis that explains both.
- Related topic
Complete RX RTL Architecture
One synthesizable receiver assembled from the module's five preceding chapters — assumptions stated first, walked block by block with the invariant each maintains, then reviewed the way a reviewer would, including the defects found during its own development.
- Related topic
Stimulus Architecture and the Serial BFM
A bus-functional model that drives and samples a UART line at an arbitrary and deliberately imperfect baud rate — and the receiver tolerance it measures.
- Related topic
Verifying the Transmitter
Checking bit timing, frame structure, ordering and the ready/busy contract against the specification rather than the RTL — and measuring the fractional baud generator from outside.
Where this fits
Part of the UART curriculum.
