Skip to content
code0.HDL

ChiselEnum and state machines

Give each state a name with ChiselEnum, hold it in a register, branch on it with switch and is, convert between an enum and raw bits safely, and read the width Chisel picked.

In this chapter

  • Declare a ChiselEnum, read the number and width Chisel gives each member, and set an explicit encoding
  • Hold a state in a register and write one is branch per state, knowing a missing branch is a hold
  • Convert an enum to bits with asUInt and back with safe, and say what a plain cast risks
  • Use an enum type for ports, wires, and Bundle fields, not only for the state register
Concepts in this chapter
  • ChiselEnum, state register, branches
Prerequisite concepts

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
}
CodeMeaning
object Color extends ChiselEnumDeclares the enumeration Color; Chisel needs no chisel3.util import for this
val red, green, blue = ValueThree values, numbered 0, 1, 2 in the order written
Color.redA hardware literal of type Color.Type with value 0
Color.allA Scala Seq of every value, in numbering order
Color.getWidthThe 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 declaredLargest numberWidth
211 bit
3 or 42 or 32 bits
5 to 84 to 73 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
}
RuleExample above
Explicit numbers must strictly increase in declaration order0, then 4
A bare Value after an explicit one takes the previous number plus onewrite is 5
The width still fits the largest number5 needs 3 bits
Numbers between declared values are not values of the enum1, 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 value
class Light extends Module {
  val io = IO(new Bundle {
    val color = Output(Color())
  })
  val state = RegInit(Color.red)
  io.color := state
}
Written asWhat it isComparable to
Color()A 2-bit Chisel typeUInt(2.W)
Color.redA hardware literal0.U(2.W)
Color.TypeThe Scala type of every Color value, for function parametersUInt 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 }
}
PartMeaning
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 isNothing 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 State declares three states with no explicit numbers.
  • state is a register reset to sIdle; left is a 2-bit down-counter.
  • The switch has one is per state. In sIdle, a high start moves to sCount and loads left with 2. In sCount, the machine decrements left until it reaches 0 and then moves to sDone. sDone returns to sIdle after one cycle.
  • done is a comparison with State.sDone.
Timer.scala
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.

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

  1. What width does the ENCODING line report, and which number does sDone get?
  2. start is high in cycle 0. Is the state already sCount in cycle 0?
  3. In which cycle is done first 1, and for how many cycles does it stay 1?

Work it out with this trace:

CycleState during the cycleleftAt the clock edge
0sIdle0start is high: go to sCount, load 2
1sCount2not 0: decrement
2sCount1not 0: decrement
3sCount00: go to sDone
4sDone0return to sIdle
5–6sIdle0start is low: stay

Check the result.

Output lineWhat it shows
ENCODINGThe three values are numbered 0, 1, 2 in declaration order, and the width is 2
cycle=0Still sIdle: the register changes only at the clock edge after start was seen
cycle=1 to cycle=3sCount for three cycles while left counts 2, 1, 0
cycle=4sDone with done=1, the only cycle where done is 1
cycle=5, cycle=6Back in sIdle and waiting, because start stayed low

Try it yourself.

  1. Add two unused states, sWait and sError, after sDone. The ENCODING line lists five values and the width becomes 3. Add only sWait and the width stays 2. The trace is the same in both cases, because the existing states keep their numbers.
  2. Hold start high in every cycle (dut.io.start.poke(true.B)). The trace is the same through cycle 5, but cycle 6 is sCount again: sIdle sees start and leaves immediately.
  3. Delete the is(State.sDone) branch. In sDone no branch assigns state, so the register keeps its value: from cycle 4 on the machine stays in sDone and done stays 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.asUInt
val code = state.asUInt   // Color.blue becomes 2.U, 2 bits wide

3.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 flag
val (color, valid) = Color.safe(io.raw)   // io.raw is UInt(2.W)
FormReturnsWhen io.raw is 3
Color(io.raw)Color.TypePasses 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 is raw declared to match?
  • What does Opcode.safe(io.raw) return, and which output receives each part?
  • memory is the AND of valid and isOneOf(load, store). Why include valid?
Decode.scala
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.

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

  1. Which width and which three numbers does the OPCODE line show?
  2. For 0x05, what are valid, bits, and memory?
  3. Does safe change the 0x05 pattern into some legal opcode?

Check the result.

Output lineWhat it shows
OPCODEWidth 6, values 3, 19, 35: the explicit encodings in decimal
raw=0x03, raw=0x23Legal memory opcodes: valid=1, bits unchanged, memory=1
raw=0x13Legal, but not a memory opcode: memory=0
raw=0x05valid=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.

  1. Replace Opcode.safe(io.raw) with the plain cast Opcode(io.raw) and set io.valid := true.B so 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 reports valid=1.
  2. Widen raw to UInt(7.W). Elaboration stops with a ChiselException: The UInt being cast to Opcode is wider than Opcode's width (6).
  3. Add val next = Value after store. It gets 36, one more than store, and the OPCODE line 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.sDone

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

Your answer

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

TopicKey point
Declarationobject E extends ChiselEnum { val a, b = Value }; values count from 0
WidthThe smallest width that holds the largest value; you do not choose it
Explicit encodingValue(n.U), strictly increasing; a bare Value continues with +1
Type and valueE() 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/isFrom chisel3.util; no matching branch means registers hold
Enum to bitsasUInt always succeeds
Bits to enumE(u) warns; E.safe(u) returns the value and a validity Bool

8. Check your understanding

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

  2. In the FSM trace, start is high in cycle 0, but the state in cycle 0 is still sIdle. Why?

    Show answer

    state is a register. The switch computes 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.

  3. What does Opcode.safe change about the bits it receives, and what does it add?

    Show answer

    It changes nothing: 0x05 stays 0x05. It adds a Bool that is 1 only when the bits equal one of the declared encodings.

  4. 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-bit raw, and Opcode.safe(io.raw) with a 7-bit raw?

    Show answer

    state === 2.U is a Scala compile error (type mismatch). Opcode(io.raw) with a 6-bit input elaborates and prints a [W001] warning. safe with a 7-bit input stops elaboration because the input is wider than the enum.

  5. A designer deletes the is(State.sDone) branch and the timer sticks in sDone. Is that a Chisel error?

    Show answer

    No. With no matching branch, nothing assigns the state register, so it keeps its value. A missing branch on a register means "hold"; the design is legal but wrong.

Checkpoint

Submit the Practice problem for grading

Grading runs on the server against hidden tests; the result is recorded in this browser only.

Sources

Licences and notices for the sources this site follows