477 lines
16 KiB
Markdown
477 lines
16 KiB
Markdown
[[_TOC_]]
|
|
|
|
# Introduction
|
|
|
|
This exercise session introduces SpinalHDL, a Scala-based Hardware Description
|
|
Language (HDL). SpinalHDL is implemented as a Scala library that generates RTL
|
|
in the form of either Verilog or VHDL code. It is also capable of running, and
|
|
interacting with, a Verilog simulation from Scala.
|
|
|
|
SpinalHDL is a RTL code generator rather than a higher-level HDL. Of course, the
|
|
full power of Scala can be used to build abstractions on top of the basic RTL
|
|
components but only by combining those basic components. Having a good
|
|
understanding of RTL is therefore imperative before using SpinalHDL.
|
|
|
|
This text explains the basic concepts of SpinalHDL before introducing the
|
|
exercises. However, it is not meant to be a comprehensive description of all its
|
|
features. For that, we refer to the official
|
|
[documentation](https://spinalhdl.github.io/SpinalDoc-RTD/).
|
|
|
|
# Setup
|
|
|
|
Read [this](SpinalTest) to learn how to install all prerequisites and test your
|
|
setup.
|
|
|
|
# Basic logic
|
|
|
|
We will introduce the basic concepts of RTL modelling in SpinalHDL by building
|
|
the same 16-bit timer module we built in Verilog during the lectures. As in
|
|
Verilog, we will create an 8-bit timer module first and use that as the building
|
|
blocks for a 16-bit timer.
|
|
|
|
While designing the combinational path in Verilog, we ended-up with the
|
|
following code (ignoring the declaration of all signals that are read):
|
|
|
|
```verilog
|
|
reg [7:0] t2;
|
|
|
|
always @* begin
|
|
if (rst) t2 = 0;
|
|
else if (enable) t2 = counter + 1;
|
|
else t2 = counter ;
|
|
end
|
|
```
|
|
|
|
In SpinalHDL, the same logic could be modelled as follows:
|
|
|
|
```scala
|
|
val t2 = UInt(8 bits)
|
|
|
|
when (rst) {
|
|
t2 := 0
|
|
} elsewhen (enable) {
|
|
t2 := counter + 1
|
|
} otherwise {
|
|
t2 := counter
|
|
}
|
|
```
|
|
|
|
Structurally, this looks very similar to the Verilog code. The most important
|
|
difference is the lack of an equivalent of Verilog's `always` block. When a
|
|
value is assigned to a variable, SpinalHDL uses its type annotations to figure
|
|
out if it should create combinational or synchronous logic. In this case, since
|
|
`t2` is not declared as a register, combinational logic will automatically be
|
|
created that is equivalent to the Verilog code above.
|
|
|
|
While in Verilog we only specify the bit width of signals, SpinalHDL has
|
|
stricter
|
|
[types](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Data%20types). The
|
|
most important simple types are `Bool` (single-bit signal), `Bits` (bit-vector),
|
|
`UInt`/`SInt` (multi-bit signal supporting unsigned/signed arithmetic
|
|
operations). The multi-bit types take their bit width as a constructor argument,
|
|
which is specified as a positive integer followed by the keyword `bits`.
|
|
|
|
Signals can freely be "cast" between types using the methods `asBits`, `asUInt`,
|
|
`asSInt`, and `asBools`. The latter method converts a multi-bit signal to a
|
|
`Vec[Bool]` ([see here](#aggregate-data-types)).
|
|
|
|
SpinalHDL uses the `:=` operator to connect signals. For most arithmetic
|
|
operations, the same operators as in Verilog or Scala are used (e.g., `+`,
|
|
`&&`,...). The two exceptions are tests for equality (`===`) and inequality
|
|
(`=/=`) because using the standard operators (`==` and `!=`) would clash with
|
|
the use of those operators in Scala's class hierarchy.
|
|
|
|
Because Scala already has keywords named `if` and `else`, SpinalHDL introduces
|
|
the keywords `when` (`if` in Verilog), `elsewhen` (`else if`), and `otherwise`
|
|
(`else`). Besides their names, they are syntactically similar as normal
|
|
conditional execution in Scala.
|
|
|
|
For storing the value in a register, we initially had the following Verilog code:
|
|
|
|
```verilog
|
|
reg [7:0] counter;
|
|
|
|
always @(posedge clk) begin
|
|
counter <= t2;
|
|
end
|
|
```
|
|
|
|
which can be coded like this in SpinalHDL:
|
|
|
|
```scala
|
|
val counter = Reg(UInt(8 bits))
|
|
counter := t2
|
|
```
|
|
|
|
Next to its type (`UInt`), the `counter` signal is explicitly annotated to be a
|
|
register
|
|
([`Reg`](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Sequential%20logic/registers.html)).
|
|
Since SpinalHDL knows that `counter` is a register, we do not have specify any
|
|
clock edge sensitivity like we have to in Verilog. Note that there is no mention
|
|
of any clock signal at all. By default, SpinalHDL uses a global "clock domain"
|
|
(which contains, among others, a clock signal) to drive all registers in the
|
|
design. This means that in most designs it is not necessary to ever explicitly
|
|
refer to a clock.
|
|
|
|
The 8-bit counter design with all logic combined looked like this in Verilog:
|
|
|
|
```verilog
|
|
reg [7:0] counter;
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) counter <= 0;
|
|
else if (enable) counter <= counter + 1;
|
|
end
|
|
```
|
|
|
|
which is concisely specified as follows in SpinalHDL:
|
|
|
|
```scala
|
|
val counter = Reg(UInt(8 bits)).init(0)
|
|
|
|
when (enable) {
|
|
counter := counter + 1
|
|
}
|
|
```
|
|
|
|
The most important difference with Verilog is that we don't have to explicitly
|
|
deal with the reset signal. Like the clock signal, an implicit reset signal is
|
|
contained within the global clock domain. A reset value can be specified for a
|
|
register by calling its `init` method.
|
|
|
|
# Advanced muxing
|
|
|
|
We ran into trouble in Verilog when trying to model a 3-input mux like this:
|
|
|
|
```verilog
|
|
reg o;
|
|
|
|
always @* begin
|
|
if (s == 0) o = i0;
|
|
else if (s == 1) o = i1;
|
|
else if (s == 2) o = i2;
|
|
end
|
|
```
|
|
|
|
Here, Verilog would silently create a latch for `o`.
|
|
|
|
If we model the same logic in SpinalHDL:
|
|
|
|
```scala
|
|
val o = Bool
|
|
|
|
when (s === 0) {
|
|
o := i0
|
|
} elsewhen (s === 1) {
|
|
o := i1
|
|
} elsewhen (s === 2) {
|
|
o := i2
|
|
}
|
|
```
|
|
|
|
we get a very useful error (SpinalHDL checks for many [common design
|
|
errors](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Design%20errors):
|
|
|
|
```
|
|
LATCH DETECTED from the combinatorial signal (toplevel/o : Bool), defined at ...
|
|
```
|
|
|
|
The solution is the same as in Verilog: make sure a value is assigned to `o` under all conditions.
|
|
Like in Verilog, later assignments override earlier ones so we could do something like this:
|
|
|
|
```scala
|
|
val o = Bool
|
|
o := False // or o.assignDontCare()
|
|
...
|
|
```
|
|
|
|
SpinalHDL also supports a [switch
|
|
construct](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Semantic/when_switch.html):
|
|
|
|
```scala
|
|
switch (s) {
|
|
is (0) { o := i0 }
|
|
is (1) { o := i1 }
|
|
is (2) { o := i0 }
|
|
default { o := False }
|
|
}
|
|
```
|
|
|
|
and a more explicit `mux` method:
|
|
|
|
```scala
|
|
o := s.mux(
|
|
0 -> i1,
|
|
1 -> i1,
|
|
2 -> i2,
|
|
default -> False
|
|
)
|
|
```
|
|
|
|
# Modules
|
|
|
|
The full 8-bit counter module looked as follows in Verilog:
|
|
|
|
```verilog
|
|
module counter8(
|
|
input wire clk,
|
|
input wire rst,
|
|
input wire enable,
|
|
output reg[7:0] counter
|
|
);
|
|
always @(posedge clk) begin
|
|
if (rst) counter <= 0;
|
|
else if (enable) counter <= counter + 1;
|
|
end
|
|
endmodule
|
|
```
|
|
|
|
In SpinalHDL, modules are created by wrapping logic in the constructor of a
|
|
class that inherits from `Component`:
|
|
|
|
```scala
|
|
class Counter8 extends Component {
|
|
val enable = in(Bool)
|
|
val counter = out(Reg(UInt(8 bits)).init(0))
|
|
|
|
when (enable) {
|
|
counter := counter + 1
|
|
}
|
|
}
|
|
```
|
|
|
|
I/O ports are created by wrapping signal types with the `in` or `out`
|
|
annotations. Signals that are not annotated as I/O are local signals which
|
|
cannot be accessed from outside the surrounding module. Note again that the
|
|
`clk` and `rst` signals are not visible since they are part of the implicit
|
|
global clock domain.
|
|
|
|
Modules can be instantiated by creating objects of their classes.
|
|
I/O ports are then connected by simply assigning to or reading from them:
|
|
|
|
```scala
|
|
class Counter16 extends Component {
|
|
val enable = in(Bool)
|
|
val counter = out(UInt(16 bits))
|
|
|
|
val lsb = new Counter8
|
|
lsb.enable := enable
|
|
|
|
val msb = new Counter8
|
|
msb.enable := enable && (lsb.counter === 0xff);
|
|
|
|
counter := msb.counter @@ lsb.counter
|
|
}
|
|
```
|
|
|
|
# Workflow
|
|
|
|
SpinalHDL is implemented as a Scala library and all "keywords" discussed so for
|
|
are, in fact, not keywords but simply methods defined by this library. When
|
|
running a Scala program using SpinalHDL, an internal RTL representation is
|
|
created that corresponds to the logic defined by the library calls. This RTL can
|
|
then be converted to Verilog or VHDL:
|
|
|
|
```scala
|
|
object Generate {
|
|
def main(args: Array[String]): Unit = {
|
|
SpinalVerilog(new Counter16)
|
|
}
|
|
}
|
|
```
|
|
|
|
Here, the instantiation of the `Counter16` module builds an internal
|
|
representation of all `RTL` for this module which is then converted to Verilog
|
|
by passing the instance to the `SpinalVerilog` method. When running this main
|
|
method, a file called `Counter16.v` will be created that contains the generated
|
|
Verilog code.
|
|
|
|
SpinalHDL also supports running [Verilog
|
|
simulations](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Simulation/)
|
|
directly from Scala. This is implemented by first generating Verilog and then
|
|
simulating this code using [Verilator](https://www.veripool.org/wiki/verilator).
|
|
While the simulation is running, it is possible to access Verilog signals from
|
|
Scala which allows, for example, writing test cases in Scala. For this exercise
|
|
session, all necessary simulation code is provided.
|
|
|
|
The temporary files and results of the simulation are stored in the
|
|
`simWorkspace` directory. When a top-level component called `Foo` is simulated,
|
|
the following files are created (among others):
|
|
|
|
- `Foo/test.vcd`: VCD file resulting from the simulation;
|
|
- `Foo/rtl/Foo.v`: Verilog code used for the simulation.
|
|
|
|
# Naming
|
|
|
|
Since SpinalHDL generates Verilog or runs simulations using a Verilog simulator,
|
|
it is important that modules and signals defined in Scala are properly named in
|
|
Verilog. For the most common cases, SpinalHDL is able to automatically figure
|
|
out good names to use in Verilog using the following rules:
|
|
|
|
- Modules get the same name as the `Component` subclass used to generate them;
|
|
- Top-level signals defined in the constructor of a `Component` subclass get the
|
|
same name as the Scala instance variable used to store them.
|
|
|
|
In both cases, SpinalHDL will resolve naming conflicts with Verilog keywords by
|
|
appending a number to the name. It is recommended to avoid using Verilog
|
|
keywords for modules or signal names.
|
|
|
|
In more advanced use cases where signals are not necessarily stored in top-level
|
|
Scala instance variables, SpinalHDL will not be able to automatically select a
|
|
good name. Instead, it will generate a name of the form `_zz_n_` where `n` is
|
|
a natural number. Such signals are not dumped to theVCD file during simulation.
|
|
In these cases, the `setName` method can be used to manually select a name.
|
|
|
|
# Aggregate data types
|
|
|
|
SpinalHDL offers an array-like data structure called
|
|
[`Vec`](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Data%20types/Vec.html)
|
|
that holds a fixed number of signals of the same type. It implements Scala's
|
|
`IndexedSeq` trait. The main advantage of using `Vec` over one of Scala's
|
|
built-in collections is that SpinalHDL's naming algorithm recognizes it. It
|
|
creates a signal for each element that is named by appending its index to the
|
|
name of the `Vec`.
|
|
|
|
Multiple signals of different types can be bundled in a struct-like data
|
|
structure using the
|
|
[`Bundle`](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Data%20types/bundle.html)
|
|
base class. This is mainly useful to define complex buses in which multiple
|
|
signals are always used together. For example, an [SPI
|
|
bus](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface) could be defined
|
|
as follows:
|
|
|
|
```scala
|
|
class SpiBus(numSlaves: Int) extends Bundle {
|
|
val sclk = Bool
|
|
val mosi = Bool
|
|
val miso = Bool
|
|
val ss = Bits(numSlaves bits)
|
|
}
|
|
```
|
|
|
|
Buses often have an I/O direction and what is an output on one side of the bus,
|
|
is an input on the other side. The two sides of a bus are called master and
|
|
slave. To handle this, SpinalHDL defines the `IMasterSlave` trait. When
|
|
implementing this trait, the `asMaster` method must be implemented which should
|
|
set the I/O direction of all signals when the bus is used as a master (when used
|
|
as a slave, SpinalHDL automatically flips the direction of all signals):
|
|
|
|
```scala
|
|
class SpiBus(numSlaves: Int) extends Bundle with IMasterSlave {
|
|
...
|
|
override def asMaster(): Unit = {
|
|
out(sclk, mosi, ss)
|
|
in(ss)
|
|
}
|
|
}
|
|
```
|
|
|
|
SpinalHDL also supports an "auto-connect" operator (`<>`) that automatically
|
|
connects all signals of a bus to the corresponding signals of another bus
|
|
(taking their directions into account):
|
|
|
|
```scala
|
|
class SpiMaster extends Component {
|
|
val bus = master(new SpiBus)
|
|
...
|
|
}
|
|
|
|
class SpiSlave extends Component {
|
|
val bus = slave(new SpiBus)
|
|
...
|
|
}
|
|
|
|
class Soc extends Component {
|
|
val spiMaster = new SpiMaster
|
|
val spiSlave = new SpiSlave
|
|
spiMaster.bus <> spiSlave.bus
|
|
}
|
|
```
|
|
|
|
# Exercise: popcnt
|
|
|
|
For the first exercise, the `popcnt` module discussed during the Verilog lecture
|
|
should be implemented in SpinalHDL. The end goal is to implement a class that is
|
|
configurable in the number of bits of the input and in whether the operation
|
|
should be implemented in multiple cycles. The logic should be implemented in
|
|
`Popcnt.scala` and the simulation can be run using the `PopcntSim` object in
|
|
`Top.scala`.
|
|
|
|
The `Popcnt` component has the following constructor parameters:
|
|
|
|
- `width`: the bit width of the input;
|
|
- `multiCycle`: boolean indicating whether the logic should be implemented in
|
|
multiple cycles or asynchronous.
|
|
|
|
The following I/O ports should be defined by the `Popcnt` component:
|
|
|
|
- `start` (input): asserted when the operation should start. Not needed for the
|
|
asynchronous logic but should still be defined;
|
|
- `value` (input): input to the operation;
|
|
- `count` (output): result of the operation;
|
|
- `ready` (output): asserted when the operation is ready. Not needed for the
|
|
asynchronous logic but should still be defined;
|
|
|
|
It is recommended to implement the logic incrementally:
|
|
|
|
1. Implement the logic for a 4-bit asynchronous circuit. Beware that even though
|
|
the operation should be implemented using purely combinational logic, the
|
|
result should still be stored in a register. Also, the simulation will fail
|
|
with an obscure error message if you try to run it before defining a register
|
|
in your implementation;
|
|
1. Generalize this logic to support arbitrary bit widths. Hint: one way to do
|
|
this is to use a fold operation on the bits of the input;
|
|
1. Add the logic for the multi-cycle implementation.
|
|
|
|
# Exercise: a configurable shift register
|
|
|
|
In this exercise, you will be implementing a component that manages [shift
|
|
registers](https://en.wikipedia.org/wiki/Shift_register). This component
|
|
provides the following method to dynamically add a shift register:
|
|
|
|
```scala
|
|
def addReg(name: String, width: BitCount, delay: Int): (Bits, Bits)
|
|
```
|
|
|
|
This methods adds a new shift register to the component containing `delay`
|
|
`width`-bit registers in cascade. The `name` argument can be used to generate
|
|
readable names for all created I/O ports and registers. The return value of this
|
|
method is a 2-tuple where the first element is an input connected to the first
|
|
register in the cascade and the second element an output connected to the last.
|
|
The method `getReg` should return the same tuple.
|
|
|
|
As an example, the image below shows the configuration of the component after
|
|
calling
|
|
|
|
```scala
|
|
addReg("foo", 8 bits, 3)|
|
|
```
|
|
|
|
In this case, the tuple returned would be `(foo_in, foo_out)`.
|
|
|
|

|
|
|
|
SpinalHDL globally keeps track of the "current component". Whenever a new
|
|
instance is created of a `Component` subclass, the current component is set to
|
|
this instance. When logic is created, it is added to the current component. This
|
|
means that if logic is created inside the `addReg` method, it will be added to
|
|
the component that called this method, which is not what we want. To solve this,
|
|
SpinalHDL offers the `rework` method which can be called on a `Component` to add
|
|
logic to it after its creation:
|
|
|
|
```scala
|
|
class ExtendableComponent extends Component {
|
|
def addLogic(): Unit = {
|
|
rework {
|
|
// Add logic here
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The logic for this exercise should be implemented in `ShiftReg.scala` and the
|
|
simulation can be run using the `ShiftRegSim` object in `Top.scala`. The
|
|
simulation adds two registers (see method `createShiftReg`) and runs for 10
|
|
cycles, setting the input of the first register to increasing values starting at
|
|
0, and the input of the second register to decreasing values starting at 10.
|
|
|