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:
| step | what it establishes | if you skip it |
|---|---|---|
| 1. validate the model against outside vectors | the model matches the specification | Chapter 27.4's failure — the regression cannot fail |
| 2. define the interface | what crosses the boundary, and at what granularity | a per-cycle seam, or a model that cannot be unit-tested |
| 3. compare every item | the verdict, and a diagnosable report | Chapter 27.6's two indistinguishable messages |
| 4. run a mutated DUT | the flow can fail | a green dashboard that means nothing |
| 5. test the flow itself | determinism, work count, the comparator | Chapter 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:
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:
model CRC-8/SMBUS("123456789") = 0xF4
published check value = 0xF4
model validated = YESThat 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:
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
endmoduleAnd the testbench calls one system task per byte, passing the stimulus and the DUT's output:
$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:
correct DUT:
bytes checked : 256
mismatches : 0
VERDICT : PASSThe same design with the polynomial bug:
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 : FAILThe 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.
const auto a = model.run(input);
const auto b = model.run(input); // same input, fresh model
assert(a == b); // or report, in a test frameworkUnit-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.
| file | role |
|---|---|
| the model | plain types, no simulator headers — the validated crc8 |
| the step-1 checker | validates against 0xF4, exit-coded, no simulator |
| the glue | argument fetching, run state, statistics, reporting |
| the design and its testbench | Verilog |
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:
clang++ -std=c++20 -O2 -fPIC -I<simulator include> -c crc_vpi.cpp -o crc_vpi.o
iverilog-vpi --name=crcmodel crc_vpi.o7. 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 caught | why | where it belongs |
|---|---|---|
| timing and latency | the model has no clock | SVA |
| X propagation | C++ has no third value | 4-state SystemVerilog |
the valid handshake protocol | the flow only sees accepted bytes | SVA or the monitor |
| reset behaviour mid-stream | the stimulus does not exercise it | a directed test |
| a specification error | both artefacts implement it faithfully | review, Chapter 27.4 §7 |
| anything the stimulus did not reach | coverage, not comparison | a 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 question | the answer | measured |
|---|---|---|
| what is step 1 | validate the model against an outside authority | §2 |
| what was the authority | the published CRC-8/SMBUS check value 0xF4 | §2 |
| did the model match | yes, exit 0 | §2 |
| why is step 1 first | every other step's evidence is conditional on it | §1, §4 callout |
| what granularity is the seam | per byte, not per cycle | §3 |
| the correct DUT | 256 bytes checked, 0 mismatches, PASS | §4 |
| the mutated DUT | 256 of 256 mismatch, FAIL | §4 |
| what does the bit histogram say | all eight bits, 121–137 each — diffuse | §4 |
| how is that distinguishable | Chapter 27.6's stuck bit was b3 only | §4 |
| what does step 4 prove | the flow can fail — not that the model is right | §4 callout |
| what validates the oracle | only external vectors | §4 callout |
| the universal determinism test | run it twice, diff the output | §5 |
| the cheapest missing test | corrupt one byte, assert one mismatch at that index | §5 |
| why must the model avoid simulator headers | step 1, the unit tests, and firmware reuse | §6 |
| what can this flow never catch | timing, 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
validhandshake, 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
validhandshake, 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.
