Skip to content
code0.HDL

Printing from hardware

Print from inside the circuit with printf and the cf interpolator, tell hardware printing from elaboration-time println, and read the signal names the compiler gives your vals.

In this chapter

  • Place a printf in a module and say which cycles it prints on
  • Format hardware values with cf, including Vecs and Bundles
  • Tell a println at elaboration apart from a printf in simulation, and name a signal with suggestName
  • Predict the exact text a cf format prints, including the fixed field width each specifier gives a signal
Concepts in this chapter
  • Printing from hardware
Prerequisite concepts

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")
}
PartMeaning
printf(...)Adds a print node clocked by the module's implicit clock
cf"..."Builds the message from text and hardware values
$countInserts the register's value, in decimal because no specifier follows
${io.in}Braces are needed when the value is a path or an expression
\nEnds 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?

Your answer

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.

  1. The enclosing when blocks. A printf inside when (c) writes only on edges where c is true.
  2. Reset. Chisel adds an implicit "not in reset" condition to every printf. While reset is 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 edgeDoes the printf write?
reset is highNo, for every printf in the module
reset is low, no enclosing whenYes
reset is low, enclosing when condition falseNo
reset is low, enclosing when condition trueYes

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?

Your answer

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 value

2.2 Format specifiers

SpecifierPrints8-bit value 65
%d, or noneDecimal65, right-aligned with spaces
%xHexadecimal, padded with zeros41
%bBinary, right-aligned with spaces1000001 after one space
%cThe value as an ASCII characterA

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, spaces2 digits, zeros8 columns, spaces
UInt(4.W)3 columns, spaces1 digit4 columns, spaces
Bool2 columns, spaces1 digit1 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?

Your answer

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 cf line using four formats, a Vec, and a Bundle;
  • a line inside when (io.in === 0.U);
  • a C-style line that reports io.in and last.
Tracer.scala
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.

Tb.scala
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:

Stepcycleio.inlastwindowExtra line
reset000None: reset disables every printf
10650Vec(65, 0)None
21065Vec(0, 65)zero input
322550Vec(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.

  1. After each TB poke marker, which comes first: the next testbench marker or the circuit's messages?
  2. What does the C-style line print for last on the third step, when the input is 255?
  3. How many zero input lines appear in total?
  4. What does s= show for ok on the first step, given that ok is bit 0 of the input?

Check the result.

OutputWhat it shows
TB reset released with no circuit line before itThe reset step produced no printf output
Circuit lines after each TB poke markerMessages are written inside clock.step(), so they follow the marker in time order
First [0] line: hex=41, bin= 1000001, ok -> 1One value in three formats; 65 is odd, so bit 0 is 1
win=Vec(0, 65) on the cycle 1 linelast holds the previous cycle's input
last=00 on the third C-style lineThe previous input was 0; %x pads to two digits
A single zero input line, at cycle 1The when branch fires only when its condition holds; [cycle] without a specifier still prints decimal

Try it yourself.

  1. 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 default Sample(addr -> ..., ok -> ...) form to a compact one: @41/y for input 65, @00/n for 0, and @ff/y for 255. The override is ordinary hardware: the Mux picks one of two character codes on each cycle, and %c prints the chosen code as a letter.

  2. Move the C-style printf into a new when (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.

StatementRuns duringHow oftenCan show
println(...) in a module bodyElaborationOnce per module instanceScala values, widths, types
println(...) in a testbenchTest executionEach time the line runsValues read with peek
printf(...) in a module bodySimulationEvery enabled rising edgeHardware values

Syntax

println(s"text ${scalaValue}")   // elaboration: once
printf(cf"text $hardware\n")     // simulation: every enabled edge
class 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?

Your answer

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.

Probe.scala
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.

ItemDetail
Wrong codeprintf(s"in = $in\n")
Failing stageScala compilation, before any circuit exists; Main never runs
Error messageerror: The s-interpolator prints the Scala .toString of Data objects, followed by the two alternatives
FixUse 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 = hardwareExpression
val sum = io.a + io.b
io.y := sum
io.z := io.a - io.b
SourceNames in the generated FIRRTL
val sum = io.a + io.bsum, 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 callsprintf, 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 update

A 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 module

Every 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

TopicKey point
printfA hardware node that writes a message at each enabled rising edge
TimingReports values just before the edge updates registers
EnableEnclosing when conditions and an implicit not-in-reset condition
cf"..."$x or ${expr}, optional %d, %x, %b, %c; %% for a percent sign
Field widthFixed per signal width, so columns stay aligned
AggregatesVec(...) and Class(field -> value) by default; override toPrintable to change
printlnScala; once per instance during elaboration, or per call in a testbench
Namingval names become signal names; unnamed values get _-prefixed names; suggestName adds a preference

7. Check your understanding

  1. Why does the first printf line in the printf example show last as 0 even though the testbench has just poked 65?

    Show answer

    A printf reports values just before the edge. Before the first edge, last still holds its reset value 0. It takes 65 at that edge, after the message is written.

  2. What two conditions can stop a printf from writing on a given edge?

    Show answer

    An enclosing when whose condition is false, and reset being high.

  3. Why is printf(s"...") a compile error instead of a message that prints something unhelpful?

    Show answer

    The s interpolator 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.

  4. How wide is the %d field for a UInt(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.

  5. After val total = a + b, what is the result node called in FIRRTL? What names would you expect after out := a + b?

    Show answer

    total, with the full-width addition inside it named _total_T. With out := a + b there is no val, so the nodes get generated names derived from the target, _out_T and _out_T_1.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows