Skip to content
code0.HDL

Child modules, parameters, and generators

Place a module inside another with Module(new ...), drive every child input, build a family of definitions from one parameterized class with require and desiredName, call a module through a companion apply, and read a generator written with for and def by counting what it built.

In this chapter

  • Instantiate a child with Module(new ...), wire its ports from the parent's side, and tell a wrong connection from a missing one by the phase that catches it
  • Parameterize a module with constructor arguments, reject bad parameters with require, and name each definition with desiredName
  • Read a Scala def or for loop that builds hardware by counting the copies it adds during elaboration
  • Choose between a child module, a Scala function, and a companion apply
Concepts in this chapter
  • Child modules and Bundles
Prerequisite concepts

1. Defining a module

Up to now every example was a single module with one io bundle. Real designs are trees: a top module instantiates children and wires their ports together, and each child may do the same. This chapter shows how to define a module, how to place children inside it, how one parameterized class becomes several module definitions, how a Scala function builds hardware each time it is called, and how to call a module as if it were a function. You will use Bundle and Vec ports from Bundles and Vecs, widths and +& from Widths, SInt, and arithmetic, and Mux plus the "every sink must be driven" check from Mux, when, Wire, and a default for every path.

Everything in this chapter happens during elaboration. Running the Scala code builds the module tree and its connections; simulation only confirms that the wiring you built is the wiring you meant.

1.1 The module class

A Chisel module is a Scala class that extends Module. Its constructor body describes the hardware: it declares ports with IO(...), creates internal logic, and connects signals with :=.

Form

class ModuleName extends Module {
  val io = IO(new Bundle {
    val inputName  = Input(type)
    val outputName = Output(type)
  })
  // hardware description
}

Here is the smallest useful module, a two-input AND gate:

class And2 extends Module {
  val io = IO(new Bundle {
    val a = Input(Bool())
    val b = Input(Bool())
    val y = Output(Bool())
  })
  io.y := io.a && io.b
}
CodeMeaning
class And2 extends ModuleDefines a module class named And2
IO(new Bundle { ... })Turns each field of the bundle into a port
Input(Bool()), Output(Bool())Give each port a direction and a type
io.y := io.a && io.bDrives the output with the AND of the inputs

By convention the port bundle is called io. In generated Verilog the fields are flattened into port names such as io_a and io_y.

1.2 The implicit clock and reset

Every class that extends Module also gets a clock input and a reset input that you never write in the port list. When you instantiate a child, Chisel connects its clock and reset to the parent's automatically, which is why none of the examples in this chapter mention them. The registers of Registers, RegInit, and stepping the clock put them to work.

Note. RawModule is the variant without an implicit clock or reset. It is for designs that must expose clock and reset ports under specific names or polarities; this course does not need it.

1.3 Class, instance, and module definition

Three things look similar in module code, and it pays to keep them apart.

ThingWhen it existsWhat it is
class Mux2 extends ModuleScala sourceA recipe; nothing is built yet
Module(new Mux2)During elaboration, each time it executesOne instance inside the parent, with its own ports
Module definition in CHIRRTLAfter elaborationThe emitted circuit text; one per elaborated instance, with a suffix such as _1 when names repeat (firtool deduplicates identical definitions before emitting Verilog)

Every call to Module(...) creates a new instance. There is no implicit sharing: two calls give two copies of the hardware, exactly like two chips on a board.

2. Instantiating children and wiring ports

2.1 Module(new ...)

To create a child inside a parent, wrap the object made by new in Module(...). Module(...) registers the object as a child of the module currently being built, connects its clock and reset, and gives it an instance name.

Form

val instanceName = Module(new ModuleClass(constructorArgs))

This And3 builds a three-input AND from two And2 instances:

class And3 extends Module {
  val io = IO(new Bundle {
    val a = Input(Bool())
    val b = Input(Bool())
    val c = Input(Bool())
    val y = Output(Bool())
  })
  val first  = Module(new And2)
  val second = Module(new And2)
  first.io.a  := io.a
  first.io.b  := io.b
  second.io.a := first.io.y
  second.io.b := io.c
  io.y := second.io.y
}
CodeMeaning
val first = Module(new And2)Creates an And2 instance named first
first.io.a := io.aDrives a child input from a parent input
second.io.a := first.io.yFeeds one child's output into another child's input
io.y := second.io.yDrives the parent's output from a child output

Caution. Writing only val m = new And2, without Module(...), does not quietly create a detached object. Elaboration stops with ChiselException: Error: attempted to instantiate a Module without wrapping it in Module().

2.2 Port directions as the parent sees them

A parent cannot see inside a child; it reaches only the ports, written child.io.field. Their directions are read from the parent's point of view.

Child portRole in the parent
InputA sink the parent must drive
OutputA source the parent may read

Caution. The unconnected-wire check from the when chapter applies to child inputs too. If any input of any instance is left undriven, the FIRRTL check stops the run before simulation and reports it as not fully initialized.

A parent port may be a Vec: in the example below io.in is Vec(4, UInt(4.W)), four 4-bit values in one port, and io.in(0) with a Scala index is wiring to element 0, decided during elaboration, as Scala indices and hardware indices explained.

3. Example: a four-way multiplexer from three two-way ones

Mux4 places three Mux2 instances in two levels. Bit 0 of the select picks inside each pair, and bit 1 picks between the two pair winners. As you read the code, check the connections against this table:

Instancesel inputData inputs
loio.sel(0)in(0), in(1)
hiio.sel(0)in(2), in(3)
topio.sel(1)outputs of lo and hi

The println inside the Mux2 constructor runs once per constructor execution, so it counts instances.

Mux4.scala
import chisel3._

class Mux2 extends Module {
  val io = IO(new Bundle {
    val sel = Input(Bool())
    val in0 = Input(UInt(4.W))
    val in1 = Input(UInt(4.W))
    val out = Output(UInt(4.W))
  })
  println("ELABORATE Mux2")
  io.out := Mux(io.sel, io.in1, io.in0)
}

class Mux4 extends Module {
  val io = IO(new Bundle {
    val sel = Input(UInt(2.W))
    val in  = Input(Vec(4, UInt(4.W)))
    val out = Output(UInt(4.W))
  })

  // The low select bit chooses inside each pair.
  val lo = Module(new Mux2)
  lo.io.sel := io.sel(0)
  lo.io.in0 := io.in(0)
  lo.io.in1 := io.in(1)

  val hi = Module(new Mux2)
  hi.io.sel := io.sel(0)
  hi.io.in0 := io.in(2)
  hi.io.in1 := io.in(3)

  // The high select bit chooses between the two pair winners.
  val top = Module(new Mux2)
  top.io.sel := io.sel(1)
  top.io.in0 := lo.io.out
  top.io.in1 := hi.io.out

  io.out := top.io.out
}

The testbench pokes four distinct values, 3, 5, 9, and 12, into in(0) to in(3), then steps the select through 0 to 3 and checks each output with expect, which fails the test on the spot if the value is wrong.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Mux4) { dut =>
    // Distinct inputs make every routing mistake visible.
    val data = Seq(3, 5, 9, 12)
    for ((v, i) <- data.zipWithIndex) dut.io.in(i).poke(v.U)
    for (sel <- 0 until 4) {
      dut.io.sel.poke(sel.U)
      dut.io.out.expect(data(sel).U)
      println(s"SIMULATE sel=$sel out=${dut.io.out.peek().litValue}")
    }
    println("CHECK all 4 selections matched")
  }
}

Practice this concept

Use this concept in a short Chisel problem as your next step.

Predict before you run.

  1. How many ELABORATE Mux2 lines appear, and do they come before or after the SIMULATE lines?
  2. For sel = 2 (binary 10), which input does each of lo, hi, and top pass through?
  3. Why does the testbench use four different input values?

Check

How many ELABORATE Mux2 lines appear before the first SIMULATE line?

Check

For sel = 2, binary 10, what does out show?

Check the result. After pressing Run, compare each line with this table.

Output lineWhat it shows
ELABORATE Mux2Exactly three lines, one per Module(new Mux2) call, all printed before simulation starts
SIMULATE sel=0 to 33, 5, 9, 12. For sel = 2, bit 0 is 0 so hi passes in(2), and bit 1 is 1 so top takes hi, giving 9
CHECKAll four expect checks passed

Because the four inputs are distinct, any wrong route would show up as a wrong number, and expect turns that number into a failure instead of a line you might skim past. Four select values with distinct data test this routing exhaustively.

Try it yourself. In the lab, change hi.io.sel := io.sel(0) to io.sel(1) and run again.

Check

With hi selecting on sel(1), what happens?

Your answer

EditWhat happens
Change hi.io.sel := io.sel(0) to io.sel(1)sel = 0 and 1 still pass, because top still picks lo. At sel = 2, hi now picks in(3), and the run stops with chiseltest.FailedExpectException: io_out=12 (0xc) did not equal expected=9 (0x9)
Delete hi.io.in1 := io.in(3)Elaboration finishes, but the FIRRTL check stops the run with RefNotInitializedException: Reference hi is not fully initialized, with a detail line naming hi.io.in1 <= VOID

The two edits are caught in different phases. The first leaves every port connected, one of them to the wrong source, so only the testbench can see it. The second leaves a child input undriven, which is the same error as leaving one of your own outputs undriven.

4. Parameterized modules

4.1 Constructor parameters

A module class can take ordinary Scala constructor parameters. They are read while the constructor runs during elaboration, so they can size ports, choose internal structure, or reject impossible configurations.

Form

class ModuleName(param: Type, ...) extends Module {
  // ports and logic built from param
}

val instanceName = Module(new ModuleName(arg, ...))

This Inc adds one to a width-bit value:

class Inc(width: Int) extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(width.W))
    val out = Output(UInt(width.W))
  })
  io.out := io.in + 1.U
}

val inc4 = Module(new Inc(4))
val inc8 = Module(new Inc(8))

Inc(4) and Inc(8) build hardware with different port widths, so the emitted circuit contains two separate module definitions. Without a custom name they are called Inc and Inc_1.

4.2 Checking parameters with require

require(condition, message) is plain Scala. When the condition is false it throws an IllegalArgumentException, so a bad configuration is rejected before any of its hardware exists.

Form

require(condition, "error message")
class Inc(width: Int) extends Module {
  require(width >= 1, s"Inc width must be positive, got $width")
  // ...
}

4.3 Naming definitions with desiredName

Overriding desiredName sets the name of the emitted module definition. Put the parameter in the name, and anyone reading the generated Verilog can tell the configurations apart.

Form

override def desiredName = s"Name$param"
PartRole
override defReplaces the desiredName that Module provides
s"..."Scala string interpolation; $width becomes the parameter's value

5. Example: saturating adders of two widths

SatAdd(width) is a saturating adder: when the sum overflows, it outputs the largest representable value instead of wrapping. Four lines carry the idea:

CodeRole
require(width >= 2, ...)Rejects an out-of-range parameter before any hardware is built
io.a +& io.bProduces a width + 1 bit sum; the top bit full(width) is the carry
((1 << width) - 1).U(width.W)The maximum, computed by Scala
override def desiredNameNames the definitions SatAdd4, SatAdd8, and so on

Top instantiates SatAdd(4) and SatAdd(8) and feeds the narrow one only the low nibble of each input.

SatAdd.scala
import chisel3._

class SatAdd(width: Int) extends Module {
  // Scala checks the parameter before any hardware is built.
  require(width >= 2, s"SatAdd width must be at least 2, got $width")

  val io = IO(new Bundle {
    val a   = Input(UInt(width.W))
    val b   = Input(UInt(width.W))
    val sum = Output(UInt(width.W))
  })
  override def desiredName = s"SatAdd$width"

  // +& keeps the carry bit, so overflow is visible.
  val full = io.a +& io.b
  val max  = ((1 << width) - 1).U(width.W)
  io.sum := Mux(full(width), max, full(width - 1, 0))
}

class Top extends Module {
  val io = IO(new Bundle {
    val a      = Input(UInt(8.W))
    val b      = Input(UInt(8.W))
    val narrow = Output(UInt(4.W))
    val wide   = Output(UInt(8.W))
  })

  val add4 = Module(new SatAdd(4))
  val add8 = Module(new SatAdd(8))

  // The 4-bit instance only sees the low nibble of each input.
  add4.io.a := io.a(3, 0)
  add4.io.b := io.b(3, 0)
  add8.io.a := io.a
  add8.io.b := io.b

  io.narrow := add4.io.sum
  io.wide   := add8.io.sum
}

The testbench first elaborates Top to CHIRRTL and prints one MODULE line per module definition, then simulates three input pairs.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  // List the module definitions the generator produced.
  val chirrtl = circt.stage.ChiselStage.emitCHIRRTL(new Top)
  val ModuleLine = """\s*(?:public )?module (\w+) :.*""".r
  for (ModuleLine(name) <- chirrtl.split("\n")) println(s"MODULE $name")

  test(new Top) { dut =>
    for ((a, b) <- Seq((3, 4), (9, 8), (200, 100))) {
      dut.io.a.poke(a.U)
      dut.io.b.poke(b.U)
      val n = dut.io.narrow.peek().litValue
      val w = dut.io.wide.peek().litValue
      println(s"SIMULATE a=$a b=$b narrow=$n wide=$w")
    }
  }
}

Predict before you run.

  1. Which names appear on the MODULE lines, and how many are there?
  2. Fill in the missing outputs:
a, bWhat the 4-bit instance seesnarrowwide
3, 43, 4??
9, 89, 8??
200, 100low nibbles 8, 4??

Check

For a = 9 and b = 8, what do narrow and wide show?

Your answer

Check the result.

Output lineWhat it shows
MODULESatAdd4, SatAdd8, and Top. One Scala class produced two definitions because the two constructor runs built different hardware
a=3 b=4Neither instance overflows: 7 and 7
a=9 b=817 overflows 4 bits, so narrow saturates to 15; wide shows 17
a=200 b=100The narrow instance adds 8 + 4 = 12 without overflow; the wide one gets 300, which saturates to 255

Try it yourself.

EditWhat happens
Delete the desiredName lineThe list becomes SatAdd and SatAdd_1; Chisel can only tell them apart by a numeric suffix
Add a third instance Module(new SatAdd(8)) to Top and connect its inputsThe list now contains SatAdd8 and SatAdd8_1: one definition per elaborated instance in CHIRRTL, with the duplicate name made unique
Change new SatAdd(4) to new SatAdd(1)Nothing is printed; the run fails with java.lang.IllegalArgumentException: requirement failed: SatAdd width must be at least 2, got 1, and the stack trace points at the Module(new SatAdd(1)) line in Top

require runs before the ports of that instance exist, so an invalid configuration never reaches circuit checking. Checking parameters where they enter is cheaper than chasing a confusing width error later, far from its cause.

6. Functions that return hardware

6.1 A def builds hardware each time it is called

A Scala function whose body uses Chisel operators builds hardware each time it is called. It is the lightest form of reuse: no new module, no ports.

Form

def name(arguments): ReturnType = {
  // Chisel expressions
}
def max2(a: UInt, b: UInt): UInt = Mux(a > b, a, b)

io.maxv := max2(io.x, 50.U)    // one comparator and one Mux
io.maxw := max2(io.y, io.z)    // a second, separate comparator and Mux
PartMeaning
def max2(a: UInt, b: UInt): UIntA Scala function that takes and returns hardware
Body Mux(a > b, a, b)Runs during elaboration and adds a comparator and a Mux
Each callAdds a new copy of that hardware to the elaborated circuit; calls do not share it (firtool and synthesis may later merge identical logic)

With x = 197, max2(io.x, 50.U) is 197; with x = 3, it is 50. A function can also return several nodes at once as a Scala tuple, as the example in section 7 does.

Note. Unlike a software subroutine, a hardware function is not "jumped to". Ten calls produce ten copies of the gates in the elaborated circuit. When you want one named block that appears separately in the generated design, use a Module, as section 8 shows.

6.2 Reading a generator: for and def

The first chapter met for (i <- 1 to stages). A Scala range 1 to 3 is 1, 2, 3; 0 until 3 is 0, 1, 2. A Scala function is declared with def name(arg: Type, ...): ReturnType = { body }; its body runs when it is called, and when the body uses Chisel operators, each call adds a new copy of that hardware.

Form

def addTimes(in: UInt, terms: Int): UInt = {
  var x: UInt = in
  for (i <- 1 to terms) {
    x = x + in          // one adder per iteration
  }
  x                     // in * (terms + 1)
}

Nothing in a loop or a function runs during simulation. The way to read a generator is to count what it built: the Practice problem gain-stages prints one ELABORATE line per adder, and its loop bound reads the wrong parameter.

Check

The starter's loop is for (i <- 1 to bias). How many doubling lines does gain(io.in, 2, 3) print?

7. Example: a ripple-carry adder from a function

Ripple.scala builds a ripple-carry adder. fullAdd takes three Bools and returns a Scala tuple of two Bool nodes, the sum and the carry; its println runs on each call. Ripple calls it once per bit in a Scala for, passes the carry along in a Scala var, and finally joins the carry and the sum bits with Cat.

Ripple.scala
import chisel3._
import chisel3.util.Cat

object Logic {
  // Every call adds one more full adder to the module being built.
  def fullAdd(a: Bool, b: Bool, cin: Bool): (Bool, Bool) = {
    println("ELABORATE fullAdd")
    val sum  = a ^ b ^ cin
    val cout = (a && b) || (a && cin) || (b && cin)
    (sum, cout)
  }
}

class Ripple(width: Int) extends Module {
  val io = IO(new Bundle {
    val a   = Input(UInt(width.W))
    val b   = Input(UInt(width.W))
    val sum = Output(UInt((width + 1).W))
  })

  // carry is a Scala var naming the newest carry node.
  var carry = false.B
  val bits = for (i <- 0 until width) yield {
    val (s, c) = Logic.fullAdd(io.a(i), io.b(i), carry)
    carry = c
    s
  }
  // Reversing puts bit 0 last, so Cat places it in the LSB.
  io.sum := Cat(carry +: bits.reverse)
}
Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test


object Main extends App {
  test(new Ripple(4)) { dut =>
    for ((a, b) <- Seq((3, 5), (9, 7), (15, 15))) {
      dut.io.a.poke(a.U)
      dut.io.b.poke(b.U)
      println(s"SIMULATE a=$a b=$b sum=${dut.io.sum.peek().litValue}")
    }
  }
}

Predict before you run.

  1. How many ELABORATE lines does Ripple(4) print?
  2. Do any of them appear between SIMULATE lines?
  3. What is sum for 9 + 7?

Check

How many ELABORATE lines does Ripple(4) print, and where do they appear?

Your answer

Check the result. fullAdd runs four times during elaboration, so four ELABORATE lines come first and none later: every poke reuses the same four full adders. Trace 9 + 7, or 1001 + 0111, bit by bit.

Bita, b, carry inSumCarry out
01, 1, 001
10, 1, 101
20, 1, 101
31, 0, 101

Cat puts the final carry on top, so sum is 10000, which is 16. The other rows are 3 + 5 = 8 and 15 + 15 = 30; the 5-bit output is wide enough for the largest case.

Try it yourself.

  1. Pass false.B instead of carry to fullAdd. The four calls still happen, but no adder sees the previous carry. Each sum bit becomes a ^ b, and only bit 3's carry reaches the output: 3 + 5 gives 6, 9 + 7 gives 14, and 15 + 15 gives 16. The way the calls are connected decides the circuit; the function body did not change.
  2. Change the testbench to new Ripple(8). Eight ELABORATE lines appear and the sums are unchanged. Width is a Scala parameter, so the same source builds a different circuit.

8. Functional module creation

8.1 A module you can call

Writing m.io.a := ...; m.io.b := ... for every instance gets verbose when a module is really a function of its inputs. A companion object with the same name can define apply to do the instantiation and input wiring and return the output port.

Form

object ModuleName {
  def apply(arg: Type, ...): ReturnType = {
    val m = Module(new ModuleName)
    m.io.input := arg
    m.io.output
  }
}
object And2 {
  def apply(a: Bool, b: Bool): Bool = {
    val m = Module(new And2)
    m.io.a := a
    m.io.b := b
    m.io.y
  }
}

io.y := And2(And2(io.a, io.b), io.c)   // two And2 instances
CodeMeaning
object And2A companion object with the same name as the class
def apply(...)The method Scala calls when you write And2(x, y)
val m = Module(new And2)Creates one instance per call
m.io.yReturns the output port, so the call can sit inside an expression

Caution. And2(x, y) reads like an expression, but each call still runs Module(new And2). Writing the same call twice gives you two instances.

8.2 Example: a tree of maximum units

Max2 outputs the larger of two 8-bit values, and its companion apply handles instantiation and wiring. Max4 nests three calls into a two-level tree on one line.

Max4.scala
import chisel3._

class Max2 extends Module {
  val io = IO(new Bundle {
    val a   = Input(UInt(8.W))
    val b   = Input(UInt(8.W))
    val out = Output(UInt(8.W))
  })
  println("ELABORATE Max2")
  io.out := Mux(io.a > io.b, io.a, io.b)
}

// The companion apply hides the port wiring behind a function call.
object Max2 {
  def apply(a: UInt, b: UInt): UInt = {
    val m = Module(new Max2)
    m.io.a := a
    m.io.b := b
    m.io.out
  }
}

class Max4 extends Module {
  val io = IO(new Bundle {
    val in  = Input(Vec(4, UInt(8.W)))
    val out = Output(UInt(8.W))
  })
  // Every Max2(...) call instantiates one more module.
  io.out := Max2(Max2(io.in(0), io.in(1)), Max2(io.in(2), io.in(3)))
}

The testbench applies three input vectors. The third, 50, 50, 10, 10, contains a tie.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Max4) { dut =>
    for (vs <- Seq(Seq(4, 9, 2, 7), Seq(1, 2, 3, 200), Seq(50, 50, 10, 10))) {
      for ((v, i) <- vs.zipWithIndex) dut.io.in(i).poke(v.U)
      println(s"SIMULATE in=${vs.mkString(",")} out=${dut.io.out.peek().litValue}")
    }
  }
}

Predict before you run.

  1. How many ELABORATE Max2 lines appear? Count calls, not lines of code.
  2. What does each input vector produce?
  3. When 50 meets 50, which operand does the Mux pick?

Check

How many ELABORATE Max2 lines does Max4 print?

Check the result.

Output lineWhat it shows
ELABORATE Max2Three lines, one per call. Scala evaluates the two inner calls first, and the outer instance receives their output ports as arguments
Three SIMULATE lines9, 200, and 50
The tie> is false, so the Mux picks b, which is equal anyway

Try it yourself.

EditWhat happens
Replace the expression with io.in.reduce(Max2(_, _))Same values and still three instances, but the shape is now a chain, Max2(Max2(Max2(in0, in1), in2), in3), three levels deep instead of two
Then change the port to Vec(8, UInt(8.W)) and poke eight valuesThe reduce line is unchanged, yet seven ELABORATE Max2 lines appear: the Scala collection method decided the instance count from the Vec length during elaboration

8.3 Child module, Scala function, or companion apply

A companion apply changes how a parent writes the instantiation, not what it builds; the module boundary stays in the generated hierarchy. A plain Scala def that returns a Mux creates no boundary at all and adds its logic directly to the calling module.

ChoiceGood fit
Instantiate a child directlyA block reused in many places, a unit you want to test alone, a block that owns state, or one with several outputs
Scala function returning hardwareA small expression where a module boundary would only add names
Companion applyYou want the boundary but expression syntax at the call; works best for a module with one output

The elaborated hierarchy gives downstream tools module boundaries and waveform scopes. Later optimization or flattening may remove boundaries; a Chisel module is not automatically a physical placement unit.

9. Common mistakes

9.1 Using new without Module(...)

// Wrong: the module object is not registered
val m = new And2

// Right
val m = Module(new And2)

Only Module(...) puts a child into the hierarchy. With new alone, elaboration stops with attempted to instantiate a Module without wrapping it in Module().

9.2 Leaving a child input undriven

// Wrong: second.io.b is never connected
second.io.a := first.io.y
io.y := second.io.y

// Right
second.io.a := first.io.y
second.io.b := io.c
io.y := second.io.y

A child input is a sink inside the parent. Leave one empty and the FIRRTL check reports RefNotInitializedException with Reference second is not fully initialized; the detail line marked <= VOID names the missing connection.

9.3 Expecting two identical apply calls to share one instance

// Wrong: this builds two Max2 instances
io.out1 := Max2(io.a, io.b)
io.out2 := Max2(io.a, io.b)

// Right: call once and reuse the result
val m = Max2(io.a, io.b)
io.out1 := m
io.out2 := m

Every apply call runs Module(...). To use one result in several places, store it in a val and connect that.

9.4 Leaving parameterized definitions unnamed

// Wrong: definitions come out as Inc and Inc_1
class Inc(width: Int) extends Module { /* ... */ }

// Right: definitions come out as Inc4 and Inc8
class Inc(width: Int) extends Module {
  override def desiredName = s"Inc$width"
  /* ... */
}

This is not an error, but nobody reading the generated Verilog or a waveform can tell which configuration Inc_1 is. Give a module whose parameters change its structure a desiredName that includes them.

10. Apply it: finish a sorting network

The graded Practice problem below builds a three-value sorter from Sort2 children. Two instances exist; after them the maximum is settled, but the two remaining candidates come from two different children and have never been compared. Place one more Sort2, drive both of its inputs, and take min and mid from its outputs. Count the Sort2 instances the finished design contains the way section 3 counted ELABORATE lines.

In the second problem linked to the hierarchy example, four lanes of absolute differences all report the same value, and you find out why and make every lane report its own result. In both, compare the number of ELABORATE lines you predicted with the number you see. The gain-stages problem from section 6 asks the same question of a generator loop: count the doubling lines, then find the parameter the loop ignores.

11. Summary

TopicKey point
Module definitionA class extending Module; its constructor describes ports and logic, and it has an implicit clock and reset
InstantiationEach Module(new ...) call creates one instance; new alone is an error
Port wiringThe parent reaches ports as child.io.field and must drive every child input
ParametersScala values that shape each elaboration; differently shaped instances become different module definitions
require, desiredNameReject bad parameters before hardware exists; name definitions after their parameters
Functionsfor (i <- a to b) and def f(...): T = { ... } are elaboration-time repetition and reuse; each call of a hardware-building function adds a new copy, so count what they build
Companion applyPackages instantiation and input wiring into a call; every call adds one instance

12. Check your understanding

  1. Why does Mux4 produce exactly three ELABORATE Mux2 lines, no matter how many cycles the testbench simulates?

    Show answer

    The println is in the constructor, and constructors run only during elaboration, once per Module(new Mux2) call. Mux4 makes three such calls. Simulation runs the circuit that already exists; it never reruns a constructor.

  2. Which test catches a child input connected to the wrong signal, and which check catches a child input connected to nothing?

    Show answer

    A wrong connection is structurally fine, so only the testbench's expect catches it during simulation. A missing connection is caught before simulation by the FIRRTL check as RefNotInitializedException.

  3. What makes SatAdd(4) and SatAdd(8) separate module definitions, and what would their names be without desiredName?

    Show answer

    The two constructor runs build hardware with different port widths, so they cannot share a definition. Without desiredName they are named after the class: SatAdd and SatAdd_1.

  4. How many full adders exist after Ripple(4) elaborates, and how many after the testbench has poked ten input pairs?

    Show answer

    Four in both cases. The function runs only during elaboration; poking inputs during simulation creates no hardware.

  5. How many Max2 instances does io.in.reduce(Max2(_, _)) create for a Vec of length n, and when is that number decided?

    Show answer

    reduce combines n elements with n - 1 calls, so it creates n - 1 instances. The number is decided during elaboration, when Scala runs reduce.

  6. If you write Max2(io.a, io.b) once for each of two outputs, how many instances do you get?

    Show answer

    Two. Each call to apply runs Module(new Max2). To share one instance, store the result in a val and connect it to both outputs.

Checkpoint

Submit the Practice problem for grading

Grading runs on the server against hidden tests; the result is recorded in this browser only.

Practice

Use this concept in a short Chisel problem as your next step.

Sources

Licences and notices for the sources this site follows