[[_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)`. ![Example shift register configuration](Images/shift_reg.svg) 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.