Skip to content
VLSI Mentor

C++ Part II · Module 27 · C++ for Semiconductor Verification

Building a Small Reference Model End to End

The module closes by assembling its pieces into one flow that actually runs, because the individual chapters each measured one property and the thing worth knowing is the order they go in. That order has a load-bearing first step which almost every real flow skips: the model is validated against an authority outside the project before it is permitted to judge anything. Here that authority is a published CRC check value, and the model reproduces it exactly — which is the only evidence in the whole flow that does not depend on something else inside the project being right. Then the model becomes the oracle for a real Verilog design through the co-simulation route Chapter 27.1 found working, comparing every byte rather than stopping at the first disagreement. Then the flow is run twice: against the correct design, which passes 256 of 256 bytes, and against the same design with one bit changed in its polynomial, which fails 256 of 256 with a bit histogram whose shape names the fault class on sight. The last of those runs is not a demonstration of the bug. It is the evidence that the flow is capable of failing at all.

Expert22 min readC++Part IIVerificationModeling

Part II · Module 27 · Chapter 27.11 · Building a Small Reference Model End to End

1. The Flow, and Why the Order Matters

Five steps, and the ordering is the content:

stepwhat it establishesif you skip it
1. validate the model against outside vectorsthe model matches the specificationChapter 27.4's failure — the regression cannot fail
2. define the interfacewhat crosses the boundary, and at what granularitya per-cycle seam, or a model that cannot be unit-tested
3. compare every itemthe verdict, and a diagnosable reportChapter 27.6's two indistinguishable messages
4. run a mutated DUTthe flow can faila green dashboard that means nothing
5. test the flow itselfdeterminism, work count, the comparatorChapter 27.3's unreproducible oracle

Step 1 is first for a reason that is not convenience. Every other step's evidence is conditional on the model being right: a passing comparison means the DUT and the model agree, a failing one means they disagree, and neither statement is about the specification unless the model's relationship to the specification has been established independently. So the first thing to do is the only thing in the flow whose authority comes from outside it.

The worked example is a CRC-8 accumulator, chosen because it is small enough to read in full and because it has a published check value, which is what makes step 1 possible at all.

2. Step 1: Validate the Model Against an Outside Authority

The specification is CRC-8/SMBUS: polynomial 0x07, initial value 0x00, no reflection, no final XOR. The model is eight lines:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static std::uint8_t crc8(const std::uint8_t* d, std::size_t n)
{
    std::uint8_t crc = 0x00;
    for (std::size_t i = 0; i < n; ++i) {
        crc ^= d[i];
        for (int b = 0; b < 8; ++b)
            crc = (crc & 0x80) ? (std::uint8_t)((crc << 1) ^ 0x07) : (std::uint8_t)(crc << 1);
    }
    return crc;
}

And the catalogued check value for this CRC over the ASCII string "123456789" is 0xF4:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  model CRC-8/SMBUS("123456789") = 0xF4
  published check value           = 0xF4
  model validated                 = YES

That one line is the foundation of everything that follows. It is a comparison against a number that was computed by someone else, from the specification, with no knowledge of this project — so it establishes what no amount of co-simulation can: that the model implements the named algorithm rather than some neighbouring algorithm that happens to be self-consistent.

Three properties make it strong evidence.

The authority is external. Chapter 27.4's whole argument is that a model's answers must not derive from the thing being judged; a published check value is as far from the DUT as it is possible to get.

It is a single sharp test. A CRC check value is sensitive to every parameter — change the polynomial, the initial value, the reflection or the final XOR and it changes. One match pins down the entire configuration, which is why CRC specifications publish them.

And the step is exit-coded, so it is a gate rather than a report: the validation program returns 0 only on a match, and the flow runs it before the simulation. If step 1 fails, nothing else should run, because a mismatch downstream would be uninterpretable.

What to do when the specification publishes no check value, which is the common case: use the specification's own worked examples, a conformance suite, a vector set from the standards body, or an independent implementation from outside the project. Failing all of those, Chapter 27.3 §7's algebraic properties — a CRC of a message with its CRC appended is zero, an encoder followed by a decoder is the identity — are weaker but still external to the design.

3. Step 2: The Interface

The model is a plain function over plain types, and the simulator glue is a separate layer that never leaks into it. That separation is what lets step 1 exist at all — crc8 above was validated with no simulator in the picture.

The design under test is a clocked accumulator:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module crc8_acc(input clk, input rst, input valid, input [7:0] data, output reg [7:0] crc);
  integer b;
  reg [7:0] c;
  always @(posedge clk) begin
    if (rst) crc <= 8'h00;
    else if (valid) begin
      c = crc ^ data;
      for (b = 0; b < 8; b = b + 1)
        c = (c[7]) ? ((c << 1) ^ `POLY) : (c << 1);
      crc <= c;
    end
  end
endmodule

And the testbench calls one system task per byte, passing the stimulus and the DUT's output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$crc_check(data, crc);

Three decisions about that seam are worth naming, because they are the ones Chapter 27.10 §6 warns about.

The granularity is per byte, not per cycle. The model is advanced once per accepted data item, which is the coarsest granularity the problem allows. A per-cycle crossing would be a thousand times more calls for the same information.

The DUT's value is passed in, not read out by the glue. The testbench already samples crc at the right moment; having the VPI layer go hunting for a signal handle would put timing knowledge into C++, which is exactly what Chapter 27.1 §3 says not to do.

And the model holds its own state. The C++ side accumulates its CRC across calls, so the comparison is of the accumulated value — a predictor in Chapter 27.7 §4's sense — and a divergence at byte 7 reports at byte 7.

4. Step 3 and Step 4: Judge the DUT, and Prove the Flow Can Fail

Two runs of the identical flow. The design has a BUG switch that changes the polynomial from 0x07 to 0x06 — one bit of one constant.

The correct design:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  correct DUT:
  bytes checked : 256
  mismatches    : 0
  VERDICT       : PASS

The same design with the polynomial bug:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  DUT with the polynomial bug:
  bytes checked : 256
  mismatches    : 256
  first at byte 0 : model=0x23 dut=0x2E
  bits that ever differed: b0(137) b1(135) b2(125) b3(121) b4(126) b5(133) b6(136) b7(133)
  VERDICT       : FAIL

The second run is the one that gives the first one meaning. A PASS from a flow that has never been shown to fail is a statement about the flow's silence; a PASS from a flow that reported 256 mismatches on a one-bit mutation is a statement about the design. This is Chapter 27.4 §8's habit, and it costs one -DBUG build.

Three things to read in that failing report.

The work count is on the line above the verdict. bytes checked : 256 — without it, mismatches : 0 from a run that checked nothing is indistinguishable from a pass, which is Chapter 23.8 §2's measured defect.

The bit histogram names the fault class. All eight bits appear, with counts between 121 and 137 — diffuse, and roughly even. Compare that with Chapter 27.6 §3's stuck-bit signature, which was b3(99) and nothing else. A wrong polynomial feeds back into every bit position over successive shifts, so its error spreads across all eight; a tied net touches one. The two reports are distinguishable on sight, which is the entire purpose of computing the histogram.

And the first divergence is at byte 0, which tells you the fault is not data-dependent and not a late-onset effect — the very first accumulation already disagrees, so the arithmetic itself is wrong rather than some boundary being mishandled.

5. Step 5: Testing the Flow Itself

Four tests on the flow, none of which involves the DUT.

Run the model twice on the same input and diff the output. Chapter 27.3 §5's universal determinism test, and the one most models lack. It catches an uninitialised member, an unseeded random draw, a container iteration order and a wall-clock dependency in one assertion — all four of Chapter 27.3 §3's hazard candidates, including the one that actually fired.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
const auto a = model.run(input);
const auto b = model.run(input);            // same input, fresh model
assert(a == b);                              // or report, in a test framework

Unit-test the model with no simulator. The validated crc8 is a pure function, so the boundary cases are a few lines each: the empty message, a single zero byte, a single 0xFF byte, the published vector, and a message long enough to wrap the accumulator many times.

Unit-test the comparator with no simulator. Chapter 27.6 §7's list: empty streams, all-matching, exactly one wrong element at a known index, an off-by-one, and one stream shorter than the other.

Assert the work count. bytes checked : 256 must be compared against an expectation, not merely printed. A floor of > 0 catches the empty run; == 256 also catches a stimulus file truncated to three lines, which is Chapter 23.8 §2's stronger form.

The cheapest high-value addition to any flow that lacks it: a test that corrupts exactly one byte of the actual stream and asserts exactly one mismatch at exactly that index. If that test is absent, nothing in the project establishes that the comparison runs at all.

6. The Model as a Library

The structure that makes all of the above possible is that the model is a library and the glue is a thin separate layer.

filerole
the modelplain types, no simulator headers — the validated crc8
the step-1 checkervalidates against 0xF4, exit-coded, no simulator
the glueargument fetching, run state, statistics, reporting
the design and its testbenchVerilog

In the worked experiment behind this chapter the model and its checker are one file (crc8_check.cpp) and the design and testbench are another (crc_dut.v), because each is a few dozen lines. On anything larger the model belongs in its own header and source so the checker, the glue and the firmware build can all include it without duplication.

The model never includes vpi_user.h. That is what lets step 1 and §5's unit tests run in milliseconds with no simulator, and it is what lets the same code be handed to firmware — Chapter 27.1 §7's three consumers.

The glue holds the accumulated state and the statistics, because those are properties of a run, not of the algorithm. Keeping them out of the model is also what makes the model re-entrant enough to be called twice in §5's determinism test.

And the build is the two-step one from Chapter 27.1 §5, because iverilog-vpi compiles at C++11 and the model is C++20:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clang++ -std=c++20 -O2 -fPIC -I<simulator include> -c crc_vpi.cpp -o crc_vpi.o
iverilog-vpi --name=crcmodel crc_vpi.o

7. What This Flow Cannot Catch

Stated explicitly, because Chapter 27.3 §1's third obligation applies to the flow as much as to the model.

not caughtwhywhere it belongs
timing and latencythe model has no clockSVA
X propagationC++ has no third value4-state SystemVerilog
the valid handshake protocolthe flow only sees accepted bytesSVA or the monitor
reset behaviour mid-streamthe stimulus does not exercise ita directed test
a specification errorboth artefacts implement it faithfullyreview, Chapter 27.4 §7
anything the stimulus did not reachcoverage, not comparisona coverage model

The valid row is the one specific to this example and worth noticing. The testbench calls $crc_check only when it has driven a byte, so the model never learns about cycles where valid was low. If the DUT wrongly accumulated on an idle cycle, this flow would not see it — the model and the DUT would simply have consumed different numbers of bytes, and the comparison would report a data mismatch pointing at the wrong thing. The check that valid gates accumulation is a protocol property and belongs in an assertion, not here.

8. The Table

the questionthe answermeasured
what is step 1validate the model against an outside authority§2
what was the authoritythe published CRC-8/SMBUS check value 0xF4§2
did the model matchyes, exit 0§2
why is step 1 firstevery other step's evidence is conditional on it§1, §4 callout
what granularity is the seamper byte, not per cycle§3
the correct DUT256 bytes checked, 0 mismatches, PASS§4
the mutated DUT256 of 256 mismatch, FAIL§4
what does the bit histogram sayall eight bits, 121–137 each — diffuse§4
how is that distinguishableChapter 27.6's stuck bit was b3 only§4
what does step 4 provethe flow can fail — not that the model is right§4 callout
what validates the oracleonly external vectors§4 callout
the universal determinism testrun it twice, diff the output§5
the cheapest missing testcorrupt one byte, assert one mismatch at that index§5
why must the model avoid simulator headersstep 1, the unit tests, and firmware reuse§6
what can this flow never catchtiming, X, the handshake, specification errors, unreached cases§7

9. What to Actually Do

  • Validate the model against an authority outside the project first, and make it an exit-coded gate that blocks the rest of the flow.
  • If no published vectors exist, find the nearest external authority — worked examples, a conformance suite, an independent implementation, an algebraic property — and say in the model's documentation which one you used.
  • Keep the model a library of plain types with no simulator headers, so step 1 and the unit tests need no simulation.
  • Cross the boundary once per data item, with the DUT's value passed in rather than fetched by the glue.
  • Hold run state and statistics in the glue, not in the model.
  • Compare every item, and print the work count on the line above the verdict.
  • Compute the bit histogram — it is what makes a diffuse arithmetic fault distinguishable from a stuck bit.
  • Run the flow against a deliberately mutated DUT every time you build one, and keep the mutation in the repository.
  • Run the model twice on the same input and diff, and unit-test the model and the comparator with no simulator.
  • Assert the work count against an expected number, not merely against zero.
  • Write down what the flow cannot catch, and name where each of those checks lives instead.

10. Mistakes Worth Making Once

Skipping step 1 because co-simulation passes. Agreement between two artefacts is not conformance.

Treating fault injection as validation of the model. It validates the plumbing; an RTL-derived model passes it too.

Letting the model include the simulator's headers. Now it cannot be unit-tested or handed to firmware.

Crossing the boundary per cycle. A thousand times the calls for the same information.

Having the glue hunt for signal handles. Timing knowledge has moved into C++.

Reporting a verdict with no work count. A run that checked nothing passes.

No bit histogram. A wrong polynomial and a stuck bit look the same.

Never running the mutated DUT. The dashboard is green and means nothing.

No run-it-twice test. An uninitialised member in the model, undetected.

Asserting only checked > 0. A stimulus file truncated to three lines passes.

Assuming the flow checks the handshake. It only sees the bytes the testbench chose to report.

11. Interview Relevance

The frozen curriculum marks this Critical and Expert, and it is often the closing question of a verification interview: walk me through building a reference model for a block.

Lead with the order, and put validation first. The model is written from the specification, then validated against vectors from outside the project, and only then allowed to judge a design — because a passing comparison establishes that the DUT and the model agree, which says nothing about the specification unless the model's relationship to the specification was established independently. The concrete example lands well: a CRC-8 model checked against the published check value 0xF4 for "123456789", as an exit-coded gate that blocks the rest of the flow.

Then the flow: a model that is a plain library with no simulator headers, a thin glue layer crossing the boundary once per data item with the DUT's value passed in, and a comparison over every item that prints the work count beside the verdict. Measured, the correct design passed 256 of 256 bytes and a one-bit polynomial change failed 256 of 256.

Then the two points that mark seniority. First, running the mutated design is what makes the passing run meaningful — and it proves the plumbing rather than the oracle, since a model copied from the RTL would also catch a freshly injected mutation. Second, the bit histogram distinguishes fault classes: all eight bits affected roughly evenly is a wrong polynomial, where a single bit in every mismatch is a tied net.

And finish with the limits, because volunteering them is what makes the rest credible: this flow cannot see timing, cannot represent X, and never learns about cycles where valid was low — so the handshake property belongs in an assertion, not in the model.

12. What You Should Be Able to Say Now

  • The five steps, in order, and that validation against an outside authority is first because every later step's evidence is conditional on it.
  • That the model reproduced the published CRC-8/SMBUS check value 0xF4 for "123456789", exit 0, with no simulator involved.
  • Why that evidence is different in kind: it is the only step whose authority comes from outside the project.
  • That the seam is per byte, with the DUT's value passed in rather than fetched by the glue, and the model holding its own accumulated state.
  • That the correct DUT gave 256 bytes checked, 0 mismatches, PASS, and the one-bit polynomial mutation gave 256 of 256 mismatches, FAIL.
  • That the mutated run is what gives the passing run meaning — and that it proves the plumbing, not the oracle.
  • That the failing report's bit histogram is diffuse (all eight bits, 121–137 each), which distinguishes a wrong polynomial from Chapter 27.6's single-bit b3(99) stuck-at signature on sight.
  • That the work count sits above the verdict, and is asserted against an expected number rather than against zero.
  • The four tests of the flow itself: run-it-twice determinism, model unit tests, comparator unit tests, and the one-byte-corrupted self-test.
  • That the model must contain no simulator headers, which is what makes step 1, the unit tests and firmware reuse possible.
  • That this flow cannot catch timing, X propagation, the valid handshake, reset behaviour it did not exercise, a specification error, or anything the stimulus did not reach.

13. Exercises

Exercise 1 — step 1 alone. Write the CRC-8/SMBUS model and validate it against 0xF4. Make the program exit non-zero on a mismatch, and confirm it does by changing the polynomial.

Exercise 2 — break each parameter. Change the initial value, then add a final XOR, then reflect the input, re-running the check value each time. Satisfy yourself that one vector pins the whole configuration.

Exercise 3 — the flow. Build the clocked DUT, the VPI glue and the testbench, and get 0 mismatches on the correct design with the work count printed.

Exercise 4 — prove it can fail. Add the one-bit polynomial mutation behind a build switch and run both. Keep the switch in the repository.

Exercise 5 — the two signatures. Inject a stuck output bit instead, and compare its histogram with the polynomial mutation's. Write down the rule that distinguishes them.

Exercise 6 — determinism. Add the run-it-twice test. Then introduce an uninitialised member into the model and confirm the test catches it.

Exercise 7 — the handshake hole. Make the DUT accumulate on a cycle where valid is low, and confirm this flow misreports it. Then write the assertion that catches it.

Exercise 8 — no published vector. Pick a block with no published check value and decide what your external authority would be. Write it down in the model's documentation.

14. Summary

  • The flow has five steps and the order is the content: validate against outside vectors, define the interface, compare every item, run a mutated DUT, and test the flow itself.
  • Step 1 is first because it is the only step whose authority is external. The model reproduced the published CRC-8/SMBUS check value 0xF4 for "123456789", as an exit-coded gate — and a single CRC check value pins the polynomial, initial value, reflection and final XOR all at once.
  • Every other step is internally referential. Steps 3, 4 and 5 establish relationships between artefacts inside the project; a model, a DUT and a scoreboard can be mutually consistent and all wrong, which Chapter 27.4 measured exhaustively.
  • The interface crosses once per byte, with the DUT's sampled value passed in rather than fetched by the glue, and the model holding its own accumulated state so a divergence reports where it happened.
  • Measured: the correct design gave 256 bytes checked, 0 mismatches, PASS. The same design with one bit changed in its polynomial gave 256 of 256 mismatches, FAIL, first diverging at byte 0.
  • The failing report's bit histogram is diffuse — all eight bits, counts from 121 to 137 — which is the signature of wrong arithmetic, against Chapter 27.6's b3(99) single-bit stuck-at signature. The two are distinguishable without opening a waveform.
  • Running the mutated design is what makes the passing run mean anything — and it validates the plumbing rather than the oracle, because a model copied from the RTL would also catch a freshly injected mutation. Only external vectors validate the oracle.
  • The flow's own tests need no DUT: run the model twice and diff, unit-test the model and the comparator, and assert the work count against an expected number rather than against zero.
  • The model is a library with no simulator headers, which is what makes step 1, the unit tests and firmware reuse possible; the glue holds run state and statistics.
  • And the flow cannot catch timing, X propagation, the valid handshake, unexercised reset behaviour, a specification error, or anything the stimulus never reached — each of which has to live somewhere else, named explicitly.

That closes Module 27. The module's through-line is that almost every property worth having in a verification model is established before the comparison runs — the invariant in the transaction's constructor, the determinism of the oracle, the independence of its derivation, the reproducibility of the stimulus — and that the comparison itself mainly decides how cheaply you will understand the failure it finds.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

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 C++ Programming curriculum.