Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fearless Refactoring with Types

Refactoring is an unavoidable aspect of agile design, as requirements change and you learn more about your problem space, your code has to change to adjust. However, doing so is often an almost scary process, are you sure that the changes you make do not affect another part of the program? Good use of types in a powerful type system can mitigate this and allow you to make big changes to your code, almost without thinking while being relatively confident that you cover all cases. This is what we will explore in this lab.

A Simple Processor

In this lab, you will work with a simple processor design whose ISA is defined by a single enum, Insn,

enum Insn {
    Nop,
    Set{dest: uint<4>, value: uint<16>},
    Add{dest: uint<4>, opa: uint<4>, opb: uint<4>},
    Jump{offset: int<16>},
    Out{op: uint<4>}
}

While doing this means you lose out on some tricks you can do with instruction encoding to build a more efficient processor, it does make adapting the processor very easy. This may for example be embedded deep in your chip to do management tasks that are not particularly well suited to writing a big FSM, but where using a whole RISC-V core is excessive.

Setting up

Like the last exercise, you will work in an existing codebase. If you didn't clone the code for the tutorial already, do so

git clone https://gitlab.com/spade-lang/agile-tutorial.git

Then navigate into the processor folder which is where we will do the rest of this part of the tutorial. Again, we can compile the Spade code and run the simulation with the following commands

cd game
swim build && swim cmd cargo run

This prints a sequence of timestamps (@xxxx) and an output value for those timestamps.

A Tour of the Processor

The processor we are working with is very simple, it is a non-pipelined processor that spreads execution out over several clock cycles in order to deal with things like latency from memories. In order to reduce the risk of documentation/code mismatches, we will not describe the processor in detail here, instead have a look at the src/main.spade file and the comments in it.

Task 1

For the next few tasks, we will gradually extend the processor to support more instructions. First, in the example program which increments a counter in a loop, we have to allocate a register to store the increment value. Instead, we should add an AddImmediate instruction to allow adding a constant instead.

With that change, the following program should have the same output as the original

Insn::Set$(dest: 0, value: 0),
Insn::Set$(dest: 2, value: 100),
Insn::AddImmediate$(dest: 0, opa: 1, value: 1),
Insn::Out$(op: 0),
Insn::Jump$(offset: -3),
Insn::Jump$(offset: 0)

Since the point here is that the language will let you make this change without giving git too much thought and just following the compiler. To try this, make the required change to the Insn enum, or even just the program, and then follow the compiler errors until your code compiles again.

Debugging

If everything went well, the compiler told you all the points you had to adjust, and you adjusted those points correctly, but in case something went wrong you probably want to do some debugging in a waveform viewer. The Surfer waveform viewer was built for Spade and includes automatic translation of types from their simulated bit values back into Spade types. To look at the waveforms from your simulation, simply run

swim cmd surfer build/vcd.vcd

and you should get a waveform viewer where you can look at all the signals in your design to help guide you to a solution to whatever problem you have.

Task 2

Let us keep expanding the instruction set of our little processor, this time by adding a conditional jump instruction. Like RISC-V, it should take two registers and a comparison operator, for example LessThan, GreaterThan etc., and jump if opa Operator opb is true. For the operator, use

enum Cmp {
  Lt,
  Gt,
  Eq
}

Add the instruction, and use it in the example program to exit the counter loop if the counter goes past 100.

Task 3

Software is nice, but it is of course often nice to have some "accelerated" instructions in a processor to speed things up. Let's try that with a hardware divider which we can get from [https://gitlab.com/spade-lang/lib/dividers/-/blob/main/src/main.spade?ref_type=heads#L21]. You can add this as a dependency by adding a line in the [libraries] section in swim.toml:

dividers.git = "https://gitlab.com/spade-lang/lib/dividers.git"

In order to save resources, this divider, as the serial part of the name implies, computes a value over multiple clock cycles so to integrate it in your core you either have to pause the core until the divider is done, or use a two stage process where one instruction starts the computation, and another reads the result when it is ready.

Pick one of these options and add a divide instruction to your core.

NOTE The ready "input" to the serial_div entity is out of scope for this tutorial, for now, just set it to port#1.