This commit is contained in:
marton bognar 2025-03-12 13:34:09 +01:00
parent ac3cf23632
commit 2256bc29f7
5 changed files with 399 additions and 392 deletions

381
Tutorial.md Normal file
View file

@ -0,0 +1,381 @@
# 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/).
# Table of Contents
[[_TOC_]]
# 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 modeling in SpinalHDL by building
the same 16-bit counter module we saw during the lectures. As in
Verilog, we will create an 8-bit counter module first and use that as the building
blocks for a 16-bit counter.
While designing the combinational path in Verilog, we could write 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
}
```