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:
- Simple tests of candidates, similar to swtich statements in C;
- A more complex test of the ad-hoc combination of values;
- When the type of a variable is unknown or to be specified, act based on the type of the variable, for example:
- Variables come from a heterogeneous list: val mixedList = List (1,'string', false);
- Or a known variable is a member of a superclass but does not know which subclass is specified;
- 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:
- The code block after each =>operator continues until the right curly brace or before the next case statement;
- The match statement searches sequentially, and once matched, it does not check for subsequent situations.
- _ 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:
- Two or more implicit values of a given type are defined within the range.
- 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:
- 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.
- 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;
- 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.