Run the RTL core.
Run a golden instruction-set reference beside it.
Compare every retirement, and let the disagreement point at the gate.
A passing RISC-V testbench tells you the firmware finished. It does not tell you the pipeline computed every value correctly along the way. The difference shows up on the day a branch is taken on stale operands and the program still ends in a state the self-check accepts. Co-simulation debug closes that gap: the RTL core runs against a golden instruction-set simulator, retirement for retirement, and the first place the two disagree is the first place to look.
The design under debug is AH1Q, a WIOWIZ-original RV64 core used here as a stand-in for any in-order pipeline. The golden reference is a standard ISA simulator run with commit logging. FSiMX Studio drives the RTL run, records each retired instruction, and hands both streams to the waveform tool for a retirement-level compare.
The retirement stream, side by side
Each side emits one record per retired instruction: order, program counter, the encoding, and the register writeback. The RTL side comes from the core's retirement monitor; the golden side comes from the reference's commit log. FSiMX aligns them and compares field by field.
$ fsxrun -f ah1q.f -top tb_ah1q -trace-retire ah1q_rtl.trace $ vwiz-cli isscmp ah1q_rtl.trace ah1q_golden.trace RVVIP_RETIRE format : order pc insn priv rd_we rd rd_data ISSCMP golden=5018 rtl=33 aligned=1 skipped=13 compared=20 result : MATCH (20/20 retirements, 20/20 register writebacks)
Alignment is the whole problem
The reference boots from its own reset vector at 0x1000 and runs a bootrom before it reaches the payload, and it runs the program to completion. The RTL simulation stops after a fixed number of cycles. On the first paired run that meant 5,018 golden commits against 33 RTL retirements, with the payload's first instruction appearing only at golden line 13.
Compare those by sequence number and you pit the reference bootrom against the RTL's first instruction, and report a divergence at index 0 that is an artefact of the harness, not a bug in the core. The golden stream is advanced to the RTL's entry program counter first, and only the overlap is compared. The exit codes stay distinct: identical, divergence, and comparison-could-not-run are three different outcomes, and collapsing them is how a broken harness passes as a green build.
A compressed instruction is not a divergence
The first comparison run flagged a divergence at retirement 3:
golden : 0x0000431d rtl : 0x00700313
Decoded, those are c.li x6, 7 and addi x6, x0, 7, the same operation. The reference logs the compressed 16-bit halfword it fetched; the RTL monitor logs the decompressed 32-bit equivalent while advancing the program counter by 2. Before writing a line of code against that theory, we measured it across the window: 33/33 program counters identical, 33/33 register writebacks identical, with exactly 8 differing encodings that were all this case. Twenty-five plus eight is thirty-three.
Trusting the tool's first answer would have meant reporting a CPU bug to the core team that did not exist. The fix was to expand the compressed forms with a full RVC decoder for the integer C extension, and to return "unverified" rather than a plausible guess for float C forms, reserved encodings and HINTs. The comparison also prints what it could not check: the retirement trace carries no memory fields, so the reference's memory-effect records are counted as unverified on every run.
The blind test
Every check above proves the comparator can flag a mismatch we planted. It does not prove it catches a bug nobody described. So we took a package we could not see into: one RTL build with an injected mutation, its golden trace, and no disclosure of what changed or where. Hashes verified, comparator run, no hints.
ISSCMP golden=5018 rtl=9 aligned=1 skipped=5 compared=9 DIVERGENCE at retirement 8 (pc) golden : pc=0x000000008000001a rtl : pc=0x0000000080000054
Reading the result
The divergence is reported at retirement 8, but the fault is at retirement 7. Reconstructing the operands around it:
| idx | pc | instruction | golden | RTL |
|---|---|---|---|---|
| 5 | 0x80000010 | add s0, t1, t2 | s0 = 0xc | s0 = 0xc |
| 6 | 0x80000014 | li s1, 12 | s1 = 0xc | s1 = 0xc |
| 7 | 0x80000016 | bne s0, s1, +62 | not taken | taken |
| 8 | - | - | 0x8000001a | 0x80000054 <fail> |
The branch was taken with its two operands equal. The target is correct: 0x80000016 plus 62 is 0x80000054, which the disassembly confirms is the label fail. The target arithmetic is sound and the decision is wrong. The core jumped into the test's own failure handler, which is why the run ends after 9 retirements.
Narrowing it to one bypass
Two defect classes fit a branch taken on equal operands:
- Broken EX-stage forwarding into the branch comparator. Here s1 is written by the immediately preceding instruction and consumed by the branch, the textbook distance-1 hazard. With that bypass missing, the comparator sees a stale s1 against a correct s0 and takes the branch on wrong data.
- Inverted branch condition, bne evaluating as beq.
The trace separates them. s0 is written two instructions before the branch. If the bypass were broken at both distances, the comparator would see 0 against 0, bne would not be taken, and the test would have passed. The failure requires a stale s1 with a correct s0: the distance-1 path broken while distance-2 is intact. That is a forwarding fault, not an inverted condition. The output does not say "something is wrong"; it says which retirement, which operand, and which bypass.
What it caught, and what it does not claim
Four defects surfaced during this work, none of them by a passing test, all of them by looking at the output. The comparison is worth exactly what it inspects, so it prints its gaps: compressed forms it could not expand stay counted as unverified, and memory effects the retirement trace does not carry are never reported as matched. A comparator that quietly skips half the state while printing MATCH is worse than one that checks less and says so.
Co-simulation debug says whether the pipeline was right the whole way.
