Silicon From Scratch

    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.

    Think you got this already? Skip to Check Yourself
    A hand-drawn illustration of two multimeter probes, one black and one red, touching the pins of a chip labeled CPU on a circuit board, as if testing it.

    What’s New

    The first page of the Design Verification Report for the RISC-V single cycle CPU, showing the title, a verification result of PASS with zero errors, and the Silicon From Scratch logo. See the full report on GitHub The complete testbench, results, and coverage

    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.

    The full testbench waveform: the left panel lists every signal in the datapath — clk, reset, pc, fetchedInstr, opcode, funct3, immediate, the control lines, ALUControl, readData1 and 2, ALUresult, dataToWrite, and the lessThan and equalsZero flags — and the right panel plots each one across time as the thirty instruction program runs one clock at a time.
    program.mem
    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?

    A hand-drawn test program titled Test Program: addi x1, x0, 4; addi x2, x0, 4; beq x1, x2, 8; addi x3, x0, 1; addi x4, x1, x2. Because x1 and x2 are both set to 4, the beq condition is always true, so the branch is always taken and the addi x3 instruction is skipped.

    Congratulations! You have completed the Single Cycle CPU! 👏