class MAC extends Module { val io = IO(new Bundle { val in_a = Input(UInt(4.W)) val in_b = Input(UInt(4.W)) val in_c = Input(UInt(4.W)) val out = Output(UInt(8.W)) })io.out := (io.in_a * io.in_b) + io.in_c }
Prev: Your First Chisel Module
Next: Control Flow
In this section you will see how to use Chisel components to implement combinational logic.
We will demonstrate how three of the basic Chisel types: UInt
- unsigned integer; SInt
- signed integer, and Bool
- true or false may be connected and operated upon.
Notice how all Chisel variables are declared as Scala val
s.
Never use a Scala var
for a hardware construct, since the construct itself may never change once defined; only its value may change when running the hardware.
Wires may be used for parameterized types.
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
Now that you understand how Module
s are constructed, let's make some hardware! Take a look at the empty module below.
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
}
We've called our class MyModule
, and it extends Module
. This means it gets mapped to a hardware module in Verilog. Our MyModule
module has one input and one output. The input is a 4-bit unsigned integer (UInt
), and so is the output.
Example: Scala and Chisel Operators Look the Same
Let's look at different operations we can perform on data.
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
val two = 1 + 1
println(two)
val utwo = 1.U + 1.U
println(utwo)
io.out := io.in
}
println(getVerilog(new MyModule))
We create two val
s. The first adds two Scala Int
s, so println
prints out the integer 2. The second val
adds two Chisel UInt
s together, so println
sees this as a hardware node and prints out the type name and pointer (chisel3.core.UInt@d
). Note that 1.U
is a type cast from a Scala Int
(1) to a Chisel UInt
literal.
We need to drive the output to something, so we just connect it to the input for now, as with the passthrough module in the previous tutorial.
Example: Incompatible Operation
What happens if we add a Chisel 1.U
to the literal 1
? These types are incompatible, as the former is a hardware wire of value 1, while the latter is a Scala value of 1. So Chisel will give a type mismatch error.
class MyModuleTwo extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
val twotwo = 1.U + 1
println(twotwo)
io.out := io.in
}
println(getVerilog(new MyModule))
It's important to remember the distinction between types when performing operations. Scala is a strongly typed language, so any type casting must be explicit.
Example: More Chisel Operators
Other common operations are subtraction and multiplication. These are handled on unsigned integers as expected. Let's see these in action. We show the Verilog, though there's some underlying Chisel features that obfuscate the simple code we would expect.
class MyOperators extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out_add = Output(UInt(4.W))
val out_sub = Output(UInt(4.W))
val out_mul = Output(UInt(4.W))
})
io.out_add := 1.U + 4.U
io.out_sub := 2.U - 1.U
io.out_mul := 4.U * 2.U
}
println(getVerilog(new MyOperators))
And here's a sample tester for the above operations. Instead of using an anonymous tester class like in the previous tutorial, we'll create an explicit tester class. This is just an alternative way of writing a tester.
class MyOperatorsTester(c: MyOperators) extends PeekPokeTester(c) {
expect(c.io.out_add, 5)
expect(c.io.out_sub, 1)
expect(c.io.out_mul, 8)
}
assert(Driver(() => new MyOperators) {c => new MyOperatorsTester(c)})
println("SUCCESS!!")
Example: Mux and Concatenation
In addition to addition, subtraction, and multplication, Chisel has mux and concatenation operators. These are shown below. The Mux
operates like a traditional ternary operator, with the order (select, value if true, value if false). Note that true.B
and false.B
are the preferred ways to create Chisel Bool literals. The Cat
ordering is MSB then LSB (where B refers to bit or bits), and only takes two arguments. Concatenating more than two values requires multiple Cat
calls or advanced Chisel and Scala features covered in later sections.
class MyOperatorsTwo extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out_mux = Output(UInt(4.W))
val out_cat = Output(UInt(4.W))
})
val s = true.B
io.out_mux := Mux(s, 3.U, 0.U) // should return 3.U, since s is true
io.out_cat := Cat(2.U, 1.U) // concatenates 2 (b10) with 1 (b1) to give 5 (101)
}
println(getVerilog(new MyOperatorsTwo))
class MyOperatorsTwoTester(c: MyOperatorsTwo) extends PeekPokeTester(c) {
expect(c.io.out_mux, 3)
expect(c.io.out_cat, 5)
}
assert(Driver(() => new MyOperatorsTwo) {c => new MyOperatorsTwoTester(c)})
println("SUCCESS!!")
Notice how the Verilog contains constants instead of actual mux or concatenation logic. This is because FIRRTL transformations have simplified the circuit, eliminating obvious logic.
For a more complete list of Chisel operators, see the Chisel cheatsheet. For the most complete list of operators and their implementation details, look through the Chisel API.
Exercise: MAC
Create a Chisel module that implements the multiply accumulate function, (A*B)+C
, and passes the testbench.
class MAC extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(4.W))
val in_b = Input(UInt(4.W))
val in_c = Input(UInt(4.W))
val out = Output(UInt(8.W))
})
???
}
class MACTester(c: MAC) extends PeekPokeTester(c) {
val cycles = 100
import scala.util.Random
for (i <- 0 until cycles) {
val in_a = Random.nextInt(16)
val in_b = Random.nextInt(16)
val in_c = Random.nextInt(16)
poke(c.io.in_a, in_a)
poke(c.io.in_b, in_b)
poke(c.io.in_c, in_c)
expect(c.io.out, in_a*in_b+in_c)
}
}
assert(Driver(() => new MAC) {c => new MACTester(c)})
println("SUCCESS!!")
class MAC extends Module { val io = IO(new Bundle { val in_a = Input(UInt(4.W)) val in_b = Input(UInt(4.W)) val in_c = Input(UInt(4.W)) val out = Output(UInt(8.W)) })io.out := (io.in_a * io.in_b) + io.in_c }
Exercise: Arbiter
The following circuit arbitrates data coming from a FIFO into two parallel processing units. The FIFO and processing elements (PEs) communicate with ready-valid interfaces. Construct the arbiter to send data to whichever PE is ready to receive data, prioritizing PE0 if both are ready to receive data. Remember that the arbiter should tell the FIFO that it's ready to receive data when at least one of the PEs can receive data. Also, wait for a PE to assert that it's ready before asserting that the data are valid. You will likely need binary operators to complete this exercise.
class Arbiter extends Module {
val io = IO(new Bundle {
// FIFO
val fifo_valid = Input(Bool())
val fifo_ready = Output(Bool())
val fifo_data = Input(UInt(16.W))
// PE0
val pe0_valid = Output(Bool())
val pe0_ready = Input(Bool())
val pe0_data = Output(UInt(16.W))
// PE1
val pe1_valid = Output(Bool())
val pe1_ready = Input(Bool())
val pe1_data = Output(UInt(16.W))
})
???
}
class ArbiterTester(c: Arbiter) extends PeekPokeTester(c) {
import scala.util.Random
val data = Random.nextInt(65536)
poke(c.io.fifo_data, data)
for (i <- 0 until 8) {
poke(c.io.fifo_valid, (i>>0)%2)
poke(c.io.pe0_ready, (i>>1)%2)
poke(c.io.pe1_ready, (i>>2)%2)
expect(c.io.fifo_ready, i>1)
expect(c.io.pe0_valid, i==3 || i==7)
expect(c.io.pe1_valid, i==5)
if (i == 3 || i ==7) {
expect(c.io.pe0_data, data)
} else if (i == 5) {
expect(c.io.pe1_data, data)
}
}
}
assert(Driver(() => new Arbiter) {c => new ArbiterTester(c)})
println("SUCCESS!!")
io.fifo_ready := io.pe0_ready || io.pe1_ready io.pe0_valid := io.fifo_valid && io.pe0_ready io.pe1_valid := io.fifo_valid && io.pe1_ready && !io.pe0_ready io.pe0_data := io.fifo_data io.pe1_data := io.fifo_data
Exercise: Parameterized Adder (Optional)
This optional exercise exposes you to one of the most powerful features of Chisel, it's parameterization capabilities. To demonstrate this, we'll construct a parameterized adder that can either saturate the output when overflow occurs, or truncate the results (i.e. wrap around).
First, look at the Module
below. The parameter we pass into it is called saturate
and has type Scala Boolean
. This is not a Chisel Bool
. So, we're not creating a single hardware adder that can either saturate or truncate, but rather we're creating a generator that produces either a saturating hardware adder or a truncating hardware adder. The decision is made at compile time.
Next, notice the inputs and outputs are all 4-bit UInt
s. Chisel has built-in width inferencing, and if you look at the cheatsheet, you'll see that the bitwidth of a normal summation is equal to the maximum bitwidth of the two inputs. This means that
val sum = io.in_a + io.in_b
will make sum
a 4-bit wire, and the value will be the truncated result for 4-bit inputs. To check if the summation should saturate, you need to place the result in a 5-bit wire. This can be done with the +&
summation, as seen on the cheatsheet.
val sum = io.in_a +& io.in_b
Finally, note that connecting a 4-bit UInt
wire to a 5-bit UInt
wire will truncate the MSB by default. You can use this to easily truncate the 5-bit sum for the non-saturating adder.
class ParameterizedAdder(saturate: Boolean) extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(4.W))
val in_b = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
???
}
class ParameterizedAdderTester(c: ParameterizedAdder, saturate: Boolean) extends PeekPokeTester(c) {
// 100 random tests
val cycles = 100
import scala.util.Random
import scala.math.min
for (i <- 0 until cycles) {
val in_a = Random.nextInt(16)
val in_b = Random.nextInt(16)
poke(c.io.in_a, in_a)
poke(c.io.in_b, in_b)
if (saturate) {
expect(c.io.out, min(in_a+in_b, 15))
} else {
expect(c.io.out, (in_a+in_b)%16)
}
}
// ensure we test saturation vs. truncation
poke(c.io.in_a, 15)
poke(c.io.in_b, 15)
if (saturate) {
expect(c.io.out, 15)
} else {
expect(c.io.out, 14)
}
}
for (saturate <- Seq(true, false)) {
assert(Driver(() => new ParameterizedAdder(saturate)) {c => new ParameterizedAdderTester(c, saturate)})
}
println("SUCCESS!!")
val sum = io.in_a +& io.in_b if (saturate) { io.out := Mux(sum > 15.U, 15.U, sum) } else { io.out := sum }