1. Aggregates
Real interfaces are rarely a single number. A pixel has three color channels, a register file has
many entries, and a handshake carries data one way and an acknowledgment the other. Chisel's
aggregate types, Bundle and Vec, describe such groups as one value, so you can declare, connect,
and pack them as a unit. An aggregate type is a Scala class that exists during elaboration. Like
UInt(8.W), it becomes hardware only inside IO, Wire, or a register. You will use the ground
types, widths, and asUInt from Widths, SInt, and
arithmetic, Cat from Bool, UInt, and the
operators, and := and Wire from Mux, when, Wire, and
a default for every path.
Every aggregate is still a bit vector. Bundle and Vec give names or positions to slices of it.
| Aggregate | Elements | Access | Packing with asUInt |
|---|---|---|---|
Bundle | Named fields, each with its own type | b.field, fixed during elaboration | First declared field in the most significant bits |
Vec(n, t) | n elements of the same type t | v(i) with a Scala Int, or v(idx) with a hardware UInt | Element 0 in the least significant bits |
Bundle, Vec, UInt, SInt, and Bool are all subclasses of Data, so any construct that
accepts a Data accepts an aggregate. Aggregates nest freely: a Bundle may hold a Vec, and a
Vec may hold Bundles.
2. Bundle
2.1 Defining a Bundle
A Bundle is a Scala class that extends Bundle. Each field is a val holding a Chisel type.
Form
class BundleName extends Bundle {
val field1 = ChiselType
val field2 = ChiselType
}class Header extends Bundle {
val valid = Bool() // 1 bit
val tag = UInt(3.W) // 3 bits
val len = UInt(4.W) // 4 bits
}| Code | Meaning |
|---|---|
class Header extends Bundle | A new aggregate type named Header |
val valid = Bool() | A field named valid of type Bool |
| Field order | The order of the vals decides the bit layout, as section 2.3 shows |
Header is 1 + 3 + 4 = 8 bits wide. Like UInt(8.W), the class describes a type; nothing exists in
the circuit until you bind it.
Note. Since Chisel 3.5, the compiler plugin generates the cloning code that parameterized bundles
need. Do not write cloneType yourself.
2.2 Using a Bundle as a port or wire
Create a Bundle with new, then use it anywhere a Chisel type is expected. Built-in types such as
UInt do not need new, because their companion objects provide apply; a class you define does.
Form
Input(new BundleName)
Output(new BundleName)
Wire(new BundleName)val io = IO(new Bundle {
val hdr = Input(new Header)
val bits = Output(UInt(8.W))
})
val h = Wire(new Header)
h.valid := true.B
h.tag := 5.U
h.len := 2.UField access such as io.hdr.tag or h.len is plain Scala member access and adds no logic.
Input(new Header) makes every field of hdr an input, so a testbench pokes the fields one by one.
2.3 Packing and unpacking
asUInt concatenates the fields in declaration order, first field on top, exactly like Cat.
asTypeOf reads a flat value back through the same layout.
Form
bundleValue.asUInt // Bundle to bits
bitsValue.asTypeOf(new BundleName) // bits to Bundleh.asUInt // valid=1, tag=5, len=2: 1_101_0010 = 0xd2
"hd2".U.asTypeOf(new Header).tag // 5| Field | Bits in the packed value | Value in 0xd2 |
|---|---|---|
valid, declared first | 7 | 1 |
tag | 6–4 | 101 = 5 |
len, declared last | 3–0 | 0010 = 2 |
Caution. Neither conversion checks meaning. If you reorder the fields, every packed value changes and nothing warns you. When a bundle describes an external format, such as a register map or a packet header, declare the fields in the documented order.
3. Example: a Bundle and its bit layout
Pixel is an RGB565 pixel: a 5-bit red, 6-bit green, and 5-bit blue field, declared in that order.
PixelPack takes a Pixel input in and packs it into packed. It also takes a flat 16-bit input
raw and extracts the green field with asTypeOf; io.in.g and the .g of the unpacked value
reach one field with no logic. The println reports the total width of the bundle during
elaboration.
import chisel3._
class Pixel extends Bundle {
val r = UInt(5.W)
val g = UInt(6.W)
val b = UInt(5.W)
}
class PixelPack extends Module {
val io = IO(new Bundle {
val in = Input(new Pixel)
val packed = Output(UInt(16.W))
val raw = Input(UInt(16.W))
val green = Output(UInt(6.W))
})
println(s"WIDTH Pixel=${(new Pixel).getWidth}")
// The first declared field lands in the most significant bits.
io.packed := io.in.asUInt
// asTypeOf reads the same bits back through the Bundle layout.
io.green := io.raw.asTypeOf(new Pixel).g
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new PixelPack) { dut =>
for ((r, g, b) <- Seq((31, 0, 0), (0, 63, 0), (0, 0, 31))) {
dut.io.in.r.poke(r.U)
dut.io.in.g.poke(g.U)
dut.io.in.b.poke(b.U)
println(f"PACK r=$r g=$g b=$b packed=${dut.io.packed.peek().litValue}%04x")
}
for (raw <- Seq(0x07e0, 0x1234)) {
dut.io.raw.poke(raw.U)
println(f"UNPACK raw=$raw%04x green=${dut.io.green.peek().litValue}")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run.
- What width does the
WIDTHline report? - What are the three
packedvalues for a pure red, pure green, and pure blue pixel, each field at its maximum? - What
greenvalue comes out of0x1234?
Check
For r = 31, g = 0, b = 0, what is packed in hex?
Check the result.
| Field | Bits | packed for the maximum pixel |
|---|---|---|
r, declared first | 15–11 | f800 |
g | 10–5 | 07e0 |
b, declared last | 4–0 | 001f |
| Output line | What it shows |
|---|---|
WIDTH | 5 + 6 + 5 = 16 bits |
PACK lines | The table above: the first field sits on top |
UNPACK raw=07e0 | All green bits are 1, so green is 63 |
UNPACK raw=1234 | 0001_0010_0011_0100 has 010001 in bits 10–5, which is 17 |
Try it yourself.
- Reorder the fields to
b,g,rand change nothing else. The red pixel now packs to001fand the blue pixel tof800. Green stays07e0, and bothUNPACKrows are unchanged, because green is still the middle six bits. Nothing fails to compile: the layout comes from the declaration alone. - In
Tb.scala, poker = 1,g = 1, andb = 1in one row. Before running, place each 1 at the lowest bit of its field: bits 11, 5, and 0.packedshows0821.
4. Vec
4.1 Vec types
A Vec holds n elements of one type.
Form
Vec(count, elementType)val io = IO(new Bundle {
val regs = Input(Vec(4, UInt(8.W))) // four 8-bit values, 32 bits in total
val flags = Output(Vec(8, Bool())) // eight separate Bool outputs
})All elements share one type and width. A Vec of bundles is legal too: a Vec of two 16-bit pixel
bundles is 32 bits wide. For the rare case of elements with different widths, chisel3.util.MixedVec
exists.
4.2 VecInit: a Vec of hardware values
Vec(n, t) is a type. VecInit builds a Vec hardware value from existing hardware values, which
is how you write a constant lookup table.
Form
VecInit(value0, value1, ...)
VecInit(scalaSeqOfHardware)
VecInit.fill(n)(value)val pair = VecInit(1.U(4.W), 2.U(4.W))
pair.asUInt // 0x21: element 0 is the low nibble
val fives = VecInit.fill(3)(5.U(4.W))
fives.asUInt // 0x555
val table = VecInit(Seq(3, 1, 4).map(_.U(8.W)))VecInit(Seq(...).map(_.U(8.W))) turns a Scala list of Ints into a hardware table during
elaboration. The packing order is the opposite of a Bundle's: element 0 lands in the least
significant bits, like index 0 at the lowest address of an array.
4.3 Scala indices and hardware indices
The kind of index decides what gets built.
| Index | Decided | Builds | Index out of range |
|---|---|---|---|
Scala Int, as in v(2) | During elaboration | A wire to one element | Elaboration exception |
Hardware UInt, as in v(sel) | During simulation | A multiplexer over all elements | Only the low bits are used |
io.out0 := io.regs(0) // wiring only
io.outS := io.regs(io.sel) // a 4-way multiplexer; sel should be 2 bitsFor n > 1, use log2Ceil(n) bits for a hardware index: round log2(n) up to an
integer. A wider index produces a warning and is truncated to that width. If n is not a power of
two, some remaining bit patterns still name no element; keep the index below n.
5. Example: Vec, VecInit, and indexing
VecPick has a Vec input table of four 8-bit entries and a 2-bit selector sel. Read the four
outputs in order.
| Output | How it is built |
|---|---|
picked | table indexed with the hardware sel |
mask | A constant VecInit table indexed with the same sel |
third | table indexed with the Scala literal 2 |
packed | The whole table flattened with asUInt |
import chisel3._
class VecPick extends Module {
val io = IO(new Bundle {
val table = Input(Vec(4, UInt(8.W)))
val sel = Input(UInt(2.W))
val picked = Output(UInt(8.W))
val mask = Output(UInt(8.W))
val third = Output(UInt(8.W))
val packed = Output(UInt(32.W))
})
// A hardware index builds a multiplexer over all four elements.
io.picked := io.table(io.sel)
// VecInit turns Scala-side constants into a hardware lookup table.
val masks = VecInit(Seq(0x01, 0x02, 0x04, 0x80).map(_.U(8.W)))
io.mask := masks(io.sel)
// A Scala Int index selects one element during elaboration.
io.third := io.table(2)
// Element 0 lands in the least significant byte.
io.packed := io.table.asUInt
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new VecPick) { dut =>
for ((v, i) <- Seq(0x11, 0x22, 0x33, 0x44).zipWithIndex) {
dut.io.table(i).poke(v.U)
}
for (sel <- 0 until 4) {
dut.io.sel.poke(sel.U)
val picked = dut.io.picked.peek().litValue
val mask = dut.io.mask.peek().litValue
println(f"SELECT sel=$sel picked=$picked%02x mask=$mask%02x")
}
println(f"FIXED third=${dut.io.third.peek().litValue}%02x packed=${dut.io.packed.peek().litValue}%08x")
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
The testbench writes 0x11, 0x22, 0x33, and 0x44 into entries 0 to 3 and sweeps sel from 0
to 3.
Predict before you run.
- What are
pickedandmaskfor eachsel? - Does
thirdchange asselchanges? - Is
packed11223344or44332211?
Check
The table holds 0x11, 0x22, 0x33, 0x44 in entries 0 to 3. What does packed show?
Check the result.
| Output line | What it shows |
|---|---|
SELECT lines, picked | 11, 22, 33, 44: the multiplexer follows sel |
SELECT lines, mask | 01, 02, 04, 80: the constant table built from four Scala Ints |
FIXED, third | Always 33. Index 2 was resolved during elaboration; it is a wire, not a multiplexer |
FIXED, packed | 44332211. Element 0 is the low byte, the reverse of a bundle's first field |
Try it yourself.
- Widen
seltoUInt(3.W)and poke 5. Elaboration prints warningW004, Dynamic index with width 3 is too wide for Vec of size 4 (expected index width 2), once for each dynamic index, and the run continues. Withsel = 5,pickedshows22andmaskshows02: only the low two bits of101were used, selecting element 1. A wider selector does not give a range error; it aliases. - Change
io.table(2)toio.table(4). A Scala index is checked while the circuit is built, and elaboration stops withIndexOutOfBoundsException: 4 is out of bounds (min 0, max 3).
6. Port directions and Flipped
6.1 Directions on fields
Input and Output can wrap a whole type, or individual fields inside a Bundle. Field directions
make one bundle describe a two-way interface.
Form
class InterfaceName extends Bundle {
val field1 = Output(ChiselType)
val field2 = Input(ChiselType)
}class Req extends Bundle {
val addr = Output(UInt(8.W)) // from the requester
val ack = Input(Bool()) // back to the requester
}The directions are written from the point of view of one side, here the requester. They take effect
when IO(...) binds the bundle to a module boundary.
6.2 Flipped
Flipped(t) swaps every direction inside t, recursively. One declaration then serves both ends of
a connection.
Form
IO(new InterfaceName) // directions as declared
IO(Flipped(new InterfaceName)) // every direction swappedval toMem = IO(new Req) // addr: Output, ack: Input
val fromCpu = IO(Flipped(new Req)) // addr: Input, ack: Output| Port | addr | ack |
|---|---|---|
IO(new Req) | Output | Input |
IO(Flipped(new Req)) | Input | Output |
6.3 What a module may drive
A module drives its outputs and reads its inputs, never the reverse. Connecting to an input from
inside the module fails elaboration with a cannot be written from module exception. Because
Flipped changes which fields are inputs, it also changes which connections are legal.
7. Example: Flipped and port directions
Link describes one side of a simple handshake: data flows out and ready flows back in. Stage
uses it twice: down, as declared, faces the next stage; up, as Flipped(new Link), faces the
previous stage. A println loop asks DataMirror.directionOf for each field's actual direction. The
logic passes data forward incremented by one and passes ready backward.
import chisel3._
import chisel3.reflect.DataMirror
class Link extends Bundle {
val data = Output(UInt(8.W))
val ready = Input(Bool())
}
class Stage extends Module {
// Flipped turns every Output in Link into an Input and the reverse.
val up = IO(Flipped(new Link))
val down = IO(new Link)
for ((name, port) <- Seq("up.data" -> up.data, "up.ready" -> up.ready,
"down.data" -> down.data, "down.ready" -> down.ready)) {
println(s"DIRECTION $name = ${DataMirror.directionOf(port)}")
}
down.data := up.data + 1.U
up.ready := down.ready
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Stage) { dut =>
for ((data, ready) <- Seq((41, true), (255, false))) {
dut.up.data.poke(data.U)
dut.down.ready.poke(ready.B)
val out = dut.down.data.peek().litValue
val back = dut.up.ready.peek().litValue
println(s"SIMULATE up.data=$data down.ready=$ready down.data=$out up.ready=$back")
}
}
}Predict before you run.
- What are the four
DIRECTIONlines? - The testbench can poke only inputs. Which two fields does it poke?
- For
up.data = 255, what doesdown.datashow?
Check
The testbench can poke only inputs of Stage. Which two fields does it poke?
Check the result.
| Field | Direction | Testbench action |
|---|---|---|
up.data | Input, flipped | poke |
up.ready | Output, flipped | peek |
down.data | Output | peek |
down.ready | Input | poke |
The first SIMULATE row sends 41 forward and receives 42 on down.data, and ready passes back as
- The second row pokes 255, and
down.datashows 0: the+inup.data + 1.Uwraps in 8 bits, the rule from the width chapter. OneLinkdeclaration served both ends; the receiving side did not need a second bundle with the directions written backwards.
Try it yourself.
- Remove
Flippedfromup. Both ports now havedataas an output andreadyas an input. Elaboration stops atup.ready := down.readywith aChiselExceptionsaying up.ready in Stage cannot be written from module Stage:up.readyhas become an input, and a module cannot drive its own inputs. - Keep
Flippedand addup.data := 0.U. The same exception now namesup.data. Directions are enforced per field, so the message points at exactly the field whose direction forbids the connection.
8. Common mistakes
8.1 Forgetting new for a user-defined Bundle
// Wrong: Pixel is a class, not a value
val in = Input(Pixel)
// Right
val in = Input(new Pixel)The Scala compiler reports not found: value Pixel. Built-in types such as UInt(8.W) need no
new; your own bundles do.
8.2 Declaring fields in the wrong order
// Wrong for a format whose top two bits are kind: payload lands on top
class Packet extends Bundle {
val payload = UInt(6.W)
val kind = UInt(2.W)
}
// Right: declare from the most significant field down
class Packet extends Bundle {
val kind = UInt(2.W)
val payload = UInt(6.W)
}Both versions compile, and both are 8 bits wide. Only the packed values differ, so the mistake shows up as wrong data at the other end of a link.
8.3 Expecting a Vec to pack like a Bundle
VecInit(1.U(4.W), 2.U(4.W)).asUInt // 0x21: element 0 at the bottom
Cat(1.U(4.W), 2.U(4.W)) // 0x12: first argument on topA Vec puts element 0 in the low bits; a Bundle and Cat put the first item in the high bits.
When a packed value looks byte-reversed, check which of the two you used.
8.4 Using a selector that is too wide
// Wrong: 3 bits for a 4-element Vec; index 5 silently selects element 1
val sel = Input(UInt(3.W))
// Right: log2(4) = 2 bits
val sel = Input(UInt(2.W))Elaboration prints warning W004 but continues. Treat that warning as a bug.
8.5 Missing Flipped on the receiving side
// Wrong: up.ready is an input, so the module cannot drive it
val up = IO(new Link)
up.ready := down.ready
// Right
val up = IO(Flipped(new Link))
up.ready := down.readyThe elaboration exception says cannot be written from module and names the field. Check whether that port should be flipped.
9. Summary
| Topic | Key point |
|---|---|
Bundle | A Scala class with named fields of any Chisel type; create it with new |
| Bundle layout | asUInt puts the first declared field on top; asTypeOf reads bits back |
Vec(n, t) | n elements of one type; element 0 packs into the low bits |
VecInit | Builds a Vec hardware value, such as a constant table |
| Indices | A Scala Int index is wiring decided during elaboration; a hardware index builds a multiplexer |
| Directions | Input/Output on fields make a two-way interface; Flipped swaps them all |
| Driving | A module drives outputs and reads inputs; writing an input is an elaboration error |
10. Check your understanding
-
In
Pixel, why does the pure blue pixel pack to001fwhile the pure red one packs tof800? What single source change swaps them?Show answer
ris declared first, so it occupies the top five bits;bis declared last and occupies the bottom five. Reversing the declaration order ofrandbswaps the two packed values. -
What 8-bit value does
VecInit(1.U(4.W), 2.U(4.W)).asUIntproduce?Show answer
0x21. Element 0, the value 1, is the low nibble, and element 1, the value 2, is the high nibble. -
Why does
io.table(2)add no multiplexer whileio.table(io.sel)does?Show answer
2 is a Scala
Int, so the element is chosen during elaboration and the result is a plain wire.io.selis hardware whose value changes during simulation, so the circuit needs a multiplexer over all elements. -
A module declares
val in = IO(Flipped(new Link)). Which fields ofinmay the module drive?Show answer
Only
in.ready.Flippedturnsdatainto an input andreadyinto an output, and a module may drive only its outputs. -
What is the width of
Vec(3, new Header), whereHeaderis the 8-bit bundle from section 2.1?Show answer
24 bits: three elements of 8 bits each.
The two Practice problems below work with bit layouts. The one below the bundle example packs and
unpacks a 16-bit status word with a documented field layout; both printed rows disagree with that
layout. The one below vec should reverse the byte order of a 32-bit word, but the starter returns
every word unchanged.