Skip to content
code0.HDL

A Module, its IO, and the := connection

Read a Chisel module as a Scala class that builds a circuit, tell the connection := apart from a Scala name, predict what the testbench prints, run it, then change one parameter and see a different circuit.

In this chapter

  • Tell the two phases of a Chisel run apart and say in which phase a given line of Scala runs
  • Read a module as class, IO bundle, Input/Output ports, and := connections, and tell := from Scala =
  • Predict the ELABORATE and SIMULATE output of a generator and its testbench before running it
  • Name the stage (Scala compilation, elaboration, simulation) that reports a given error
Concepts in this chapter
  • Module, IO, Input, Output, :=
Prerequisite concepts

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.

PhaseWhat runsWhat it produces
ElaborationYour Scala code, once per module instanceA circuit graph of ports, operators, wires, and registers
SimulationThe generated circuit, cycle by cyclePort 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.

FamilyExamplesWhen the value exists
Scala typesInt, Boolean, Seq, StringDuring elaboration; the generator can compute with them immediately
Chisel typesUInt, SInt, BoolFor 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
}
CodeScala meaningWhat it does here
import chisel3._Brings a package's names into scopeMakes Module, IO, UInt, Bool, :=, and the rest of Chisel available
class AndGate extends ModuleDeclares a class that inherits from ModuleDeclares a hardware block named AndGate; its body runs once, during elaboration
val io = IO(...)val gives a name to a value, once and for goodDeclares the module's ports; io is the conventional name
new Bundle { ... }Creates an object with the listed fieldsGroups several named ports into one value, reached as io.a, io.y
Input(...), Output(...)Ordinary function callsGive a port its direction
io.y := io.a && io.bA method call on io.yConnects 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 := source
io.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
FormWhat it doesPhase
val name = exprBuilds the hardware for expr and binds a Scala name to it; no wiring by itselfElaboration
sink := sourceAdds a connection from source to sinkRecorded 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}")
  }
}
CallRole
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
.litValueTurns 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
PartRole
First javaThe 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:reflectiveCallsAllows field access on the anonymous new Bundle { ... } port lists used throughout
Second javaRuns 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 shapeFailing stage
File.scala:12: error: ...Scala compilation
Exception in thread "main" raised while the module is builtElaboration
A failed expect or a wrong value in the outputSimulation

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.

Doubler.scala
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
}
Tb.scala
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?

Your answer

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.

OutputWhat it shows
Three ELABORATE lines firstThe Scala loop ran exactly once, while new Doubler(3) was being constructed, and built three adders
No ELABORATE line between SIMULATE linesEvery poke reuses the same three adders; the loop never runs again
SIMULATE outputs 8, 24, and 40 for inputs 1, 3, and 5Three 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.

  1. Change the parameter to Doubler(5). Five ELABORATE lines appear and every output becomes 32 times its input: 1, 3, and 5 produce 32, 96, and 160.
  2. 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 choice
if (withParity) { ... }   // fine: withParity is a Scala Boolean
if (io.sel) { ... }       // wrong: io.sel is a hardware Bool

This 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?

Your answer

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))
}
ItemDetail
Wrong codeif (io.sel) uses a Bool port as the condition of a Scala if
Failing stageScala compilation, before any circuit exists
Error messagetype mismatch; found: chisel3.Bool, required: Boolean
FixDescribe 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.

StatementPhaseMeaning
val y = a + bElaborationBuilds an adder and binds the Scala name y to it
x = x + x with var xElaborationPoints the Scala variable x at a new node
io.out := xCircuitDrives 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?

Your answer

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))
}
ItemDetail
Wrong codeio.out = io.in uses = on a field of the val io
Failing stageScala compilation
Error messagereassignment to val
FixWrite 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.U

The 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

TopicKey point
Chisel programA Scala program that builds a circuit; Chisel is a generator, not a fixed netlist
ElaborationScala code runs once per module instance and leaves a circuit graph
SimulationThe graph runs cycle by cycle; poke and peek act here
Scala vs Chisel typesInt and Boolean steer generation; UInt, SInt, Bool describe hardware
Moduleclass X extends Module with ports in IO(new Bundle { ... }), each Input or Output
val name = exprBuilds 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
ErrorsFile.scala:N: error: is compilation; an exception while building is elaboration

9. Check your understanding

  1. Why does every ELABORATE line appear before the first SIMULATE line?

    Show answer

    The loop that prints ELABORATE is Scala code in the module constructor. It runs once while new Doubler(3) is elaborated. Simulation starts only after the circuit exists, and it reuses the same adders for every poke.

  2. Why can a Scala if test stages > 2 but not io.sel?

    Show answer

    stages is a Scala Int, so stages > 2 is a Scala Boolean known during elaboration. io.sel is a hardware Bool whose value exists only during simulation, so Scala has nothing to branch on.

  3. What does io.out := x add to the circuit that x = x + x does not?

    Show answer

    A connection from node x to the output port. x = x + x builds an adder, but the assignment itself only moves a Scala variable; without a :=, the adder drives nothing.

  4. A module prints println(io.in) in its body and the testbench pokes ten values. How many lines does that println produce, 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.

  5. If stages were read from a configuration file instead of being written in Tb.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.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows