A testbench only sees a module through its ports. When a register deep inside
the module holds the wrong value, you want the circuit itself to report what it
holds, cycle by cycle, without adding debug ports. Chisel's printf does that:
it is a print statement built into the simulated hardware. This chapter covers
the official Printing explanation and the part of Naming that decides which
names your signals carry in generated output.
Before you start. This chapter follows
Polymorphism and parameterization.
The examples use registers and RegNext from
Registers and stepping,
Bundles and VecInit from
Bundles and Vecs, and
when blocks. You have already used println in every Tb.scala; this
chapter explains how it differs from printf.
1. printf inside a module
1.1 A print statement is hardware
Calling printf in a module body prints nothing at the moment the Scala line
runs. Like + or RegNext, the call adds a node to the circuit. That node is
attached to the module's clock and carries a format string, a list of signals,
and an enable condition. During simulation, at every rising clock edge where
the enable is true, the simulator formats the signals and writes one message.
Syntax
printf(cf"text $signal%specifier ${expression}%specifier\n")Here is a module that reports a free-running counter on every cycle:
class Ticker extends Module {
val io = IO(new Bundle { val in = Input(UInt(4.W)) })
val count = RegInit(0.U(4.W))
count := count + 1.U
printf(cf"count=$count in=${io.in}\n")
}| Part | Meaning |
|---|---|
printf(...) | Adds a print node clocked by the module's implicit clock |
cf"..." | Builds the message from text and hardware values |
$count | Inserts the register's value, in decimal because no specifier follows |
${io.in} | Braces are needed when the value is a path or an expression |
\n | Ends the line; printf does not add a newline for you |
The values printed at an edge are the values just before that edge
updates any register. In Ticker, the first message shows count=0, and
count becomes 1 only after the message is written.
Check
Tb.scala pokes in = 3 and steps Ticker three times after reset. Which count value does the third message show?
Note. printf is a simulation aid. It does not become logic in a chip, and
it does not change what the circuit computes.
1.2 When a printf fires
Two conditions decide whether a printf writes on a given edge.
- The enclosing
whenblocks. Aprintfinsidewhen (c)writes only on edges wherecis true. - Reset. Chisel adds an implicit "not in reset" condition to every
printf. Whileresetis high, the module prints nothing, which keeps the initialization cycle out of your log.
Syntax
when (condition) {
printf(cf"...\n")
}when (io.valid && io.ready) {
printf(cf"accepted ${io.data}%x\n")
}This printf writes one line per accepted transfer and stays silent on every
other cycle. Guarding a message with the event you care about keeps long runs
readable.
| Situation on an edge | Does the printf write? |
|---|---|
reset is high | No, for every printf in the module |
reset is low, no enclosing when | Yes |
reset is low, enclosing when condition false | No |
reset is low, enclosing when condition true | Yes |
Check
The accepted printf above sits in a module. The testbench holds reset high for two steps, then sets valid high and steps four times with ready = 1, 0, 1, 1. How many accepted lines appear?
1.3 Reading hardware, not Scala variables
A printf can mention any hardware value in scope: ports, wires, registers,
and expressions built from them. It cannot follow a Scala var that changes
during the test, because the module body runs only once, during elaboration.
Whatever Scala values the format string mentions are fixed when the circuit is
built.
2. Formatting values
2.1 The cf interpolator
cf"..." looks like a Scala string interpolator, but it builds a Printable,
a description of a message that refers to hardware nodes. Each $name or
${expression} may be followed directly by a format specifier. Without a
specifier, a number prints in decimal. Write %% for a literal percent sign.
Syntax
cf"label=$signal%x" // one value, hexadecimal
cf"sum=${a + b}%d" // an expression, decimal
cf"load=$pct%%" // a literal % after the value2.2 Format specifiers
| Specifier | Prints | 8-bit value 65 |
|---|---|---|
%d, or none | Decimal | 65, right-aligned with spaces |
%x | Hexadecimal, padded with zeros | 41 |
%b | Binary, right-aligned with spaces | 1000001 after one space |
%c | The value as an ASCII character | A |
The simulator in this lab gives every field a fixed width taken from the signal's width, so columns line up from cycle to cycle:
| Signal | %d field | %x field | %b field |
|---|---|---|---|
UInt(8.W) | 4 columns, spaces | 2 digits, zeros | 8 columns, spaces |
UInt(4.W) | 3 columns, spaces | 1 digit | 4 columns, spaces |
Bool | 2 columns, spaces | 1 digit | 1 column |
An 8-bit value 7 therefore prints as 7 with %d and 07 with %x. A
signed value prints with its sign in decimal: an SInt(8.W) holding -3 prints
-3 with %d and fd, its two's-complement bits, with %x.
Note. %c prints the raw character code. Values below 32 are control
characters; 7, for example, prints an invisible bell character. Use %c for
signals that hold printable ASCII.
Check
x is a UInt(4.W) holding 10. The format is cf followed by [$x%x|$x%b|$x]. What does that printf print?
2.3 Printing Vecs and Bundles
A whole aggregate can go into one $ slot. A Vec prints as Vec(...) with
its elements in index order. A Bundle prints as its class name followed by
field -> value pairs in declaration order.
class Pkt extends Bundle {
val kind = UInt(2.W)
val len = UInt(4.W)
}
val p = Wire(new Pkt)
val v = VecInit(io.b, io.b + 1.U)
printf(cf"p=$p v=$v\n")The snippet shows formatting, not a complete circuit: p.kind and p.len still need
drivers. If they are driven with 1.U and io.b, then with io.b equal to 5,
p= shows Pkt(kind -> ..., len -> ...) holding 1 and 5, and v= shows
Vec(...) holding 5 and 6, each number padded like a %d field.
A Bundle class can choose its own format by overriding toPrintable.
Syntax
override def toPrintable: Printable = cf"..."class Pkt extends Bundle {
val kind = UInt(2.W)
val len = UInt(4.W)
override def toPrintable: Printable = cf"<$kind:$len%x>"
}Every cf that mentions a Pkt, in any module, now uses this format.
2.4 C-style printf
printf also accepts a plain format string followed by the arguments in
order, as in C.
Syntax
printf("format with %d and %x\n", first, second)printf("a=%d b=%x\n", io.a, io.b)The specifiers and padding are the same as with cf. The C style is useful
when the format string is built elsewhere. For hand-written messages, cf
keeps each value next to its label, which makes a wrong argument order
impossible.
2.5 Example: tracing a module from inside
Tracer has one 8-bit input. It keeps a free-running counter cycle, delays
the input by one cycle in last, collects the current and previous input in a
two-element Vec called window, and builds a Sample Bundle from the input.
Look at the three printfs at the end:
- an unconditional
cfline using four formats, aVec, and a Bundle; - a line inside
when (io.in === 0.U); - a C-style line that reports
io.inandlast.
import chisel3._
class Sample extends Bundle {
val addr = UInt(8.W)
val ok = Bool()
}
class Tracer extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
})
val cycle = RegInit(0.U(8.W))
cycle := cycle + 1.U
val last = RegNext(io.in, 0.U)
val window = VecInit(io.in, last)
val s = Wire(new Sample)
s.addr := io.in
s.ok := io.in(0)
// One line per rising edge, with the values seen just before it.
printf(cf"[$cycle%d] in=${io.in}%d hex=${io.in}%x bin=${io.in}%b win=$window s=$s\n")
// A printf inside when fires only on cycles where the condition holds.
when (io.in === 0.U) {
printf(cf"[$cycle] zero input\n")
}
// C-style: a plain format string followed by the arguments.
printf("C-style in=%d last=%x\n", io.in, last)
}Tb.scala holds reset high for one step, releases it, and prints a marker.
Then it pokes 65, 0, and 255 in turn, printing a TB poke marker before each
clock step.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Tracer) { dut =>
// printf is silent while reset is high.
dut.reset.poke(true.B)
dut.clock.step()
dut.reset.poke(false.B)
println("TB reset released")
for (v <- Seq(65, 0, 255)) {
dut.io.in.poke(v.U)
println(s"TB poke $v")
dut.clock.step()
}
println("TB done")
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Each printf reports the values just before an edge. The table traces the
three steps after reset:
| Step | cycle | io.in | last | window | Extra line |
|---|---|---|---|---|---|
| reset | 0 | 0 | 0 | None: reset disables every printf | |
| 1 | 0 | 65 | 0 | Vec(65, 0) | None |
| 2 | 1 | 0 | 65 | Vec(0, 65) | zero input |
| 3 | 2 | 255 | 0 | Vec(255, 0) | None |
After each edge, cycle goes up by one and last takes the input of the step
that just ended.
Predict before you run.
- After each
TB pokemarker, which comes first: the next testbench marker or the circuit's messages? - What does the C-style line print for
laston the third step, when the input is 255? - How many
zero inputlines appear in total? - What does
s=show forokon the first step, given thatokis bit 0 of the input?
Check the result.
| Output | What it shows |
|---|---|
TB reset released with no circuit line before it | The reset step produced no printf output |
Circuit lines after each TB poke marker | Messages are written inside clock.step(), so they follow the marker in time order |
First [0] line: hex=41, bin= 1000001, ok -> 1 | One value in three formats; 65 is odd, so bit 0 is 1 |
win=Vec(0, 65) on the cycle 1 line | last holds the previous cycle's input |
last=00 on the third C-style line | The previous input was 0; %x pads to two digits |
A single zero input line, at cycle 1 | The when branch fires only when its condition holds; [cycle] without a specifier still prints decimal |
Try it yourself.
-
Add this override to
Sample:override def toPrintable: Printable = cf"@$addr%x/${Mux(ok, 'y'.U, 'n'.U)}%c"Every
s=field changes from the defaultSample(addr -> ..., ok -> ...)form to a compact one:@41/yfor input 65,@00/nfor 0, and@ff/yfor 255. The override is ordinary hardware: theMuxpicks one of two character codes on each cycle, and%cprints the chosen code as a letter. -
Move the C-style
printfinto a newwhen (io.in =/= 0.U)block. The C-style line disappears from cycle 1, where the input is 0, and stays on cycles 0 and 2.
3. println and printf
3.1 Two phases, two print calls
Chisel code runs in two phases. During elaboration, Scala executes your
module bodies once to build the circuit. During simulation, the circuit
runs cycle by cycle. println is ordinary Scala and runs in whichever phase
executes it. printf always runs during simulation.
| Statement | Runs during | How often | Can show |
|---|---|---|---|
println(...) in a module body | Elaboration | Once per module instance | Scala values, widths, types |
println(...) in a testbench | Test execution | Each time the line runs | Values read with peek |
printf(...) in a module body | Simulation | Every enabled rising edge | Hardware values |
Syntax
println(s"text ${scalaValue}") // elaboration: once
printf(cf"text $hardware\n") // simulation: every enabled edgeclass Widths extends Module {
val io = IO(new Bundle { val a = Input(UInt(8.W)) })
println(s"a is ${io.a.getWidth} bits") // prints once: a is 8 bits
println(s"a = ${io.a}") // prints a description, not a value
}The second println prints the object's Scala description, something like
Widths.io.a: IO[UInt<8>]. During elaboration a port has a type and a width
but no value yet.
Check
Ticker has a println that writes ELAB Ticker and one unguarded printf in its body. The testbench builds it once, holds reset for two steps, then steps four more times. How many ELAB lines and printf lines appear?
3.2 Example: s"..." inside printf
Scala programmers reach for s"..." by habit. Inside a printf, the s
interpolator would call toString on the Chisel object while the circuit is
built and freeze that text into the message, so every cycle would print the
same description instead of the wire's value. Chisel catches this pattern and
turns it into a Scala compile error.
This example fails to compile on purpose. Probe passes s"in = $in\n" to
printf.
import chisel3._
class Probe extends Module {
val in = IO(Input(UInt(8.W)))
// The s-interpolator would format the Scala object, not the wire value.
printf(s"in = $in\n")
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new Probe))
}Check the result.
| Item | Detail |
|---|---|
| Wrong code | printf(s"in = $in\n") |
| Failing stage | Scala compilation, before any circuit exists; Main never runs |
| Error message | error: The s-interpolator prints the Scala .toString of Data objects, followed by the two alternatives |
| Fix | Use cf" for a per-cycle hardware print. For a one-time message while the module is built, use println with s"..." |
4. How signals get their names
4.1 val names become signal names
Generated FIRRTL, Verilog, and waveforms show your signals by name. The Chisel
compiler plugin, loaded by the -Xplugin flag in every example's command,
names each hardware value after the val it is bound to. Values that are not
bound to a val get generated names that start with an underscore and often
include the connection's target.
Syntax
val name = hardwareExpressionval sum = io.a + io.b
io.y := sum
io.z := io.a - io.b| Source | Names in the generated FIRRTL |
|---|---|
val sum = io.a + io.b | sum, plus _sum_T for the full-width addition inside it |
io.z := io.a - io.b | _io_z_T and _io_z_T_1, derived from the target io.z |
Three unnamed printf calls | printf, printf_1, printf_2 |
In Tracer, the registers are called cycle and last and the wires
window and s, even though no name was ever written as a string.
4.2 suggestName
When an expression is not worth its own val, suggestName attaches a
preferred name to it.
Syntax
expression.suggestName("name")io.w := (io.a ^ io.b).suggestName("mix")Here the XOR node is named io_w_mix. Chisel prefixes names of values
created inside a connection with the target's name, so the suggestion
becomes part of the final name rather than all of it. A val declared on its
own line, such as val mix = io.a ^ io.b, is named exactly mix.
Note. Generated names such as _io_z_T_1 can change when nearby code
changes. If you will search for a signal in a log or waveform, give it a val
name. The waveform chapter shows that only named values appear in a VCD file.
5. Common mistakes
5.1 Using s"..." instead of cf"..."
// Wrong: a compile error
printf(s"in = $in\n")
// Right: the value on every enabled edge
printf(cf"in = $in\n")The s interpolator formats Scala objects during elaboration. Only cf refers
to the hardware value, so Chisel rejects the s form with
The s-interpolator prints the Scala .toString of Data objects.
5.2 Forgetting \n
// Wrong: the two messages run together on one line
printf(cf"a=$a")
printf(cf"b=$b")
// Right
printf(cf"a=$a\n")
printf(cf"b=$b\n")printf writes exactly the characters you give it. Without \n, the next
message, or the testbench's next println, continues on the same line.
5.3 Expecting the value after the edge
count := count + 1.U
printf(cf"count=$count\n") // shows the value before this edge's updateA printf reports values just before the edge. A register printed on the same
edge that updates it shows its old value, so the trace looks one cycle "late".
This is correct behavior. Read the new value on the next line.
5.4 Printing hardware with println
// Wrong: prints a type description once, during elaboration
println(s"sum = $sum")
// Right: prints the value on every edge
printf(cf"sum = $sum\n")A module body runs once. println there sees the Chisel object, not a
simulated value.
5.5 Looking for output during reset
// The testbench holds reset high for two steps
dut.reset.poke(true.B)
dut.clock.step(2) // no printf output from the moduleEvery printf is disabled while reset is high. If a log seems to start late,
count the reset cycles before looking for a bug.
6. Summary
| Topic | Key point |
|---|---|
printf | A hardware node that writes a message at each enabled rising edge |
| Timing | Reports values just before the edge updates registers |
| Enable | Enclosing when conditions and an implicit not-in-reset condition |
cf"..." | $x or ${expr}, optional %d, %x, %b, %c; %% for a percent sign |
| Field width | Fixed per signal width, so columns stay aligned |
| Aggregates | Vec(...) and Class(field -> value) by default; override toPrintable to change |
println | Scala; once per instance during elaboration, or per call in a testbench |
| Naming | val names become signal names; unnamed values get _-prefixed names; suggestName adds a preference |
7. Check your understanding
-
Why does the first
printfline in theprintfexample showlastas 0 even though the testbench has just poked 65?Show answer
A
printfreports values just before the edge. Before the first edge,laststill holds its reset value 0. It takes 65 at that edge, after the message is written. -
What two conditions can stop a
printffrom writing on a given edge?Show answer
An enclosing
whenwhose condition is false, andresetbeing high. -
Why is
printf(s"...")a compile error instead of a message that prints something unhelpful?Show answer
The
sinterpolator would turn the Chisel object into text once, during elaboration, and every cycle would repeat that text. The pattern is almost always a mistake, so Chisel reports it when the Scala code is compiled. -
How wide is the
%dfield for aUInt(4.W), and why does a fixed width help?Show answer
Three columns. Because every line uses the same width for the same signal, values stay in the same columns from cycle to cycle and are easy to compare.
-
After
val total = a + b, what is the result node called in FIRRTL? What names would you expect afterout := a + b?Show answer
total, with the full-width addition inside it named_total_T. Without := a + bthere is noval, so the nodes get generated names derived from the target,_out_Tand_out_T_1.