Skip to content
code0.HDL

Interfaces and connections

Describe an interface as a Bundle, connect two of them in bulk with <>, see what happens to a field with no partner, and run a ready-valid link with Decoupled.

In this chapter

  • Write an interface as a Bundle and connect two instances in bulk
  • Predict the elaboration error a field with no partner produces
  • Drive and read a Decoupled link so both ends agree on every transfer
  • Work out each leaf's direction through Flipped, including a flip inside a flipped bundle
  • Choose between :=, <>, :<>= and :#= from the directions of a link's leaves
Concepts in this chapter
  • Interfaces and connections
Prerequisite concepts

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
}
FieldsDirection
in.addr, in.valid, in.idFlipped, so all inputs
out.addr, out.valid, out.idAs 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?

Your answer

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 <> right
class Pass extends Module {
  val io = IO(new PassIO)
  io.out <> io.in   // three connections: addr, valid, id
}
StepWhat <> does
1Pairs every field on the left with the field of the same name on the right
2Stops elaboration if a name exists on only one side
3For each leaf pair, finds which side may be driven: an output of this module or an input of a child
4Records 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

RuleWhy
<> needs matching field names on both sidesChisel cannot tell whether an unmatched field is a forgotten wire or one you meant to drop
<> needs one legal driver for every leafIf both ends of a leaf want to drive, there is no direction to choose
:= is one-way and ignores extra fields on the rightConvenient, but a dropped field produces no warning

2.3 Example: an interface family connected in a chain

Read Chain.scala from the top.

DeclarationWhat it is
LinkA minimal producer-side interface with data and valid
ParityLinkExtends Link with a parity field
StageIOin is flipped, so all three of its fields are inputs; out's fields are outputs
AddOnePasses 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
ChainTwo AddOne stages joined by three <> statements: parent input to first stage, first to second, second to parent output
Chain.scala
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).

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

  1. How many fields does the FIELDS line list, and in what order?
  2. For each input, what are the chain's output data, parity, and valid?
  3. Does a word that the first stage marks invalid stay invalid through the second?

Check the result.

Output lineWhat it shows
FIELDSThree names, parity, valid, data: the subclass field plus both inherited ones, which is the observable proof of inheritance
in=5 parity=0Both checks pass; 5 becomes 6, then 7, whose parity is 1: out=7 parity=1 valid=1
in=5 parity=1The 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=1The 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.

EditWhat happens
Change second.io.in <> first.io.out to second.io.in := first.io.outIdentical output. Every field of this link flows the same way, so := can express it
Remove Flipped from StageIO.infirst.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.

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

ItemDetail
Wrong codeA 2-field Link connected with <> to a 3-field TaggedLink
Failing phaseElaboration. The exception is raised while new Bridge is being built, so no FIRRTL is printed
Error messageThe operands Bridge.io.out: IO[Link] and Bridge.io.in: IO[TaggedLink], ending failed @.tag: Left Record missing field (tag)
FixIf 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.

EditWhat happens
Swap the operands: io.in <> io.outThe same failure, now reported as Right Record missing field (tag)
Use io.out := io.inElaborates. The emitted FIRRTL connects only data and valid; tag is silently dropped
Use io.out :<>= io.inFails earlier, at Scala compile time: Cannot prove that Link =:= TaggedLink
Declare out as a TaggedLink tooElaborates; 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
FieldDirection in the producer's viewMeaning
validoutputbits holds a real value this cycle
bitsoutputThe data being offered
readyinputThe 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 cycles

4.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 form
io.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.

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

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

  1. In which cycle does the producer first fail to hand off a value?
  2. In cycle 3, when the consumer starts reading, does the queue's input also fire?
  3. 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.

CycleConsumer readyQueue inputCountOutput
0010 accepted0none
1011 accepted1none
20full, producer holds 122none
31still full at the start of the cycle210
4–6112, 13, 14 accepted111, 12, 13
71producer done114
81none0none

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.

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

Your answer

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".

OperatorDrivesUse when
c := pEvery leaf of c from p, flipped ones included; fails when such a leaf cannot be driven from this side, as with a child's readyPlain values, all-one-direction bundles
c <> pEach leaf from whichever side is its sourceOlder code; connecting ports of modules
c :<>= pAligned leaves of c from p, flipped leaves of p from cReady-valid and other mixed-direction links in new code
c :<= p / c :>= pOnly the aligned / only the flipped leavesSplitting a mixed link into its two halves
c :#= pEvery leaf of c from p, ignoring flipsMonitoring 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?

Your answer

6. Common mistakes

// 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

TopicKey point
InterfaceA type defined as a Bundle class; inheritance adds fields
FlippedRecursively 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
DecoupledA bundle of valid, bits, and ready; a transfer happens exactly when valid and ready are both high, which fire computes
QueueSits 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

  1. Why does the FIELDS line list parity first even though ParityLink declares 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.

  2. In the chain, why does a wrong parity bit on the input clear valid at the output of the second stage?

    Show answer

    The first stage fails its check and sends valid low. The second stage ANDs its own check with the incoming valid, which is already 0, so its output valid is 0 as well.

  3. Why can := connect two stages of the chain but not a Decoupled producer to a queue?

    Show answer

    Every field of the chain link flows the same way, so a one-way connection is enough. A Decoupled link's ready flows the other way, and := tries to drive that leaf right to left too, which is an elaboration error.

  4. 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 ready high only while it is not full. The slot freed by cycle 3's dequeue reaches the input side one cycle later.

  5. Which operator would have caught the tag mismatch at Scala compile time, and which would have let it through silently?

    Show answer

    :<>= catches it with the compile error Cannot prove that Link =:= TaggedLink. := elaborates and drops tag without a warning.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows