How do you get an answer key for your design that does not come from the design?
A testbench that copies its expected values from the design code repeats the design mistakes and passes on both. You need an answer key with an independent source: a small program written from the spec, never from the RTL, that predicts what the design must output on every edge.
That program is a golden model, which means a reference implementation of the spec used only for checking. RTL is register-transfer level: hardware described as registers and the logic between them. This lesson runs the course golden model for mini_controller, explains it line by line, and then states the register contract at awareness level: the agreement software would see if this counter were visible to a program.
The figure below shows the arrangement. Look at the two arrows leaving the spec first: one team builds the design while the other writes the model, and neither reads the other work. Then follow both output arrows into the comparator, which feeds both sides the same inputs each edge and reports the first mismatch.
Independence is the whole point. If the model author reads the RTL, the model copies the RTL logic including its bugs, and the comparator compares the design against itself. Written from the spec table instead, the model disagrees with a buggy design on exactly the edge where the design leaves the spec.
The listing below is the complete golden model. It uses only the Python standard library and checks its own results, so you can run it anywhere you have Python 3:
"""Golden model of mini_controller, written from docs/spec.md (not from the RTL)."""
import random
class MiniController:
WIDTH = 4
def __init__(self):
self.count = None # unknown until the first reset edge
def step(self, rst_n, clear, valid):
"""Advance one rising clock edge. Priority: reset > clear > count > hold."""
if not rst_n or clear:
self.count = 0
elif valid:
self.count = (self.count + 1) % (1 << self.WIDTH)
return self.count
if __name__ == "__main__":
m = MiniController()
m.step(0, 0, 0) # reset
seen = [m.step(1, 0, 1) for _ in range(17)]
assert seen[14] == 15 and seen[15] == 0 # wraps 15 -> 0
assert m.step(1, 1, 1) == 0 # clear beats valid
rng = random.Random(1)
for _ in range(1000):
out = m.step(rng.random() > 0.1, rng.random() > 0.8, rng.random() > 0.3)
assert 0 <= out <= 15
print("PASS: model follows the spec table")This listing is the independent answer key: the spec behaviour table as a runnable program.
# captured terminal output
$ python3 -I model/mini_controller_model.py
PASS: model follows the spec tableThis output came from Python 3.14.3 on macOS. The first line runs the model in isolated mode from the model folder; the second line is the verdict after the model checks itself.
Read the model against the spec behaviour table, not against the design. The class holds one value, count, starting unknown until the first reset edge, which mirrors hardware whose register holds no valid value before reset.
The step method advances one rising edge. Its first branch answers the top two spec rows: reset asserted or clear high sets the count to 0. Its second branch answers the counting row: a valid event adds 1 modulo 16, which wraps 15 to 0. Falling through both branches answers the hold row: the count returns unchanged.
The main block checks the model against the spec in three stages. It resets, then counts 17 events and asserts the wrap: the fifteenth value is 15 and the sixteenth is 0. It asserts that clear beats a simultaneous valid event. Then it runs 1000 seeded random steps and asserts every result stays within 0 to 15. The seed fixes the sequence, so the run repeats identically everywhere. Only after all three stages pass does the verdict line appear.
1. The first fifteen events climb 1 to 15, the sixteenth wraps to 0, and the seventeenth counts once more to 1. The wrap row is the one most hand-written tests never reach, which is why the model asserts it directly.
A register map is the agreement between hardware and software: which addresses software reads and writes, what each field means, and what value each field holds after reset. Access type is part of the contract: read-only fields are observed, and write-one-to-clear fields are cleared by writing a 1 while a write of 0 leaves them alone.
The mini_controller itself has no register interface: it exposes the count as a plain output with no address bus, so no software can read or clear it through registers. The table below is illustrative: an example of what the contract would look like if this counter were software-visible, given only to teach the shape of such a table:
Address (illustrative) | Name | Width | Access | Reset value |
|---|---|---|---|---|
0x0 | COUNT | 4 bits | read-only | 0 |
0x4 | CLEAR | 1 bit | write-one-to-clear | 0 (no pending clear) |
Read the two rows as the two halves of the job. COUNT lets software observe the value without changing it. CLEAR lets software request a clear without touching the count path: writing 1 clears, writing 0 does nothing. The golden model and the RTL would both obey this table, and the comparator would check both against it.
Writing the model by reading the RTL. A model transcribed from design code agrees with the design by construction, so the comparator stays green over real bugs. Write from the spec table with the design file closed, then let the first mismatch teach you which side misread the spec.
Letting the model and the RTL share code. A shared helper, constant or copy-pasted branch carries one mistake into both sides at once. Keep the two implementations separate down to the small details, so each one can surprise the other.
Rule: the answer key earns trust only when it is written from the spec with the design closed.
Neither side gets the benefit of the doubt: you open the spec behaviour table. It orders clear above counting, so the expected count is 0. Whichever side produced the other value misread the spec, and the spec row settles it without debate.
Because the hardware holds no valid value before the first reset edge. Starting unknown forces every user of the model to reset first, which matches the acceptance criterion that reset works from any state. Starting at 0 would hide a design that forgets its reset.
The answer below is a fragment, not a runnable program on its own:
# snippet
SATURATE = True
count = 15
valid = True
if valid:
if SATURATE and count == 15:
count = 15 # stick: saturation replaces the wrap
else:
count = (count + 1) % 16
assert count == 15This fragment shows the decision point: saturation replaces the wrap branch while reset, clear and hold stay untouched.
A golden model predicts the design outputs from the spec alone, with the design file closed.
Independence is what makes mismatches meaningful: shared code or shared readings hide bugs on both sides.
The register contract states address, name, width, access type and reset value for every software-visible field.