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:
Entities together with Functions together with entity and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike entities and pipelines, functions have no internal state, they are combinational (pure). and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike Functions together with entity and Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. are the basic building blocks of Spade circuits. Unlike entities and pipelines, functions have no internal state, they are combinational (pure)., entity can contain registers and therefore have state and unlike Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline., they do not have a statically known latency or Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. structure. blinky(clk: Type used to represent clock signals., rst: Type used to represent 1 bit values) -> Type used to represent 1 bit values {
let is used to define a variable. Spade infers the type of most variables from context, but you can also specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.y the type with : <type> before the =. duration = 100_000_000;
reg(clk) is used to define registers which maintain the state of your circuit.
All registers have a Type used to represent clock signals. (clk), a name and a new value after the = which is given as a function of the current value. Registers can also have a Defines the reset value of a register as (trigger: value). When trigger ist rue, the reset is synchronously reset to valuetrigger: value) wich means that the reset is synchronously set to value when trigger is true.
In pipelines, you can also define registers with reg; is used in a Defines a pipeline. The number in in the parentheses is input-to-output latency of the pipeline. to separate stages. When you refer to a variable defined above a reg; statement below a reg; statement you refer to a registered version of the original value. Registers defined with an explicit Type used to represent clock signals. (reg(clk)) are used for state registers both inside and outside pipelines. which are used to separate stages. count: Type used to represent unsigned integers. The generic parameter (<N>) specReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.ies the number of bits Defines the reset value of a register as (trigger: value). When trigger ist rue, the reset is synchronously reset to valuerst: 0) = Returns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it. count == duration {
0
} else {
Truncates the input to a smaller size, throwing away the most signReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.icant bits. The size of the resulting value is type inferred.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 produce 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.countis 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 sincecountonly refers to itself andduration.reset(rst: 0)says that the register should be reset back to0wheneverrstis asserted. At the moment, this is always done using a synchronous 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:
Returns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it. count == duration {
count = 0;
} else {
count = Truncates the input to a smaller size, throwing away the most signReturns the value of the first branch if the condition is true, otherwise the second branch.
Note that unlike software languages, Spade does not have conditional execution. You do not conditionally assign values inside if expressiosn, you compute a value and return it.icant bits. The size of the resulting value is type inferred.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 uint<28>. The trunc function explicitly truncates the result back to fit in the register’s type.
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
entitycalledbtnwhich can be used to pause the counter.Use
btnto invert the blink pattern.
You can try the code directly in your browser at ▶️ play.spade-lang.org.