A port list written field by field works for one module. It stops working when
the same group of signals crosses five module boundaries and every change must
be repeated at each one. Chisel treats an interface as a type instead: a
Bundle class that captures the group once, grows by inheritance, and connects
to a matching interface in one statement.
Before you start. You will use Bundle, Flipped, and Output
directions from
Bundles and Vecs
and Module(new ...) instances from
Child modules and generators.
The producer in the ready-valid example is a RegInit counter from
Registers and stepping,
and the chapter adds flow control to the kind of data paths built through
Memories.
Interface classes and bulk connections are entirely an elaboration concern:
<> walks two bundle types in Scala and records one ordinary connection per
leaf, and the generated circuit keeps only flattened port names such as
io_out_valid. Whether a ready-valid transfer happens, by contrast, is decided
during simulation, every cycle, by two wires.
1. Interfaces as Bundle types
1.1 An interface class
An interface is a class that extends Bundle. Each field gets a direction,
Output or Input, read from the point of view of the module that uses the
interface as a port.
Syntax
class InterfaceName extends Bundle {
val fieldName = Output(type)
val fieldName = Input(type)
}class Req extends Bundle {
val addr = Output(UInt(8.W))
val valid = Output(Bool())
}Req is the sender's view of a request, so both fields are outputs.
1.2 Adding fields by inheritance
A subclass of a Bundle class inherits every field and can add new ones.
Syntax
class SubName extends BaseName {
val extraField = Output(type)
}class ReqWithId extends Req {
val id = Output(UInt(2.W))
}ReqWithId has three fields: addr, valid, and id.
Caution. Scala lets you pass a ReqWithId wherever a Req is expected,
but connection operators compare field names, not Scala classes. A Req and
a ReqWithId are not interchangeable on the two sides of <>.
1.3 Reversing directions with Flipped
Flipped(...) inverts the direction of every leaf inside the bundle it wraps,
recursively through nested bundles; two flips cancel. That lets one class,
written from the producer's side, describe the consumer's side as well.
Syntax
val name = Flipped(new InterfaceName)class PassIO extends Bundle {
val in = Flipped(new ReqWithId) // addr, valid, id are all inputs
val out = new ReqWithId // addr, valid, id are all outputs
}| Fields | Direction |
|---|---|
in.addr, in.valid, in.id | Flipped, so all inputs |
out.addr, out.valid, out.id | As declared in the class, so all outputs |
Check
class Resp extends Bundle { val data = Output(UInt(8.W)) } and class Port extends Bundle { val addr = Output(UInt(8.W)); val resp = Flipped(new Resp) }. A module declares io = IO(Flipped(new Port)). Which are inputs of that module?
1.4 A Vec of interfaces
An interface class is a type, so it can be the element of a Vec. The
official documentation describes a crossbar port this way:
class CrossbarIO(n: Int) extends Bundle {
val in = Vec(n, Flipped(new ParityLink))
val sel = Input(UInt(log2Ceil(n).W))
val out = Vec(n, new ParityLink)
}<> walks a Vec element by element. Two Vecs must have the same length;
a 2-element and a 3-element Vec stop elaboration with Left and Right are different length Vecs.
2. Bulk connection with <>
2.1 What <> does
A bulk connection does not move a "bundle value". It walks both bundle types together and makes one connection per leaf.
Syntax
left <> rightclass Pass extends Module {
val io = IO(new PassIO)
io.out <> io.in // three connections: addr, valid, id
}| Step | What <> does |
|---|---|
| 1 | Pairs every field on the left with the field of the same name on the right |
| 2 | Stops elaboration if a name exists on only one side |
| 3 | For each leaf pair, finds which side may be driven: an output of this module or an input of a child |
| 4 | Records one := per leaf in whichever direction is legal |
Step 3 is why <> exists. A ready-valid link has valid and the data flowing
one way and ready flowing back. A plain := always drives left from right,
so it cannot express that link in one statement; <> looks at each leaf and
drives it from whichever end is its source.
2.2 Connection rules
| Rule | Why |
|---|---|
<> needs matching field names on both sides | Chisel cannot tell whether an unmatched field is a forgotten wire or one you meant to drop |
<> needs one legal driver for every leaf | If both ends of a leaf want to drive, there is no direction to choose |
:= is one-way and ignores extra fields on the right | Convenient, but a dropped field produces no warning |
2.3 Example: an interface family connected in a chain
Read Chain.scala from the top.
| Declaration | What it is |
|---|---|
Link | A minimal producer-side interface with data and valid |
ParityLink | Extends Link with a parity field |
StageIO | in is flipped, so all three of its fields are inputs; out's fields are outputs |
AddOne | Passes data + 1 forward and regenerates parity for the new value; clears valid if the incoming parity bit disagrees with the reduction XOR (xorR) of the incoming data |
Chain | Two AddOne stages joined by three <> statements: parent input to first stage, first to second, second to parent output |
import chisel3._
class Link extends Bundle {
val data = Output(UInt(8.W))
val valid = Output(Bool())
}
// A subclass inherits data and valid and adds one more field.
class ParityLink extends Link {
val parity = Output(Bool())
}
// Flipped turns every field of in into an input of the stage.
class StageIO extends Bundle {
val in = Flipped(new ParityLink)
val out = new ParityLink
}
class AddOne extends Module {
val io = IO(new StageIO)
// Drop the word if its parity bit disagrees with the data.
val parityOk = io.in.data.xorR === io.in.parity
val sum = io.in.data + 1.U
io.out.data := sum
io.out.valid := io.in.valid && parityOk
io.out.parity := sum.xorR
}
class Chain extends Module {
val io = IO(new StageIO)
val first = Module(new AddOne)
val second = Module(new AddOne)
first.io.in <> io.in
second.io.in <> first.io.out
io.out <> second.io.out
}Tb.scala prints the field names of a fresh ParityLink during elaboration
on the FIELDS line, then drives three words into the chain: 5 with correct
parity (two ones, so 0), 5 with a wrong parity bit, and 254 with correct parity
(seven ones, so 1).
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
println(s"FIELDS ${(new ParityLink).elements.keys.mkString(",")}")
test(new Chain) { dut =>
for ((value, parity) <- Seq((5, false), (5, true), (254, true))) {
dut.io.in.data.poke(value.U)
dut.io.in.parity.poke(parity.B)
dut.io.in.valid.poke(true.B)
val out = dut.io.out
println(s"CHAIN in=$value parity=${parity.B.litValue} -> out=${out.data.peek().litValue} parity=${out.parity.peek().litValue} valid=${out.valid.peek().litValue}")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run.
- How many fields does the
FIELDSline list, and in what order? - For each input, what are the chain's output
data,parity, andvalid? - Does a word that the first stage marks invalid stay invalid through the second?
Check the result.
| Output line | What it shows |
|---|---|
FIELDS | Three names, parity, valid, data: the subclass field plus both inherited ones, which is the observable proof of inheritance |
in=5 parity=0 | Both checks pass; 5 becomes 6, then 7, whose parity is 1: out=7 parity=1 valid=1 |
in=5 parity=1 | The first stage fails its check and drops valid. The second ANDs its own check with the incoming valid, so the chain reports valid=0; data is still 7 |
in=254 parity=1 | The stages produce 255 and then 0. + keeps 8 bits, so the sum wraps, and the parity of 0 is 0 |
The FIELDS order is reversed from declaration order. Chisel stores bundle
elements that way so that the first-declared field ends up in the most
significant bits when the bundle is packed with asUInt.
Try it yourself.
| Edit | What happens |
|---|---|
Change second.io.in <> first.io.out to second.io.in := first.io.out | Identical output. Every field of this link flows the same way, so := can express it |
Remove Flipped from StageIO.in | first.io.out and second.io.in are now both child outputs, so both ends of every leaf want to drive. Elaboration stops with a ChiselException ending failed @.parity: Both Left and Right are drivers |
Caution. The first edit fails on a ready-valid link. Section 4.3 repeats it there so you can compare.
3. A field with no partner
3.1 Example: two interfaces with different fields
Bulk connection is convenient because it is strict. The next example connects
an input TaggedLink, which adds a 4-bit tag, to an output Link. The
connection is written io.out <> io.in, so io.out is the left side.
This example fails on purpose, during elaboration. <> finds no partner
for tag on the left and raises a ChiselException that names both operands
with their Scala types, the path of the offending field, and which side lacks
it.
import chisel3._
class Link extends Bundle {
val data = Output(UInt(8.W))
val valid = Output(Bool())
}
class TaggedLink extends Link {
val tag = Output(UInt(4.W))
}
class Bridge extends Module {
val io = IO(new Bundle {
val in = Flipped(new TaggedLink)
val out = new Link
})
// <> pairs fields by name; tag has no partner on the left.
io.out <> io.in
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new Bridge))
}Predict before you run. Will the message say the field is missing on the
Left or on the Right? Will any FIRRTL be printed?
Check the result.
| Item | Detail |
|---|---|
| Wrong code | A 2-field Link connected with <> to a 3-field TaggedLink |
| Failing phase | Elaboration. The exception is raised while new Bridge is being built, so no FIRRTL is printed |
| Error message | The operands Bridge.io.out: IO[Link] and Bridge.io.in: IO[TaggedLink], ending failed @.tag: Left Record missing field (tag) |
| Fix | If tag should travel, declare the output as a TaggedLink too; if it should be dropped, connect the shared fields explicitly |
@.tag is the path inside the bundle where the walk stopped, and "Left Record
missing field" says the left operand, the Link, has no tag.
Try it yourself.
| Edit | What happens |
|---|---|
Swap the operands: io.in <> io.out | The same failure, now reported as Right Record missing field (tag) |
Use io.out := io.in | Elaborates. The emitted FIRRTL connects only data and valid; tag is silently dropped |
Use io.out :<>= io.in | Fails earlier, at Scala compile time: Cannot prove that Link =:= TaggedLink |
Declare out as a TaggedLink too | Elaborates; every name now has a partner |
:= accepts the mismatch and loses a signal without warning. :<>= refuses
mismatched Scala types before Chisel even runs.
4. Ready-valid with Decoupled
4.1 The Decoupled interface
For any flow-controlled link, use Decoupled from chisel3.util instead of
inventing your own valid/ready pair. Library components such as Queue
and arbiters speak it, and <> connects it in one statement.
Syntax
val name = Decoupled(dataType) // producer's view
val name = Flipped(Decoupled(dataType)) // consumer's view| Field | Direction in the producer's view | Meaning |
|---|---|---|
valid | output | bits holds a real value this cycle |
bits | output | The data being offered |
ready | input | The consumer can take a value this cycle |
A transfer happens in a cycle exactly when valid and ready are both high.
The fire method computes that condition, valid && ready.
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val seen = Output(UInt(8.W))
})
io.in.ready := true.B // always willing to accept
io.seen := Mux(io.in.fire, io.in.bits, 0.U) // value only in transfer cycles4.2 Queue
Queue is a FIFO with a Decoupled input enq and a Decoupled output
deq. Placed between a producer and a consumer, it decouples their timing
without changing either one.
Syntax
val q = Module(new Queue(dataType, depth))
q.io.enq <> producerOut
consumerIn <> q.io.deq
out <> Queue(producerOut, depth) // functional formio.out <> Queue(io.in, 2)The functional Queue(...) creates a queue instance, connects its enq to the
argument, and returns its deq, the same pattern as a companion apply from
the modules chapter.
Note. A default Queue holds its input ready high only while it is not
full.
4.3 Example: ready-valid through a queue
Producer has a Decoupled(UInt(8.W)) output. It offers 10 + sent while
fewer than five values have been sent, and its counter advances only in
cycles where io.out.fire is true, meaning the consumer actually took the
value. Pipeline places a two-entry Queue between the producer and its own
Decoupled output. Each of the two <> lines connects three leaves: valid
and bits flow toward the consumer, ready flows back. Two extra outputs
expose whether the queue accepted a value this cycle and how many entries it
holds.
import chisel3._
import chisel3.util._
class Producer extends Module {
val io = IO(new Bundle {
val out = Decoupled(UInt(8.W))
})
val sent = RegInit(0.U(3.W))
// Offer five values; advance only when the consumer took one.
io.out.valid := sent < 5.U
io.out.bits := 10.U + sent
when(io.out.fire) {
sent := sent + 1.U
}
}
class Pipeline extends Module {
val io = IO(new Bundle {
val out = Decoupled(UInt(8.W))
val enqFire = Output(Bool())
val count = Output(UInt(2.W))
})
val producer = Module(new Producer)
val queue = Module(new Queue(UInt(8.W), 2))
// One <> per link connects valid, bits, and ready in their own directions.
queue.io.enq <> producer.io.out
io.out <> queue.io.deq
io.enqFire := queue.io.enq.fire
io.count := queue.io.count
}Tb.scala plays the consumer. It holds ready low for cycles 0–2 and high
from cycle 3 on. Before each clock edge it prints ready, whether the queue's
input fired, the occupancy, and the value transferred at the output, or -
when nothing transferred.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Pipeline) { dut =>
for (cycle <- 0 until 9) {
// The testbench is the consumer: it refuses data for three cycles.
val ready = cycle >= 3
dut.io.out.ready.poke(ready.B)
val enq = dut.io.enqFire.peek().litToBoolean
val valid = dut.io.out.valid.peek().litToBoolean
val bits = dut.io.out.bits.peek().litValue
val deq = if (valid && ready) s"$bits" else "-"
println(s"cycle=$cycle ready=${ready.B.litValue} enq=${enq.B.litValue} count=${dut.io.count.peek().litValue} deq=$deq")
dut.clock.step()
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run.
- In which cycle does the producer first fail to hand off a value?
- In cycle 3, when the consumer starts reading, does the queue's input also fire?
- In which cycle does the last value, 14, leave the pipeline?
Check the result. The trace follows from two facts: a transfer happens
when valid and ready are both high in the same cycle, and a default
Queue holds its input ready high only while it is not full.
| Cycle | Consumer ready | Queue input | Count | Output |
|---|---|---|---|---|
| 0 | 0 | 10 accepted | 0 | none |
| 1 | 0 | 11 accepted | 1 | none |
| 2 | 0 | full, producer holds 12 | 2 | none |
| 3 | 1 | still full at the start of the cycle | 2 | 10 |
| 4–6 | 1 | 12, 13, 14 accepted | 1 | 11, 12, 13 |
| 7 | 1 | producer done | 1 | 14 |
| 8 | 1 | none | 0 | none |
Cycle 2 is backpressure: the producer's valid is high, but the queue drops
ready, so fire is false and sent does not advance. The value 12 stays on
bits without being lost or duplicated. Cycle 3 shows that a queue without
the pipe option does not accept and release in the same cycle while full;
the slot freed by the dequeue becomes visible one cycle later. From cycle 4 on,
one value enters and one leaves each cycle.
Try it yourself.
| Edit | What happens |
|---|---|
Replace queue.io.enq <> producer.io.out with := | Elaboration fails with a ChiselException reporting that io.enq.ready in Queue2_UInt8 cannot be written from module Producer: := tried to drive every leaf right to left, including ready, which flows the other way |
Replace both <> with :<>= | The trace is identical: for two DecoupledIO of the same type, valid/bits connect forward and ready backward exactly as with <> |
Construct the queue as new Queue(UInt(8.W), 2, pipe = true) | A pipelined queue may accept while full if the output drains in the same cycle. In cycle 3 the queue input fires too, the producer finishes one cycle sooner, and the count stays at 2. The output column does not change: 10 through 14 still leave in cycles 3 to 7 |
Change the producer's when(io.out.fire) to when(io.out.valid) | The counter also advances in cycles 2 and 3, while the full queue refuses the offer, so 12 and 13 are skipped and the consumer receives only 10, 11, and 14 |
Note. Decoupled makes no promise that valid stays high or that bits
stays stable until a transfer happens. IrrevocableIO follows the convention
that once valid is raised, bits holds until the transfer. That promise is a
convention only; Chisel does not generate checks for it.
Check
Change Tb.scala so the consumer holds ready high from cycle 0. In which cycle does the last value, 14, leave the pipeline?
5. Choosing a connection operator
The Connectable explanation recommends a family of operators for new code,
and each symbol carries meaning: : marks the consumer (left), = the
producer (right), < leaves driven right to left, > leaves driven left to
right, and # means "ignore directions".
| Operator | Drives | Use when |
|---|---|---|
c := p | Every leaf of c from p, flipped ones included; fails when such a leaf cannot be driven from this side, as with a child's ready | Plain values, all-one-direction bundles |
c <> p | Each leaf from whichever side is its source | Older code; connecting ports of modules |
c :<>= p | Aligned leaves of c from p, flipped leaves of p from c | Ready-valid and other mixed-direction links in new code |
c :<= p / c :>= p | Only the aligned / only the flipped leaves | Splitting a mixed link into its two halves |
c :#= p | Every leaf of c from p, ignoring flips | Monitoring a link, copying a whole interface into a wire |
:<>= also requires both sides to have the same structure, checked partly at
Scala compile time, which is why the mismatch variation in section 3 failed
before elaboration even started. The Connectable page also describes
waivers and squeezing for deliberately mismatched interfaces; this track does
not need them.
Check
A monitor wants a Wire holding every leaf of a Decoupled port it observes, including ready, all driven from the port. Which operator copies the whole interface?
6. Common mistakes
6.1 Connecting a ready-valid link with :=
// Wrong: tries to drive ready right to left as well
queue.io.enq := producer.io.out
// Right: each leaf connected in its own direction
queue.io.enq <> producer.io.out:= drives every leaf one way. ready flows from consumer to producer, so
elaboration stops with ... cannot be written from module Producer.
6.2 Forgetting Flipped on the consumer side
// Wrong: in's fields are outputs too
class StageIO extends Bundle {
val in = new ParityLink
val out = new ParityLink
}
// Right: in is the consumer's view
class StageIO extends Bundle {
val in = Flipped(new ParityLink)
val out = new ParityLink
}Without Flipped, both ends of the link between two stages are child
outputs. <> has no direction to choose and reports Both Left and Right are drivers.
6.3 Using := between interfaces with different fields
// Wrong: tag is silently dropped
io.out := io.in // out: Link, in: TaggedLink
// Right: give the output the tag too
val out = new TaggedLink
io.out <> io.in:= ignores fields that exist only on the right. There is no error and no
warning, and tag never arrives downstream. When a field goes missing, check
the connection operator first.
6.4 Advancing on valid instead of fire
// Wrong: advances even when the consumer did not take the value
when(io.out.valid) { sent := sent + 1.U }
// Right: advance only in transfer cycles
when(io.out.fire) { sent := sent + 1.U }valid only means the producer is offering a value. Move to the next value in
a cycle where the consumer's ready is low and the offered value is lost.
7. Summary
| Topic | Key point |
|---|---|
| Interface | A type defined as a Bundle class; inheritance adds fields |
Flipped | Recursively inverts every leaf of the wrapped bundle, so one producer-side class also describes the consumer side |
<> | Pairs same-named fields and picks a driver per leaf; mismatched names are an elaboration error |
:= | Drives one way only and silently drops extra right-side fields |
Decoupled | A bundle of valid, bits, and ready; a transfer happens exactly when valid and ready are both high, which fire computes |
Queue | Sits between Decoupled links to decouple timing; a default queue drops its input ready when full |
:<>= | The recommended mixed-direction connection for new code; catches type mismatches at Scala compile time |
8. Check your understanding
-
Why does the
FIELDSline listparityfirst even thoughParityLinkdeclares it last?Show answer
Chisel stores bundle elements in reverse declaration order, so that the first-declared field lands in the most significant bits when the bundle is packed with
asUInt. -
In the chain, why does a wrong parity bit on the input clear
validat the output of the second stage?Show answer
The first stage fails its check and sends
validlow. The second stage ANDs its own check with the incomingvalid, which is already 0, so its outputvalidis 0 as well. -
Why can
:=connect two stages of the chain but not aDecoupledproducer to a queue?Show answer
Every field of the chain link flows the same way, so a one-way connection is enough. A
Decoupledlink'sreadyflows the other way, and:=tries to drive that leaf right to left too, which is an elaboration error. -
In the queue trace, what single fact explains why nothing enters the queue in cycle 3 even though the consumer starts reading?
Show answer
At the start of cycle 3 the queue is full, and a default queue holds its input
readyhigh only while it is not full. The slot freed by cycle 3's dequeue reaches the input side one cycle later. -
Which operator would have caught the
tagmismatch at Scala compile time, and which would have let it through silently?Show answer
:<>=catches it with the compile errorCannot prove that Link =:= TaggedLink.:=elaborates and dropstagwithout a warning.