1. Ground types
Every port, wire, and register in a Chisel design carries a value of some Chisel data type. The type
decides how many bits the value occupies and how those bits are read: as an unsigned number, a
two's-complement number, or a single truth value. Width mistakes rarely produce an error message —
bits simply disappear — so learning the width rules early saves a lot of debugging later. Every
example in this chapter prints WIDTH lines during elaboration and SIMULATE lines from the
testbench; the split between the two phases is the one from
the first chapter.
1.1 UInt, SInt, and Bool
A Chisel ground type is a bit vector plus a rule for reading it.
Syntax
UInt(width.W) // unsigned integer
SInt(width.W) // two's-complement signed integer
Bool() // one bit: true or falseval io = IO(new Bundle {
val count = Output(UInt(4.W)) // 0 to 15
val offset = Input(SInt(4.W)) // -8 to 7
val enable = Input(Bool()) // true or false
})| Type | Bits | Range |
|---|---|---|
UInt(n.W) | n | 0 to 2n−1 |
SInt(n.W) | n | −2n−1 to 2n−1−1, in two's complement |
Bool() | 1 | true.B or false.B. A Bool is also a 1-bit UInt |
1.2 Widths are Chisel values: .W
The width argument is not a plain Int. .W converts a Scala Int into a Chisel width, and
UInt(...) accepts only that. UInt(8.W) reads as "unsigned, 8 bits wide".
Syntax
n.W // the Scala Int n as a Chisel widthval width = 12 // a Scala Int, known during elaboration
val bus = UInt(width.W) // a 12-bit unsigned typeBecause the width is computed by Scala, it can come from a generator parameter. A class declared as
class Buffer(width: Int) extends Module receives width when it is built; the name in parentheses
after the class name is a constructor parameter, and Int is its Scala type. UInt(width.W) inside
the class makes ports of that width, so new Buffer(8) and new Buffer(16) are two different circuits
from one source.
Caution. Inside a module, you can leave a wire's width unspecified and let the compiler infer it
from the connections. A port declared without a width, such as Output(UInt()), makes the testbench
fail with Width of ... is unknown!, so declare every port width.
1.3 The same bits, different readings
The bits alone do not say what number they are. The four bits 1101 are 13 as a UInt and −3 as an
SInt. The type is what decides. This matters for the rest of the chapter: a cast changes only the
reading, while an extension or truncation changes the number of bits.
2. Literals
2.1 Literal forms
A literal turns a Scala value into a hardware constant.
Syntax
value.U // unsigned literal, minimum width
value.U(n.W) // unsigned literal, width n
value.S // signed literal, minimum width
value.S(n.W) // signed literal, width n
"hXX".U // from a hex string; "b..." for binary, "o..." for octal
true.B / false.B // Bool literalsval limit = 200.U(8.W)
val mask = "hf0".U
val minus = -3.S(4.W)
val on = true.BThe suffix decides the type: .U gives UInt, .S gives SInt, .B gives Bool. The value comes
from Scala, so (width * 2).U is a legal literal whose value the generator computes.
2.2 How wide is a literal?
A literal written without a width gets the minimum number of bits that holds its value. A signed literal reserves room for the sign. A string literal is stored as a number, so leading zeros in the string do not count.
| Literal | Width | Why |
|---|---|---|
0.U | 1 | Even zero needs one bit |
5.U | 3 | 101 |
255.U | 8 | 1111_1111 |
256.U | 9 | 1_0000_0000 |
5.S | 4 | 0101, sign bit included |
3.S | 3 | 011 |
-1.S | 1 | In two's complement, the single bit 1 already means −1 |
-8.S | 4 | 1000 |
"b0001".U | 1 | The value is 1; leading zeros are dropped |
"h00ff".U | 8 | The value is 255 |
8.U(6.W) | 6 | The width you wrote, zero-extended |
2.3 An explicit width must fit
When you write a width on a literal, Chisel checks it.
8.U(6.W) // fine: 001000
8.U(3.W) // elaboration error: 8 needs at least 4 bitsThe check runs during elaboration, not at Scala compile time, because the value 8 is just a Scala
Int until Chisel builds the literal. Section 8 runs this failure.
3. Changing widths and readings
3.1 Extension and truncation at a connection
Connecting a value to a sink of a different width adjusts the width silently, provided the sink has a
fixed width and the same signedness: UInt to UInt or SInt to SInt. A sink whose width is still
being inferred, such as Wire(UInt()), takes the width of what is connected instead of truncating,
and connecting a UInt to an SInt is a type error rather than an adjustment.
| Case | What happens | Example: a 4-bit value into an 8-bit sink |
|---|---|---|
Narrow UInt into a wider sink | Zero extension: new high bits are 0 | 1101 becomes 0000_1101 |
Narrow SInt into a wider sink | Sign extension: new high bits copy the sign bit | 1101 becomes 1111_1101 |
| Wide value into a narrower sink | Truncation: the high bits are dropped | an 8-bit 1100_1000 into 4 bits leaves 1000 |
None of these fixed-width connections produces a warning. That convenience is also the most common way bits go missing, so declare port widths explicitly and check them first when a value looks wrong.
3.2 Making the width change explicit
You can also change a width inside an expression.
Syntax
x.pad(n) // extend to n bits; no effect if x is already n bits or wider
x(hi, lo) // keep bits hi down to lo
u.zext // UInt to SInt, one bit wider, value unchangedval x = io.x // 8-bit UInt
x.pad(12) // 12 bits, zero-extended
x.pad(4) // still 8 bits
x(3, 0) // the low 4 bits
io.nibble.zext // a 4-bit UInt 13 becomes the 5-bit SInt 13pad follows the type: on a UInt it zero-extends, on an SInt it sign-extends. Bit selection
x(hi, lo) is covered with the other bit-field operators in
Bool, UInt, and the operators.
3.3 Casts reinterpret bits
asUInt and asSInt keep the width and every bit, and change only the reading.
Syntax
x.asUInt // read the bits of x as unsigned
x.asSInt // read the bits of x as signedval raw = "hc5".U(8.W) // 1100_0101 = 197
raw.asSInt // the same 8 bits read as -59Casts take no width argument. If you need a different width, connect the result to a sink of that
width, or use pad, and the extension or truncation happens there.
Note. The order of "extend" and "cast" matters. Extending an SInt and then casting gives a
different number from casting first and then extending: casting a 4-bit −3 to UInt and then
widening gives 13, while widening to 8 signed bits first and casting afterwards gives 253. The next
example shows both orders.
4. Example: literal widths and extension
Literals.scala has two parts. The lits sequence pairs the text of each literal with the literal
itself, and a loop prints the width Chisel assigned; .getWidth asks a hardware value for its width as
a Scala number. Below it, a 4-bit signed input in drives three outputs: an 8-bit SInt wide, a
4-bit UInt bits through asUInt, and an 8-bit UInt widebits through the same cast. The
testbench pokes 5, −3, and −8 into in.
import chisel3._
class Literals extends Module {
val io = IO(new Bundle {
val in = Input(SInt(4.W))
val wide = Output(SInt(8.W))
val bits = Output(UInt(4.W))
val widebits = Output(UInt(8.W))
})
// Literal widths are fixed while the generator runs.
val lits = Seq(
"5.U" -> 5.U,
"5.S" -> 5.S,
"-8.S" -> -8.S,
"\"b1010\".U" -> "b1010".U,
"\"h00ff\".U" -> "h00ff".U,
"8.U(6.W)" -> 8.U(6.W),
"true.B" -> true.B
)
for ((text, lit) <- lits) {
println(s"WIDTH $text = ${lit.getWidth}")
}
// A narrow SInt connected to a wider SInt is sign-extended.
io.wide := io.in
// asUInt keeps the same bits and the same width.
io.bits := io.in.asUInt
// A narrow UInt connected to a wider UInt is zero-extended.
io.widebits := io.in.asUInt
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Literals) { dut =>
for (value <- Seq(5, -3, -8)) {
dut.io.in.poke(value.S)
val wide = dut.io.wide.peek().litValue
val bits = dut.io.bits.peek().litValue
val widebits = dut.io.widebits.peek().litValue
println(s"SIMULATE in=$value wide=$wide bits=$bits widebits=$widebits")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run. What are the seven WIDTH values, and do the WIDTH lines appear before
or after the SIMULATE lines? For an input of −3, what do wide, bits, and widebits show?
Check
How many bits does the literal -8.S get?
Check
For in = -3, what does bits show, and what does widebits show?
Check the result. Press Run, read the seven WIDTH lines against the table in section 2.2,
then trace −3 through the circuit and compare with the output.
| Step | Bits | Value read |
|---|---|---|
in, 4-bit SInt | 1101 | −3 |
wide, sign-extended to 8 bits | 1111_1101 | −3 |
in.asUInt into bits | 1101 | 13 |
in.asUInt into widebits, zero-extended | 0000_1101 | 13 |
| Output line | What it shows |
|---|---|
WIDTH lines | The widths from the table in section 2.2: 3, 4, 4, 4, 8, 6, 1. They print first because widths are settled during elaboration |
in=5 | A positive value survives all three paths unchanged |
in=-3 | wide keeps −3; both unsigned outputs show 13 |
in=-8 | 1000 is 8 when read unsigned, so bits and widebits show 8 |
widebits is wider than bits, but the value reaching it is already a UInt, so the connection pads
with zeros and the number does not change.
Try it yourself.
- Change the last connection to
io.widebits := io.wide.asUInt. The cast now happens after sign extension and reinterprets the 8-bit pattern1111_1101. −3 produces 253 and −8 produces 248 onwidebits, whilebitsstays at 13 and 8. The input bits never changed; only the order of extension and cast did. - Add
"b0001".Uto thelitssequence. ItsWIDTHline reports 1, because the string's leading zeros are not part of the value.
5. Operator result widths
Every operator has a fixed rule for its result width, applied during elaboration.
| Expression | Result width |
|---|---|
x + y, x - y | max(w(x), w(y)) |
x +& y, x -& y | max(w(x), w(y)) + 1 |
x * y | w(x) + w(y) |
x & y, x | y, x ^ y | max(w(x), w(y)) |
x << n with a Scala Int n | w(x) + n |
x >> n with a Scala Int n | max(w(x) - n, 0); 0 once n >= w(x) |
x === y, x < y, other comparisons | 1 |
Cat(x, y) | w(x) + w(y) |
val x = io.x // 8 bits
x + x // 8 bits: the carry is dropped
x +& x // 9 bits: the carry is kept
x * x // 16 bits
x << 3 // 11 bits: 0xc5 << 3 is 1576
x >> 3 // 5 bits: 0xc5 >> 3 is 24+ wraps modulo 2max width. That is a deliberate choice: a processor's adder is as wide as
its datapath and discards the overflow. +&, the expanding add, keeps the carry in one extra bit.
- and -& follow the same pattern. A wider port on the left of := cannot bring the carry back,
because the operator already produced the narrower result before the connection saw it.
6. Example: +, +&, and truncation
Adder.scala computes the same sum twice: plainSum with + and expandSum with +&. Both feed
9-bit output ports. A third output, low, is only 4 bits wide and is driven straight from the 8-bit
input a. The println reports both sum widths during elaboration.
import chisel3._
class Adder extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W))
val b = Input(UInt(8.W))
val plain = Output(UInt(9.W))
val expand = Output(UInt(9.W))
val low = Output(UInt(4.W))
})
val plainSum = io.a + io.b
val expandSum = io.a +& io.b
// Operator result widths are decided during elaboration.
println(s"WIDTH a+b=${plainSum.getWidth} a+&b=${expandSum.getWidth}")
// A 9-bit port cannot recover a carry that + already dropped.
io.plain := plainSum
io.expand := expandSum
// Connecting 8 bits to a 4-bit port keeps only the low bits.
io.low := io.a
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Adder) { dut =>
for ((a, b) <- Seq((100, 27), (200, 100), (0xab, 0x10))) {
dut.io.a.poke(a.U)
dut.io.b.poke(b.U)
val plain = dut.io.plain.peek().litValue
val expand = dut.io.expand.peek().litValue
val low = dut.io.low.peek().litValue
println(s"SIMULATE a=$a b=$b plain=$plain expand=$expand low=$low")
}
}
}Practice this concept
Use this concept in a short Chisel problem as your next step.
Predict before you run. Both sum ports are 9 bits wide. What widths does the WIDTH line report
for a + b and a +& b? Then predict the row for a = 200, b = 100; the true sum is 300, which is
1_0010_1100.
Check
What does plain show for 200 + 100?
Check
low is UInt(4.W) driven from a. For a = 200, what does it show?
Check the result.
| Output | Value | Why |
|---|---|---|
WIDTH | 8 and 9 | The rules from section 5 |
plain | 44 | + produced only the low 8 bits, 0010_1100. The 9-bit port padded 44 with a zero; the carry was gone before the port saw it |
expand | 300 | +& produced all nine bits |
low | 8 | 200 is 1100_1000; truncating to 4 bits leaves 1000 |
The other rows confirm the pattern: 100 + 27 fits in 8 bits, so plain and expand agree at 127.
For 0xab, low keeps 0xb, which is 11. None of these rows print a warning. The circuit is legal;
it simply discards bits.
Try it yourself. First change io.a + io.b to io.a +& io.b so that both sums use the expanding
add, and run again. Before you do, decide which rows change.
Check
With both sums using +&, which rows show a different plain value than before?
Then two more edits.
- Replace
+with-and+&with-&, and add the input pair(3, 5)to the testbench.3 − 5is negative, but the operands areUInt.-wraps in 8 bits and gives 254;-&gives a 9-bit result, 510. The extra bit is not a sign, because the result is still unsigned. A negative answer needsSIntoperands. - Change
lowtoUInt(8.W). Every row then showslowequal toa, because no bits are dropped.
7. A Chisel type is not hardware
UInt(8.W) is a Scala object that describes eight unsigned bits. It becomes hardware only when
something binds it to the circuit.
| Code | What it is |
|---|---|
UInt(8.W) | A Chisel type. Nothing in the circuit carries it |
IO(Output(UInt(8.W))) | A port |
Wire(UInt(8.W)) | A wire, covered in Mux, when, Wire, and a default for every path |
0.U(8.W) | A constant node with a known value |
Only hardware may appear on either side of :=. The Scala compiler cannot tell a type from hardware
here, because UInt(8.W) and 0.U(8.W) both have the Scala type UInt. A mix-up therefore compiles
and is caught during elaboration.
8. Failure: a literal that does not fit
This example fails on purpose. It asks for the value 8 in a 3-bit literal. Before you press Run, decide whether the Scala compiler or elaboration will catch it.
Check
Which stage reports the error in TooNarrow.scala?
import chisel3._
class TooNarrow extends Module {
val io = IO(new Bundle {
val out = Output(UInt(3.W))
})
// Eight needs four bits, so a 3-bit literal cannot hold it.
io.out := 8.U(3.W)
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new TooNarrow))
}| Item | Detail |
|---|---|
| Wrong code | 8.U(3.W): the value 8 needs at least four bits |
| Failing stage | Elaboration. 8.U(3.W) is well-typed Scala, so compilation passes; the stack trace points at the connection inside the module |
| Error message | IllegalArgumentException: requirement failed: The literal value 8 was elaborated with a specified width of 3 bits, but at least 4 bits are required |
| Fix | Write 8.U(4.W) |
Note. After the fix, connecting the 4-bit literal to the 3-bit output is legal and silently
truncates it to 0, the same rule that shaped low in section 6. A width written on a literal is
checked; a width difference at a connection is not.
9. Failure: a Chisel type used as hardware
This example also fails on purpose. It binds UInt(8.W) to a Scala val and connects that val to
an output.
import chisel3._
class BareType extends Module {
val io = IO(new Bundle {
val out = Output(UInt(8.W))
})
// UInt(8.W) only describes a type; nothing in the circuit carries it.
val zero = UInt(8.W)
io.out := zero
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new BareType))
}| Item | Detail |
|---|---|
| Wrong code | val zero = UInt(8.W) followed by io.out := zero |
| Failing stage | Elaboration. The val is ordinary Scala and compiles |
| Error message | ExpectedHardwareException: data to be connected 'UInt<8>' must be hardware, not a bare Chisel type. The message suggests wrapping the type in Wire(_) or IO(_) |
| Fix | Use the literal 0.U(8.W), which is hardware with a known value |
A port declaration follows the same rule from the other side: Output(UInt(8.W)) stays a type until
IO(...) binds it.
10. Apply it: keep a carry
The graded Practice problem below has a 4-bit adder whose 5-bit sum port comes out exactly 16 too
small whenever the true sum reaches 16. The port is wide enough; the operator is not. The second
problem on this page, the signed offset, is the cast-order question of section 3.3 applied to an
address: a negative 4-bit offset is read as unsigned before it is widened, so the extension pads with
zeros and the address moves forward. Attempt it after the graded one.
11. Common mistakes
11.1 Writing a width without .W
// Wrong: 8 is a Scala Int, not a width
val a = Input(UInt(8))
// Right
val a = Input(UInt(8.W))The Scala compiler reports type mismatch; found: Int(8), required: chisel3.Width.
11.2 5.U(3) instead of 5.U(3.W)
// Wrong: compiles, but means "bit 3 of 5", a 1-bit value 0
io.out := 5.U(3)
// Right: the literal 5 at width 3
io.out := 5.U(3.W)This one compiles. Applying an Int to a UInt selects a bit, so 5.U(3) is bit 3 of 101, which
is 0. The compiler prints a warning that starts with Passing an Int to .U is usually a mistake and
suggests .U(3.W). Read compiler warnings; this is the only sign of the mistake.
11.3 Expecting a wide port to rescue a carry
// Wrong: + is already 8 bits; the 9-bit port gets a zero on top
io.sum9 := io.a + io.b
// Right: +& keeps the carry
io.sum9 := io.a +& io.bThe result width belongs to the operator, not to the port it drives. When a sum comes out exactly
2n too small, look for a + that should be +&.
11.4 Casting after sign extension
// Gives 253 for -3: the cast reads the sign-extended 1111_1101
io.widebits := io.wide.asUInt
// Gives 13 for -3: the cast reads the original 4-bit 1101
io.widebits := io.in.asUIntNeither line is wrong in itself; they compute different things. Decide whether you want the signed value or the original bit pattern before choosing the order.
11.5 Connecting a bare type
// Wrong: a type, not hardware
val zero = UInt(8.W)
io.out := zero
// Right: a literal is hardware
io.out := 0.U(8.W)The elaboration exception names the type, UInt<8>, and says it must be hardware.
12. Summary
| Topic | Key point |
|---|---|
| Ground types | UInt(n.W), SInt(n.W), Bool(): bits plus a reading |
| Widths | .W turns a Scala Int into a width, in port and literal declarations; a constructor Int can supply it |
| Literals | .U, .S, .B; minimum width unless you write one, and a written width is checked during elaboration |
| Connections | Narrow to wide extends (zeros for UInt, sign for SInt); wide to narrow truncates; same signedness only; no warning |
| Casts | asUInt, asSInt keep every bit and the width; only the reading changes |
| Operators | + keeps the operand width, +& adds one bit, * adds widths, comparisons give 1 bit |
| Type vs hardware | UInt(8.W) is a type; ports, wires, registers, and literals are hardware |
13. Check your understanding
-
Why is
"h00ff".U8 bits wide rather than 16?Show answer
Chisel stores a string literal as a number, and leading zeros are not part of a number. The value 255 needs eight bits.
-
A 4-bit
SIntholding −1 drives an 8-bitUIntport throughasUInt. What value appears? What changes if the value first goes through an 8-bitSIntand is cast afterwards?Show answer
−1 is
1111.asUIntreads it as 15, and the 8-bit port zero-extends it, so the port shows 15. Sign extension first gives1111_1111, and the cast reads that as 255. -
Why does
8.U(3.W)fail, while connecting8.U(4.W)to a 3-bit port does not?Show answer
A width written on a literal is checked during elaboration, and 8 does not fit in 3 bits. A width difference at a connection is never checked: the 4-bit literal is truncated to its low three bits, 0.
-
What is the width of
x * y + zwhenx,y, andzare all 8-bitUInts?Show answer
x * yis 8 + 8 = 16 bits, and+takes the wider operand's width,max(16, 8).
-
Both
UInt(8.W)and0.U(8.W)have Scala typeUInt. Which is hardware, and how is a mix-up detected?Show answer
0.U(8.W)is hardware: a constant node.UInt(8.W)is only a type. The Scala compiler cannot tell them apart, so the mix-up is detected during elaboration by anExpectedHardwareException.
Two Practice problems below apply these rules. The graded one has a 5-bit sum port that is wide enough, yet every sum of 16 or more comes out exactly 16 too small. The other adds a signed offset to an 8-bit address; positive offsets work, but every negative offset moves the address forward instead of back.