Blinky (for hardware people)

This chapter will show the very basics of Spade and is aimed at people who are already familiar with basic digital hardware and want to learn the language. If you come here as a software developer, the Blinky (for software people) chapter is probably more approachable.

A blinky circuit in Spade is written as

entity blinky(clk: clock, rst: bool) -> bool {
    let duration = 100_000_000;
    reg(clk) count: uint<28> reset(rst: 0) = if count == duration {
        0
    } else {
        trunc(count + 1)
    };

    count > duration / 2
}

The first line defines a "unit" 1 called blinky which takes a clock and a reset signal and returns (->) a bool which will be true when the blinking LED should be on. This highlights an important difference between Spade and traditional HDLs: most2 units in Spade take a number of input signals and produces an output signal instead of operating on a set of input or output ports. In general, Spade units are much more "linear" than their VHDL and Verilog counterparts - Variables can only be read after their definition (unless pre-declared using decl) and units do not mix inputs with output.

The first line in the body of the entity uses let to define a new variable called duration whose value is the number of clock cycles in a blink period, here we assume a 100 MHz clock. Spade is a statically typed language so duration will have a fixed type known at compile time, however, the compiler uses type inference to infer the types of variables where possible. In this case, the duration variable is compared to count on the next line which forces its type to be the same as count, i.e. uint<28> and the compiler will ensure that the value fits in the inferred type's range. If needed, the type of a variable can be specified explicitly using let duration: uint<28> = ....

The next few lines are a reg statement which is used to declare a register. The syntax for these can be hard to take in at first, but it helps to break it up into pieces:

  • reg(clk) specifies which clock is used to clock this register
  • count is the name of the variable which will hold the register value
  • : uint<28> specifies the type of the register. Normally this can be omitted but in this case the compiler is unable to infer the size without it since count only refers to itself and duration.
  • reset(rst: 0) says that the register should be reset back to 0 whenever rst is asserted. At the moment, this is always done using an asynchronous reset.

Finally, the statement is ended with an = sign followed by an expression that gives the new value of the register as a "function" of its previous value. Here, the register is set back to 0 if it has reached the duration, otherwise it is incremented by 1. A significant difference between Spade and most other HDLs here is that its semantics are not "imperative". We do not write

if count == duration {
  count = 0
} else {
  count = trunc(count + 1)
}

which is conceptually hard to map to hardware, instead the if construct returns a value which is assigned to the register's new value. This is much closer to the multiplexers that will be generated here than the imperative description is, and prevents bugs if one for example, forgets to give count a value in the else branch.

The trunc function call in the else branch is another effect of Spade's type system. The type system is designed to prevent accidental destruction of information. Since a + 1 can require one more bit than a itself, the type of count + 1 is uint<28+1>, which cannot be implicitly converted to a uint<28>. The trunc function explicitly truncates the result back to fit in the register's value.

The final line count > duration / 2 is what sets the output of the unit. Whenever count is greater than half the duration of the counter, its output will be true. The final expression in a unit is its return value which may feel unfamiliar at first, but eventually feels quite natural, especially when combined with other block-based constructs. For example, the same thing is true in if-expressions. The 0 and trunc(count + 1) are the final expressions in the blocks, and therefore their "return" values.

A note on division: You may question the use of / in the above example since division is usually a very expensive operation in hardware. However, divisions by powers of two are cheap, so spade explicitly allows those. If the code was changed to / 3, you would get a compiler error telling you about the performance implication and telling you to explicitly use combinational division if you are OK with the performance.

error: Division can only be performed on powers of two
   ┌─ src/blinky.spade:10:24
   │
10 │     count > duration / 3
   │                        ^ Division by non-power-of-two value
   │
   = help: Non-power-of-two division is generally slow and should usually be done over multiple cycles.
   = If you are sure you want to divide by 3, use `std::ops::comb_div`
   │
10 │     count > duration `std::ops::comb_div` 3
   │                      ~~~~~~~~~~~~~~~~~~~~

Play around

If you want to play around with the language at this point, you can try to modify the code to do some of these things:

  • Add an additional input to the entity called btn which can be used to pause the counter
  • Use btn to invert the blink pattern

You can try the code directly in your browser at ▶️ play.spade-lang.org

1

A "unit" in spade is similar to entity in VHDL and module in Verilog.

2

The input -> output flow is not always well suited to hardware, in those cases, ports may be used.