Updates
This commit is contained in:
parent
ac3cf23632
commit
2256bc29f7
5 changed files with 399 additions and 392 deletions
396
README.md
396
README.md
|
|
@ -1,5 +1,3 @@
|
|||
[[_TOC_]]
|
||||
|
||||
# Introduction
|
||||
|
||||
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
|
||||
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/).
|
||||
Before diving into this session, it is strongly recommended to read our
|
||||
[tutorial](Tutorial.md).
|
||||
|
||||
# Setup
|
||||
# Exercises
|
||||
|
||||
Read [this](SpinalTest) to learn how to install all prerequisites and test your
|
||||
setup.
|
||||
# 1. popcnt
|
||||
|
||||
# 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
|
||||
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
|
||||
returns the number of `1` bits in it.
|
||||
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
|
||||
|
|
@ -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;
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
Find the generated VCD file and examine it in GTKWave to confirm that your
|
||||
implementation works as expected.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue