Chisel Tutorial - 08.Chisel parameterized generator (from Scala)

Chisel parameterized generator (from Scala)

motivation

To make the Chisel module a code generator, there must be something that tells the generator how to do this. This section describes the parameterization of modules, covering a variety of methods and Scala language features. The enrichment of the parameter transfer implementation is directly transferred to the enrichment of the generated circuit. Parameters should provide useful default values, be easy to set, and not set to illegal or meaningless values. For more complex systems, it is useful to locally overload them in a way that does not affect the use of other modules.

Parameter Transfer

Chisel provides a powerful construction for writing hardware generators. Generator is a program that accepts some circuit parameters and generates a circuit description. This section discusses how the Chisel generator gets parameters.

Example: a parameterized Scala object

Each Mudule in Chisel is also a class of Scala, which can be parameterized as follows:

class ParameterizedScalaObject(param1: Int, param2: String) {
  println(s"I have parameters: param1 = $param1 and param2 = $param2")
}
val obj1 = new ParameterizedScalaObject(4,     "Hello")
val obj2 = new ParameterizedScalaObject(4 + 2, "World")

The result of execution is:

I have parameters: param1 = 4 and param2 = Hello
I have parameters: param1 = 6 and param2 = World

Example: parameterized Chisel object

The Chisel module can also be parameterized in the same way. The following module has all its input and output widths as parameters. Run the code below to print the generated Verilog code. Modify the parameters to see the output change.

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test

class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int) extends Module {
  require(in0Width >= 0)
  require(in1Width >= 0)
  require(sumWidth >= 0)
  val io = IO(new Bundle {
    val in0 = Input(UInt(in0Width.W))
    val in1 = Input(UInt(in1Width.W))
    val sum = Output(UInt(sumWidth.W))
  })
  // a +& b includes the carry, a + b does not
  io.sum := io.in0 +& io.in1
}

object MyModule extends App {
  println(getVerilogString(new ParameterizedWidthAdder(1, 4, 6)))
}

The output is as follows:

module ParameterizedWidthAdder(
  input        clock,
  input        reset,
  input        io_in0,
  input  [3:0] io_in1,
  output [5:0] io_sum
);
  wire [3:0] _GEN_0 = {{3'd0}, io_in0}; // @[MyModule.scala 16:20]
  wire [4:0] _io_sum_T = _GEN_0 + io_in1; // @[MyModule.scala 16:20]
  assign io_sum = {{1'd0}, _io_sum_T}; // @[MyModule.scala 16:10]
endmodule

There is some require(...) in the preceding code Sentence. These are pre-expanded assertions, which are useful when you want the generator to be only a certain parameterization or some parameterization to be mutually exclusive or meaningless. The code above checks that the width should be non-negative.

There are separate assertion constructs for simulation, assert(...).

Sorting using parameterized modules

The code block below is a parameterized sort, similar to the previous Sort4. Unlike the parameterized adder example above, the input and output of this example are fixed. The parameters here control hardware generation within the module.

Example: parameterized 4-input sorting

Unlike the previous Sort4, the implementation here is parameterized to a descending or ascending sort.

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test

/** Sort4 sorts its 4 inputs to its 4 outputs */
class Sort4(ascending: Boolean) extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(16.W))
    val in1 = Input(UInt(16.W))
    val in2 = Input(UInt(16.W))
    val in3 = Input(UInt(16.W))
    val out0 = Output(UInt(16.W))
    val out1 = Output(UInt(16.W))
    val out2 = Output(UInt(16.W))
    val out3 = Output(UInt(16.W))
  })
    
  // this comparison funtion decides < or > based on the module's parameterization
  def comp(l: UInt, r: UInt): Bool = {
      if (ascending) {
        l < r
      } else {
        l > r
    }
  }

  val row10 = Wire(UInt(16.W))
  val row11 = Wire(UInt(16.W))
  val row12 = Wire(UInt(16.W))
  val row13 = Wire(UInt(16.W))

  when(comp(io.in0, io.in1)) {
    row10 := io.in0            // preserve first two elements
    row11 := io.in1
  }.otherwise {
    row10 := io.in1            // swap first two elements
    row11 := io.in0
  }

  when(comp(io.in2, io.in3)) {
    row12 := io.in2            // preserve last two elements
    row13 := io.in3
  }.otherwise {
    row12 := io.in3            // swap last two elements
    row13 := io.in2
  }

  val row21 = Wire(UInt(16.W))
  val row22 = Wire(UInt(16.W))

  when(comp(row11, row12)) {
    row21 := row11            // preserve middle 2 elements
    row22 := row12
  }.otherwise {
    row21 := row12            // swap middle two elements
    row22 := row11
  }

  val row20 = Wire(UInt(16.W))
  val row23 = Wire(UInt(16.W))
  when(comp(row10, row13)) {
    row20 := row10            // preserve the first and the forth elements
    row23 := row13
  }.otherwise {
    row20 := row13            // swap the first and the forth elements
    row23 := row10
  }

  when(comp(row20, row21)) {
    io.out0 := row20            // preserve first two elements
    io.out1 := row21
  }.otherwise {
    io.out0 := row21            // swap first two elements
    io.out1 := row20
  }

  when(comp(row22, row23)) {
    io.out2 := row22            // preserve first two elements
    io.out3 := row23
  }.otherwise {
    io.out2 := row23            // swap first two elements
    io.out3 := row22
  }
}

object MyModule extends App {
  // Here are the testers
  test(new Sort4(true)) { c => 
    c.io.in0.poke(3.U)
    c.io.in1.poke(6.U)
    c.io.in2.poke(9.U)
    c.io.in3.poke(12.U)
    c.io.out0.expect(3.U)
    c.io.out1.expect(6.U)
    c.io.out2.expect(9.U)
    c.io.out3.expect(12.U)

    c.io.in0.poke(13.U)
    c.io.in1.poke(4.U)
    c.io.in2.poke(6.U)
    c.io.in3.poke(1.U)
    c.io.out0.expect(1.U)
    c.io.out1.expect(4.U)
    c.io.out2.expect(6.U)
    c.io.out3.expect(13.U)

    c.io.in0.poke(13.U)
    c.io.in1.poke(6.U)
    c.io.in2.poke(4.U)
    c.io.in3.poke(1.U)
    c.io.out0.expect(1.U)
    c.io.out1.expect(4.U)
    c.io.out2.expect(6.U)
    c.io.out3.expect(13.U)
  }
  test(new Sort4(false)) { c =>
    c.io.in0.poke(3.U)
    c.io.in1.poke(6.U)
    c.io.in2.poke(9.U)
    c.io.in3.poke(12.U)
    c.io.out0.expect(12.U)
    c.io.out1.expect(9.U)
    c.io.out2.expect(6.U)
    c.io.out3.expect(3.U)

    c.io.in0.poke(13.U)
    c.io.in1.poke(4.U)
    c.io.in2.poke(6.U)
    c.io.in3.poke(1.U)
    c.io.out0.expect(13.U)
    c.io.out1.expect(6.U)
    c.io.out2.expect(4.U)
    c.io.out3.expect(1.U)

    c.io.in0.poke(1.U)
    c.io.in1.poke(6.U)
    c.io.in2.poke(4.U)
    c.io.in3.poke(13.U)
    c.io.out0.expect(13.U)
    c.io.out1.expect(6.U)
    c.io.out2.expect(4.U)
    c.io.out3.expect(1.U)
  }
  println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}

The test passed.

Options and default parameters

Sometimes a function returns a value, sometimes it does not. In addition to errors when values cannot be returned, Scala has a mechanism to encode them into the type system.

Example: Wrong mapping index call

In the following example, there is a Map that contains several key-value pairs (Key-Value Pairs). If we try to access a missing key-value pair, we get a runtime error.

val map = Map("a" -> 1)
val a = map("a")
println(a)
val b = map("b")
println(b)

Output:

java.util.NoSuchElementException: key not found: b

Example: Get indeterminate slices

However, Map provides another way to access a key-value pair, through the get method. Using the get method returns the value of an abstract class, Option. Option has two subclasses, Some and None.

val map = Map("a" -> 1)
val a = map.get("a")
println(a)
val b = map.get("b")
println(b)

The output is:

Some(1)
None

The types are:

map: Map[String, Int] = Map("a" -> 1)
a: Option[Int] = Some(1)
b: Option[Int] = None

As you will see in the following sections, Option is extremely important because it allows users to check Scala types or values with matching statements.

Example: getOrElse

Like Map, Option also has a get method that will error if called on None. For this example, we can use getOrElse to provide a default value, which is to return a value if there is one, or a parameter value for the method if there is none.

val some = Some(1)
val none = None
println(some.get)          // Returns 1
// println(none.get)       // Errors!
println(some.getOrElse(2)) // Returns 1
println(none.getOrElse(2)) // Returns 2

Output:

1
1
2

The types are:

some: Some[Int] = Some(1)
none: None.type = None

Option for parameters with default values

When an object or function has many parameters, specifying them all at once is obviously cumbersome and error-prone. Previously, we introduced named parameters and parameter defaults. Sometimes parameters do not have a good default value. Option can be used with the default value None in these situations.

Example: Optional Reet

A code block is shown below, delaying its input by one clock cycle. If resetValue = None, which is the default value, the register will not be reset and will be initialized to a spicy chicken value. This avoids the common but ugly situation of using values outside the normal range to indicate None, such as -1 as a reset value to indicate that a register has not been reset.

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test


class DelayBy1(resetValue: Option[UInt] = None) extends Module {
  val io = IO(new Bundle {
    val in  = Input( UInt(16.W))
    val out = Output(UInt(16.W))
  })
  val reg = if (resetValue.isDefined) { // resetValue = Some(number)
    RegInit(resetValue.get)
  } else { //resetValue = None
    Reg(UInt())
  }
  reg := io.in
  io.out := reg
}

object MyModule extends App {
  println(getVerilogString(new DelayBy1))
  println(getVerilogString(new DelayBy1(Some(3.U))))
}

The output is as follows:

module DelayBy1(
  input         clock,
  input         reset,
  input  [15:0] io_in,
  output [15:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [15:0] reg_; // @[MyModule.scala 15:8]
  assign io_out = reg_; // @[MyModule.scala 18:10]
  always @(posedge clock) begin
    reg_ <= io_in; // @[MyModule.scala 17:7]
  end
// Register and memory initialization
...
endmodule

module DelayBy1(
  input         clock,
  input         reset,
  input  [15:0] io_in,
  output [15:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [15:0] reg_; // @[MyModule.scala 13:12]
  assign io_out = reg_; // @[MyModule.scala 18:10]
  always @(posedge clock) begin
    if (reset) begin // @[MyModule.scala 13:12]
      reg_ <= 16'h3; // @[MyModule.scala 13:12]
    end else begin
      reg_ <= io_in; // @[MyModule.scala 17:7]
    end
  end
// Register and memory initialization
...
endmodule

The difference is that the former does not correspond to the reset signal, while the latter resets the value of the register to 16'h3 after receiving the reset signal at the clock rise edge.

match/case statement

Scala's matching concept runs through the entire Chisel language and should be the basic understanding of every Chisel programmer. Scala provides the match operator to support the following functions:

  1. Simple tests of candidates, similar to swtich statements in C;
  2. A more complex test of the ad-hoc combination of values;
  3. When the type of a variable is unknown or to be specified, act based on the type of the variable, for example:
    1. Variables come from a heterogeneous list: val mixedList = List (1,'string', false);
    2. Or a known variable is a member of a superclass but does not know which subclass is specified;
  4. Extracts a substring from a string based on a regular expression;

Example: Value Matching

The following example allows us to execute different case statements based on the value of the matched variable:

// y is an integer variable defined somewhere else in the code
val y = 7
/// ...
val x = y match {
  case 0 => "zero" // One common syntax, preferred if fits in one line
  case 1 =>        // Another common syntax, preferred if does not fit in one line.
      "one"        // Note the code block continues until the next case
  case 2 => {      // Another syntax, but curly braces are not required
      "two"
  }
  case _ => "many" // _ is a wildcard that matches all values
}
println("y is " + x)

The output is:

y is many

In the example, the match operator checks for possible values and returns a string for each case. There are also a few points to note:

  1. The code block after each =>operator continues until the right curly brace or before the next case statement;
  2. The match statement searches sequentially, and once matched, it does not check for subsequent situations.
  3. _ Underlines are wildcards used to match anything else that has not been matched before;

Example: Multivalue matching

Multiple values can be matched at the same time. Here is a simple example of a true value table constructed from a match statement and several values:

def animalType(biggerThanBreadBox: Boolean, meanAsCanBe: Boolean): String = {
  (biggerThanBreadBox, meanAsCanBe) match {
    case (true, true) => "wolverine"
    case (true, false) => "elephant"
    case (false, true) => "shrew"
    case (false, false) => "puppy"
  }
}
println(animalType(true, true))

The output is as follows:

wolverine

Example: Type Matching

Scala is a strongly typed language, so each object knows the type when it executes. We can use the match statement to indicate the control flow with this information:

val sequence = Seq("a", 1, 0.0)
sequence.foreach { x =>
  x match {
    case s: String => println(s"$x is a String")
    case s: Int    => println(s"$x is an Int")
    case s: Double => println(s"$x is a Double")
    case _ => println(s"$x is an unknown type!")
  }
}

The output is:

a is a String
1 is an Int
0.0 is a Double

The s in case s in the code is the object to be entered, either a s a or a s x.

Example: Type matching and erasing

Type matching has some limitations. Because Scala runs on a JVM that does not maintain a polymorphic type, it cannot match at run time because it will be erased. The following example always matches the first conditional statement because the [String], [Int] and [Double] composite types are erased, and the case statement matches only the Seq:

val sequence = Seq(Seq("a"), Seq(1), Seq(0.0))
sequence.foreach { x =>
  x match {
    case s: Seq[String] => println(s"$x is a String")
    case s: Seq[Int]    => println(s"$x is an Int")
    case s: Seq[Double] => println(s"$x is a Double")
  }
}

Output as (warning when compiling or running):

<console>:15: warning: non-variable type argument String in type pattern Seq[String] (the underlying of Seq[String]) is unchecked since it is eliminated by erasure
           case s: Seq[String] => println(s"$x is a String")
                   ^
<console>:16: warning: non-variable type argument Int in type pattern Seq[Int] (the underlying of Seq[Int]) is unchecked since it is eliminated by erasure
           case s: Seq[Int]    => println(s"$x is an Int")
                   ^
<console>:17: warning: non-variable type argument Double in type pattern Seq[Double] (the underlying of Seq[Double]) is unchecked since it is eliminated by erasure
           case s: Seq[Double] => println(s"$x is a Double")
                   ^
<console>:16: warning: unreachable code
           case s: Seq[Int]    => println(s"$x is an Int")
           
List(a) is a String
List(1) is a String
List(0.0) is a String

Example: Optional reset matching

The following example shows the same DelayBy1 module, but replaces if/else with a match statement:

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test


class DelayBy1(resetValue: Option[UInt] = None) extends Module {
  val io = IO(new Bundle {
    val in  = Input( UInt(16.W))
    val out = Output(UInt(16.W))
  })
  val reg = resetValue match {
    case Some(r) => RegInit(r)
    case None    => Reg(UInt())
  }
  reg := io.in
  io.out := reg
}

object MyModule extends App {
  println(getVerilogString(new DelayBy1))
  println(getVerilogString(new DelayBy1(Some(3.U))))
}

The output is as follows:

module DelayBy1(
  input         clock,
  input         reset,
  input  [15:0] io_in,
  output [15:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [15:0] reg_; // @[MyModule.scala 14:24]
  assign io_out = reg_; // @[MyModule.scala 17:10]
  always @(posedge clock) begin
    reg_ <= io_in; // @[MyModule.scala 16:7]
  end
// Register and memory initialization
...
endmodule

module DelayBy1(
  input         clock,
  input         reset,
  input  [15:0] io_in,
  output [15:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [15:0] reg_; // @[MyModule.scala 13:28]
  assign io_out = reg_; // @[MyModule.scala 17:10]
  always @(posedge clock) begin
    if (reset) begin // @[MyModule.scala 13:28]
      reg_ <= 16'h3; // @[MyModule.scala 13:28]
    end else begin
      reg_ <= io_in; // @[MyModule.scala 16:7]
    end
  end
// Register and memory initialization
...
endmodule

The effect is the same.

IO with optional fields

Sometimes we want IO interfaces to be optionally included or excluded. There may be built-in statements that are useful when debugging, but you want to hide them when the generator is used on the system. Perhaps your generator has some inputs that don't need to be connected in every situation because there is a meaningful default value.

Example: Optional IO with options

The optional bundle field is a way to get this functionality. In the following example, a single bit adder is shown, and the carry input interface is optional. If there is a carry input interface, io. CaryIn will have Some[UInt] type and be included in IO. If the carry input interface is not included, io. CaryIn has a None type and is excluded from IO:

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test


class HalfFullAdder(val hasCarry: Boolean) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(1.W))
    val b = Input(UInt(1.W))
    val carryIn = if (hasCarry) Some(Input(UInt(1.W))) else None
    val s = Output(UInt(1.W))
    val carryOut = Output(UInt(1.W))
  })
  val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U)
  io.s := sum(0)
  io.carryOut := sum(1)
}

object MyModule extends App {
  test(new HalfFullAdder(false)) { c =>
    require(!c.hasCarry, "DUT must be half adder")
    // 0 + 0 = 0
    c.io.a.poke(0.U)
    c.io.b.poke(0.U)
    c.io.s.expect(0.U)
    c.io.carryOut.expect(0.U)
    // 0 + 1 = 1
    c.io.b.poke(1.U)
    c.io.s.expect(1.U)
    c.io.carryOut.expect(0.U)
    // 1 + 1 = 2
    c.io.a.poke(1.U)
    c.io.s.expect(0.U)
    c.io.carryOut.expect(1.U)
    // 1 + 0 = 1
    c.io.b.poke(0.U)
    c.io.s.expect(1.U)
    c.io.carryOut.expect(0.U)
  }

  test(new HalfFullAdder(true)) { c =>
    require(c.hasCarry, "DUT must be full adder")
    c.io.carryIn.get.poke(0.U)
    // 0 + 0 + 0 = 0
    c.io.a.poke(0.U)
    c.io.b.poke(0.U)
    c.io.s.expect(0.U)
    c.io.carryOut.expect(0.U)
    // 0 + 0 + 1 = 1
    c.io.b.poke(1.U)
    c.io.s.expect(1.U)
    c.io.carryOut.expect(0.U)
    // 0 + 1 + 1 = 2
    c.io.a.poke(1.U)
    c.io.s.expect(0.U)
    c.io.carryOut.expect(1.U)
    // 0 + 1 + 0 = 1
    c.io.b.poke(0.U)
    c.io.s.expect(1.U)
    c.io.carryOut.expect(0.U)

    c.io.carryIn.get.poke(1.U)
    // 1 + 0 + 0 = 1
    c.io.a.poke(0.U)
    c.io.b.poke(0.U)
    c.io.s.expect(1.U)
    c.io.carryOut.expect(0.U)
    // 1 + 0 + 1 = 2
    c.io.b.poke(1.U)
    c.io.s.expect(0.U)
    c.io.carryOut.expect(1.U)
    // 1 + 1 + 1 = 3
    c.io.a.poke(1.U)
    c.io.s.expect(1.U)
    c.io.carryOut.expect(1.U)
    // 1 + 1 + 0 = 2
    c.io.b.poke(0.U)
    c.io.s.expect(0.U)
    c.io.carryOut.expect(1.U)
  }

  println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}

The test passed.

Example: Optional IO with zero-width network cable

Another way to achieve similar functionality for Option is a zero-width wire. Zero width allowed for Chisel type. A zero-width IO interface is trimmed from the generated Verilog code, and all attempts to use a zero-width value result in a constant of 0. If 0 is a meaningful default, then a zero-width wire is good because it eliminates the need for matching options or calling getOrElse.

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test


class HalfFullAdder(val hasCarry: Boolean) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(1.W))
    val b = Input(UInt(1.W))
    val carryIn = Input(if (hasCarry) UInt(1.W) else UInt(0.W))
    val s = Output(UInt(1.W))
    val carryOut = Output(UInt(1.W))
  })
  val sum = io.a +& io.b +& io.carryIn
  io.s := sum(0)
  io.carryOut := sum(1)
}

object MyModule extends App {
  println("// Half Adder:")
  println(getVerilogString(new HalfFullAdder(false)))
  println("\n\n// Full Adder:")
  println(getVerilogString(new HalfFullAdder(true)))
}

The output is as follows:

// Half Adder:
module HalfFullAdder(
  input   clock,
  input   reset,
  input   io_a,
  input   io_b,
  output  io_s,
  output  io_carryOut
);
  wire [1:0] _sum_T = io_a + io_b; // @[MyModule.scala 15:18]
  wire [2:0] sum = {{1'd0}, _sum_T}; // @[MyModule.scala 15:26]
  assign io_s = sum[0]; // @[MyModule.scala 16:14]
  assign io_carryOut = sum[1]; // @[MyModule.scala 17:21]
endmodule



// Full Adder:
module HalfFullAdder(
  input   clock,
  input   reset,
  input   io_a,
  input   io_b,
  input   io_carryIn,
  output  io_s,
  output  io_carryOut
);
  wire [1:0] _sum_T = io_a + io_b; // @[MyModule.scala 15:18]
  wire [1:0] _GEN_0 = {{1'd0}, io_carryIn}; // @[MyModule.scala 15:26]
  wire [2:0] sum = _sum_T + _GEN_0; // @[MyModule.scala 15:26]
  assign io_s = sum[0]; // @[MyModule.scala 16:14]
  assign io_carryOut = sum[1]; // @[MyModule.scala 17:21]
endmodule

Implicit

Programming often requires a lot of template code. To address this use case, Scala introduced the keyword implicit, which allows the compiler to use some syntax sugar. Because many things happen after one scene, they can be magical implicitly. This section has some basic examples of what they are and when they are usually used.

Implicit parameters

Sometimes your code needs to access a top-level variable in some order from a very deep series of calls. Instead of manually passing this variable through each function call, you can use implicit parameters to do this.

Example: Implicit Cat

In the following example, we can explicitly or implicitly pass on the number of cats:

object CatDog {
  implicit val numberOfCats: Int = 3
  //implicit val numberOfDogs: Int = 5

  def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
    
  val imp = tooManyCats(2)    // Argument passed implicitly!
  val exp = tooManyCats(2)(1) // Argument passed explicitly!
}
CatDog.imp
CatDog.exp

The output is:

res16_1: Boolean = true
res16_2: Boolean = false

What happened?! First, we define an implicit value, numberOfCats. There can only be one implicit value of a given type within a given range. Then, we define a function that accepts two parameters, the first is any explicit parameter and the second is any implicit parameter. When we call tooManyCats, we can omit the second implicit parameter and let the compiler look for it, or we can explicitly give a value that can be different from the implicit value.

There are two cases where implicit parameters fail:

  1. Two or more implicit values of a given type are defined within the range.
  2. The compiler cannot find the implicit value required for a function call;

Example: Implicit Logging

The code below shows a possible way to implement logging in the Chisel generator using implicit parameters (better logging in Scala):

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test

object MyModule extends App {
  sealed trait Verbosity
  implicit case object Silent extends Verbosity
  case object Verbose extends Verbosity

  class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int)(implicit verbosity: Verbosity)
  extends Module {
    def log(msg: => String): Unit = verbosity match {
      case Silent =>
      case Verbose => println(msg)
    }
    require(in0Width >= 0)
    log(s"// in0Width of $in0Width OK")
    require(in1Width >= 0)
    log(s"// in1Width of $in1Width OK")
    require(sumWidth >= 0)
    log(s"// sumWidth of $sumWidth OK")
    val io = IO(new Bundle {
      val in0 = Input(UInt(in0Width.W))
      val in1 = Input(UInt(in1Width.W))
      val sum = Output(UInt(sumWidth.W))
    })
    log("// Made IO")
    io.sum := io.in0 + io.in1
    log("// Assigned output")
  }
  println(getVerilogString(new ParameterizedWidthAdder(1, 4, 5)))
  println(getVerilogString(new ParameterizedWidthAdder(1, 4, 5)(Verbose)))
}

When the output is given, the parameter Silent is implicitly given, and the log() function does nothing. When the Verbose parameter is given, log() outputs the log. The output is as follows:

module ParameterizedWidthAdder(
  input        clock,
  input        reset,
  input        io_in0,
  input  [3:0] io_in1,
  output [4:0] io_sum
);
  wire [3:0] _GEN_0 = {{3'd0}, io_in0}; // @[MyModule.scala 29:22]
  wire [3:0] _io_sum_T_1 = _GEN_0 + io_in1; // @[MyModule.scala 29:22]
  assign io_sum = {{1'd0}, _io_sum_T_1}; // @[MyModule.scala 29:12]
endmodule

// in0Width of 1 OK
// in1Width of 4 OK
// sumWidth of 5 OK
// Made IO
// Assigned output
module ParameterizedWidthAdder(
  input        clock,
  input        reset,
  input        io_in0,
  input  [3:0] io_in1,
  output [4:0] io_sum
);
  wire [3:0] _GEN_0 = {{3'd0}, io_in0}; // @[MyModule.scala 29:22]
  wire [3:0] _io_sum_T_1 = _GEN_0 + io_in1; // @[MyModule.scala 29:22]
  assign io_sum = {{1'd0}, _io_sum_T_1}; // @[MyModule.scala 29:12]
endmodule

Implicit Conversion

Like implicit parameters, implicit functions (also known as implicit conversions) are used to reduce template code. Specifically, they are used to automatically convert one Scala object to another.

Example: Implicit conversion

In the example below, we have two classes, Animal and Human. Animal has a species field, but Human does not. However, by implementing an implicit conversion, we can also call species on Human:

class Animal(val name: String, val species: String)
class Human(val name: String)
implicit def human2animal(h: Human): Animal = new Animal(h.name, "Homo sapiens")
val me = new Human("Adam")
println(me.species)

The output is:

Homo sapiens

Generally speaking, implicit can make your code difficult to understand, so it's recommended that you reuse it when you really can't.

Try inheritance, trait, or method overload first.

Mealy State Machine Generator Example

Mealy State Machine and Moore FSM Unlike Mealy, the output of a finite state machine depends not only on the current state but also on the current value of the input signal.

The output of the Mealy FSM is directly affected by the current value of the input signal, and the input signal may change at any time in a clock cycle, which makes the response of the Mealy FSM to the input occur in the current clock cycle, one cycle earlier than the response of the Moore FSM to the input signal. Therefore, the noise of the input signal may affect the output signal. Baidu Encyclopedia

The following example shows the generator of a Mealy state machine with a single bit of input. You can follow the code to see what happened:

import chisel3._
import chisel3.util._
import chisel3.tester._
import chisel3.tester.RawTester.test

object MyModule extends App {
  // Mealy machine has
  case class BinaryMealyParams(
    // number of states
    nStates: Int,
    // initial state
    s0: Int,
    // function describing state transition
    stateTransition: (Int, Boolean) => Int,
    // function describing output
    output: (Int, Boolean) => Int
  ) {
    require(nStates >= 0)
    require(s0 < nStates && s0 >= 0)
  }

  class BinaryMealy(val mp: BinaryMealyParams) extends Module {
    val io = IO(new Bundle {
      val in = Input(Bool())
      val out = Output(UInt())
    })

    val state = RegInit(UInt(), mp.s0.U)

    // output zero if no states
    io.out := 0.U
    for (i <- 0 until mp.nStates) {
      when (state === i.U) {
        when (io.in) {
          state  := mp.stateTransition(i, true).U
          io.out := mp.output(i, true).U
        }.otherwise {
          state  := mp.stateTransition(i, false).U
          io.out := mp.output(i, false).U
        }
      }
    }
  }

  // example from https://en.wikipedia.org/wiki/Mealy_machine
  val nStates = 3
  val s0 = 2
  def stateTransition(state: Int, in: Boolean): Int = {
    if (in) {
      1
    } else {
      0
    }
  }
  def output(state: Int, in: Boolean): Int = {
    if (state == 2) {
      return 0
    }
    if ((state == 1 && !in) || (state == 0 && in)) {
      return 1
    } else {
      return 0
    }
  }

  val testParams = BinaryMealyParams(nStates, s0, stateTransition, output)

  test(new BinaryMealy(testParams)) { c =>
    c.io.in.poke(false.B)
    c.io.out.expect(0.U)
    c.clock.step(1)
    c.io.in.poke(false.B)
    c.io.out.expect(0.U)
    c.clock.step(1)
    c.io.in.poke(false.B)
    c.io.out.expect(0.U)
    c.clock.step(1)
    c.io.in.poke(true.B)
    c.io.out.expect(1.U)
    c.clock.step(1)
    c.io.in.poke(true.B)
    c.io.out.expect(0.U)
    c.clock.step(1)
    c.io.in.poke(false.B)
    c.io.out.expect(1.U)
    c.clock.step(1)
    c.io.in.poke(true.B)
    c.io.out.expect(1.U)
    c.clock.step(1)
    c.io.in.poke(false.B)
    c.io.out.expect(1.U)
    c.clock.step(1)
    c.io.in.poke(true.B)
    c.io.out.expect(1.U)
  }

  println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}

The test passed.

To put it simply:

  1. The BinaryMealyParams class contains some parameters of the Mealy machine, including the number of states, the initialization state, the state transition function and the output function, and limits the number of States and the value of the initial state.
  2. The BinaryMealy class defines the Mealy machine, and takes the BinaryMealyParams class as a parameter, defines the behavior (how to call) that performs state transitions and outputs;
  3. Following is an example from Wikipedia above, which gives the parameters of a Mealy machine, then constructs the parameter classes, then constructs and tests the Mealy machine.

Tags: Scala Back-end programming language

Posted by dfownz on Thu, 12 May 2022 20:15:19 +0300