Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Quick Start: Your First Green Build
  • Part 1: How Chips Get Made6
  • Part 2: Your Workstation5
  • Part 3: Specify and Architect3
  • Part 4: The Design Loop6
  • Part 5: The Verification Ladder6
    • Tests That Can Fail
    • Self-Checking and Golden Model
    • Random Stimulus and Seeds
    • Assertions
    • Coverage
    • Formal in One Lesson
  • Part 6: Automation and Reproducibility5
  • Part 7: Hand-Off to the Back-End3
  • Part 8: When Things Go Wrong3
  • Part 9: Project and Next Steps3
  • Reference4
Loading lesson

‹Part 5: The Verification Ladder
  1. Topics
  2. Start Here: RTL and Verification
  3. Part 5: The Verification Ladder
  4. Self-Checking and Golden Model

Part 5 · The Verification Ladder · Working (W)

Self-Checking and Golden Model

Hand-written expected values stop scaling after a few cycles. A reference model written from the spec table computes the right answer every cycle, and a second check watches the gaps between edges.

On this page

    Note

    Level: Working (W) · Answers: How does a testbench know the right answer without you working it out by hand for every cycle?

    Before this: Tests That Can Fail

    How does a testbench know the right answer without you working it out by hand for every cycle?

    The directed test from lesson 4.2 checks eight edges with expected values written by hand, and that works while the run is short. The strong testbench in this lesson runs 239 checking cycles. Nobody hand-computes 239 answers without mistakes, and a hand-written answer you got wrong teaches the testbench your error. This lesson replaces hand-written answers with a small model of the spec that computes every expected value itself.

    The model answers from the spec, not from the design

    The model is one function, spec_next, with four branches that match the four rows of the spec table: reset wins first, then clear, then a counting event that wraps from 15 to 0 by the width of the register, then holding the old value when nothing is asked. Each rising edge advances the model with the same inputs the design receives, so the two should always agree.

    Independence is the whole point. The function is written from the spec table in docs/spec.md, never by reading the RTL, which is register-transfer level: hardware described as registers and the logic between them. A model copied from the design code repeats the design's mistakes, and then every check passes on broken code and means nothing. The testbench source says this directly: the function is not a copy of the design's branch structure by accident, and when the spec changes, the model changes first and the RTL follows.

    The Python golden model from lesson 3.3, Golden Model and Register Contract, is the same idea in another language. Both models are written from the spec table, and both exist so that two independent readings of one table have to agree before anyone trusts the design.

    Three beats in every cycle: drive, watch, compare

    Each cycle of the testbench has three beats. On the falling edge it drives the new inputs, so every input is stable when the design samples it on the rising edge. Driving and sampling on opposite edges keeps the two programs from tripping over each other.

    Before the edge, the testbench checks that the count did not move since the previous check, 1 ns after driving and still before the rising edge. The spec says the count changes only after a rising edge and that reset is synchronous, so any movement between edges is a bug by definition. This is the check that catches an asynchronous reset: it fires the moment reset acts without waiting for the clock.

    After the edge, the testbench advances the model with the same inputs and compares 1 ns later, once the new count has settled. A mismatch reports the full input combination, the expected and actual values, and the simulation time. Mismatches are counted rather than stopping at the first one, and the run ends with a fatal error unless the count of errors is zero.

    Every directed step hunts one bug class

    The directed phase is not a tour of the inputs. Each step is aimed at one bug class from lesson 5.1, and the table below maps every step to its target. Read it row by row and match each step to the mutant it kills.

    Directed step

    Inputs driven

    Bug class it exposes

    Reset from unknown

    reset asserted, count unknown

    reset_ignored: only reset can define the count here

    Hold at 5 after counting

    idle with a nonzero count

    idle_clears: a hold at 0 would pass a design that clears when idle

    Reset while the count is 5

    reset asserted, count nonzero

    Reset must work from any nonzero value, not only from unknown

    Reset with an event present

    reset and valid together

    reset_last: the one combination where reset-last counts instead of clearing

    Reset with clear and an event

    all three together

    The three-way collision must still resolve to reset

    Clear with an event present

    clear and valid together

    valid_before_clear: the one combination where valid-first counts instead of clearing

    Seventeen counting edges

    valid alone, from 0

    saturates: the run passes 15 to 0, which only a long count reaches

    The stability check, every edge

    any inputs

    async_reset: no post-edge value exposes it, only movement between edges

    After the directed steps comes a 200-cycle random phase with a fixed seed, so the whole tail replays identically on every run. It covers the combinations the directed steps never named, and the next lesson explains where that randomness comes from.

    Five boxes that check each cycle twice

    The figure below shows the structure to hold in your head. The stimulus driver feeds the same inputs to two boxes side by side: the design and the model. The comparator takes both answers and checks them twice per cycle. Look at the two arrows entering the comparator: one carries the design's count, the other the model's answer, and the two labels inside say which check runs on each.

    The strong testbench as five boxes: the stimulus driver feeds the same inputs to the design and the model, and the comparator checks the design's count against the model's answer after each edge and checks stability before each edge.

    Build it in four stages

    The finished testbench is about seventy lines, which is a lot to take in at once. You would not write it in one sitting, and you should not read it that way. Build it in four stages, each one runnable on its own, and watch what each stage adds. The listing after this section is all four stages together.

    Stage 1: the model. Write the spec table as a function, from the spec and not from the design. It takes the current value and the three inputs and returns what the count must become. Nothing else exists yet.

    Diagram
    // snippet: stage 1, the model alone
    function automatic logic [3:0] spec_next(input logic [3:0] cur,
                                             input logic r, c, v);
        if (!r)      return 4'd0;        // reset wins
        else if (c)  return 4'd0;        // clear next
        else if (v)  return cur + 4'd1;  // count (wraps by width)
        else         return cur;         // hold
    endfunction

    Stage 2: one cycle. Add a task that drives the inputs on the falling edge, waits for the rising edge and 1 ns, updates the model, and compares. With this single routine you can already run one cycle and see a verdict.

    Diagram
    // snippet: stage 2, one cycle, after-the-edge check only
    task automatic cycle(input logic r, c, v);
        @(negedge clk);
        {rst_n, clear_count, event_valid} = {r, c, v};
        @(posedge clk);
        model = spec_next(model, r, c, v);
        #1;
        if (count !== model) begin
            errors++; $display("ERROR: expected %0d got %0d", model, count);
        end
    endtask

    Stage 3: directed cases. Call the task with hand-picked inputs, one bug class at a time: reset from unknown, a count to 5, a hold at a nonzero value, reset while the count is nonzero, reset beating valid, clear beating valid, and enough counts to wrap past 15.

    Diagram
    // snippet: stage 3, directed cases
    cycle(0,0,0);                 // reset from unknown
    repeat (5) cycle(1,0,1);      // count to 5
    cycle(1,0,0);                 // hold a NONZERO value
    cycle(0,0,1);                 // reset beats valid

    Stage 4: the extra checks and the random phase. Last, check that the count did not move before the clock edge (which catches a reset that acts without the clock), then let a seeded random generator drive 200 more cycles. The model and the task stay exactly as they were.

    Diagram
    // snippet: stage 4, the random phase added at the end
    repeat (200) cycle($random(seed) & 1,
                       $random(seed) & 1,
                       $random(seed) & 1);

    Each stage answers one question: what is right (the model), how do I compare one cycle (the task), which cases matter (directed), what did I not think of (random).

    Two modules that check 239 cycles

    mini_controller.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    `timescale 1ns/1ps
    // Counts accepted events, 4 bits, wraps 15 -> 0.
    // Priority: reset > clear > count > hold. Reset is synchronous, active low.
    module mini_controller (
        input  logic       clk,
        input  logic       rst_n,
        input  logic       event_valid,
        input  logic       clear_count,
        output logic [3:0] count
    );
        always_ff @(posedge clk) begin
            if (!rst_n)
                count <= 4'd0;
            else if (clear_count)
                count <= 4'd0;
            else if (event_valid)
                count <= count + 4'd1;
        end
    endmodule
    
    `timescale 1ns/1ps

    This listing is the complete strong test: the design plus the testbench that checks it, including the spec_next model, the three-beat cycle task, the directed steps and the seeded random tail.

    Diagram
    # captured terminal output
    $ make all 2>&1 | grep -E '^(PASS|iverilog|vvp|verilator|yosys)' | cut -c1-110
    verilator --lint-only -Wall --top-module mini_controller rtl/mini_controller.sv
    iverilog -g2012 -Wall -s tb_mini_controller -o build/sim.vvp rtl/mini_controller.sv tb/tb_mini_controller.sv
    PASS: mini_controller
    yosys -Q -T -p 'read_verilog -sv rtl/mini_controller.sv; hierarchy -check -top mini_controller; proc; opt; che

    This output was measured on macOS with Icarus Verilog 13.0, Verilator 5.048 and Yosys 0.69+post. Lint runs first, then the simulator compiles both modules and checks all 239 cycles, then synthesis reads the design. The PASS line means every directed and random check matched; any mismatch would report an ERROR line naming the inputs and the time instead.

    Predict first The pre-edge stability check is deleted, leaving only the post-edge comparison against the model. Which mutant from lesson 5.1 now survives the whole run, and why?

    The asynchronous-reset mutant survives. Without the between-edges watch, a reset that acts the moment its input falls looks identical at every post-edge check: each reset edge still expects 0 and still sees 0. Every other mutant still fails on values, because only the timing bug hides completely inside correct-looking samples.

    Swap in a mutant and watch the strong test fail

    Cause a failure yourself with the mutant this testbench was built to catch. The steps below use the reset-last variant, where counting beats reset.

    1. Copy mutants/reset_last.sv over rtl/mini_controller.sv, keeping a copy of the original elsewhere.

    2. Run make sim and watch it fail: an ERROR line names the input combination with reset and an event together, the expected 0, and the simulation time, and make exits with a nonzero status.

    3. Restore the original design file and run make sim again to confirm the run is green before you continue.

    Three ways a self-checking testbench lies to you

    • Building the model from the RTL. A model copied from the design code shares the design's bugs and passes on broken code. Write the model from the spec table, keep it in a function of its own, and change the spec first when behavior must change.

    • Checking only after the edge. Post-edge comparisons see only settled values, so a reset that acts between edges is invisible to them. Check stability before the edge too, on every cycle, because timing bugs live in the gaps that sampling never sees.

    • Stopping at the first mismatch. Here counting errors instead of stopping is a deliberate choice: the pattern across 239 cycles points at the bug class, while a single first error could mean anything. Six wrong counting edges mean the increment; one wrong hold edge means the hold. The run still fails at the end, so nothing is forgiven.

    Rule: the testbench earns trust when a model of the spec produces every expected value.

    Check yourself The testbench resets with an event present only when the count is low. Add one directed step it does not have yet and say what it would catch.

    Count up to 15 and then drive reset asserted with valid high, a cycle(0, 0, 1) step at the top count, and expect 0. Reset must beat the event that would wrap the count, so a design whose wrap logic overrode reset at the top count would fail the new step. None of the lesson's mutants does that, which shows the matrix is a starting set: a new directed step can guard a bug nobody has written yet.

    Check yourself Why does the expected value live in a function instead of a table of hand-written numbers?

    A function covers every input combination on every cycle, all 239 of them including the random tail, from four lines that mirror the spec table. A hand-written table would need one correct entry per cycle, which is exactly the tedious and error-prone work the model removes.

    Check yourself What does the pre-edge check catch that no post-edge check can?

    The asynchronous-reset mutant. Every post-edge value still matches, because each reset edge expects 0 and sees 0, so only the movement between edges exposes it. Without the stability check that mutant survives the whole run.

    Interview

    Asked: "What is a reference model and why must it be independent of the design?"

    Weak answer: A copy of the design logic that double-checks the outputs; it passes when both agree.

    Strong answer: An independent executable statement of the spec: here spec_next, written from the spec table, producing the expected count for the same inputs each cycle. It must be independent because a model copied from the RTL repeats the design's mistakes and passes on broken code. This testbench kills all 7 real-bug mutants, measured on macOS with Icarus Verilog 13.0, because the model and the design are two separate readings of one table.

    Follow-ups: (1) What do you change first when the spec changes, and why? (2) A mutant survives your best test. When is that good news?

    Key takeaways

    • A reference model turns the spec table into expected values for every cycle, so nobody hand-writes 239 answers.

    • Check twice per cycle: stability before the edge catches timing bugs, comparison after the edge catches value bugs.

    • Aim each directed step at one bug class, then let a seeded random phase cover the combinations you did not name.

    Keep going

    Next: Lesson 5.3, Random Stimulus and Seeds. The directed steps end and a 200-cycle random phase begins. The next lesson says where that randomness comes from and how a seed makes chance replayable.

    Related topics

    • RTL course: Self-Checking and Observability

    • Lesson 5.1, Tests That Can Fail

    Previous

    25 / 47

    Next: Random Stimulus and Seeds

    Up next

    Up nextRandom Stimulus and Seeds

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Assertions
    • Coverage
    • Formal in One Lesson