Every chapter so far has written generators with a fixed shape: an 8-bit counter, a
4-entry table, a multiplexer over UInt. Real designs repeat the same structure for
many payloads and sizes. A pipeline register looks the same whether it carries a
byte or a packet header, and an accumulator is the same circuit at 4 bits or 32.
This chapter follows the Polymorphism and Parameterization explanation of the
official Chisel documentation: how Scala type parameters and ordinary constructor
arguments let one class describe that whole family of circuits.
Before you start. You need Bundle from
Bundles and Vecs, module
instantiation from
Child modules and generators,
RegInit and RegNext from
Registers and stepping,
the width-expanding add +& from
Widths, SInt, and arithmetic, and Mux
from Mux, when, and Wire.
Everything in this chapter is decided during elaboration. The Scala compiler
resolves type parameters before any circuit exists, and constructor arguments are
read while the generator runs. The simulated circuit never sees T or a
configuration object; it sees only the ports and nodes those values caused the
generator to build.
1. Two kinds of parameter
A Chisel class can be parameterized in two ways, and mistakes in each show up at a different time.
| Kind | Example | Decided by | A wrong value is caught by |
|---|---|---|---|
| Type parameter | class Pick[T <: Data](gen: T) | The Scala compiler, from the argument's type | The Scala compiler, through the bound |
| Value parameter | class Accumulator(cfg: AccConfig) | Generator code, while it runs | Your own require, during elaboration |
A type parameter says "this circuit works for any payload in some family of types". A value parameter is any ordinary Scala value the generator reads: a width, a feature switch, a list of constants.
Check
A FIFO generator needs four settings. Which one must be a hardware input port rather than a constructor argument?
2. Type parameters
2.1 Generic functions
A Scala function can take a type parameter in square brackets. Inside the function,
T stands for whatever type the caller passes.
Syntax
def name[T <: UpperBound](arg1: T, arg2: T): ResultType = bodydef swapIf[T <: Data](c: Bool, a: T, b: T): (T, T) =
(Mux(c, b, a), Mux(c, a, b))| Part | Meaning |
|---|---|
[T <: Data] | Declares the type parameter T; <: Data is its upper bound |
a: T, b: T | Both arguments are declared with the same T |
(T, T) | The result is a pair of values of that same type |
Call swapIf(sel, x, y) with UInts | Scala infers T = UInt; with Bundles, the Bundle type |
Called with two 8-bit UInts 5 and 8, swapIf returns (5, 8) when c is 0 and
(8, 5) when c is 1. The same function body works for SInt, Bundle, or Vec
values, because it only uses Mux.
Caution. Declaring both arguments as T does not guarantee that they have the
same hardware shape. Scala infers the most specific type that fits both arguments.
swapIf(c, 1.U, -1.S) compiles, with T inferred as a common supertype of UInt
and SInt, and then stops during elaboration inside Mux with
can't create Mux with heterogeneous types. Two UInts of different widths are
also accepted: the Mux simply takes the wider width. Chisel checks the shapes at
elaboration, not the Scala compiler.
2.2 Upper bounds
The type after <: is the upper bound: the family of types T may be. The bound is
a promise in two directions. The body may use only the operations the bound
guarantees, and callers may pass only types inside it.
Syntax
[T <: Data] // any Chisel type
[T <: Bits] // only flat bit vectors: UInt, SInt, Booldef lsb[T <: Bits](x: T): Bool = x(0)| Bound | Admits | Body may use |
|---|---|---|
T <: Data | UInt, SInt, Bool, Bundle, Vec, enums | Mux, :=, asUInt, getWidth: what every Data has |
T <: Bits | UInt, SInt, Bool | The above plus bit extraction such as x(0) and x(3, 0) |
With T <: Bits, lsb(5.U) and lsb(-3.S) both return 1. Write the same body with
T <: Data and the compiler reports T does not take parameters: not every Data
has bits you can index, since a Bundle does not.
Note. Choose the loosest bound the body actually needs. A tighter bound than necessary rejects useful payloads at compile time; a looser bound than necessary does not compile.
Check
def msb[T <: ???](x: T): Bool = x(x.getWidth - 1). Which is the loosest bound that compiles?
2.3 Generic modules and the gen template
A module class can take a type parameter too. To declare ports, though, it needs
more than the Scala type T: it needs the exact width and field layout, and a Scala
type parameter is not available as a value while the generator runs. Generic Chisel
classes therefore take a gen: T argument, a Chisel type object such as UInt(8.W)
or new Pixel, and use it as a template.
Syntax
class Name[T <: Data](gen: T) extends Module {
val io = IO(new Bundle {
val in = Input(gen)
val out = Output(gen)
})
}class Passthrough[T <: Data](gen: T) extends Module {
val io = IO(new Bundle {
val in = Input(gen)
val out = Output(gen)
})
val tmp = Wire(gen)
tmp := io.in
io.out := tmp
}
// new Passthrough(SInt(6.W)) builds 6-bit signed portsUse of gen | Creates |
|---|---|
Input(gen), Output(gen) | A port shaped like the template |
Wire(gen) | A wire shaped like the template |
Reg(gen) | A register shaped like the template |
gen.getWidth | The template's width, as a Scala Int, during elaboration |
Each use makes a fresh piece of hardware. The template itself is never connected to anything.
Caution. gen must be a Chisel type, never hardware. UInt(8.W) and new Pixel
are types; 0.U(8.W), io.in, and a Wire are hardware. Passing hardware compiles,
because a literal is still a UInt to Scala, but elaboration stops with
ExpectedChiselTypeException: 'UInt<8>(0)' must be a Chisel type, not hardware.
When you only have a hardware value, chiselTypeOf(io.in) returns its type, so
Wire(chiselTypeOf(io.in)) is a wire shaped like io.in.
2.4 Example: one class, two payload types
Pick.scala puts the three ideas together. Read it in this order:
Pixelis a plain three-fieldBundle, 8 bits per field.Chooseis a generic function: aBooland two values of typeTin, aMuxof typeTout.Pick[T <: Data]takes a templategenand uses it for three ports.- The
printlnruns during elaboration and reports which template it received and how wide it is.
import chisel3._
class Pixel extends Bundle {
val r = UInt(8.W)
val g = UInt(8.W)
val b = UInt(8.W)
}
object Choose {
// A generic function: both choices must share one type T.
def apply[T <: Data](sel: Bool, whenFalse: T, whenTrue: T): T =
Mux(sel, whenTrue, whenFalse)
}
class Pick[T <: Data](gen: T) extends Module {
// gen is a Chisel type; it only shapes the ports.
val io = IO(new Bundle {
val sel = Input(Bool())
val a = Input(gen)
val b = Input(gen)
val out = Output(gen)
})
// Runs once per instance, after T is already fixed.
println(s"ELABORATE Pick of ${gen.getClass.getSimpleName}, ${gen.getWidth} bits")
io.out := Choose(io.sel, io.a, io.b)
}Tb.scala runs two separate tests. The first builds Pick(UInt(8.W)), pokes 17 and
99 into a and b, and reads out for both sel values. The second builds
Pick(new Pixel), pokes all six fields of the two inputs, and prints each output
field plus the whole Bundle packed into one UInt.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
// The same class, built for a plain 8-bit UInt.
test(new Pick(UInt(8.W))) { dut =>
dut.io.a.poke(17.U)
dut.io.b.poke(99.U)
for (sel <- Seq(0, 1)) {
dut.io.sel.poke((sel == 1).B)
println(s"PICK UInt sel=$sel out=${dut.io.out.peek().litValue}")
}
}
// The same class again, now carrying a three-field Bundle.
test(new Pick(new Pixel)) { dut =>
dut.io.a.r.poke(1.U)
dut.io.a.g.poke(2.U)
dut.io.a.b.poke(3.U)
dut.io.b.r.poke(10.U)
dut.io.b.g.poke(20.U)
dut.io.b.b.poke(30.U)
for (sel <- Seq(0, 1)) {
dut.io.sel.poke((sel == 1).B)
val o = dut.io.out
val packed = o.peek().asUInt.litValue.toString(16)
println(s"PICK Pixel sel=$sel r=${o.r.peek().litValue} g=${o.g.peek().litValue} b=${o.b.peek().litValue} packed=0x$packed")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run.
- What widths do the two
ELABORATElines report? - With
sel = 1, which input reachesoutin each test? - Packing puts the fields in declaration order with the first field in the most
significant bits. What hex value does
r=1, g=2, b=3produce?
Check the result.
| Output line | What it shows |
|---|---|
ELABORATE Pick of UInt, 8 bits | The first instance was built with T = UInt and 8-bit ports |
PICK UInt sel=0, sel=1 | 17, then 99: sel = 1 selects b |
ELABORATE Pick of Pixel, 24 bits | The same class built a second, different circuit with 24-bit Bundle ports |
PICK Pixel sel=0, sel=1 | All three fields switch together; packed is 0x10203, then 0xa141e, with r on top |
Each ELABORATE line appears right before that test's simulation lines. Scala resolves the type
parameter at compile time; during elaboration, gen supplies each instance's hardware shape.
The two resulting circuits are as different as two hand-written modules. Nothing generic survives into
simulation.
Try it yourself.
- Change the bound in
class PickfromT <: DatatoT <: Bits. The compiler now rejectsnew Pick(new Pixel)withinferred type arguments [Pixel] do not conform to ... type parameter bounds [T <: chisel3.Bits]. The validUInttest does not run either, because the whole program must compile first. - Restore the bound and change the first test to
new Pick(0.U(8.W)). It compiles, then stops insideInput(gen)withExpectedChiselTypeException: 'UInt<8>(0)' must be a Chisel type, not hardware.
3. Value parameters
3.1 Constructor arguments and Scala if
The simplest value parameter is an ordinary constructor argument. The generator
reads it with plain Scala, including if and loops, and builds different hardware
for different values.
Syntax
class Name(param1: Int, param2: Boolean) extends Module { ... }class Delay(width: Int, stages: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(width.W))
val out = Output(UInt(width.W))
})
io.out := (0 until stages).foldLeft(io.in)((d, _) => RegNext(d))
}new Delay(8, 3) builds three 8-bit registers in a row; new Delay(16, 1) builds
one 16-bit register. Neither width nor stages exists in the circuit. They only
decided what the generator built.
Check
What does new Delay(4, 0) build?
3.2 Grouping parameters in a case class
When a module has several parameters, collect them in a case class. It gives the
parameter set a name, default values, and one place for checks and derived values.
Syntax
case class Params(field1: Type = default, field2: Type = default) {
require(condition, "message")
val derived = expression
}case class BusParams(width: Int = 32, withDebug: Boolean = false) {
require(width % 8 == 0, s"width must be a multiple of 8, got $width")
val lanes = width / 8
}| Part | Meaning |
|---|---|
width: Int = 32 | A field with a default; BusParams() means 32 bits |
require(...) | Runs when the object is constructed, before any module uses it |
val lanes = width / 8 | A derived value every module computes the same way |
BusParams(64) | lanes is 8 |
BusParams(12) | Throws IllegalArgumentException: requirement failed: width must be a multiple of 8, got 12 |
Note. Put the explanation in the require message. A failed check that names the
parameter and its value is far easier to fix than a width error several modules
later.
3.3 Optional ports with Option
A Scala if can decide whether a port exists at all. Wrap the port in Some(...)
when it exists and use None when it does not.
Syntax
val port = if (condition) Some(IO(Output(Type))) else None
port.foreach(_ := value) // drive it only if it existsval debug = if (p.withDebug) Some(IO(Output(UInt(p.width.W)))) else None
debug.foreach(_ := data)| Value | Effect |
|---|---|
Some(IO(...)) | The port is created, and foreach connects it |
None | The port is absent from the circuit, and foreach does nothing |
Code that touches an optional port must handle its absence, in the module and in the
testbench alike: use foreach, map, or isDefined, never .get.
3.4 Example: a configuration object and an optional port
Accumulator.scala applies section 3 to a running sum. Read it in this order:
AccConfighas two fields and arequirethat rejects widths below 2.Accumulatorusescfg.widthfor every port and register width.carryisSome(port)only whencfg.withCarryis true.acc +& io.inis one bit wider thanacc. The low bits go back into the register; the top bit drivescarryif it exists.
import chisel3._
// Elaboration-time parameters, checked before any hardware exists.
case class AccConfig(width: Int, withCarry: Boolean) {
require(width >= 2, s"width must be at least 2, got $width")
}
class Accumulator(cfg: AccConfig) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(cfg.width.W))
val sum = Output(UInt(cfg.width.W))
})
// The carry port exists only when the configuration asks for it.
val carry = if (cfg.withCarry) Some(IO(Output(Bool()))) else None
val acc = RegInit(0.U(cfg.width.W))
// +& keeps the carry bit; the register keeps only the low bits.
val next = acc +& io.in
acc := next(cfg.width - 1, 0)
io.sum := acc
carry.foreach(_ := next(cfg.width))
}Tb.scala loops over two configurations: 4 bits with a carry port, and 8 bits
without. For each it reports whether carry exists, then adds 5, 6, and 7, printing
the registered sum and the carry of the pending addition before each clock step.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
val configs = Seq(AccConfig(4, withCarry = true), AccConfig(8, withCarry = false))
// Each configuration elaborates a different circuit.
for (cfg <- configs) {
test(new Accumulator(cfg)) { dut =>
println(s"CONFIG width=${cfg.width} carryPort=${dut.carry.isDefined}")
for (v <- Seq(5, 6, 7)) {
dut.io.in.poke(v.U)
// An absent port is None, so the testbench prints a dash.
val c = dut.carry.map(_.peek().litValue.toString).getOrElse("-")
println(s"ADD in=$v sum=${dut.io.sum.peek().litValue} carry=$c")
dut.clock.step()
}
println(s"FINAL sum=${dut.io.sum.peek().litValue}")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run. Fill in the 4-bit trace yourself, then compare.
| Before step | acc | io.in | acc +& io.in | carry | Next acc |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 5 | 0 | 5 |
| 2 | 5 | 6 | 11 | 0 | 11 |
| 3 | 11 | 7 | 18 (1_0010) | 1 | 2 |
- How many
carryvalues does the 8-bit test print, and what appears in their place? - What final sum does each configuration reach?
Check the result.
| Output line | What it shows |
|---|---|
CONFIG width=4 carryPort=true | The first configuration built the carry port |
ADD in=7 sum=11 carry=1 | 11 + 7 = 18 overflows 4 bits; the carry port shows the fifth bit |
FINAL sum=2 | The register kept only the low 4 bits of 18 |
CONFIG width=8 carryPort=false | The second configuration has no carry port at all |
carry=- on every 8-bit line | dut.carry is None, so the testbench's map/getOrElse prints a dash |
FINAL sum=18 | 8 bits hold 18 without overflow |
The final sums are the same three additions taken modulo 16 and modulo 256.
Try it yourself.
- Change the first configuration to
AccConfig(1, withCarry = true). No circuit is built: constructing the case class throwsjava.lang.IllegalArgumentException: requirement failed: width must be at least 2, got 1. - In the testbench, replace the
map/getOrElseline withdut.carry.get.peek(). The 4-bit test still works, but the 8-bit test throwsjava.util.NoSuchElementException: None.get.
4. Passing a module as a parameter
A generator can also take another module as its parameter. Declare a Scala trait
that lists the ports the outer module needs, and bound the type parameter by both
BaseModule and that trait. Pass the child as a by-name argument (=> T) so it is
constructed inside the parent's Module(...) call.
Syntax
class Outer[T <: BaseModule with PortTrait](child: => T) extends Module {
val inner = Module(child)
}import chisel3._
import chisel3.experimental.BaseModule
trait TwoInOne { def a: UInt; def b: UInt; def y: UInt }
class Wrap[T <: BaseModule with TwoInOne](child: => T) extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W)); val b = Input(UInt(8.W)); val y = Output(UInt(8.W))
})
val inner = Module(child)
inner.a := io.a; inner.b := io.b; io.y := inner.y
}| Part | Meaning |
|---|---|
trait TwoInOne | The ports Wrap relies on; each child declares them |
T <: BaseModule with TwoInOne | T must be a module and must have those ports |
child: => T | By-name: new Add runs inside Module(child), not before |
inner.a := io.a | Type-checks because the trait guarantees a |
With an adding RawModule and a subtracting one (a RawModule is a module without
the implicit clock and reset), new Wrap(new Add) and new Wrap(new Sub) build two
different circuits around the same wrapper: for inputs 9 and 4 they output 13 and 5.
A plain Module bound would not let the compiler check inner.a.
5. Common mistakes
5.1 Passing hardware as gen
// Wrong: 0.U(8.W) is a hardware literal
val p = Module(new Pick(0.U(8.W)))
// Right: pass the Chisel type
val p = Module(new Pick(UInt(8.W)))The wrong line compiles and then fails during elaboration with
ExpectedChiselTypeException: ... must be a Chisel type, not hardware. If you only
have a hardware value, pass chiselTypeOf(value).
5.2 A bound too loose for the body
// Wrong: not every Data can be indexed
def lsb[T <: Data](x: T): Bool = x(0)
// Right: bit extraction needs Bits
def lsb[T <: Bits](x: T): Bool = x(0)The wrong version fails to compile with T does not take parameters. The bound must
promise every operation the body uses.
5.3 Branching on hardware with Scala if
// Wrong: io.c is a hardware Bool, unknown during elaboration
io.y := (if (io.c) 1.U else 2.U)
// Right: a hardware choice is a Mux (or when)
io.y := Mux(io.c, 1.U, 2.U)The compiler reports type mismatch; found: chisel3.Bool, required: Boolean. Scala
if is for parameters that are known while the generator runs, like
cfg.withCarry; Mux and when are for signals that change while the circuit runs.
5.4 Calling .get on an optional port
// Wrong: throws when the configuration omits the port
println(dut.carry.get.peek().litValue)
// Right: handle the absent case
println(dut.carry.map(_.peek().litValue.toString).getOrElse("-")).get on None throws java.util.NoSuchElementException: None.get. It works in one
configuration and fails in another, which makes the bug easy to miss.
6. Summary
| Topic | Key point |
|---|---|
| Type parameter | [T <: Bound]; Scala infers T from the arguments at each call |
| Upper bound | Data admits every Chisel type; Bits adds bit extraction but rejects Bundles |
Same T | Not a shape guarantee; Mux checks the hardware types during elaboration |
gen template | A Chisel type used by Input, Output, Wire, Reg; never hardware |
chiselTypeOf | Gets the Chisel type of a hardware value |
| Value parameter | Plain Scala; read with if and loops while the generator runs |
case class | Names, defaults, require checks, and derived values in one place |
| Optional port | Some(IO(...)) or None; access with foreach/map, never .get |
| Module parameter | T <: BaseModule with Trait, passed by name as => T |
7. Check your understanding
-
Pickalready has the type parameterT. Why does it also needgen?Show answer
A port needs an exact width and field layout, and the Scala type parameter is not available as a value while the generator runs.
genis a Chisel type object that carries that information, soInput(gen)can build a port of the right shape. -
T <: Bitsstops thePixeltest at compile time, butPick(0.U(8.W))fails only during elaboration. Why the difference?Show answer
The bound is a Scala type rule, and
Pixelis not aBits, so the compiler rejects it.0.U(8.W)has Scala typeUInt, which satisfies the bound; only Chisel can tell that it is hardware rather than a type, and it checks that whenInput(gen)runs. -
In the 4-bit accumulator, which node produces the carry, and why is it one bit wider than the register?
Show answer
next = acc +& io.in. The+&operator returns a result one bit wider than its widest operand, so the sum of two 4-bit values is 5 bits. Bit 4 is the carry and bits 3 to 0 go back into the register. -
Does
Choose(sel, 1.U, -1.S)compile? What happens when it runs?Show answer
It compiles: Scala infers a common supertype of
UIntandSIntforT. Elaboration then stops insideMuxwithcan't create Mux with heterogeneous types. -
You add
AccConfig(6, withCarry = true)as a third configuration. What final sum and carry pattern do you expect for the inputs 5, 6, 7?Show answer
6 bits hold up to 63, so 5 + 6 + 7 = 18 never overflows. The test prints
carryPort=true, a carry of 0 on all three lines, and a final sum of 18.