Updates
This commit is contained in:
parent
ac3cf23632
commit
2256bc29f7
5 changed files with 399 additions and 392 deletions
|
|
@ -2,7 +2,7 @@ name := "SpinalExercises"
|
||||||
version := "0.1"
|
version := "0.1"
|
||||||
|
|
||||||
scalaVersion := "2.11.12"
|
scalaVersion := "2.11.12"
|
||||||
val spinalVersion = "1.7.3"
|
val spinalVersion = "1.10.2a"
|
||||||
|
|
||||||
fork := true
|
fork := true
|
||||||
|
|
||||||
|
|
|
||||||
396
README.md
396
README.md
|
|
@ -1,5 +1,3 @@
|
||||||
[[_TOC_]]
|
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
This exercise session introduces SpinalHDL, a Scala-based Hardware Description
|
This exercise session introduces SpinalHDL, a Scala-based Hardware Description
|
||||||
|
|
@ -12,386 +10,17 @@ 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
|
components but only by combining those basic components. Having a good
|
||||||
understanding of RTL is therefore imperative before using SpinalHDL.
|
understanding of RTL is therefore imperative before using SpinalHDL.
|
||||||
|
|
||||||
This text explains the basic concepts of SpinalHDL before introducing the
|
Before diving into this session, it is strongly recommended to read our
|
||||||
exercises. However, it is not meant to be a comprehensive description of all its
|
[tutorial](Tutorial.md).
|
||||||
features. For that, we refer to the official
|
|
||||||
[documentation](https://spinalhdl.github.io/SpinalDoc-RTD/).
|
|
||||||
|
|
||||||
# Setup
|
# Exercises
|
||||||
|
|
||||||
Read [this](SpinalTest) to learn how to install all prerequisites and test your
|
# 1. popcnt
|
||||||
setup.
|
|
||||||
|
|
||||||
# Basic logic
|
For the first exercise, the `popcnt` module mentioned during the Verilog lecture
|
||||||
|
should be implemented in SpinalHDL. This module takes as input a bitvector, and
|
||||||
We will introduce the basic concepts of RTL modelling in SpinalHDL by building
|
returns the number of `1` bits in it.
|
||||||
the same 16-bit timer module we built in Verilog during the lectures. As in
|
The end goal is to implement a class that is
|
||||||
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
|
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
|
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
|
`Popcnt.scala` and the simulation can be run using the `PopcntSim` object in
|
||||||
|
|
@ -423,7 +52,11 @@ It is recommended to implement the logic incrementally:
|
||||||
this is to use a fold operation on the bits of the input;
|
this is to use a fold operation on the bits of the input;
|
||||||
1. Add the logic for the multi-cycle implementation.
|
1. Add the logic for the multi-cycle implementation.
|
||||||
|
|
||||||
# Exercise: a configurable shift register
|
You can run the tests by executing `sbt 'runMain exercises.PopcntSim'` in the
|
||||||
|
Exercises directory. Look at the test cases and add more to thoroughly test your
|
||||||
|
implementation!
|
||||||
|
|
||||||
|
# 2. Configurable shift register
|
||||||
|
|
||||||
In this exercise, you will be implementing a component that manages [shift
|
In this exercise, you will be implementing a component that manages [shift
|
||||||
registers](https://en.wikipedia.org/wiki/Shift_register). This component
|
registers](https://en.wikipedia.org/wiki/Shift_register). This component
|
||||||
|
|
@ -474,4 +107,5 @@ simulation can be run using the `ShiftRegSim` object in `Top.scala`. The
|
||||||
simulation adds two registers (see method `createShiftReg`) and runs for 10
|
simulation adds two registers (see method `createShiftReg`) and runs for 10
|
||||||
cycles, setting the input of the first register to increasing values starting at
|
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.
|
0, and the input of the second register to decreasing values starting at 10.
|
||||||
|
Find the generated VCD file and examine it in GTKWave to confirm that your
|
||||||
|
implementation works as expected.
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,12 @@
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
|
|
||||||
- [sbt](https://www.scala-sbt.org/download.html)
|
- [sbt](https://www.scala-sbt.org/download.html) (which relies on a Java JDK)
|
||||||
- [Verilator](https://verilator.org/guide/latest/install.html)
|
- [Verilator](https://verilator.org/guide/latest/install.html)
|
||||||
- A Java JDK
|
- A Java JDK
|
||||||
|
|
||||||
More info can be found in the SpinalHDL
|
More info can be found in the SpinalHDL
|
||||||
[documentation](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Getting%20Started/getting_started.html).
|
[documentation](https://spinalhdl.github.io/SpinalDoc-RTD/master/SpinalHDL/Getting%20Started/getting_started.html).
|
||||||
|
|
||||||
On Arch Linux, everything can be installed from the package repositories:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo pacman -S sbt verilator jdk-openjdk
|
|
||||||
```
|
|
||||||
|
|
||||||
The PCs in the CS department have everything installed.
|
|
||||||
|
|
||||||
# Test installation
|
# Test installation
|
||||||
|
|
||||||
To test code generation (this should create a file called `Counter.v`):
|
To test code generation (this should create a file called `Counter.v`):
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ name := "SpinalTest"
|
||||||
version := "0.1"
|
version := "0.1"
|
||||||
|
|
||||||
scalaVersion := "2.11.12"
|
scalaVersion := "2.11.12"
|
||||||
val spinalVersion = "1.7.3"
|
val spinalVersion = "1.10.2a"
|
||||||
|
|
||||||
fork := true
|
fork := true
|
||||||
|
|
||||||
|
|
|
||||||
381
Tutorial.md
Normal file
381
Tutorial.md
Normal 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
|
||||||
|
}
|
||||||
|
```
|
||||||
Loading…
Add table
Add a link
Reference in a new issue