Skip to content
code0.HDL

Widths, SInt, and arithmetic

Give a port a width with .W, read the width Chisel assigns to a literal, follow a signed value through a connection and a cast, then see why + loses a carry that +& keeps.

In this chapter

  • Declare UInt, SInt and Bool ports with .W widths and state the width Chisel gives any literal
  • Trace a value through a connection of a different width and through asUInt or asSInt, in either order
  • Give the result width of +, +&, *, shifts, comparisons and Cat, and pick +& when a carry must survive
  • Tell a Chisel type such as UInt(8.W) from hardware and name the stage that catches a literal that does not fit
Concepts in this chapter
  • UInt, SInt, expanding addition
Prerequisite concepts

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 false
val 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
})
TypeBitsRange
UInt(n.W)n0 to 2n−1
SInt(n.W)n−2n−1 to 2n−1−1, in two's complement
Bool()1true.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 width
val width = 12            // a Scala Int, known during elaboration
val bus = UInt(width.W)   // a 12-bit unsigned type

Because 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 literals
val limit = 200.U(8.W)
val mask  = "hf0".U
val minus = -3.S(4.W)
val on    = true.B

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

LiteralWidthWhy
0.U1Even zero needs one bit
5.U3101
255.U81111_1111
256.U91_0000_0000
5.S40101, sign bit included
3.S3011
-1.S1In two's complement, the single bit 1 already means −1
-8.S41000
"b0001".U1The value is 1; leading zeros are dropped
"h00ff".U8The value is 255
8.U(6.W)6The 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 bits

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

CaseWhat happensExample: a 4-bit value into an 8-bit sink
Narrow UInt into a wider sinkZero extension: new high bits are 01101 becomes 0000_1101
Narrow SInt into a wider sinkSign extension: new high bits copy the sign bit1101 becomes 1111_1101
Wide value into a narrower sinkTruncation: the high bits are droppedan 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 unchanged
val 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 13

pad 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 signed
val raw = "hc5".U(8.W)   // 1100_0101 = 197
raw.asSInt               // the same 8 bits read as -59

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

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

Your answer

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.

StepBitsValue read
in, 4-bit SInt1101−3
wide, sign-extended to 8 bits1111_1101−3
in.asUInt into bits110113
in.asUInt into widebits, zero-extended0000_110113
Output lineWhat it shows
WIDTH linesThe 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=5A positive value survives all three paths unchanged
in=-3wide keeps −3; both unsigned outputs show 13
in=-81000 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.

  1. Change the last connection to io.widebits := io.wide.asUInt. The cast now happens after sign extension and reinterprets the 8-bit pattern 1111_1101. −3 produces 253 and −8 produces 248 on widebits, while bits stays at 13 and 8. The input bits never changed; only the order of extension and cast did.
  2. Add "b0001".U to the lits sequence. Its WIDTH line 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.

ExpressionResult width
x + y, x - ymax(w(x), w(y))
x +& y, x -& ymax(w(x), w(y)) + 1
x * yw(x) + w(y)
x & y, x | y, x ^ ymax(w(x), w(y))
x << n with a Scala Int nw(x) + n
x >> n with a Scala Int nmax(w(x) - n, 0); 0 once n >= w(x)
x === y, x < y, other comparisons1
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.

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

OutputValueWhy
WIDTH8 and 9The rules from section 5
plain44+ 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
expand300+& produced all nine bits
low8200 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?

Your answer

Then two more edits.

  1. Replace + with - and +& with -&, and add the input pair (3, 5) to the testbench. 3 − 5 is negative, but the operands are UInt. - 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 needs SInt operands.
  2. Change low to UInt(8.W). Every row then shows low equal to a, 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.

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

Your answer

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))
}
ItemDetail
Wrong code8.U(3.W): the value 8 needs at least four bits
Failing stageElaboration. 8.U(3.W) is well-typed Scala, so compilation passes; the stack trace points at the connection inside the module
Error messageIllegalArgumentException: requirement failed: The literal value 8 was elaborated with a specified width of 3 bits, but at least 4 bits are required
FixWrite 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.

BareType.scala
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))
}
ItemDetail
Wrong codeval zero = UInt(8.W) followed by io.out := zero
Failing stageElaboration. The val is ordinary Scala and compiles
Error messageExpectedHardwareException: data to be connected 'UInt<8>' must be hardware, not a bare Chisel type. The message suggests wrapping the type in Wire(_) or IO(_)
FixUse 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.b

The 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.asUInt

Neither 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

TopicKey point
Ground typesUInt(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
ConnectionsNarrow to wide extends (zeros for UInt, sign for SInt); wide to narrow truncates; same signedness only; no warning
CastsasUInt, 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 hardwareUInt(8.W) is a type; ports, wires, registers, and literals are hardware

13. Check your understanding

  1. Why is "h00ff".U 8 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.

  2. A 4-bit SInt holding −1 drives an 8-bit UInt port through asUInt. What value appears? What changes if the value first goes through an 8-bit SInt and is cast afterwards?

    Show answer

    −1 is 1111. asUInt reads it as 15, and the 8-bit port zero-extends it, so the port shows 15. Sign extension first gives 1111_1111, and the cast reads that as 255.

  3. Why does 8.U(3.W) fail, while connecting 8.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.

  4. What is the width of x * y + z when x, y, and z are all 8-bit UInts?

    Show answer
    1. x * y is 8 + 8 = 16 bits, and + takes the wider operand's width, max(16, 8).
  5. Both UInt(8.W) and 0.U(8.W) have Scala type UInt. 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 an ExpectedHardwareException.

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.

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