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
}| Code | Meaning |
|---|---|
class And2 extends Module | Defines 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.b | Drives 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.
| Thing | When it exists | What it is |
|---|---|---|
class Mux2 extends Module | Scala source | A recipe; nothing is built yet |
Module(new Mux2) | During elaboration, each time it executes | One instance inside the parent, with its own ports |
| Module definition in CHIRRTL | After elaboration | The 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
}| Code | Meaning |
|---|---|
val first = Module(new And2) | Creates an And2 instance named first |
first.io.a := io.a | Drives a child input from a parent input |
second.io.a := first.io.y | Feeds one child's output into another child's input |
io.y := second.io.y | Drives 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 port | Role in the parent |
|---|---|
Input | A sink the parent must drive |
Output | A 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:
| Instance | sel input | Data inputs |
|---|---|---|
lo | io.sel(0) | in(0), in(1) |
hi | io.sel(0) | in(2), in(3) |
top | io.sel(1) | outputs of lo and hi |
The println inside the Mux2 constructor runs once per constructor execution, so it counts
instances.
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.
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.
- How many
ELABORATE Mux2lines appear, and do they come before or after theSIMULATElines? - For
sel = 2(binary10), which input does each oflo,hi, andtoppass through? - 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 line | What it shows |
|---|---|
ELABORATE Mux2 | Exactly three lines, one per Module(new Mux2) call, all printed before simulation starts |
SIMULATE sel=0 to 3 | 3, 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 |
CHECK | All 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?
| Edit | What 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"| Part | Role |
|---|---|
override def | Replaces 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:
| Code | Role |
|---|---|
require(width >= 2, ...) | Rejects an out-of-range parameter before any hardware is built |
io.a +& io.b | Produces 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 desiredName | Names 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.
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.
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.
- Which names appear on the
MODULElines, and how many are there? - Fill in the missing outputs:
a, b | What the 4-bit instance sees | narrow | wide |
|---|---|---|---|
| 3, 4 | 3, 4 | ? | ? |
| 9, 8 | 9, 8 | ? | ? |
| 200, 100 | low nibbles 8, 4 | ? | ? |
Check
For a = 9 and b = 8, what do narrow and wide show?
Check the result.
| Output line | What it shows |
|---|---|
MODULE | SatAdd4, SatAdd8, and Top. One Scala class produced two definitions because the two constructor runs built different hardware |
a=3 b=4 | Neither instance overflows: 7 and 7 |
a=9 b=8 | 17 overflows 4 bits, so narrow saturates to 15; wide shows 17 |
a=200 b=100 | The narrow instance adds 8 + 4 = 12 without overflow; the wide one gets 300, which saturates to 255 |
Try it yourself.
| Edit | What happens |
|---|---|
Delete the desiredName line | The 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 inputs | The 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| Part | Meaning |
|---|---|
def max2(a: UInt, b: UInt): UInt | A Scala function that takes and returns hardware |
Body Mux(a > b, a, b) | Runs during elaboration and adds a comparator and a Mux |
| Each call | Adds 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.
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)
}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.
- How many
ELABORATElines doesRipple(4)print? - Do any of them appear between
SIMULATElines? - What is
sumfor9 + 7?
Check
How many ELABORATE lines does Ripple(4) print, and where do they appear?
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.
| Bit | a, b, carry in | Sum | Carry out |
|---|---|---|---|
| 0 | 1, 1, 0 | 0 | 1 |
| 1 | 0, 1, 1 | 0 | 1 |
| 2 | 0, 1, 1 | 0 | 1 |
| 3 | 1, 0, 1 | 0 | 1 |
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.
- Pass
false.Binstead ofcarrytofullAdd. The four calls still happen, but no adder sees the previous carry. Each sum bit becomesa ^ b, and only bit 3's carry reaches the output:3 + 5gives 6,9 + 7gives 14, and15 + 15gives 16. The way the calls are connected decides the circuit; the function body did not change. - Change the testbench to
new Ripple(8). EightELABORATElines 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| Code | Meaning |
|---|---|
object And2 | A 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.y | Returns 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.
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.
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.
- How many
ELABORATE Max2lines appear? Count calls, not lines of code. - What does each input vector produce?
- When
50meets50, which operand does theMuxpick?
Check
How many ELABORATE Max2 lines does Max4 print?
Check the result.
| Output line | What it shows |
|---|---|
ELABORATE Max2 | Three lines, one per call. Scala evaluates the two inner calls first, and the outer instance receives their output ports as arguments |
Three SIMULATE lines | 9, 200, and 50 |
| The tie | > is false, so the Mux picks b, which is equal anyway |
Try it yourself.
| Edit | What 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 values | The 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.
| Choice | Good fit |
|---|---|
| Instantiate a child directly | A 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 hardware | A small expression where a module boundary would only add names |
Companion apply | You 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.yA 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 := mEvery 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
| Topic | Key point |
|---|---|
| Module definition | A class extending Module; its constructor describes ports and logic, and it has an implicit clock and reset |
| Instantiation | Each Module(new ...) call creates one instance; new alone is an error |
| Port wiring | The parent reaches ports as child.io.field and must drive every child input |
| Parameters | Scala values that shape each elaboration; differently shaped instances become different module definitions |
require, desiredName | Reject bad parameters before hardware exists; name definitions after their parameters |
| Functions | for (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 apply | Packages instantiation and input wiring into a call; every call adds one instance |
12. Check your understanding
-
Why does
Mux4produce exactly threeELABORATE Mux2lines, no matter how many cycles the testbench simulates?Show answer
The
printlnis in the constructor, and constructors run only during elaboration, once perModule(new Mux2)call.Mux4makes three such calls. Simulation runs the circuit that already exists; it never reruns a constructor. -
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
expectcatches it during simulation. A missing connection is caught before simulation by the FIRRTL check asRefNotInitializedException. -
What makes
SatAdd(4)andSatAdd(8)separate module definitions, and what would their names be withoutdesiredName?Show answer
The two constructor runs build hardware with different port widths, so they cannot share a definition. Without
desiredNamethey are named after the class:SatAddandSatAdd_1. -
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.
-
How many
Max2instances doesio.in.reduce(Max2(_, _))create for aVecof lengthn, and when is that number decided?Show answer
reducecombinesnelements withn - 1calls, so it createsn - 1instances. The number is decided during elaboration, when Scala runsreduce. -
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
applyrunsModule(new Max2). To share one instance, store the result in avaland connect it to both outputs.