A state register written as RegInit(0.U(2.W)) works until one file compares it
with 3.U to mean "done" while another uses 2.U for the same state. Raw numbers
carry no names and no type, so nothing catches the mix-up. Chisel's ChiselEnum
gives every value a name, picks its encoding and width, and turns the set into a
hardware type of its own. This chapter follows the Enumerations explanation of the
official Chisel documentation, together with the switch/is statement that
usually consumes enum values.
Before you start. You need when/.otherwise and Wire from
Mux, when, and Wire,
RegInit and the clock-edge timing of registers from
Registers and stepping,
and the ports and Bundles of
Bundles and Vecs.
An enumeration lives in two phases. Its declaration is Scala code that runs once during elaboration and fixes every name, number, and the width. Its values are hardware: a register of an enum type is an ordinary register of that width, and the simulator only sees bits.
1. Declaring an enumeration
1.1 ChiselEnum and Value
You declare an enumeration as a Scala object that extends ChiselEnum. Each
Value call inside it creates one named value.
Syntax
object EnumName extends ChiselEnum {
val name1, name2, name3 = Value
}import chisel3._
object Color extends ChiselEnum {
val red, green, blue = Value
}| Code | Meaning |
|---|---|
object Color extends ChiselEnum | Declares the enumeration Color; Chisel needs no chisel3.util import for this |
val red, green, blue = Value | Three values, numbered 0, 1, 2 in the order written |
Color.red | A hardware literal of type Color.Type with value 0 |
Color.all | A Scala Seq of every value, in numbering order |
Color.getWidth | The width Chisel chose, here 2 |
1.2 How Chisel numbers values and picks the width
Plain Value calls count up from 0. The width is the smallest number of bits that
can hold the largest value, so you never write a width yourself.
| Values declared | Largest number | Width |
|---|---|---|
| 2 | 1 | 1 bit |
| 3 or 4 | 2 or 3 | 2 bits |
| 5 to 8 | 4 to 7 | 3 bits |
Note. Adding a fifth state to a four-state machine widens every register and port of that enum type from 2 to 3 bits. Code that names the values keeps working unchanged, because the existing values keep their numbers.
1.3 Explicit encodings
When the bit patterns are fixed by someone else, such as an instruction opcode or a
protocol field, pass the number to Value.
Syntax
val name = Value(number.U)object Cmd extends ChiselEnum {
val nop = Value(0.U)
val read = Value(4.U)
val write = Value // continues from 4, so write is 5
}| Rule | Example above |
|---|---|
| Explicit numbers must strictly increase in declaration order | 0, then 4 |
A bare Value after an explicit one takes the previous number plus one | write is 5 |
| The width still fits the largest number | 5 needs 3 bits |
| Numbers between declared values are not values of the enum | 1, 2, 3, 6, 7 are not Cmd values |
Caution. Declaring the explicit numbers out of order is not a compile error. The
program fails when the object is first used, with
java.lang.ExceptionInInitializerError caused by
Enums must be strictly increasing: Cmd.
2. Using enum values in hardware
2.1 Enum types for ports, wires, and registers
Color() (with parentheses) is a Chisel type, like UInt(2.W). You can use it
anywhere a Chisel type is accepted. Color.red (no parentheses) is a value, like
0.U.
Syntax
Input(EnumName()) // port of the enum type
Wire(EnumName()) // wire of the enum type
RegInit(EnumName.value) // register, reset to one valueclass Light extends Module {
val io = IO(new Bundle {
val color = Output(Color())
})
val state = RegInit(Color.red)
io.color := state
}| Written as | What it is | Comparable to |
|---|---|---|
Color() | A 2-bit Chisel type | UInt(2.W) |
Color.red | A hardware literal | 0.U(2.W) |
Color.Type | The Scala type of every Color value, for function parameters | UInt as a Scala type |
A Bundle field works the same way: val color = Color() inside a Bundle
declares a 2-bit field of type Color.
2.2 Comparing enum values
=== and =/= compare two values of the same enum and return a Bool.
isOneOf tests membership in a list of values.
Syntax
x === EnumName.value
x =/= EnumName.value
x.isOneOf(EnumName.value1, EnumName.value2)val stop = state === Color.red
val ready = state.isOneOf(Color.green, Color.blue)The right-hand side must be the same enum. Comparing with a UInt such as
state === 2.U fails at Scala compile time with type mismatch; found: chisel3.UInt, required: chisel3.EnumType. That error is the point of the feature:
the number 2 may be blue today, but the type system refuses to rely on it.
2.3 Branching with switch and is
switch and is come from chisel3.util. They compare one signal with constant
values and run the block of the matching is. They are a compact form of a
when/.elsewhen chain where every condition is x === constant.
Syntax
import chisel3.util._
switch(signal) {
is(value1) { ... }
is(value2, value3) { ... } // several values share one branch
}val next = WireDefault(Color.red)
switch(state) {
is(Color.red) { next := Color.green }
is(Color.green) { next := Color.blue }
}| Part | Meaning |
|---|---|
switch(state) | The signal every is compares against |
is(Color.red) { ... } | Runs when state === Color.red |
is(a, b) { ... } | Runs when the signal equals any listed value |
No matching is | Nothing in the switch assigns; registers hold and wires keep their default |
In the snippet, blue has no branch, so next keeps the default Color.red from
WireDefault.
Caution. A plain Wire with no default and a switch that does not cover every
value is an error. Wire(Color()) assigned only in is(Color.red) stops before
simulation with Reference next is not fully initialized. Give wires a default with
WireDefault, or assign them before the switch. A register, in contrast, simply
keeps its value when no branch assigns it.
Note. The older chisel3.util.Enum(n) returns a list of plain UInt literals:
val sIdle :: sRun :: Nil = Enum(2) gives two 1-bit values, 0 and 1. They have
names but no type of their own, so comparing them with any UInt compiles. Prefer
ChiselEnum in new code.
2.4 Example: a timer state machine
Timer.scala is the usual shape of an FSM in Chisel. Read it in this order:
- The
object Statedeclares three states with no explicit numbers. stateis a register reset tosIdle;leftis a 2-bit down-counter.- The
switchhas oneisper state. InsIdle, a highstartmoves tosCountand loadsleftwith 2. InsCount, the machine decrementsleftuntil it reaches 0 and then moves tosDone.sDonereturns tosIdleafter one cycle. doneis a comparison withState.sDone.
import chisel3._
import chisel3.util._
// Three named states; Chisel picks the encoding and the width.
object State extends ChiselEnum {
val sIdle, sCount, sDone = Value
}
class Timer extends Module {
val io = IO(new Bundle {
val start = Input(Bool())
val state = Output(State())
val done = Output(Bool())
})
val state = RegInit(State.sIdle)
val left = RegInit(0.U(2.W))
switch(state) {
is(State.sIdle) {
when(io.start) {
state := State.sCount
left := 2.U
}
}
is(State.sCount) {
when(left === 0.U) {
state := State.sDone
}.otherwise {
left := left - 1.U
}
}
is(State.sDone) {
state := State.sIdle
}
}
io.state := state
io.done := state === State.sDone
}Tb.scala prints the enum's values and width during elaboration. It then drives
start high in cycle 0 only, and prints the state and done before each clock
edge. A peeked enum port prints as the enum name with the number and value name in
parentheses, so you can read the trace without a lookup table.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
println(s"ENCODING ${State.all.mkString(" ")} width=${State.getWidth}")
test(new Timer) { dut =>
for (cycle <- 0 until 7) {
// Pulse start for one cycle only.
dut.io.start.poke((cycle == 0).B)
println(s"cycle=$cycle state=${dut.io.state.peek()} done=${dut.io.done.peek().litValue}")
dut.clock.step()
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run.
- What width does the
ENCODINGline report, and which number doessDoneget? startis high in cycle 0. Is the state alreadysCountin cycle 0?- In which cycle is
donefirst 1, and for how many cycles does it stay 1?
Work it out with this trace:
| Cycle | State during the cycle | left | At the clock edge |
|---|---|---|---|
| 0 | sIdle | 0 | start is high: go to sCount, load 2 |
| 1 | sCount | 2 | not 0: decrement |
| 2 | sCount | 1 | not 0: decrement |
| 3 | sCount | 0 | 0: go to sDone |
| 4 | sDone | 0 | return to sIdle |
| 5–6 | sIdle | 0 | start is low: stay |
Check the result.
| Output line | What it shows |
|---|---|
ENCODING | The three values are numbered 0, 1, 2 in declaration order, and the width is 2 |
cycle=0 | Still sIdle: the register changes only at the clock edge after start was seen |
cycle=1 to cycle=3 | sCount for three cycles while left counts 2, 1, 0 |
cycle=4 | sDone with done=1, the only cycle where done is 1 |
cycle=5, cycle=6 | Back in sIdle and waiting, because start stayed low |
Try it yourself.
- Add two unused states,
sWaitandsError, aftersDone. TheENCODINGline lists five values and the width becomes 3. Add onlysWaitand the width stays 2. The trace is the same in both cases, because the existing states keep their numbers. - Hold
starthigh in every cycle (dut.io.start.poke(true.B)). The trace is the same through cycle 5, but cycle 6 issCountagain:sIdleseesstartand leaves immediately. - Delete the
is(State.sDone)branch. InsDoneno branch assignsstate, so the register keeps its value: from cycle 4 on the machine stays insDoneanddonestays 1. A missing branch is a hold, not an error.
3. Converting between enums and bits
3.1 From enum to bits: asUInt
asUInt returns the enum's bits as a UInt of the same width. It does not
check whether those bits are a declared value; use the flag from safe when decoding raw input.
Syntax
enumValue.asUIntval code = state.asUInt // Color.blue becomes 2.U, 2 bits wide3.2 From bits to enum: plain cast and safe
Going the other way is a claim, not a conversion: a raw UInt may hold a pattern
that is not a declared value. Chisel has two forms.
Syntax
EnumName(rawBits) // plain cast
val (value, valid) = EnumName.safe(rawBits) // cast plus validity flagval (color, valid) = Color.safe(io.raw) // io.raw is UInt(2.W)| Form | Returns | When io.raw is 3 |
|---|---|---|
Color(io.raw) | Color.Type | Passes 3 through silently; elaboration prints a [W001] warning |
Color.safe(io.raw) | (Color.Type, Bool) | Same bits, but valid is 0 |
safe does not change any bits. It adds a comparator whose output is 1 only when
the input equals one of the declared numbers.
Caution. The input UInt must not be wider than the enum. Casting a 7-bit value
to a 6-bit enum stops elaboration with The UInt being cast to Opcode is wider than Opcode's width (6), for both the plain cast and safe.
3.3 Example: decoding opcodes with safe
Decode.scala models an opcode field whose encodings are fixed: load is 0x03,
imm is 0x13, and store is 0x23. Read it with three questions in mind:
- The largest encoding is 35. How wide is
Opcode, and israwdeclared to match? - What does
Opcode.safe(io.raw)return, and which output receives each part? memoryis the AND ofvalidandisOneOf(load, store). Why includevalid?
import chisel3._
// Explicit encodings must increase; the gaps are not legal opcodes.
object Opcode extends ChiselEnum {
val load = Value(0x03.U)
val imm = Value(0x13.U)
val store = Value(0x23.U)
}
class Decode extends Module {
val io = IO(new Bundle {
val raw = Input(UInt(6.W))
val valid = Output(Bool())
val bits = Output(UInt(6.W))
val memory = Output(Bool())
})
// safe returns the enum plus a flag that is true only for a declared value.
val (op, valid) = Opcode.safe(io.raw)
io.valid := valid
io.bits := op.asUInt
io.memory := valid && op.isOneOf(Opcode.load, Opcode.store)
}Tb.scala prints the width and the numeric values from Opcode.all during
elaboration. It then pokes the three legal encodings and one illegal pattern, 0x05.
It reads the decoded enum back as UInt bits through io.bits on purpose; the
reason is in the result table.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
println(s"OPCODE width=${Opcode.getWidth} values=${Opcode.all.map(_.litValue).mkString(",")}")
test(new Decode) { dut =>
for (raw <- Seq(0x03, 0x13, 0x23, 0x05)) {
dut.io.raw.poke(raw.U)
println(f"raw=0x$raw%02x valid=${dut.io.valid.peek().litValue} bits=0x${dut.io.bits.peek().litValue}%02x memory=${dut.io.memory.peek().litValue}")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run.
- Which width and which three numbers does the
OPCODEline show? - For 0x05, what are
valid,bits, andmemory? - Does
safechange the 0x05 pattern into some legal opcode?
Check the result.
| Output line | What it shows |
|---|---|
OPCODE | Width 6, values 3, 19, 35: the explicit encodings in decimal |
raw=0x03, raw=0x23 | Legal memory opcodes: valid=1, bits unchanged, memory=1 |
raw=0x13 | Legal, but not a memory opcode: memory=0 |
raw=0x05 | valid=0 while bits still shows 0x05; safe only reports, it never repairs |
memory is 0 for 0x05 because the module ANDs isOneOf with valid. The
testbench reads io.bits instead of an enum-typed port: calling peek() on an enum
port that holds an undeclared pattern fails with
java.util.NoSuchElementException: None.get, because there is no name to attach.
Try it yourself.
- Replace
Opcode.safe(io.raw)with the plain castOpcode(io.raw)and setio.valid := true.Bso the module still compiles. The run still succeeds, but before the simulation lines it prints[W001] Casting non-literal UInt to Opcode. You can use Opcode.safe to cast without this warning., and 0x05 now reportsvalid=1. - Widen
rawtoUInt(7.W). Elaboration stops with aChiselException:The UInt being cast to Opcode is wider than Opcode's width (6). - Add
val next = Valueafterstore. It gets 36, one more thanstore, and theOPCODEline shows width 6 and four values.
Note. When a raw value is known to be legal, for example a Bundle read from a
memory that only ever stores legal values, suppressEnumCastWarning { ... }
silences the warning for that region. Everywhere else prefer safe: the flag costs
one comparator and makes illegal inputs visible.
4. Common mistakes
4.1 Comparing an enum with a number
// Wrong: 2.U is a UInt, not a State value
io.done := state === 2.U
// Right: compare with the named value
io.done := state === State.sDoneThe wrong line fails at Scala compile time with type mismatch; found: chisel3.UInt, required: chisel3.EnumType. Numbers change when states are added;
names do not.
4.2 Forgetting chisel3.util for switch
// Wrong: only chisel3._ is imported
import chisel3._
switch(state) { is(State.sIdle) { ... } }
// Right
import chisel3._
import chisel3.util._
switch(state) { is(State.sIdle) { ... } }ChiselEnum lives in chisel3, but switch and is live in chisel3.util.
Without the second import the compiler reports not found: value switch and
not found: value is.
4.3 Casting raw bits without safe
// Wrong: an illegal pattern passes through unnoticed
val op = Opcode(io.raw)
// Right: keep the validity flag and use it
val (op, valid) = Opcode.safe(io.raw)The plain cast elaborates with only a [W001] warning, and nothing in the hardware
tells an illegal input apart from a legal one.
4.4 Explicit encodings out of order
// Wrong: 0x13 comes before 0x03
object Opcode extends ChiselEnum {
val imm = Value(0x13.U)
val load = Value(0x03.U)
}
// Right: declare in increasing order
object Opcode extends ChiselEnum {
val load = Value(0x03.U)
val imm = Value(0x13.U)
}The wrong version compiles, then fails at its first use with
ExceptionInInitializerError caused by Enums must be strictly increasing: Opcode.
4.5 A wire that some switch branch never assigns
// Wrong: next has no value when state is green or blue
val next = Wire(Color())
switch(state) { is(Color.red) { next := Color.green } }
// Right: give the wire a default
val next = WireDefault(Color.red)
switch(state) { is(Color.red) { next := Color.green } }The wrong version stops before simulation with
Reference next is not fully initialized. Registers hold their value when no branch
assigns them; wires have no value to hold.
5. Change one thing: remove a branch, then add two states
In the lab, delete the whole is(State.sDone) { ... } branch of the timer and run again.
Check
Without the sDone branch, what happens from cycle 4 on?
Restore the branch, then add two members after sDone in the declaration, for example
val sIdle, sCount, sDone, sWait, sError = Value.
Check
With five members, what width does the ENCODING line report?
6. Apply it: a yellow phase
The graded Practice problem below is a traffic light with two states, red and green, that advances on
each cycle where tick is high. Add yellow between green and red: one new member in the
declaration, one changed transition, one new is branch, and a stop output that is 1 in red and
in yellow. Compare states only with Light members; isOneOf tests several at once. Write the
eight-cycle trace before running, remembering that a tick seen in one cycle shows as the new state in
the next.
7. Summary
| Topic | Key point |
|---|---|
| Declaration | object E extends ChiselEnum { val a, b = Value }; values count from 0 |
| Width | The smallest width that holds the largest value; you do not choose it |
| Explicit encoding | Value(n.U), strictly increasing; a bare Value continues with +1 |
| Type and value | E() is a Chisel type for ports, wires, and fields; E.a is a literal |
| Comparison | ===, =/=, isOneOf with the same enum only; UInt is a compile error |
switch/is | From chisel3.util; no matching branch means registers hold |
| Enum to bits | asUInt always succeeds |
| Bits to enum | E(u) warns; E.safe(u) returns the value and a validity Bool |
8. Check your understanding
-
Adding a fourth state leaves the width at 2, but a fifth state changes it. Why?
Show answer
Four values are numbered 0 to 3, which fit in 2 bits. A fifth value gets number 4, and 4 needs 3 bits. The width is always the smallest one that holds the largest number.
-
In the FSM trace,
startis high in cycle 0, but the state in cycle 0 is stillsIdle. Why?Show answer
stateis a register. Theswitchcomputes the next state during cycle 0, and the register takes that value at the clock edge at the end of the cycle. The new state,sCount, appears in cycle 1. -
What does
Opcode.safechange about the bits it receives, and what does it add?Show answer
It changes nothing: 0x05 stays 0x05. It adds a
Boolthat is 1 only when the bits equal one of the declared encodings. -
Which of these fails at Scala compile time, which during elaboration, and which only prints a warning:
state === 2.U,Opcode(io.raw)with a 6-bitraw, andOpcode.safe(io.raw)with a 7-bitraw?Show answer
state === 2.Uis a Scala compile error (type mismatch).Opcode(io.raw)with a 6-bit input elaborates and prints a[W001]warning.safewith a 7-bit input stops elaboration because the input is wider than the enum. -
A designer deletes the
is(State.sDone)branch and the timer sticks insDone. Is that a Chisel error?Show answer
No. With no matching branch, nothing assigns the
stateregister, so it keeps its value. A missing branch on a register means "hold"; the design is legal but wrong.