1. A Chisel module is a Scala program that builds a circuit
Chisel is a hardware construction language embedded in Scala. A Chisel design is an ordinary Scala program: when it runs, it builds a circuit, and that circuit is then simulated or turned into Verilog. You do not need to know Scala in advance; this course explains each piece of Scala just before the chapter that uses it. Almost every surprise a beginner meets in Chisel comes from mixing up "the program that builds the circuit" with "the circuit itself", so this chapter starts there.
Chisel separates two things that other HDLs mix in one language: the hardware types and operators,
every one of which describes synthesizable binary logic, and the program that assembles them.
Simulation-only constructs still exist, as printf, assert and stop, but they are visibly apart
from the hardware. The full power of Scala — loops, functions, classes, collections — is available for
deciding which circuit to build. A Chisel program that produces a family of designs from parameters
is called a circuit generator.
1.1 Elaboration and simulation
A Chisel run has two phases, and they never overlap.
| Phase | What runs | What it produces |
|---|---|---|
| Elaboration | Your Scala code, once per module instance | A circuit graph of ports, operators, wires, and registers |
| Simulation | The generated circuit, cycle by cycle | Port values that a testbench observes |
During elaboration, a line such as val sum = a + b does not add two numbers. It adds an adder node
whose inputs are a and b to the circuit, and binds the Scala name sum to that node's output. Wrap
the line in a Scala for loop and you get one adder per iteration. Once elaboration ends, the loop is
gone; the circuit contains only the nodes it left behind. Only then does simulation start, and only
then do wires carry values.
Note. The official documentation describes Chisel's scope as binary, synchronous logic. An
Analog type exists for tri-state and other undirected nets, but standard flows support it poorly and
this course does not use it.
1.2 Scala values and Chisel values
The two phases split types into two families.
| Family | Examples | When the value exists |
|---|---|---|
| Scala types | Int, Boolean, Seq, String | During elaboration; the generator can compute with them immediately |
| Chisel types | UInt, SInt, Bool | For a hardware node, only once the circuit runs, in simulation here; a literal such as 3.U also has its value during elaboration |
Elaboration can inspect a Chisel value's type and width, but it can never read the number a wire will carry, because that number does not exist yet. Keep this table in mind; the two failure examples later in the chapter are both violations of it.
2. The shape of a Chisel module
2.1 Defining a module
A hardware block is a Scala class that extends Module. The class body runs once during elaboration,
and every statement in it adds ports, nodes, or connections to the circuit.
Syntax
import chisel3._
class ModuleName(parameters) extends Module {
val io = IO(new Bundle {
// port declarations
})
// circuit description
}Here is the smallest useful module: a two-input AND gate.
import chisel3._
class AndGate 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 | Scala meaning | What it does here |
|---|---|---|
import chisel3._ | Brings a package's names into scope | Makes Module, IO, UInt, Bool, :=, and the rest of Chisel available |
class AndGate extends Module | Declares a class that inherits from Module | Declares a hardware block named AndGate; its body runs once, during elaboration |
val io = IO(...) | val gives a name to a value, once and for good | Declares the module's ports; io is the conventional name |
new Bundle { ... } | Creates an object with the listed fields | Groups several named ports into one value, reached as io.a, io.y |
Input(...), Output(...) | Ordinary function calls | Give a port its direction |
io.y := io.a && io.b | A method call on io.y | Connects the AND of the two inputs to the output |
Note. Every Module also receives an implicit clock and reset. The AND gate does not use them;
the registers in Registers, RegInit, and stepping the clock
do.
2.2 Ports: IO, Input, and Output
A port is a Chisel type with a direction, bound to the module boundary by IO.
Syntax
val name = Input(chiselType)
val name = Output(chiselType)val io = IO(new Bundle {
val data = Input(UInt(8.W)) // 8-bit unsigned input
val valid = Input(Bool()) // 1-bit input
val out = Output(UInt(8.W)) // 8-bit unsigned output
})UInt(8.W) reads as "an unsigned integer 8 bits wide"; .W turns the Scala Int 8 into a Chisel
width. Bool() is a single bit. Inside the module you read inputs and drive outputs through the io
value, as io.data or io.out. Bool, UInt, and their operators are the subject of
Bool, UInt, and the operators; widths, SInt, and
literals are the subject of Widths, SInt, and arithmetic.
2.3 Connecting with :=
The connection operator := records "drive the left-hand side from the right-hand side" in the
circuit graph: the sink is driven by the source for good, like a wire laid in the circuit.
Syntax
sink := sourceio.out := io.data // pass the input straight through
val doubled = io.data + io.data
io.out := doubled // a later connection to the same sink wins| Form | What it does | Phase |
|---|---|---|
val name = expr | Builds the hardware for expr and binds a Scala name to it; no wiring by itself | Elaboration |
sink := source | Adds a connection from source to sink | Recorded at elaboration, active in the circuit |
The left side of := must be hardware that the module is allowed to drive: an output port or a wire.
Scala's = does nothing to the circuit; it only binds a name. val x = io.in + io.in builds one adder
and names its output x. When two connections drive the same sink, the one written later takes
effect; Mux, when, Wire, and a default for every path
builds on that rule.
2.4 Generator parameters
Because a module is a Scala class, its constructor parameters — the names in parentheses after the class name — are ordinary Scala values. They exist only during elaboration, and they decide what hardware gets built.
Syntax
class ModuleName(param: ScalaType) extends Module { ... }class Buffer(width: Int, withParity: Boolean) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(width.W))
val out = Output(UInt(width.W))
})
io.out := io.in
if (withParity) {
println("building a parity port") // runs during elaboration only
}
}new Buffer(16, false) and new Buffer(8, true) produce two different circuits from the same
source. A Scala if is the correct tool here because withParity is a Scala Boolean that is known
while the generator runs.
2.5 A minimal testbench
The examples in this course drive a module with chiseltest. A testbench is a Scala
object Main extends App: an object is a class with exactly one instance, and extends App makes
its body the program that runs when the run starts. The body elaborates the module and then simulates
it.
Syntax
test(new ModuleName(...)) { dut =>
dut.io.port.poke(value)
dut.io.port.peek()
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new AndGate) { dut =>
dut.io.a.poke(true.B)
dut.io.b.poke(false.B)
println(s"y=${dut.io.y.peek().litValue}")
}
}| Call | Role |
|---|---|
test(new AndGate) { dut => ... } | Elaborates AndGate, starts a simulator, and runs the block between the braces with the device under test named dut |
poke(value) | Drives an input port with a hardware literal such as true.B or 3.U |
peek() | Reads the current value of a port |
.litValue | Turns the peeked literal into a Scala BigInt you can print |
s"y=${...}" | A Scala string with values inserted where $ appears; ${expr} inserts the value of any expression |
Note. poke and peek run during simulation. Anything else in the module body, including a
println, ran earlier during elaboration.
3. How the lab runs a Chisel example
Every example in this course uses the same command. It compiles all .scala files, then runs Main:
CP=$(printf '%s:' $CHISEL_JARS/*.jar)
java -cp "$CP" scala.tools.nsc.Main -classpath "$CP" \
-Xplugin:$(ls $CHISEL_JARS/chisel-plugin*.jar) -language:reflectiveCalls \
-d classes *.scala
java -cp "$CP:classes" Main| Part | Role |
|---|---|
First java | The Scala compiler. The sandbox ships jars rather than the scalac launcher, so the compiler's main class is called directly |
-Xplugin:...chisel-plugin... | The Chisel compiler plugin, which recovers the names of vals for the generated circuit |
-language:reflectiveCalls | Allows field access on the anonymous new Bundle { ... } port lists used throughout |
Second java | Runs Main. chiseltest elaborates the module, hands the circuit to the treadle simulator on the JVM, and executes the test body |
An error can therefore come from three stages, and its shape tells you which.
| Message shape | Failing stage |
|---|---|
File.scala:12: error: ... | Scala compilation |
Exception in thread "main" raised while the module is built | Elaboration |
A failed expect or a wrong value in the output | Simulation |
4. Example: one generator, two phases
Doubler takes a Scala Int parameter, stages. Inside the constructor, var x starts as a
reference to the input port; var, unlike val, declares a name that may be re-pointed. The line
for (i <- 1 to stages) repeats its body for i = 1, 2, …, stages. Each iteration builds a new adder
whose two inputs are the previous node, x = x + x, and moves x to that adder's output, so stages
adders end up in a chain. The println inside the loop is plain Scala, so it prints while the loop
runs. Read the module first, then the testbench, which builds Doubler(3) and pokes three input
values; for (value <- Seq(1, 3, 5)) runs its block once for each of the three listed values.
import chisel3._
class Doubler(stages: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
// A Scala var holds a reference to a hardware node, not a runtime value.
var x = io.in
for (i <- 1 to stages) {
println(s"ELABORATE adder $i")
x = x + x
}
io.out := x
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Doubler(3)) { dut =>
for (value <- Seq(1, 3, 5)) {
dut.io.in.poke(value.U)
println(s"SIMULATE in=$value out=${dut.io.out.peek().litValue}")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Only the final io.out := x is wiring. Everything before it decides what to wire.
Predict before you run. The loop in Doubler prints one ELABORATE line per adder, and the
testbench prints one SIMULATE line per poked value.
Check
How many ELABORATE lines will appear, and where?
Check
With stages = 3, what does out show for in = 3?
Check the result. Press Run. The lab compiles the two files, elaborates Doubler(3), and
simulates it. Compare each part of the output.
| Output | What it shows |
|---|---|
Three ELABORATE lines first | The Scala loop ran exactly once, while new Doubler(3) was being constructed, and built three adders |
No ELABORATE line between SIMULATE lines | Every poke reuses the same three adders; the loop never runs again |
SIMULATE outputs 8, 24, and 40 for inputs 1, 3, and 5 | Three chained doublings multiply the input by eight |
Caution. Output appearing means the circuit simulated correctly with chiseltest's treadle
simulator. Chisel's hardware constructs describe synthesizable logic, but this course only simulates;
synthesis is never run. The println calls exist only in the program that builds and tests the
circuit, not in the circuit.
Try it yourself. First change new Doubler(3) in Tb.scala to new Doubler(2) and run again.
You changed a Scala number, not hardware syntax, and a different circuit is built.
Check
With stages = 2, what does out show for in = 3?
Then try two more edits.
- Change the parameter to
Doubler(5). FiveELABORATElines appear and every output becomes 32 times its input: 1, 3, and 5 produce 32, 96, and 160. - Keep
Doubler(5)and add 10 to the poked values. The product 320 does not fit in the 8-bit output, and the port shows 64.+keeps the wider operand's width, so each adder wraps modulo 256. Widths, SInt, and arithmetic explains this width rule.
The edited code stays in the lab until you press Run on the page again, which reloads the example.
5. Scala control flow cannot read hardware
A Scala if chooses between two pieces of generator code. It needs a Boolean while elaboration is
running. A Bool port has no value at that moment: it only names a wire that will carry 0 or 1 in
every future cycle.
Syntax of the two kinds of choice:
if (scalaBoolean) { ... } else { ... } // chooses what to build
when (hardwareBool) { ... } .otherwise { ... } // builds a run-time choiceif (withParity) { ... } // fine: withParity is a Scala Boolean
if (io.sel) { ... } // wrong: io.sel is a hardware BoolThis example fails to compile on purpose. Select asks Scala to branch on the hardware port io.sel.
Before you press Run, decide which of the three stages from section 3 will report the error.
Check
Which stage reports the error in ScalaIf.scala?
import chisel3._
class Select extends Module {
val io = IO(new Bundle {
val sel = Input(Bool())
val a = Input(UInt(8.W))
val b = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
// Scala if needs a Scala Boolean while the module is being built.
if (io.sel) {
io.out := io.a
} else {
io.out := io.b
}
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new Select))
}| Item | Detail |
|---|---|
| Wrong code | if (io.sel) uses a Bool port as the condition of a Scala if |
| Failing stage | Scala compilation, before any circuit exists |
| Error message | type mismatch; found: chisel3.Bool, required: Boolean |
| Fix | Describe a hardware multiplexer with when/.otherwise or Mux(sel, a, b) instead of a Scala branch |
The message is the table from section 1.2 in one line: the condition has a hardware type, and Scala
control flow needs a Scala type. when and Mux are covered in
Mux, when, Wire, and a default for every path.
6. Connection is not assignment
Chisel hardware objects are bound to Scala vals, and a val cannot be rebound. The connection
operator := is a method on the left-hand hardware object; it does not replace anything, it adds a
connection to the graph.
| Statement | Phase | Meaning |
|---|---|---|
val y = a + b | Elaboration | Builds an adder and binds the Scala name y to it |
x = x + x with var x | Elaboration | Points the Scala variable x at a new node |
io.out := x | Circuit | Drives the output port from node x |
This example fails to compile on purpose. It writes Scala = where a hardware connection is meant.
Predict the stage again before you press Run.
Check
Which stage reports the error in Assign.scala?
import chisel3._
class Passthrough extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
// Scala = tries to rebind the val; hardware wiring needs :=.
io.out = io.in
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new Passthrough))
}| Item | Detail |
|---|---|
| Wrong code | io.out = io.in uses = on a field of the val io |
| Failing stage | Scala compilation |
| Error message | reassignment to val |
| Fix | Write io.out := io.in; the module becomes a valid passthrough |
The Doubler example used both operators correctly: x = x + x moves a Scala var during
elaboration, and io.out := x wires the final node to the output.
7. Common mistakes
7.1 Branching on hardware with Scala if
// Wrong: io.sel is hardware; Scala needs a Boolean now
if (io.sel) { io.out := io.a } else { io.out := io.b }
// Right: build a multiplexer
when (io.sel) { io.out := io.a } .otherwise { io.out := io.b }Scala if, while, and match run during elaboration. The compiler reports
found: chisel3.Bool, required: Boolean. Use if only for generator parameters.
7.2 Using = for wiring
// Wrong: tries to rebind a field of a val
io.out = io.in
// Right: record a connection
io.out := io.in= belongs to Scala names, := to circuit wiring. The compiler reports reassignment to val.
7.3 Connecting a Scala number to a port
// Wrong: 5 is a Scala Int, not hardware
io.out := 5
// Right: turn it into a hardware literal
io.out := 5.UThe right-hand side of := must be hardware. The Scala compiler reports
type mismatch; found: Int(5), required: chisel3.Data. The suffix .U makes an unsigned literal;
Widths, SInt, and arithmetic
lists the others.
7.4 Expecting println to show simulated values
// Wrong: runs once, during elaboration, and prints a description of the port
println(io.in)
// Right: read the value in the testbench, during simulation
println(dut.io.in.peek().litValue)A println in the module body runs while the circuit is being built. It prints the port's name and
type, in the form ModuleName.io.in: IO[UInt<8>], never a number. Values exist only after simulation
starts.
8. Summary
| Topic | Key point |
|---|---|
| Chisel program | A Scala program that builds a circuit; Chisel is a generator, not a fixed netlist |
| Elaboration | Scala code runs once per module instance and leaves a circuit graph |
| Simulation | The graph runs cycle by cycle; poke and peek act here |
| Scala vs Chisel types | Int and Boolean steer generation; UInt, SInt, Bool describe hardware |
| Module | class X extends Module with ports in IO(new Bundle { ... }), each Input or Output |
val name = expr | Builds expr and names the result; no wiring by itself |
:= | Records a connection; Scala = only rebinds names |
test(new X) { dut => ... } | Elaborate, then simulate; poke drives, peek reads |
| Errors | File.scala:N: error: is compilation; an exception while building is elaboration |
9. Check your understanding
-
Why does every
ELABORATEline appear before the firstSIMULATEline?Show answer
The loop that prints
ELABORATEis Scala code in the module constructor. It runs once whilenew Doubler(3)is elaborated. Simulation starts only after the circuit exists, and it reuses the same adders for everypoke. -
Why can a Scala
ifteststages > 2but notio.sel?Show answer
stagesis a ScalaInt, sostages > 2is a ScalaBooleanknown during elaboration.io.selis a hardwareBoolwhose value exists only during simulation, so Scala has nothing to branch on. -
What does
io.out := xadd to the circuit thatx = x + xdoes not?Show answer
A connection from node
xto the output port.x = x + xbuilds an adder, but the assignment itself only moves a Scala variable; without a:=, the adder drives nothing. -
A module prints
println(io.in)in its body and the testbench pokes ten values. How many lines does thatprintlnproduce, and what do they contain?Show answer
One line, printed during elaboration. It describes the port, such as its name and type
UInt<8>, and contains no simulated value. -
If
stageswere read from a configuration file instead of being written inTb.scala, which phase would read the file?Show answer
Elaboration. Generator parameters are Scala values, so any code that computes them runs before the circuit exists.
The Practice problem below applies the elaboration model: a constant multiplier built from adders
compiles and runs but outputs 0 for every input, and you complete the generator loop so that a Scala
if on the factor builds exactly the adders the constant calls for.