Testing Your Single Cycle CPU
We already tested our ALU, but a whole CPU is a different challenge. It remembers values and runs a complete program one clock at a time, so proving it correct takes far more than a single check. In this lesson we build a self checking testbench for the processor, and prove it works down to the very last bit.
What’s New
When we tested the ALU, we built a self checking testbench that fed the design chosen inputs, worked out the right answer a second way with an independent model, and compared the two. The same idea carries over here. What changes is the scale. The ALU was a single combinational block, while the CPU is a whole machine that runs a program over time, and that asks for two new ideas.
The first idea is that we check the entire state of the machine on every cycle, not just one number at the end. Our reference model runs alongside the processor in lockstep, advancing one instruction for every instruction the CPU executes so the two never drift apart in time. After each clock edge we compare everything, the program counter, all thirty two registers, and every word of data memory, and the testbench flags the smallest disagreement at once.
The second idea is that we check against two independent references instead of one. Alongside the lockstep model we work out the expected final state by hand and write it into a table. When the hardware, the lockstep model, and that hand worked table all agree, we can trust the design is right and that our test did not simply miss the problem.
Go back Forgot how we tested the ALU? Revisit the self checking testbench we built for the ALU.Choosing What to Test
To test the processor we hand it a short program, about thirty instructions, and let it run from start to finish. It is written like a small computation rather than a random pile of operations: early instructions load a few starting values and later ones build on them, so each result feeds naturally into the next.
The program is built for coverage. It exercises every instruction the design supports, from register arithmetic and logic with add, sub, and, or, and slt, to their immediate forms addi, andi, ori, and slti, to memory access with lw and sw, and the branches beq, bne, and blt. But running each one once is not enough, since many behave differently depending on their inputs. So a set less than is tried both true and false, an immediate add is given a positive value and a negative one, a load uses a negative offset, and a write to x0 checks that the processor quietly ignores it.
Branches get the most attention, since each has a taken and a not-taken path through different logic, so every branch appears more than once, and a few instructions sit right after a taken branch to confirm they are skipped. Together these choices form a coverage checklist: every instruction and case we mean to reach, with anything left unchecked a gap we know about. That is how thirty instructions can give honest confidence in a processor that could one day run billions.
00100093 // addi x1, x0, 1
00500113 // addi x2, x0, 5
002081B3 // add x3, x1, x2
40118233 // sub x4, x3, x1
FFE20293 // addi x5, x4, -2
0042E333 // or x6, x5, x4
005273B3 // and x7, x4, x5
00522433 // slt x8, x4, x5
0042A4B3 // slt x9, x5, x4
00302223 // sw x3, 4(x0)
00800513 // addi x10, x0, 8
00652223 // sw x6, 4(x10)
407105B3 // sub x11, x2, x7
FFC52603 // lw x12, -4(x10)
00410663 // beq x2, x4, 12
003002B3 // add x5, x0, x3
00F17693 // andi x13, x2, 15
00A02823 // sw x10, 16(x0)
00F17713 // andi x14, x2, 15
00411A63 // bne x2, x4, 20
00311463 // bne x2, x3, 8
407105B3 // sub x11, x2, x7
00610233 // add x4, x2, x6
FE1048E3 // blt x0, x1, -16
00326793 // ori x15, x4, 3
00208663 // beq x1, x2, 12
00114463 // blt x2, x1, 8
00902A23 // sw x9, 20(x0)
00928013 // addi x0, x5, 9
40238833 // sub x16, x7, x2
Check Yourself
The short program below is meant to test the beq instruction. Which case does it fail to cover?
Silicon From Scratch