Skip to content
VLSI Mentor

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

Register Models in C++

A register map looks like the easiest thing in this module to model — a few words of state, a few masks — and it is the place where models are most reliably wrong. The reason is that hardware registers do not behave like memory. A write of zero to a status bit may leave it alone; a write of one may clear it; a write to a read-only field may be discarded entirely; and a read may change the register it is reading. None of that is visible in a declaration that says the register is thirty-two bits wide, so a model built as a plain array of words predicts software-visible behaviour that the design does not have. The chapter builds a small register model that carries fields, access types and the side effects of reading, and then measures the cost of skipping that: the same seven-step software sequence run against a correct model and against one treating every field as read-write diverged on four steps, with the divergence beginning at the most innocuous operation in the list — a write of zero.

Advanced20 min readC++Part IIVerificationModeling

Part II · Module 27 · Chapter 27.7 · Register Models in C++

1. Why a Register Is Not Memory

Memory has one behaviour: what you wrote is what you read. A register has as many behaviours as the specification gives it, and four of them break the memory analogy outright.

A write may be ignored. A read-only field — a revision number, a status bit the hardware owns — discards software writes. The register does not change and no error is reported.

A write may clear rather than set. A write-1-to-clear field, universal in interrupt status registers, is cleared by writing a 1 to it and left alone by writing a 0. Writing 0x00 to a W1C register changes nothing at all, which is the opposite of what writing zero to memory does.

A read may change the state. A read-to-clear field is cleared by the act of reading it. The second read returns something different from the first, with no write in between.

And the hardware writes it too. Status bits are set by the device, concurrently with software reading them, so the model needs two distinct entry points: what software does, and what hardware does.

A model that treats the map as std::uint32_t regs[N] captures none of this, and §4 measures how quickly it diverges.

2. The Access Types Worth Modelling

accesson a software writeon a software read
RWtakes the valuereturns it
ROignoredreturns it
W1Cbits written 1 are cleared; 0 leaves alonereturns it
RCno effectreturns it, then clears
WOtakes the valuereturns 0 or garbage
RSVDignored, must be written as 0returns 0

Two more that matter in practice and are easy to forget. A write-only field cannot be read back, so a model that stores and returns it will disagree with the design the moment software reads it. And reserved fields are a conformance obligation on software: the specification usually says they must be written as zero and read as zero, which makes a non-zero read a finding rather than a don't-care.

The set above covers the overwhelming majority of real register maps. The ones it does not cover — write-1-to-set, read-to-set, atomic set/clear alias registers — follow the same pattern and are added the same way.

3. The Model

The structure that holds up is a field table plus a value, with the access type as data rather than as code:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
enum class Access { RW, RO, W1C, RC };

struct Field {
    const char*   name;
    unsigned      lsb, width;
    Access        access;
    std::uint32_t reset;
};

Then the two software operations and the one hardware operation, each a loop over the fields:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void Reg::write(std::uint32_t data)              // what hardware does on a sw WRITE
{
    for (const auto& f : fields_) {
        const std::uint32_t in  = (data >> f.lsb) & mask(f);
        const std::uint32_t cur = (value_ >> f.lsb) & mask(f);
        std::uint32_t next = cur;
        switch (f.access) {
            case Access::RW:  next = in;        break;
            case Access::RO:  next = cur;       break;   // writes ignored
            case Access::W1C: next = cur & ~in; break;   // 1 clears
            case Access::RC:  next = cur;       break;   // write has no effect
        }
        value_ = (value_ & ~(mask(f) << f.lsb)) | (next << f.lsb);
    }
}

std::uint32_t Reg::read()                        // NOTE: this MUTATES
{
    const std::uint32_t out = value_;
    for (const auto& f : fields_)
        if (f.access == Access::RC)
            value_ &= ~(mask(f) << f.lsb);               // cleared by reading
    return out;
}

Four design decisions in there are worth naming, because each is a place models go wrong.

read() is not const. It changes the register, so the signature says so. A const read that secretly mutates through a mutable member is how a read-to-clear field gets lost — and the compiler is the only reviewer that never forgets. For the genuinely side-effect-free case there is a separate peek().

The access type is data, not a switch scattered through the code. A new register is a new table, not new logic, which means the model's behaviour can be reviewed against the specification field by field.

Hardware has its own entry point. hwSet("done") is how the device sets a status bit; software's write cannot do it, which is exactly the asymmetry W1C exists to express.

And the reset value lives in the table too, so reset() is derived rather than a separately maintained constant that drifts.

4. The Measurement

A STATUS register from a real-looking specification: done and error are W1C, busy is RO, evt is RC, and rev is a RO revision field reset to 3. Two models — one honouring those access types, one treating every field as plain RW — driven through the identical sequence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  step                         spec-model  RW-everything
  after reset                  0x30        0x30
  hw sets done, busy, evt      0x3D        0x3D
  sw writes 0x00               0x3D        0x00   <-- DIVERGED
  sw writes 0x01 (W1C done)    0x3C        0x01   <-- DIVERGED
  sw reads (returns)           0x3C        0x01
  after that read (RC evt)     0x34        0x01   <-- DIVERGED
  sw writes 0x04 (RO busy)     0x34        0x04   <-- DIVERGED

Four of the seven steps diverge, and the read returns a different value as well. Every number is checkable by hand, which is how I verified the transcript before publishing it:

stepwhy the spec model says what it says
resetrev = 3 in bits 7:4 → 0x30
hw sets done, busy, evt0x30 + b0 + b2 + b3 → 0x3D
sw writes 0x00W1C with 0 clears nothing; RO ignores; RC unaffected → 0x3D unchanged
sw writes 0x01W1C done is cleared → 0x3C
sw readsreturns 0x3C, and the RC bit evt clears → 0x34
sw writes 0x04RO busy ignores it → 0x34

The first divergence is the most instructive one. Writing 0x00 — the most innocent operation in the sequence, and the one a test writer would never think twice about — takes the naive model to 0x00 while the hardware stays at 0x3D. From that point the two models are describing different devices, and every subsequent prediction is wrong for a compounding reason rather than an interesting one.

And the read is where the naive model is not merely wrong but structurally incapable. 0x3C versus 0x34 is the read-to-clear bit disappearing, and an array-of-words model has no concept of a read changing anything, so no amount of masking fixes it. That is why §3 makes read() non-const.

5. The Address Map Around the Registers

Individual registers are only half of it; the map is the other half, and three things in it earn a model.

Decoding, including the gaps. A read from an unmapped address does something specific — returns zero, returns a bus error, returns the last value on the bus — and the specification says which. A model that silently returns zero for every unmapped address agrees with a design that returns an error, and the comparison passes on a decoding bug.

Access width and alignment. A 32-bit register may or may not support a byte write. If it does not, a byte write is ignored or faults; if it does, only the addressed byte changes. This is the register-level version of Chapter 27.8 §4's access-width question and needs the same explicit answer.

Aliases and shadow registers. The same state at two addresses, a set/clear alias pair, a FIFO behind a single address whose read pops. Each of these breaks the one-address-one-value assumption, and each is modelled by making the address lookup a function rather than an index.

The structure that handles all three is a map from address to a register object, consulted by a decode function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::uint32_t Map::read(std::uint32_t addr, unsigned bytes)
{
    if (addr % bytes) return onUnalignedRead(addr, bytes);
    const auto it = regs_.find(addr & ~0x3u);
    if (it == regs_.end()) return onUnmappedRead(addr);     // the SPECIFIED behaviour
    return it->second.read();
}

The two on… hooks are the point. Naming them forces the question "what does the specification say happens here?" at the moment the model is written, rather than leaving a default that quietly becomes the expected behaviour.

6. Generating the Model Rather Than Writing It

A real register map is hundreds of registers and thousands of fields, and hand-writing the table is both tedious and a source of transcription errors.

The table should be generated from the same source the documentation and the RTL come from — IP-XACT, SystemRDL, or whatever spreadsheet the project actually uses. One description, from which the model's table, the header for firmware, and the documentation are all derived.

That has a direct bearing on Chapter 27.4's independence question, and the answer is a genuine subtlety. Generating the model's table and the RTL from one specification source is not the same defect as writing the model by reading the RTL. The shared artefact is the specification, which is the authority both are supposed to have; a transcription error in the generator affects both, but a misreading by either implementer does not. What it does mean is that the generator and its input become the thing to review, and that a bug in the register description is invisible to this comparison — exactly the "specification bug both artefacts implement faithfully" row in Chapter 27.4 §7.

The practical middle ground used on most projects: generate the bulk from the description, and hand-write a small number of directed tests from the prose of the specification for the registers whose behaviour is interesting — the W1C interrupt status, the read-to-clear event register, anything with a side effect on another register.

7. Side Effects Beyond the Register

The hardest registers to model are the ones whose write does something other than store a value.

behaviourwhat the model must do
a write that starts an operationmodel the action, and the busy bit it sets
a write that clears a FIFOdrop the modelled FIFO contents too
a field that gates another register's meaningthe model's state is the pair, not two independents
a write that latches a shadow registertwo pieces of state, and the latch event
a soft reset bitre-run reset() on every register in the affected block

The common thread is that the register model stops being a map of values and becomes a small state machine whose inputs are bus operations. When that happens, it belongs with the rest of the reference model rather than in a separate "register" layer — a soft-reset bit that clears a memory is a register with a memory-sized side effect, and modelling it in isolation from Chapter 27.8's memory model guarantees the two disagree.

The sign that you have crossed this line is simple: if a write to one address changes the predicted value at another, the register map is no longer a collection of independent registers and should not be structured as one.

8. The Table

the questionthe answermeasured
why is a register not memorywrites may be ignored, may clear, and reads may mutate§1
the access types to modelRW, RO, W1C, RC, plus WO and reserved§2
what does a write of 0x00 do to W1Cnothing§4
how many steps diverged4 of 7, plus a different value returned by the read§4
where did divergence beginat the write of 0x00 — the most innocuous step§4
which failure is structural, not a masking bugread-to-clear — an array model cannot express it§4
why is read() non-constit mutates, and the signature should say so§3
what else must drive the modelhardware-side events, or the model falls behind§4 callout
why compare after every operationstate divergence is cumulative; the cause is upstream§4 callout
what makes a long report readablea load(actual) resynchronisation after a mismatch§4 callout
what must the map modelunmapped addresses, access width, aliases§5
is a generated table a loss of independenceno — the shared artefact is the specification§6
when does a register map stop being a mapwhen a write to one address changes another's value§7

9. What to Actually Do

  • Model the access type of every field, as data in a table rather than logic in a function.
  • Make read() non-const and provide a separate peek() for side-effect-free inspection.
  • Give hardware its own entry point, and make sure the monitor actually drives it — a model that only sees software operations falls behind silently.
  • Put the reset value in the field table so reset() cannot drift from it.
  • Compare after every operation, not at the end of the test.
  • Provide load(actual) to resynchronise after a reported mismatch, so one early divergence does not make every later line wrong.
  • Model the unmapped-address behaviour explicitly, and the access widths the design actually supports.
  • Generate the field table from the project's register description, and hand-write directed tests from the prose for the registers with side effects.
  • When a write to one address changes another's predicted value, stop treating the map as independent registers.
  • Test W1C with a write of zero, and read-to-clear twice — those two cases catch most register-model bugs.

10. Mistakes Worth Making Once

Modelling the map as std::uint32_t regs[N]. Four of seven steps diverge.

Writing 0x00 to a W1C register and expecting it to clear. It changes nothing.

Writing 0x00 to a W1C register and expecting nothing to change in the model. The naive model zeroes it.

A const read(). The read-to-clear field is now unmodellable, and nothing warns.

Reading an RC register twice in a test and expecting the same value.

Letting software writes be the model's only input. The hardware sets a status bit and the model never hears about it.

Comparing only at the end of the test. The report is four steps from the cause.

No resynchronisation after a mismatch. Every subsequent line is a consequence.

Returning zero for unmapped addresses because that was the default. The design returns an error and the comparison passes.

Modelling a soft-reset bit without touching the memory model. Two models, one disagreement.

Hand-transcribing a thousand-field register map. The errors are in the fields nobody tests.

11. Interview Relevance

The frozen curriculum marks this High, and the question is often deceptively simple: how would you model a register map? A nested structure of registers and fields is the expected answer, and it is incomplete in a way that is easy to probe.

The content is the access types. Say that each field carries its access type — RW, RO, W1C, RC — and that these are modelled as data in a field table rather than logic, so a new register is a new table. Then make it concrete with the measurement: driving the same seven software operations against a correct model and one treating every field as read-write diverged on four of seven steps, and the divergence started at the write of 0x00, which does nothing to a W1C field and zeroes a naive model.

The detail that marks real experience is the read with a side effect. A read-to-clear field means read() mutates, so it cannot be const — and an array-of-words model cannot express it at all, which is a structural limitation rather than a masking bug. Mentioning that you keep a separate peek() for inspection shows the design has been through a review.

Then the predictor framing, which is the senior point: a register model predicts state, so it must be driven by hardware-side events as well as software operations, or it falls behind and produces mismatches that are the model's fault. And because state divergence is cumulative, you compare after every operation and provide a way to resynchronise the model to the DUT after reporting a mismatch — otherwise the report's hundred lines carry one line's worth of information.

12. What You Should Be Able to Say Now

  • That a register is not memory: writes can be ignored, writes can clear, and reads can mutate.
  • The access types — RW, RO, W1C, RC, plus write-only and reserved — and that they belong in a field table as data.
  • That the same seven-step sequence diverged on 4 of 7 steps between a correct model and an all-RW one, with the read also returning a different value.
  • That the divergence began at the write of 0x00, which does nothing to a W1C field.
  • That every number in that transcript is hand-derivable from the field definitions — 0x30, 0x3D, 0x3C, 0x34.
  • That read-to-clear is structurally unmodellable by an array of words, which is why read() must be non-const with a separate peek().
  • That a register model is a predictor of state, so it must also be driven by hardware-side events or it silently falls behind.
  • That state divergence is cumulative, so you compare after every operation and resynchronise with load(actual) after reporting.
  • That the map needs modelling too: unmapped addresses, access widths and alignment, aliases and FIFOs behind one address.
  • That generating the table from the register description is not a loss of independence — the shared artefact is the specification — but it does make the description and the generator the things to review.
  • That once a write to one address changes another's predicted value, it is a state machine, not a register map.

13. Exercises

Exercise 1 — the two models. Build the STATUS register from §4 with access types, and a second model treating every field as RW. Drive both through the seven steps and count the divergences.

Exercise 2 — derive the transcript. Before running it, work out all seven spec-model values by hand from the field table. They must match.

Exercise 3 — the const read. Try to make read() const and see what breaks. Then add peek() and confirm which callers should use which.

Exercise 4 — the missing hardware event. Remove the hwSet call and watch the model report mismatches that are its own fault.

Exercise 5 — compare at the end only. Move the comparison to the end of the test and observe how much less the report tells you. Then add per-operation comparison and resynchronisation.

Exercise 6 — the unmapped address. Model a read from an unmapped address three ways — zero, bus error, last value — and decide which your specification actually requires.

Exercise 7 — the side effect. Add a soft-reset bit that clears a modelled FIFO, and confirm your register model and your FIFO model agree afterwards.

14. Summary

  • A register is not memory. A write may be ignored (RO), may clear rather than set (W1C), may have no effect (RC), and a read may change the register. Hardware also writes it, concurrently with software.
  • Model the access type of every field as data in a table, with the reset value alongside, so a new register is a new table rather than new logic.
  • Measured: the same seven software operations diverged on four of seven steps between a model honouring W1C/RO/RC and one treating every field as read-write, with the read also returning a different value.
  • The divergence began at the write of 0x00 — the most innocuous step available — which leaves a W1C field untouched and zeroes a naive model. Every value in the transcript (0x30, 0x3D, 0x3C, 0x34) is derivable by hand from the field definitions.
  • Read-to-clear is the structural case: an array-of-words model cannot represent a read that mutates, which is why read() is non-const and a separate peek() exists.
  • A register model predicts state, so it must be driven by hardware-side events as well as software operations — otherwise it falls behind and reports mismatches that are its own fault.
  • State divergence is cumulative and silent about its cause, so compare after every operation and provide load(actual) to resynchronise after reporting, or a hundred mismatches will carry one mismatch's information.
  • The address map needs modelling as much as the registers: unmapped-address behaviour, supported access widths and alignment, aliases and FIFOs behind a single address.
  • Generate the field table from the project's register description. That is not the independence failure of Chapter 27.4, because the shared artefact is the specification — but the description and generator become the things to review, and a bug in the description is invisible to the comparison.
  • When a write to one address changes another's predicted value, the map has become a state machine and should be structured as one, alongside the memory model rather than apart from it.

Chapter 27.8 is that memory model: sparse, address-indexed, and with the initialisation policy stated rather than inherited.

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.