Skip to content
code0.HDL

Bundles and Vecs

Group named fields in a Bundle and indexed elements in a Vec, read the bit layout that asUInt and asTypeOf follow, tell a Scala index from a hardware index, and use Flipped to put one interface declaration on both ends of a link.

In this chapter

  • Declare a Bundle and use it, with new, as a port or wire type
  • Predict the packed value of a Bundle or a Vec from its declaration order and read bits back with asTypeOf
  • Tell what a Scala Int index and a hardware UInt index each build, and size a selector to log2Ceil(n) bits for n > 1
  • Put directions on Bundle fields, swap them with Flipped, and say which fields a module may drive
Concepts in this chapter
  • Bundles and Vecs
Prerequisite concepts

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.

AggregateElementsAccessPacking with asUInt
BundleNamed fields, each with its own typeb.field, fixed during elaborationFirst declared field in the most significant bits
Vec(n, t)n elements of the same type tv(i) with a Scala Int, or v(idx) with a hardware UIntElement 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
}
CodeMeaning
class Header extends BundleA new aggregate type named Header
val valid = Bool()A field named valid of type Bool
Field orderThe 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.U

Field 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 Bundle
h.asUInt                           // valid=1, tag=5, len=2: 1_101_0010 = 0xd2
"hd2".U.asTypeOf(new Header).tag   // 5
FieldBits in the packed valueValue in 0xd2
valid, declared first71
tag6–4101 = 5
len, declared last3–00010 = 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.

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

  1. What width does the WIDTH line report?
  2. What are the three packed values for a pure red, pure green, and pure blue pixel, each field at its maximum?
  3. What green value comes out of 0x1234?

Check

For r = 31, g = 0, b = 0, what is packed in hex?

Check the result.

FieldBitspacked for the maximum pixel
r, declared first15–11f800
g10–507e0
b, declared last4–0001f
Output lineWhat it shows
WIDTH5 + 6 + 5 = 16 bits
PACK linesThe table above: the first field sits on top
UNPACK raw=07e0All green bits are 1, so green is 63
UNPACK raw=12340001_0010_0011_0100 has 010001 in bits 10–5, which is 17

Try it yourself.

  1. Reorder the fields to b, g, r and change nothing else. The red pixel now packs to 001f and the blue pixel to f800. Green stays 07e0, and both UNPACK rows are unchanged, because green is still the middle six bits. Nothing fails to compile: the layout comes from the declaration alone.
  2. In Tb.scala, poke r = 1, g = 1, and b = 1 in one row. Before running, place each 1 at the lowest bit of its field: bits 11, 5, and 0. packed shows 0821.

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.

IndexDecidedBuildsIndex out of range
Scala Int, as in v(2)During elaborationA wire to one elementElaboration exception
Hardware UInt, as in v(sel)During simulationA multiplexer over all elementsOnly 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 bits

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

OutputHow it is built
pickedtable indexed with the hardware sel
maskA constant VecInit table indexed with the same sel
thirdtable indexed with the Scala literal 2
packedThe whole table flattened with asUInt
VecPick.scala
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
}
Tb.scala
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.

  1. What are picked and mask for each sel?
  2. Does third change as sel changes?
  3. Is packed 11223344 or 44332211?

Check

The table holds 0x11, 0x22, 0x33, 0x44 in entries 0 to 3. What does packed show?

Your answer

Check the result.

Output lineWhat it shows
SELECT lines, picked11, 22, 33, 44: the multiplexer follows sel
SELECT lines, mask01, 02, 04, 80: the constant table built from four Scala Ints
FIXED, thirdAlways 33. Index 2 was resolved during elaboration; it is a wire, not a multiplexer
FIXED, packed44332211. Element 0 is the low byte, the reverse of a bundle's first field

Try it yourself.

  1. Widen sel to UInt(3.W) and poke 5. Elaboration prints warning W004, 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. With sel = 5, picked shows 22 and mask shows 02: only the low two bits of 101 were used, selecting element 1. A wider selector does not give a range error; it aliases.
  2. Change io.table(2) to io.table(4). A Scala index is checked while the circuit is built, and elaboration stops with IndexOutOfBoundsException: 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 swapped
val toMem   = IO(new Req)            // addr: Output, ack: Input
val fromCpu = IO(Flipped(new Req))   // addr: Input,  ack: Output
Portaddrack
IO(new Req)OutputInput
IO(Flipped(new Req))InputOutput

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.

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

  1. What are the four DIRECTION lines?
  2. The testbench can poke only inputs. Which two fields does it poke?
  3. For up.data = 255, what does down.data show?

Check

The testbench can poke only inputs of Stage. Which two fields does it poke?

Your answer

Check the result.

FieldDirectionTestbench action
up.dataInput, flippedpoke
up.readyOutput, flippedpeek
down.dataOutputpeek
down.readyInputpoke

The first SIMULATE row sends 41 forward and receives 42 on down.data, and ready passes back as

  1. The second row pokes 255, and down.data shows 0: the + in up.data + 1.U wraps in 8 bits, the rule from the width chapter. One Link declaration served both ends; the receiving side did not need a second bundle with the directions written backwards.

Try it yourself.

  1. Remove Flipped from up. Both ports now have data as an output and ready as an input. Elaboration stops at up.ready := down.ready with a ChiselException saying up.ready in Stage cannot be written from module Stage: up.ready has become an input, and a module cannot drive its own inputs.
  2. Keep Flipped and add up.data := 0.U. The same exception now names up.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 top

A 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.ready

The elaboration exception says cannot be written from module and names the field. Check whether that port should be flipped.

9. Summary

TopicKey point
BundleA Scala class with named fields of any Chisel type; create it with new
Bundle layoutasUInt 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
VecInitBuilds a Vec hardware value, such as a constant table
IndicesA Scala Int index is wiring decided during elaboration; a hardware index builds a multiplexer
DirectionsInput/Output on fields make a two-way interface; Flipped swaps them all
DrivingA module drives outputs and reads inputs; writing an input is an elaboration error

10. Check your understanding

  1. In Pixel, why does the pure blue pixel pack to 001f while the pure red one packs to f800? What single source change swaps them?

    Show answer

    r is declared first, so it occupies the top five bits; b is declared last and occupies the bottom five. Reversing the declaration order of r and b swaps the two packed values.

  2. What 8-bit value does VecInit(1.U(4.W), 2.U(4.W)).asUInt produce?

    Show answer

    0x21. Element 0, the value 1, is the low nibble, and element 1, the value 2, is the high nibble.

  3. Why does io.table(2) add no multiplexer while io.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.sel is hardware whose value changes during simulation, so the circuit needs a multiplexer over all elements.

  4. A module declares val in = IO(Flipped(new Link)). Which fields of in may the module drive?

    Show answer

    Only in.ready. Flipped turns data into an input and ready into an output, and a module may drive only its outputs.

  5. What is the width of Vec(3, new Header), where Header is 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.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows