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:

src/dff_schematic.svg

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:

A graphical representation of a circuit that toggles an LED on and off

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 bools with explicit dffs 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 by clk.1
  • value 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 out
  • reset(rst: reset_value) says that the register should be set back to reset_value when rst 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 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

2

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