Prev: Generators: Collections
Next: Higher-Order Functions
Chisel is all about re-use, so it only makes sense to provide a standard library of interfaces (encouraging interoperability of RTL) and generators for commonly-used hardware blocks.
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.tester._
import chisel3.tester.RawTester.test
The Chisel3 cheatsheet contains a summary of all the major hardware construction APIs, including some of the standard library utilities that we'll introduce below.
One of the commonly used interfaces provided by Chisel is DecoupledIO
, providing a ready-valid interface for transferring data. The idea is that the source drives the bits
signal with the data to be transferred and the valid
signal when there is data to be transferred. The sink drives the ready
signal when it is ready to accept data, and data is considered transferred when both ready
and valid
are asserted on a cycle.
This provides a flow control mechanism in both directions for data transfer, including a backpressure mechanism.
Note: ready
and valid
should not be combinationally coupled, otherwise this may result in unsynthesizable combinational loops. ready
should only be dependent on whether the sink is able to receive data, and valid
should only be dependent on whether the source has data. Only after the transaction (on the next clock cycle) should the values update.
Any Chisel data can be wrapped in a DecoupledIO
(used as the bits
field) as follows:
val myChiselData = UInt(8.W)
// or any Chisel data type, such as Bool(), SInt(...), or even custom Bundles
val myDecoupled = Decoupled(myChiselData)
The above creates a new DecoupledIO
Bundle with fields
valid
: Output(Bool)ready
: Input(Bool)bits
: Output(UInt(8.W))The rest of the section will be structured somewhat differently from the ones before: instead of giving you coding exercises, we're going to give some code examples and testcases that print the circuit state. Try to predict what will be printed before just running the tests.
Queue
creates a FIFO (first-in, first-out) queue with Decoupled interfaces on both sides, allowing backpressure. Both the data type and number of elements are configurable.
test(new Module {
// Example circuit using a Queue
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val out = Decoupled(UInt(8.W))
})
val queue = Queue(io.in, 2) // 2-element queue
io.out <> queue
}) { c =>
c.io.out.ready.poke(false.B)
c.io.in.valid.poke(true.B) // Enqueue an elemen.Ut
c.io.in.bits.poke(42.U)
println(s"Starting:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
c.io.in.valid.poke(true.B) // Enqueue another elemen.Ut
c.io.in.bits.poke(43.U)
// What do you think io.out.valid and io.out.bits will be?
println(s"After first enqueue:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
c.io.in.valid.poke(true.B) // Read a element, attempt to enqueu.Ue
c.io.in.bits.poke(44.U)
c.io.out.ready.poke(true.B)
// What do you think io.in.ready will be, and will this enqueue succeed, and what will be read?
println(s"On first read:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
c.io.in.valid.poke(false.B) // Read elements ou.Ut
c.io.out.ready.poke(true.B)
// What do you think will be read here?
println(s"On second read:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
// Will a third read produce anything?
println(s"On third read:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
}
Arbiters routes data from n DecoupledIO
sources to one DecoupledIO
sink, given a prioritization.
There are two types included in Chisel:
Arbiter
: prioritizes lower-index producersRRArbiter
: runs in round-robin orderNote that Arbiter routing is implemented in combinational logic.
The below example will demonstrate the use of the priority arbiter (which you will also implement in the next section):
test(new Module {
// Example circuit using a priority arbiter
val io = IO(new Bundle {
val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
val out = Decoupled(UInt(8.W))
})
// Arbiter doesn't have a convenience constructor, so it's built like any Module
val arbiter = Module(new Arbiter(UInt(8.W), 2)) // 2 to 1 Priority Arbiter
arbiter.io.in <> io.in
io.out <> arbiter.io.out
}) { c =>
c.io.in(0).valid.poke(false.B)
c.io.in(1).valid.poke(false.B)
println(s"Start:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
c.io.in(1).valid.poke(true.B) // Valid input 1
c.io.in(1).bits.poke(42.U)
// What do you think the output will be?
println(s"valid input 1:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
c.io.in(0).valid.poke(true.B) // Valid inputs 0 and 1
c.io.in(0).bits.poke(43.U)
// What do you think the output will be? Which inputs will be ready?
println(s"valid inputs 0 and 1:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
c.io.in(1).valid.poke(false.B) // Valid input 0
// What do you think the output will be?
println(s"valid input 0:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
}
test(new Module {
// Example circuit using Reverse
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
io.out := PopCount(io.in)
}) { c =>
// Integer.parseInt is used create an Integer from a binary specification
c.io.in.poke(Integer.parseInt("00000000", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
c.io.in.poke(Integer.parseInt("00001111", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
c.io.in.poke(Integer.parseInt("11001010", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
c.io.in.poke(Integer.parseInt("11111111", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
}
test(new Module {
// Example circuit using Reverse
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
io.out := Reverse(io.in)
}) { c =>
// Integer.parseInt is used create an Integer from a binary specification
c.io.in.poke(Integer.parseInt("01010101", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
c.io.in.poke(Integer.parseInt("00001111", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
c.io.in.poke(Integer.parseInt("11110000", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
c.io.in.poke(Integer.parseInt("11001010", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
}
OneHot is an encoding of integers where there is one wire for each value, and exactly one wire is high. This allows the efficient creation of some functions, for example muxes. However, behavior may be undefined if the one-wire-high condition is not held.
The below two functions provide conversion between binary (UInt
) and OneHot encodings, and are inverses of each other:
UIntToOH
OHToUInt
test(new Module {
// Example circuit using UIntToOH
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(16.W))
})
io.out := UIntToOH(io.in)
}) { c =>
c.io.in.poke(0.U)
println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
c.io.in.poke(1.U)
println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
c.io.in.poke(8.U)
println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
c.io.in.poke(15.U)
println(s"in=${c.io.in.peek().litValue}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
}
test(new Module {
// Example circuit using OHToUInt
val io = IO(new Bundle {
val in = Input(UInt(16.W))
val out = Output(UInt(4.W))
})
io.out := OHToUInt(io.in)
}) { c =>
c.io.in.poke(Integer.parseInt("0000 0000 0000 0001".replace(" ", ""), 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
c.io.in.poke(Integer.parseInt("0000 0000 1000 0000".replace(" ", ""), 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
c.io.in.poke(Integer.parseInt("1000 0000 0000 0001".replace(" ", ""), 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
// Some invalid inputs:
// None high
c.io.in.poke(Integer.parseInt("0000 0000 0000 0000".replace(" ", ""), 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
// Multiple high
c.io.in.poke(Integer.parseInt("0001 0100 0010 0000".replace(" ", ""), 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
}
These muxes take in a list of values with select signals, and output the value associated with the lowest-index select signal.
These can either take a list of (select: Bool, value: Data) tuples, or corresponding lists of selects and values as arguments. For simplicity, the examples below only demonstrate the second form.
A PriorityMux
outputs the value associated with the lowest-index asserted select signal.
An Mux1H
provides an efficient implementation when it is guaranteed that exactly one of the select signals will be high. Behavior is undefined if the assumption is not true.
test(new Module {
// Example circuit using PriorityMux
val io = IO(new Bundle {
val in_sels = Input(Vec(2, Bool()))
val in_bits = Input(Vec(2, UInt(8.W)))
val out = Output(UInt(8.W))
})
io.out := PriorityMux(io.in_sels, io.in_bits)
}) { c =>
c.io.in_bits(0).poke(10.U)
c.io.in_bits(1).poke(20.U)
// Select higher index only
c.io.in_sels(0).poke(false.B)
c.io.in_sels(1).poke(true.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
// Select both - arbitration needed
c.io.in_sels(0).poke(true.B)
c.io.in_sels(1).poke(true.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
// Select lower index only
c.io.in_sels(0).poke(true.B)
c.io.in_sels(1).poke(false.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
}
test(new Module {
// Example circuit using Mux1H
val io = IO(new Bundle {
val in_sels = Input(Vec(2, Bool()))
val in_bits = Input(Vec(2, UInt(8.W)))
val out = Output(UInt(8.W))
})
io.out := Mux1H(io.in_sels, io.in_bits)
}) { c =>
c.io.in_bits(0).poke(10.U)
c.io.in_bits(1).poke(20.U)
// Select index 1
c.io.in_sels(0).poke(false.B)
c.io.in_sels(1).poke(true.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
// Select index 0
c.io.in_sels(0).poke(true.B)
c.io.in_sels(1).poke(false.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
// Select none (invalid)
c.io.in_sels(0).poke(false.B)
c.io.in_sels(1).poke(false.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
// Select both (invalid)
c.io.in_sels(0).poke(true.B)
c.io.in_sels(1).poke(true.B)
println(s"in_sels=${c.io.in_sels(0).peek().litValue}, out=${c.io.out.peek().litValue}")
}
Counter
is a counter that can be incremented once every cycle, up to some specified limit, at which point it overflows. Note that it is not a Module, and its value is accessible.
test(new Module {
// Example circuit using Mux1H
val io = IO(new Bundle {
val count = Input(Bool())
val out = Output(UInt(2.W))
})
val counter = Counter(3) // 3-count Counter (outputs range [0...2])
when(io.count) {
counter.inc()
}
io.out := counter.value
}) { c =>
c.io.count.poke(true.B)
println(s"start: counter value=${c.io.out.peek().litValue}")
c.clock.step(1)
println(s"step 1: counter value=${c.io.out.peek().litValue}")
c.clock.step(1)
println(s"step 2: counter value=${c.io.out.peek().litValue}")
c.io.count.poke(false.B)
c.clock.step(1)
println(s"step without increment: counter value=${c.io.out.peek().litValue}")
c.io.count.poke(true.B)
c.clock.step(1)
println(s"step again: counter value=${c.io.out.peek().litValue}")
}