Appendix: Testing and Debugging

Unit Tests

The provided unit tests check functionality of your subcircuits (e.g. immediate generator, branch comparator). They do not check your entire CPU implementation. You don't need to write any unit tests yourself.

As an example, let's debug the alu-add unit test. First, run the ALU unit tests with bash test.sh test_alu. If the test doesn't pass, this will print out the difference between your subcircuit output and the reference output.

Viewing Output Files

The next step to debug unit tests is to compare the expected reference output to your subcircuit output. These output files will always be in the out directory. You can use bash test.sh format filename (replacing filename) to view the output files.

View the reference output with bash test.sh format tests/unit-alu/out/alu-add.ref. You should see this reference output:

Time ALUSel A        B        ALUResult
00   0      00002020 00000f0f 00002f2f 
01   0      ffffdead ffffbeef ffff9d9c 
02   0      00007fff 00000001 00008000 
03   0      00000000 00000000 00000000 

This shows the inputs (A, B, and ALUSel) sent to your subcircuit at each time step, and the expected output (ALUResult).

Next, run bash test.sh format tests/unit-alu/out/alu-add.out to see the output from your subcircuit. Here's the output when the test is run on unmodified starter code:

Time ALUSel A        B        ALUResult
00   0      00002020 00000f0f UUUUUUUU 
01   0      ffffdead ffffbeef UUUUUUUU 
02   0      00007fff 00000001 UUUUUUUU 
03   0      00000000 00000000 UUUUUUUU 

Note that in the example, the inputs to your subcircuit are the same, but the output (ALUResult) of your subcircuit is different (undefined).

Using Debugging Circuits

Each unit test has a .circ test circuit you can use for debugging. As an example, open tests/unit-alu/alu-add.circ, which corresponds to the failed test from the previous section. The first thing you'll see in this circuit is the testing harness:

Annotated screenshot of alu-add.circ.

This feeds a sequence of inputs (InputA, InputB, and ALUSel) to your ALU.

The ROM (in the red box) contains a list of inputs to your circuit. The first input (InputA = 0x00002020, InputB = 0x00000f0f, ALUSel = 0b0000) is highlighted in dark gray. You can also see these values being passed into your ALU (in the blue box) with the probes.

In this picture, the ALUResult output from your ALU is undefined (all Us). To see why, we can view our ALU subcircuit to see what logic it's doing. To click into your ALU, you can either right-click the ALU and select "View alu", or click the ALU and click the magnifying glass, as shown below:

Using the magnifying glass to click into a subcircuit.

Right-clicking to click into a subcircuit.

Inside your ALU subcircuit, you can see inputs (A, B, and ALUSel) provided from the harness to your subcircuit. You can click on wires to see the values in those wires.

Clicking on wires inside alu.circ to debug.

If the output of your subcircuit isn't what you expect, you can probe wires to investigate where the incorrect output is coming from. In the starter circuit, the ALUResult output is undefined. In this case, note that the ALUResult tunnel is undefined, so we probably want to send a value to this tunnel.

To return to the harness, you can click on main in the Simulate → Active Simulations tab in the top-left corner.

To view later inputs, click Simulate → Tick Full Cycle, which will highlight the next row of the ROM blocks and send this next input into your subcircuit. You can tick cycles while viewing your ALU subcircuit to see later inputs.

To reset the simulation, click Simulate → Reset Simulator. You can also close and re-open the debugging circuit.

Integration Tests

Integration tests execute RISC-V instructions on your entire CPU and compare the outputs to the result of running those instructions on Venus. You'll need to make your own integration tests by writing out RISC-V instructions (the testing framework will then create the testing and debugging circuit for you).

As an example, let's debug the addi-basic unit test. First, run the addi integration tests with bash test.sh test_addi. If the test doesn't pass, this will print out the difference between your CPU output and the reference output.

Viewing Input and Output Files

The RISC-V instructions being executed by your CPU will be inside the in directory. In this example, view tests/integration-addi/in/addi-basic.s to see what RISC-V instructions are being run in this test. You should see these instructions:

addi t0, x0, 1
addi t0, x0, 42
addi t0, x0, 256
addi t0, x0, 2047

As with the unit tests, you can use bash test.sh format filename (replacing filename) to view the output files.

View the reference output with bash test.sh format tests/integration-addi/out/addi-basic.ref. You should see this reference output:

Time PC       Instruc. ra (x1)  sp (x2)  t0 (x5)  t1 (x6)  t2 (x7)  s0 (x8)  s1 (x9)  a0 (x10)
0000 00000000 00100293 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0001 00000004 02a00293 00000000 00000000 00000001 00000000 00000000 00000000 00000000 00000000
0002 00000008 10000293 00000000 00000000 0000002a 00000000 00000000 00000000 00000000 00000000
0003 0000000c 7ff00293 00000000 00000000 00000100 00000000 00000000 00000000 00000000 00000000
0004 00000010 00000000 00000000 00000000 000007ff 00000000 00000000 00000000 00000000 00000000

This shows the program counter (PC), instruction, and values in the 8 debug registers at each time step of running the input program. For example, at time step 0, 0x00100293 corresponds to addi t0, x0, 1. At the next time step, t0 now has the value 0x00000001.

Next, run bash test.sh format tests/integration-addi/out/addi-basic.out to see the output from your CPU:

Time PC       Instruc. ra (x1)  sp (x2)  t0 (x5)  t1 (x6)  t2 (x7)  s0 (x8)  s1 (x9)  a0 (x10)
0000 00000000 00100293 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0001 00000004 02a00293 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0002 00000008 10000293 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0003 0000000c 7ff00293 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0004 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

In this case, the test failed because the registers are not being updated with the results of the instruction (they're all zeros).

Using Debugging Circuits

Each integration test has a .circ test circuit you can use for debugging. To debug the example test from the previous section, open tests/integration-addi/addi-basic.circ.

Screenshot of addi-basic.circ.

The top-level harness for each integration test contains a ROM block (bottom half of screenshot) containing the RISC-V instructions for that test, representing IMEM (instruction memory). These instructions are passed into your CPU (the circled cpu_harness block at the top). You can also see the 8 debug register outputs; the testing framework will log their values into the .out file when running the test.

To view your CPU circuit, either right-click the cpu_harness block and select "View cpu_harness", or click the cpu_harness block and click the magnifying glass. This takes you into the CPU harness, where your CPU interacts with memory. Click another time into the cpu block, and now you should see the CPU you've been wiring.

To step through the RISC-V instructions, click Simulate → Tick Full Cycle. In each clock cycle, your CPU will output a new ProgramCounter to the harness, which will use the new PC to select the next instruction for your CPU to execute. In this addi test, the instructions execute in sequence, but when testing branches and jumps later, the CPU could output a different ProgramCounter value (not always adding 4) and execute the instructions in a different order.

To reset the simulation, click Simulate → Reset Simulator. You can also close and re-open the debugging circuit.

Writing Integration Tests

To write an integration test, all you need to do is write some RISC-V instructions for your CPU to run, and the testing framework will handle the rest.

  1. Navigate to tests/integration-custom/in.
  2. Write a RISC-V test and save it in a filename ending in .s.
  3. Run bash test.sh test_custom.

Some things to keep in mind when writing your RISC-V instructions:

The testing framework only checks the values in the 8 debug registers when comparing your CPU output with the reference output, so when writing your own tests, make sure to only use the 8 debug registers.

This also means the testing framework doesn't check memory (DMEM) when comparing your CPU with the reference. To check values in memory or a non-debug register, you'll need to put the value back into a debug register. For example, to test if a store works, you'll probably have to load the value back from memory into a debug register to see if the value was successfully stored.

IMEM and DMEM are separate in Logisim, but combined in Venus. This means that if you write assembly code that tries to access memory overlapping with instructions, Venus will throw an error. Since counting exactly how many instructions your assembly code requires, and multiplying that by 4 can be annoying, we suggest you load/store using addresses greater than 0x3E8 (leaving space for 1000 bytes/250 instructions), and increase this offset if you have more instructions.

Make sure to write RISC-V instructions that behave differently on a working CPU and a buggy CPU. For example, consider this test:

addi t0, x0, 0
addi t1, x0, 0

This wouldn't be very useful to check for a working CPU, because the output in the debugging registers could be all zeros even if your CPU doesn't work. As another example:

beq t0, t0, 4
addi t1, x0, 10

On a working CPU, this would branch to the addi instruction. On a buggy CPU where the branch is incorrectly not taken, this would still execute the addi instruction, so this test doesn't do a very good job of distinguishing working circuits from buggy circuits.

Logisim Tips

This section contains some helpful Logisim tips and pitfalls to avoid.

Wiring

  • If you want to know more details about each component, go to Help -> Library Reference for more information on the component and its inputs and outputs.
  • Use tunnels! They will make your wiring cleaner and easier to follow, and will reduce your chances of encountering crossed wires or unexpected errors.
  • Ensure you name your tunnels correctly. The labels are case sensitive!
  • You can hover your cursor over an input/output on a component to get slightly more information about that input/output.

Wiring Pitfalls

  • Your circuits should always fit in the provided harnesses. This means that you should not edit the provided input/output pins or add new ones. To ensure your circuit fits int he harness, you can open the harnesses in the harnesses folder and check that there are no errors.
  • Don't create new .circ files. You can make additional subcircuits if you want, but they must be in existing files.

Subcircuits

  • Note that if you modify a subcircuit, and another circuit file uses that subcircuit, you will need to close and re-open the outer circuit to load the changes from the subcircuit. For example, if you modify imm-gen.circ, you should close and re-open cpu.circ to load your changes.
  • When modifying a subcircuit, you should always open up the subcircuit file. For example, you should modify imm-gen.circ, not the imm-gen subcircuit in cpu.circ.

Signal Tips

  • The clock input signal (clk) can be sent into subcircuits or attached directly to the clock inputs of memory units in Logisim, but should not otherwise be gated (i.e., do not invert it, do not AND it with anything, etc.).
  • We recommend not using the Enable input on your MUXes. In fact, you can turn that attribute off (Include Enable?). We also recommend that you disable the Three-state? attribute (if the plexer has it).

Banned Circuit Elements

The following circuit elements are not necessary for this project, so please don't use them in your implementation.

  • Pull Resistor
  • Transistor
  • Transmission Gate
  • Power
  • POR
  • Ground
  • Divider
  • Random
  • PLA
  • RAM
  • Random Generator