Blinky (for software people)
This chapter will show the very basics of Spade and is aimed at people who are familiar with software development but are new to hardware. If you come here with some experience in hardware design with VHDL or Verilog, the Blinky (for hardware people) chapter is probably more useful.
Before blinking an LED, we can try turning on or off an LED. We can do this as
entity blinky() -> bool {
true
}
To Rust users, this will likely feel very familiar. To those familiar with
other languages, the last value at the end of a block is "returned", so this is
an entity
which returns true
.
If we connect the output signal of this to an LED, it would turn on. If you're curious, you can try it ▶️ on the playground
This isn't particularly interesting though, so let's do something more
interesting. Blinking an LED is the typical "Hello, World" in hardware, but
even that requires some complexity so we will build up to it. Let's first start
by making the LED turn off while we hold down a button, which first requires
taking a btn
as an input:
entity blinky(btn: bool) -> bool {
and then changing the output to !btn
entity blinky(btn: bool) -> bool {
!btn
}
If you ▶️ try this, you can see that if you press the button, the LED turns off, and if you release it, it will turn on again. Here we're just simulating the resulting hardware, but if we connected this up to real hardware, it would also work!
If you think about this for a while, you may start wondering when this gets
"evaluated". In software, this "function" would be called once, giving it the value of
the button and generating a single result. But this somehow reacts to inputs!
While Spade, and many HDLs for that matter may look like software, it is
important to note that we are not describing instructions for some processor
to execute, we are describing hardware to be built on a chip. The code we
wrote says "connect input btn
to an inverter, whose output in turn should be
connected to the output of the module which we externally connect to an LED.
If we want to approximate the behaviour from a software perspective, we can view the progrmaming model of Spade either as continuously re-evaluating every value in the design, or as re-evaluating every value when the values it depends on changes.
At this point, we can start thinking about actually making an LED blink. In software we'd probably accomplish this by writing something along the lines of
def main():
led_on = Talse
while True:
led_on = not led_on;
set_led(led_on);
sleep(0.5);
However, because we are describing hardware, not software we can't really "loop". Every expression we write will correspond to some physical block of hardware, rather than instructions that get executed.
A Detour Over Software
Before talking about how we would make a LED blink in hardware and Spade, it is helpful to talk about how we might write a software function to to "blink" an LED if we can't have loops inside our function. Remember that we can view our execution model as constantly re-evaluating our function to get its new values, roughly
def blinky():
return True
while True:
print(blinky())
On the surface, it might seem very difficult to make this thing blink, but if we had some way to maintain state between calls of the function. In software, we can achieve this by using a global variable for the state of the LED
LED_ON = False
def blinky():
global LED_ON
LED_ON = not LED_ON
return LED_ON
while True:
print(blinky())
If we run this program, we'll now get alternating True
and False
True
False
True
False
...
There are some problems with this though, our value is "blinking" far too fast for us to see it blinking. If this were hardware, the LED would just look dim as opposed to clearly switching between on and off, we need to regulate it somehow. A quick way to do this would be to just call our function less often, for example, once per second. As we'll see, this is something we can kind of do in hardware, so let's try it!
import time
while True:
start = time.time()
print(blinky())
end = time.time()
# We want each iteration to take 0.5 seconds
# so we get a blinking frequency of 1 hz.
# To avoid drifting if `blinky` ends up taking
# a long time, we'll compute how long the evaluation
# took and subtract that from the period
time.sleep(0.5 - (end - start))
That works, but has a major problem: now we cannot do anything more often than
once per second, so if our program was to do more things than blinking an LED,
we're probably screwed. To solve this, we can reduce the sleep time to
something faster, but which we can still manage without having end-start
become larger than the period. Being conservative, we'll aim for a frequency of 1 KHz
import time
while True:
start = time.time()
print(blinky())
end = time.time()
time.sleep(0.001 - (end - start))
If we just run our blinky now, we're back to it blinking faster than we can see, so we'll need to adjust it to compute how long it has been running and toggling the LED accordingly
COUNTER = 0
def blinky():
global COUNTER
if COUNTER == 1000:
COUNTER = 0
else:
COUNTER = COUNTER + 1
# The LED should be on in the second counter interval
return COUNTER > 500
import time
while True:
start = time.time()
print(blinky())
end = time.time()
time.sleep(0.001 - (end - start))
Back To Hardware
At this point, you have got a sense of a (pretty cursed) programming model that approximates hardware pretty well, so we can get back to writing hardware.
Almost all primitive hardware blocks are pure (or combinatorial as it is known in hardware). They take their inputs and produce an output. This includes arithmetic operators, comparators, logic gates and "if expressions" (multiplexers). Using these to build up any form of state, like our counter, will be very difficult. Luckily there is a special kind of hardware unit called a flip_flop which can remember a single bit value. These come in several flavours and by far the most common is the D-flipflop which has a signature that is roughly
entity dff(clk: clock, new_value: bool) -> bool
Its behaviour when the clock signal (clk
) is unchanged is to simply remember
its current value. Flip flops become much more interesting when we start
toggling the clock. Whenever the clk
signal changes from 0 to 1, it will
replace its currently stored value with the value that is on its new_value
input.
Hardware is often shown graphically, and a dff
is usually drawn like this:
Using this, we can build our initial very fast blinking circuit like this:
entity blinky_dff(clk: clock) -> bool {
decl led_on;
let led_on = inst dff(clk, !led_on);
led_on
}
Don't worry too much about the syntax here, we define led_on
as a dff
whose new value is !led_on
. When the clk
goes from 0 to 1, the dff
will
take the value that is on its input (!led_on
) and set it as its internal
value, which makes the LED blink. This might be easier to understand graphically:
We can also visualize the value of the signals in the circuit over time, which looks roughly like
As soon as the clock switches from 0 to 1, the value of led_on
switches to
new_value
. This in turn makes the output of the inverter change to the
inverse which is now the "new new_value
". Then nothing happens until the
clock toggles again at which point the cycle repeats.
At this point, you should be wondering what the initial state of the register is as right now it only depends on itself. While it is possible to specify initial values in registers in FPGAs, that's not possible when building dedicated hardware, so the proper approach is to use a third input to the DFF that we left out for now: the reset. It takes a bool
which tells the flip flop to reset its current value if 1, and a value to reset to. Again, looking at the signature, this would be roughly
entity dff(clk: clock, rst_trigger: bool, initial_value: bool, new_value: bool) -> bool
When rst
is true, the internal value of the dff will get set to initial_value
.
Visualized as signal values over time, this looks like:
The clk
and rst_trigger
signal are typically fed to our hardware
externally. The clock is as you may expect from reading clock signal
specifications on hardware, quite fast. Not quite the 3-5 GHz that you may
expect from a top of the line processor, but usually between 10 500 MHz in
FPGAs. This means that we need to pull the same trick we did in our software
model to make the blinking visible: maintain a counter of the current time and
use that to derive if the led should be on or not.
Our counter needs to be quite big to count on human time scales with a 10 Mhz
clock, so building a counter from individual bool
s with explicit dff
s for
each of them is infeasible. Therefore, we almost always use "registers" for our state. These are just banks of dff
with a shared clock and reset.
Additionally, using our dff
entity isn't super ergonomic since it requires that decl
keyword, so Spade has dedicated syntax for registers. It looks like this
reg(clk) value: uint<8> reset(rst: reset_value) = new_value
which, admittedly is quite dense syntax. It helps to break it down in pieces though
reg(clk)
specifies that this is a register that is clocked byclk
.1value
is the name of the variable that will hold the current register value: uint<8>
specifies the type of the register, in this case an 8 bit unsigned value. In most cases, the type of variables can be inferred, so this can be left outreset(rst: reset_value)
says that the register should be set back toreset_value
whenrst
is true. If the register does not depend on itself, it can be omitted
Blinky, Finally
We finally have all the background we need to drumroll 🥁 blink an LED! The code to do so looks like this
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
}
Looking at the python code we wrote before, we can see some similarities. Our
global count
has been replaced with a reg
. reg
has a special scoping rule
that allows it to depend on its own value, unlike normal let
bindings which
are used to define other values. The new value of the register is given in
terms of its current value. If it is duration
, it is set to 0, otherwise it
is set to count + 1
.
trunc
is needed since Spade prevents you from overflows and underflows by
extending signals when they have the potential to overflow. count + 1
can
require one more bit than count
, so you need to explicitly convert the value
down to 28
bits. trunc
is short for "truncate" which is the hardware way of
saying "throwing away bits".
Those unfamiliar with Rust or other functional languages may be a bit surprised that the if
isn't written as
if count == duration {
count = 0
} else {
count = trunc(count + 1)
}
This is because spade is expression based -- conditional return values instead of having side effects. This is because in hardware, we can't really re-assign a value conditionally, the input to the "new value" field of the register is a single signal, so all variables in Spade are immutable.
If you are used to C or C++, you can view if
expressions as better ternary
operators (cond ? on_true : on_false
), and python users may view them as the
on_true if cond else false
construct.
Play around
At this point it might be fun to play a little bit with the language, you could try modifying the code to:
- 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
Technically, there are a whole family, but in practice we almost always use registers built from D-flip flops. 1: Most of the time when starting out you'll just have one clock, but as you build bigger systems, you'll eventually need multiple clocks