Combinational Logic
- 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.
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))
})
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.pe0_ready & io.pe1_ready
io.pe0_data := io.fifo_data
io.pe1_data := io.fifo_data
}
// generate verilog
println(getVerilog(new Arbiter))
// testbench
test(new Arbiter) { c =>
import scala.util.Random
val data = Random.nextInt(65536)
c.io.fifo_data.poke(data.U)
for (i <- 0 until 8) {
c.io.fifo_valid.poke((((i >> 0) % 2) != 0).B)
c.io.pe0_ready.poke((((i >> 1) % 2) != 0).B)
c.io.pe1_ready.poke((((i >> 2) % 2) != 0).B)
c.io.fifo_ready.expect((i > 1).B)
c.io.pe0_valid.expect((i == 3 || i == 7).B)
c.io.pe1_valid.expect((i == 5).B)
if (i == 3 || i ==7) {
c.io.pe0_data.expect((data).U)
} else if (i == 5) {
c.io.pe1_data.expect((data).U)
}
}
}
-
adder: a parameterized adder that can either saturate the output when overflow occurs, or truncate the results (i.e. wrap around). The parameter we pass into it is called
saturate
and has type ScalaBoolean
. This is not a ChiselBool
. 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. ```scala 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)) })val sum = io.in_a +& io.in_b if (saturate) { io.out := Mux(sum > 15.U, 15.U, sum) } else { io.out := sum } }
for (saturate <- Seq(true, false)) { test(new ParameterizedAdder(saturate)) { 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) c.io.in_a.poke(in_a.U) c.io.in_b.poke(in_b.U) if (saturate) { c.io.out.expect(min(in_a + in_b, 15).U) } else { c.io.out.expect(((in_a + in_b) % 16).U) } }
// ensure we test saturation vs. truncation
c.io.in_a.poke(15.U)
c.io.in_b.poke(15.U)
if (saturate) {
c.io.out.expect(15.U)
} else {
c.io.out.expect(14.U)
} } } ```
- Finite State Machine ```scala // life gets hard-er class GradLife extends Module { val io = IO(new Bundle { val state = Input(UInt(2.W)) val coffee = Input(Bool()) val idea = Input(Bool()) val pressure = Input(Bool()) val nextState = Output(UInt(2.W)) })
val idle :: coding :: writing :: grad :: Nil = Enum(4)
io.nextState := idle
when(io.state === idle) {
when(io.coffee) {io.nextState := coding}
.elsewhen(io.pressure) {io.nextState := writing}
.otherwise {io.nextState := idle}
}.elsewhen(io.state === coding) {
when(io.pressure | io.idea) {io.nextState := writing}
.otherwise {io.nextState := coding}
}.elsewhen(io.state === writing) {
when(io.pressure) {io.nextState := grad}
.otherwise {io.nextState := writing}
}
}
println(getVerilog(new GradLife))
// Test test(new GradLife) { c => // verify that the hardware matches the golden model for (state <- 0 to 3) { for (coffee <- List(true, false)) { for (idea <- List(true, false)) { for (pressure <- List(true, false)) { c.io.state.poke(state.U) c.io.coffee.poke(coffee.B) c.io.idea.poke(idea.B) c.io.pressure.poke(pressure.B) } } } } }
## Sequential Logic
1. Parameterized Shift Register
```scala
class MyOptionalShiftRegister(val n: Int, val init: BigInt = 1) extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val in = Input(Bool())
val out = Output(UInt(n.W))
})
val state = RegInit(init.U(n.W))
val nextState = (state << 1) | io.in
when (io.en) {
state := nextState
}
io.out := state
}
// test different depths
for (i <- Seq(3, 4, 8, 24, 65)) {
println(s"Testing n=$i")
test(new MyOptionalShiftRegister(n = i)) { c =>
val inSeq = Seq(0, 1, 1, 1, 0, 1, 1, 0, 0, 1)
var state = c.init
var i = 0
c.io.en.poke(true.B)
while (i < 10 * c.n) {
// poke in repeated inSeq
val toPoke = inSeq(i % inSeq.length)
c.io.in.poke((toPoke != 0).B)
// update expected state
state = ((state * 2) + toPoke) & BigInt("1"*c.n, 2)
c.clock.step(1)
c.io.out.expect(state.U)
i += 1
}
}
}class MyShiftRegister(val init: Int = 1) extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val out = Output(UInt(4.W))
})
val state = RegInit(UInt(4.W), init.U)
val nextState := (state << 1) | io.in
state := nextState
out := state
}
test(new MyShiftRegister()) { c =>
var state = c.init
for (i <- 0 until 10) {
// poke in LSB of i (i % 2)
c.io.in.poke(((i % 2) != 0).B)
// update expected state
state = ((state * 2) + (i % 2)) & 0xf
c.clock.step(1)
c.io.out.expect(state.U)
}
}