Virtual platforms and refine-and-reuse
Use an instruction-set model as the reference for a core: run one test program on both, compare at each retire, bridge the speed gap with a deep queue, and know what a virtual platform can never tell you.
- Lesson
- U9.5
- Time
- 35 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
A core passes its instruction-set comparison on every program the team owns. Then the performance team measures the first benchmark and finds it a third slower than planned: a multiply takes six cycles, not three. The reference model was right about every value and was never asked about time.
| Instruction | Retire observed (MUL 3 edges) | Retire observed (MUL 6 edges) | ISS verdict, both cores |
|---|---|---|---|
| #0 LI r1, 7 | 35 ns | 35 ns | match |
| #1 LI r2, 3 | 55 ns | 55 ns | match |
| #2 MUL r3, r1, r2 | 95 ns | 125 ns | match |
| #3 SUB r0, r3, r1 | 115 ns | 145 ns | match |
| #4 ADD r1, r0, r2 | 135 ns | 165 ns | match |
| #5 MUL r2, r1, r1 | 175 ns | 235 ns | match |
The same six-instruction program on the specified core and on a slow one. Times are when the monitor sees each retire, one edge after the core writes the register. Every retire matches the ISS on both.
One test program, two models
A virtual platform is a fast software model of a chip or subsystem: processor models, memories and peripherals joined by TLM-2.0 sockets, usually in SystemC. For a processor, its core is an instruction-set simulator (ISS). An ISS executes the program one instruction at a time and keeps the architectural state: the registers and memory a programmer can see. Platforms differ in fidelity: some are untimed, others add loosely or approximately timed models (U9.4), interrupts and shared memory. This lesson's ISS is untimed.
- Write the test once
The test builds six
instrobjects. It is the only copy of the stimulus.More context
LI, LI, MUL, SUB, ADD, MUL: a short program whose two multiplies take longer in the RTL.
- Platform side0 ns (illustrative)
chk.run_ahead(prog)steps the ISS through all six at 0 ns and queues one record per instruction.More context
Log line
0 ns ISS ran 6 instructions, 6 retire records queued. The ISS is untimed, so the whole program runs before the RTL has taken its first instruction.Follow the calls, values and waits
- data six instr objects: prog[$] → tiny_iss
- data six retire records: tiny_iss → retire records
- Waiting: retire records — 6 records waiting for retires
- RTL side
replay_seqsends the same objects to the driver at the speed the core takes them.More context
One instruction at a time: the next one goes to the pins only when the core can take it.
Follow the calls, values and waits
- data instr #0: LI r1, 7: prog[$] → replay_seq, driver
- data instr #0: LI r1, 7: replay_seq, driver → tiny_cpu
- Compare at retire35 ns (illustrative)
Each retire the monitor publishes pops the oldest ISS record: register and value must match.
More context
Log line
35 ns retire #0 LI r1, 7 -> r1=07 matches. Five records are still queued.Follow the calls, values and waits
- data retire #0: r1=07: tiny_cpu → retire monitor
- data retire #0: r1=07: retire monitor → retire_checker
- data ISS record #0: r1=07: retire records → retire_checker
- A multiply takes longer95 ns (illustrative)
The MUL retires 40 ns after the instruction before it. The checker does not care when; it pops the next record whenever a retire arrives.
More context
Log line
95 ns retire #2 MUL r3, r1, r2 -> r3=15 matches; retire #1 was at 55 ns. Hex 15 is 7 times 3.Follow the calls, values and waits
- data retire #2: r3=15: tiny_cpu → retire monitor
- data retire #2: r3=15: retire monitor → retire_checker
- data ISS record #2: r3=15: retire records → retire_checker
- The last retire175 ns (illustrative)
The sixth retire pops the last record, and the queue is empty.
More context
The summary reads
retired 6, mismatches 0, peak queue 6, last retire at 175. A retire with no record left, or a record left at the end, would be an error.Follow the calls, values and waits
- data retire #5: r2=21: tiny_cpu → retire monitor
- data retire #5: r2=21: retire monitor → retire_checker
- data ISS record #5: r2=21: retire records → retire_checker
- Released: retire records
Read the diagram as text
- prog[$]: The test program, built once
- tiny_iss: run_ahead at 0 ns
- retire records: Peak 6 queued
- replay_seq, driver: One instruction at a time
- tiny_cpu: MUL 3 edges, others 1
- retire monitor: ret_valid, ret_rd, ret_val
- retire_checker: Register and value
- prog[$] → tiny_iss: same objects
- prog[$] → replay_seq, driver: same objects
- tiny_iss → retire records: push all
- replay_seq, driver → tiny_cpu: pins
- tiny_cpu → retire monitor: retire
- retire monitor → retire_checker: write
- retire records → retire_checker: pop oldest
In a commercial flow the program comes from the platform team's tests and reaches UVM through a bridge.
Both models see exactly the same traffic because there is one copy of it. In a commercial flow the program comes from the platform team's tests, written and debugged on the platform long before the RTL exists. A bridge replays them as UVM sequences, so that work is reused at RTL instead of written again.
Compare when an instruction retires
The ISS has no clock, so it cannot be compared with the RTL cycle by cycle. What both share is the moment an instruction retires: its result is written and becomes architectural state. Between retires, the RTL holds things the ISS has no name for, such as the busy count and the result waiting in p_val. The times below are when the monitor observes a retire: the core writes the register at one edge and the monitor sees ret_valid at the next.
Each RTL retire pops the oldest ISS record. The queue starts full and drains at the RTL's pace.
The queue is U9.2's decoupling at platform scale. The ISS finishes six instructions before the RTL takes the first, so the example's queue peaks at six. A real program has millions of instructions, and a real flow bounds the queue: the platform runs ahead until the FIFO is full, then waits for a free entry. Depth does not remove the speed difference. A platform that is faster on average fills any finite FIFO, and from then on it runs at the RTL's pace. Depth buys room for bursts and fewer, larger hand-overs between the two sides.
Set SUB_SWAP to 1 on tiny_cpu and rerun. The core now computes rs2 - rs1. The first mismatch is retire #3, SUB r0, r3, r1: RTL r0=f2, ISS r0=0e. Retires #4 and #5 mismatch too, because they read the wrong r0 and r1. The first mismatch names the faulty instruction; the rest are consequences, which is why platform flows stop, or copy the RTL's state into the ISS, after the first one.
From algorithm to RTL: refine and reuse
Platform teams rarely write a model straight at the transaction level. They refine one core of code through several stages, and each stage keeps the tests of the stage before it.
- Algorithm
The functional domain is verified here, in seconds per run. In this module:
tiny_issand the U9.3 C model.More context
The tests check values only: no pins, no clock. That is why they can run so early and so fast.
Follow the calls, values and waits
- data the same tests: One test set → Algorithm
- Transaction level
The same core behind sockets (U9.4's
mem_model); the algorithm tests drive it through the sockets.More context
The core does not change; the wrapper adds the socket interface and, for an approximately timed model, phases.
Follow the calls, values and waits
- data model core: Algorithm → Transaction level
- data the same tests: One test set → Transaction level
- Cycle-accurate
A cycle-accurate wrapper turns transactions into pins; here, the UVM driver does that job.
More context
A second wrapper around the same core. Its timing can now be checked as well as its values.
Follow the calls, values and waits
- data model core: Transaction level → Cycle-accurate
- RTL
At RTL the design replaces the model, and the platform becomes the reference for the same tests.
More context
Nothing moves along replaced by: the RTL is new code, not a wrapper. The tests that ran on the algorithm now run against it.
Follow the calls, values and waits
- data the same tests: One test set → RTL
Read the diagram as text
- Algorithm: C, C++ or SystemC functions
- Transaction level: TLM-2.0 wrapper, LT or AT
- Cycle-accurate: Bus protocol wrapper
- RTL: The design itself
- One test set: Stimulus and checks
- Algorithm → Transaction level: wrap
- Transaction level → Cycle-accurate: wrap
- Cycle-accurate → RTL: replaced by
- One test set → Algorithm: run
- One test set → Transaction level: reuse
- One test set → RTL: reuse
The core stays, the wrappers change, the tests carry over. Conceptual on this runner.
The four stages side by side
| Stage | Model | Tests and checks | In this module |
|---|---|---|---|
| Algorithm | C, C++ or SystemC functions with no transactions or time | Written here: the functional domain is verified at this stage | tiny_iss, the U9.3 C model |
| Transaction level | The same core wrapped in TLM-2.0 sockets, loosely or approximately timed | Algorithm tests reused through the sockets | U9.4's mem_model |
| Cycle-accurate wrapper | Transactions turned into a bus protocol, cycle by cycle | The same tests drive pins | The UVM driver |
| RTL | The design itself | Platform tests replayed; the platform is the reference | This lesson's bench |
One model core and one test set carried through four stages.
The reuse is the return on the platform's cost. Without it the team writes stimulus twice and debugs it twice. With it, most functional bugs are found on the algorithm and transaction models, where a run takes seconds. RTL verification then concentrates on what only RTL has: interfaces, timing and concurrency.
When a platform is the wrong reference
Run the quiz's predict item: the same program on a core whose multiply takes six cycles. Every retire still matches and the run passes. Retires #2 to #4 are observed 30 ns later and #5 60 ns later, because each MUL takes three more edges. This untimed ISS answers "is the architectural state right?" and nothing else.
| Question | Can this untimed ISS answer it? | What answers it instead |
|---|---|---|
| Is each retired value right? | Yes | - |
| Does it retire on time? | No: it has no cycles. A timed platform answers only to the accuracy it was validated for | A latency or throughput meter (U8.4), assertions on the pipeline |
| What happens when two masters collide, or an interrupt lands mid-instruction? | No: it sees only instruction boundaries and models one master | Directed and random RTL tests, assertions, a cycle-accurate model |
| Is the cache or pipeline state right? | No: it models architectural state only | White-box checkers on the micro-architecture |
| Was the specification read correctly? | Only if its authors read it differently from the designers | Reviews and a second, simpler checker (U9.1) |
An untimed reference checks function at its boundaries. Everything between them, and every question of time, needs another checker.
So a platform is the wrong reference whenever the thing to check lies outside the fidelity it was built for: between its boundaries, or in time it does not model. It is also the wrong reference when it is not ready. A platform that lags the RTL by months checks nothing in the meantime; a scoreboard with its own model (U9.2, U9.3) fills that gap.
Try it
The ISS runs the whole program at 0 ns and queues six records. The core takes an instruction, retires it one edge later (three for MUL), and the monitor reports each retire on the following edge. Each report pops one record.
Change tiny_cpu dut to tiny_cpu #(.SUB_SWAP(1)) dut and rerun: the first error names retire #3, and #4 and #5 follow from it. Then try #(.MUL_LAT(6)): the run passes, later.
0 ns ISS ran 6 instructions, 6 retire records queued 35 ns retire #0 LI r1, 7 -> r1=07 matches 55 ns retire #1 LI r2, 3 -> r2=03 matches 95 ns retire #2 MUL r3, r1, r2 -> r3=15 matches 115 ns retire #3 SUB r0, r3, r1 -> r0=0e matches 135 ns retire #4 ADD r1, r0, r2 -> r1=11 matches 175 ns retire #5 MUL r2, r1, r1 -> r2=21 matches retired 6, mismatches 0, peak queue 6, last retire at 175 UVM_ERROR : 0Check yourself
Remember
- A virtual platform is only as timed as it was built; an untimed ISS like this one models architectural state and is compared with RTL at each retire.
- Write the test once and run it on both: the platform runs ahead and queues its answers, the RTL gets the same stimulus as a UVM sequence.
- A bounded FIFO lets the fast model run ahead in batches, but a faster side still fills it and then runs at the RTL's pace; the first mismatch locates the fault, later ones usually follow from it.
- Refine and reuse carries one model core and one test set from algorithm to transaction level to cycle-accurate wrapper to RTL.
- An untimed reference cannot check timing, concurrency between its boundaries or micro-architectural state; those need meters, assertions or a model validated for them.