Add exercises
This commit is contained in:
commit
ac3cf23632
19 changed files with 21160 additions and 0 deletions
477
README.md
Normal file
477
README.md
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
[[_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.
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue