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 registercount
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 sincecount
only refers to itself andduration
.reset(rst: 0)
says that the register should be reset back to0
wheneverrst
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
calledbtn
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
A "unit" in spade is similar to entity
in VHDL and module
in Verilog.
The input -> output
flow is not always well suited to hardware, in those cases, ports may be used.