// a class extends to built-in Chisel class 'Module' classPassthroughextendsModule{ // io: must be called 'io' and be an 'IO' object or instance val io = IO(newBundle { // Type: Uint, Width: 4 val in = Input(UInt(4.W)) val out = Output(UInt(4.W)) }) // ':=' is a Chisel operator (assign) io.out := io.in }
Module Generator
1 2 3 4 5 6 7 8 9 10 11
// parameterize classPassthroughGenerator(width: Int) extendsModule{ val io = IO(newBundle { val in = Input(UInt(width.W)) val out = Output(UInt(width.W)) }) io.out := io.in } // generate module with different widths println(getVerilog(newPassthroughGenerator(10))) println(getVerilog(newPassthroughGenerator(20)))
Tester
poke, expect, peek
1 2 3 4 5 6 7 8
test(newPassthrough()) { c => c.io.in.poke(0.U) // Set our input to value 0 c.io.out.expect(0.U) // Assert that the output correctly has 0 c.io.in.poke(1.U) // Set our input to value 1 c.io.out.expect(1.U) // Assert that the output correctly has 1 } println("SUCCESS!!") println(c.io.out.peek())
classPrintingModuleextendsModule{ val io = IO(newBundle { val in = Input(UInt(4.W)) val out = Output(UInt(4.W)) }) io.out := io.in
// chisel p interpolator // only when simulation, print every setp(1) printf(p"Print during simulation: IO is $io\n") // only when simulation, print every setp(1) printf("Print during simulation: Input is %d\n", io.in) // only when generation // println is a built-in Scala function that prints to the console. // It cannot be used to print during circuit simulation, // because the generated circuit is FIRRTL or Verilog- not Scala. println(s"Print during generation: Input is ${io.in}") }
test(newPrintingModule ) { c => c.io.in.poke(3.U) c.clock.step(2) // circuit will print // only when testing println(s"Print during testing: Input is ${c.io.in.peek()}") }
1 2 3 4 5 6 7 8 9 10 11
Elaborating design... Print during generation: Input is UInt<4>(IO in unelaborated PrintingModule) Done elaborating. Print during simulation: Input is 3 Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3) Print during simulation: Input is 3 Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3) Print during testing: Input is UInt<4>(3) Print during simulation: Input is 0 Print during simulation: IO is AnonymousBundle(in -> 0, out -> 0) test PrintingModule Success: 0 tests passed in 4 cycles in 0.020066 seconds 199.34 Hz
// concatenates 2 (b10) with 1 (b1) to give 5 (101) io.out_cat := Cat(2.U, 1.U) // assign io_out_cat = 4'h5 }
More operators
Example
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.
classArbiterextendsModule{ val io = IO(newBundle { // 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)) })
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 ScalaBoolean. 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 UInts. 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
1
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.
1
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
classParameterizedAdder(saturate: Boolean) extendsModule{ val io = IO(newBundle { 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 } }
// life gets hard-er classGradLifeextendsModule{ val io = IO(newBundle { 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)
A Regholds its output value until the rising edge of its clock, at which time it takes on the value of its input.
1
val register = Reg(UInt(12.W))
By default, every Chisel Module has an implicit clock that is used by every register in the design. The implicit clock can be overridden for multi-clock designs.
Operations called on a register are performed on the output of the register, and the kind of operations depend on the register’s type.
You aren’t restricted to using UInts with registers, you can use any subclass of the base type chisel3.Data. This includes SInt for signed integers and a lot of other things.
1 2 3 4
// It means the value reg is of type UInt, // and you can do things you can normally do with UInts, like +, -, etc. val reg: UInt = Reg(UInt(4.W)) val reg: SInt = Reg(SInt(4.W))
RegNext()
We didn’t need to specify the register bitwidth this time. It gets inferred from the register’s output connection, in this case io.out.
1 2 3 4 5 6 7 8 9 10 11 12
classRegNextModuleextendsModule{ val io = IO(newBundle { val in = Input(UInt(12.W)) val out = Output(UInt(12.W)) })
// With name and init value val register = RegNext(io.in + 1.U, 0.U)
// register bitwidth is inferred from io.out io.out := RegNext(io.in + 1.U) }
In the latter case, register name is generated instead of explicity defined.
Create a register that resets to a given value with RegInit.
1 2
val myReg = RegInit(UInt(12.W), 0.U) val myReg = RegInit(0.U(12.W))
The generated verilog now has a block that checks if (reset) to reset the register to 0, inside the always @(posedge clock) block.
The register is still initialized to random junk before reset is called.
The PeekPokeTesters always call reset before running your test, but you can manually call reset as well using the reset(n) function, where reset is high for n cycles.
Between calls to poke() and expect(), there is a call to step(1). This tells the test harness to tick the clock once, which will cause the register to pass its input to its output.
Calling poke() on an input immediately propagates the updated values through combinational logic. Calling step() is only needed to update state elements in sequential logic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classRegisterModuleextendsModule{ val io = IO(newBundle { val in = Input(UInt(12.W)) val out = Output(UInt(12.W)) })
Registers are very similar to wires in terms of control flow. They have last connect semantics and can be assigned to conditionally with when, elsewhen, and otherwise.
Shift Register
Each element is a single bit wide.
Has a 4-bit output signal.
Takes a single input bit, which is the next value into the shift register.
Outputs the parallel output of the shift register, with the most significant bit being the last element of the shift register and the least significant bit being the first element of the shift register. Cat may come in handy.
The output initializes at b0001.
Shifts each clock cycle (no enable signal).
Note in Chisel, subword assignment IS ILLEGAL; something like out(0) := in will not work.
1 2 3 4 5 6 7 8 9 10 11
classMyShiftRegister(val init: Int = 1) extendsModule{ val io = IO(newBundle { 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 io.out := state }
classMyOptionalShiftRegister(val n: Int, val init: BigInt = 1) extendsModule{ val io = IO(newBundle { 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 }
Clocks and resets can be overridden separately or together with withClock() {}, withReset() {}, and withClockAndReset() {}.
Clocks have their own type in Chisel Clock and should be declared as such.
Reset is always synchronous and of type Bool. Bool can be converted to Clocks by calling asClock() on them, but you should be careful that you aren’t doing something silly.
(?) Also note that chisel-testers do not currently have complete support for multi-clock designs.
// we need to import multi-clock features import chisel3.experimental.{withClock, withReset, withClockAndReset}
classClockExamplesextendsModule{ val io = IO(newBundle { val in = Input(UInt(10.W)) val alternateReset = Input(Bool()) val alternateClock = Input(Clock()) val outImplicit = Output(UInt()) val outAlternateReset = Output(UInt()) val outAlternateClock = Output(UInt()) val outAlternateBoth = Output(UInt()) })
val imp = RegInit(0.U(10.W)) imp := io.in io.outImplicit := imp
withReset(io.alternateReset) { // everything in this scope with have alternateReset as the reset val altRst = RegInit(0.U(10.W)) altRst := io.in io.outAlternateReset := altRst }
valid, a boolean that says when the input is valid
consts, a vector for all the taps
and 1 output:
out, the filtered input
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
classMyManyDynamicElementVecFir(length: Int) extendsModule{ val io = IO(newBundle { val in = Input(UInt(8.W)) val valid = Input(Bool()) val out = Output(UInt(8.W)) val consts = Input(Vec(length, UInt(8.W))) })
// Such concision! You'll learn what all this means later. val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W))) taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }
io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _) }
ChiselTest’s test method requires a bit less boiler plate. What was the PeekPokeTester is now built into the process.
The poke and expect methods are now part of each individual io element. This gives important hints to the tester to make better checking of types. The peek and step operations are also now methods on io elements.
Another difference is that values poked and expected are Chisel literals. Although pretty simple here, it also provides stronger checking in more advanced and interesting examples. This will be further enhanced with coming improvements in the ability to specify Bundle literals
iotesters (deprived ?)
ChiselTest
poke
poke(c.io.in1, 6)
c.io.in1.poke(6.U)
peek
peek(c.io.out1)
c.io.out1.peek()
expect
expect(c.io.out1, 6)
c.io.out1.expect(6.U)
step
step(1)
c.io.clock.step(1)
initiate
Driver.execute(…) { c =>
test(…) { c =>
Modules with Decoupled Interfaces
1 2 3 4 5
classQueueModule[T <: Data](ioType: T, entries: Int) extendsMultiIOModule{ val in = IO(Flipped(Decoupled(ioType))) val out = IO(Decoupled(ioType)) out <> Queue(in, entries) }
DecoupledIO provides a ready-valid interface for transferring data. Any Chisel data can be wrapped in a DecoupledIO (used as the bits field).
(master)The source drives the bits signal with the data to be transferred and the valid signal when there is data to be transferred. (slave)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.
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.
Output: Decoupled(UInt(8.W)) Bundle with fields:
1 2 3 4 5
val out = newBundle { val valid = Output(Bool()) val ready = Input(Bool()) val bits = Output(UInt(8.W)) }
Input: Flipped(Decoupled(UInt(8.W)))Bundle with fields:
1 2 3 4 5
val in = newBundle { val valid = Input(Bool()) val ready = Output(Bool()) val bits = Input(UInt(8.W)) }
Arbiter
Arbiters routes data from nDecoupledIO sources to one DecoupledIO sink, given a prioritization. There are two types included in Chisel:
Arbiter: prioritizes lower-index producers
RRArbiter: runs in round-robin order
Arbiter routing is implemented in combinational logic.
1 2 3 4 5 6 7 8 9 10 11
classModextendsModule{ // Example circuit using a priority arbiter val io = IO(newBundle { 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(newArbiter(UInt(8.W), 2)) // 2 to 1 Priority Arbiter arbiter.io.in <> io.in io.out <> arbiter.io.out }
Queue
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.
1 2 3 4 5 6 7 8
classQueueextendsModule{ val io = IO(newBundle { 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 }
Bitwise Utilities
PopCount
PopCount returns the number of high (1) bits in the input as a UInt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
test(newModule { // Example circuit using PopCount val io = IO(newBundle { 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}")
Reverse returns the bit-reversed sequence input. NOT ~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
test(newModule { // Example circuit using Reverse val io = IO(newBundle { 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("00001111", 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.
The below two functions provide conversion between binary (UInt) and OneHot encodings, and are inverses of each other:
UInt to OneHot: UIntToOH
OneHot to UInt: OHToUInt
1 2 3 4 5 6 7 8
// UIntToOH val in = Input(UInt(4.W)) val out = Output(UInt(16.W)) io.out := UIntToOH(io.in) // OHToUInt val in = Input(UInt(16.W)) val out = Output(UInt(4.W)) io.out := OHToUInt(io.in)
Mux
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.
Priority Mux
A PriorityMux outputs the value associated with the lowest-index asserted select signal.
test(newModule { // Example circuit using PriorityMux val io = IO(newBundle { 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)
c.io.in_sels(0).poke(false.B) c.io.in_sels(1).poke(true.B) // Select higher index only c.io.in_sels(0).poke(true.B) // Select both - arbitration needed c.io.in_sels(1).poke(true.B) c.io.in_sels(0).poke(true.B) // Select lower index only c.io.in_sels(1).poke(false.B) }
OneHot Mux
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(newModule { // Example circuit using Mux1H val io = IO(newBundle { 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) c.io.in_sels(0).poke(false.B) c.io.in_sels(1).poke(true.B) // Select index 1 c.io.in_sels(0).poke(true.B) // Select index 0 c.io.in_sels(1).poke(false.B) c.io.in_sels(0).poke(false.B) // Select none (invalid) c.io.in_sels(1).poke(false.B) c.io.in_sels(0).poke(true.B) // Select both (invalid) c.io.in_sels(1).poke(true.B) }
Counter
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.
1 2 3 4 5 6 7 8 9 10 11 12
classCntextendsModule{ // Example circuit using Mux1H val io = IO(newBundle { 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() // increasement } io.out := counter.value }
require(...)are pre-elaboration assertions, which are useful when your generator only works with certain parameterizations or when some parameterizations are mutually exclusive or nonsensical.
There is a separate construct for simulation-time assertions called assert(...).
1 2 3 4 5 6 7 8 9 10 11 12 13 14
classParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int) extendsModule{ require(in0Width >= 0) // Pre-elaboration assertions require(in1Width >= 0) require(sumWidth >= 0) val io = IO(newBundle { val in0 = Input(UInt(in0Width.W)) val in1 = Input(UInt(in1Width.W)) val sum = Output(UInt(sumWidth.W)) }) // a +& b includes the carry, a + b does not io.sum := io.in0 +& io.in1 }
classSort4(ascending: Boolean) extendsModule{ val io = IO(newBundle { val in0 = Input(UInt(16.W)) val in1 = Input(UInt(16.W)) val in2 = Input(UInt(16.W)) val in3 = Input(UInt(16.W)) val out0 = Output(UInt(16.W)) val out1 = Output(UInt(16.W)) val out2 = Output(UInt(16.W)) val out3 = Output(UInt(16.W)) })
// this comparison funtion decides < or > based on the module's parameterization defcomp(l: UInt, r: UInt): Bool = { if (ascending) { l < r } else { l > r } }
val row10 = Wire(UInt(16.W)) val row11 = Wire(UInt(16.W)) val row12 = Wire(UInt(16.W)) val row13 = Wire(UInt(16.W))
when(comp(io.in0, io.in1)) { row10 := io.in0 // preserve first two elements row11 := io.in1 }.otherwise { row10 := io.in1 // swap first two elements row11 := io.in0 }
when(comp(io.in2, io.in3)) { row12 := io.in2 // preserve last two elements row13 := io.in3 }.otherwise { row12 := io.in3 // swap last two elements row13 := io.in2 }
val row21 = Wire(UInt(16.W)) val row22 = Wire(UInt(16.W))
when(comp(row11, row12)) { row21 := row11 // preserve middle 2 elements row22 := row12 }.otherwise { row21 := row12 // swap middle two elements row22 := row11 }
val row20 = Wire(UInt(16.W)) val row23 = Wire(UInt(16.W)) when(comp(row10, row13)) { row20 := row10 // preserve the first and the forth elements row23 := row13 }.otherwise { row20 := row13 // swap the first and the forth elements row23 := row10 }
when(comp(row20, row21)) { io.out0 := row20 // preserve first two elements io.out1 := row21 }.otherwise { io.out0 := row21 // swap first two elements io.out1 := row20 }
when(comp(row22, row23)) { io.out2 := row22 // preserve first two elements io.out3 := row23 }.otherwise { io.out2 := row23 // swap first two elements io.out3 := row22 } }
Option
1 2 3
val map = Map("a" -> 1) val a = map("a") // 1 val b = map("b") // Errors!
Map.get returns a key’s value of abstract class Option. Option has two subclasses, **Some **and None.
1 2
val a = map.get("a") // Some(1) val b = map.get("b") // None
Option.get returns a value, which errors if called on None.
Each code block that follows a => operator continues until it reaches either the ending brace of the match or the next case statement.
A match is searched in the order of the case statements, once a case statement has been matched, no other checks against other case statements are made.
case _wildcard handle any value not found. (default)
Multiple Value Matching
1 2 3 4 5 6 7 8
(val1, val2) match { case (true, true) => "wolverine" case (true, false) => "elephant" case (false, true) => println("here") "shrew" case _ => "puppy" }
Type Matching
1 2 3 4 5 6 7 8 9
val sequence = Seq("abc", 1, 0.0, 'a', 'b') sequence.foreach { x => x match { case s: String => println(s"$x is a String") case _: Int | _: Double => println(s"$x is an Int or Double") case 'a' | _: Char => println(s"$x is a Char") case _ => println(s"$x is an unknown type!") } }
Type Matching and Erasure
Type matching has some limitations. Because Scala runs on the JVM, and the JVM does not maintain polymorphic types, you cannot match on them at runtime (because they are all erased). Note that the following example always matches the first case statement, because the [String], [Int], and [Double] polymorphic types are erased, and the case statements are actually matching on just a Seq.
1 2 3 4 5 6 7 8
val sequence = Seq(Seq("a"), Seq(1), Seq(0.0)) sequence.foreach { x => x match { case s: Seq[String] => println(s"$x is a String") case s: Seq[Int] => println(s"$x is an Int") case s: Seq[Double] => println(s"$x is a Double") } }
classHalfFullAdder(val hasCarry: Boolean) extendsModule{ val io = IO(newBundle { ... val carryIn = if (hasCarry) Some(Input(UInt(1.W))) elseNone ... }) val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U) ... }
Optional IO with Zero-Width Wires
Chisel types are allowed to have widths of zero. An IO with width zero is pruned from the emitted Verilog. Anything that tries to use the value of a zero-width wire gets a constant zero.
1 2 3 4 5 6 7 8 9
classHalfFullAdder(val hasCarry: Boolean) extendsModule{ val io = IO(newBundle { ... val carryIn = Input(if (hasCarry) UInt(1.W) elseUInt(0.W)) ... }) val sum = io.a +& io.b +& io.carryIn ... }
Implicit Argument
There can only be one implicit value of a given type.
When we call tooManyCats, we either omit the second implicit argument list (letting the compiler find it for us), or explicitly provide an argument (which can be different than the implicit value).
1 2 3 4 5 6 7 8 9
objectCatDog{ implicitval numberOfCats: Int = 3 // implicit val numberOfDogs: String = "5"
Implicit Conversions are used to automatically convert one Scala object into another.
Generally, implicits can make your code confusing, so we recommend you use them as a last resort. First try inheritance, traits, or method overloading.
1 2 3 4 5
classAnimal(val name: String, val species: String) classHuman(val name: String) implicitdefhuman2animal(h: Human): Animal = newAnimal(h.name, "Homo sapiens") val me = newHuman("Adam") println(me.species)
// Mealy machine has caseclassBinaryMealyParams( // number of states nStates: Int, // initial state s0: Int, // function describing state transition stateTransition: (Int, Boolean) =>Int, // function describing output output: (Int, Boolean) => Int ) { require(nStates >= 0) require(s0 < nStates && s0 >= 0) }
classBinaryMealy(val mp: BinaryMealyParams) extendsModule{ val io = IO(newBundle { val in = Input(Bool()) val out = Output(UInt()) })
val state = RegInit(UInt(), mp.s0.U)
// output zero if no states io.out := 0.U for (i <- 0 until mp.nStates) { when (state === i.U) { when (io.in) { state := mp.stateTransition(i, true).U io.out := mp.output(i, true).U }.otherwise { state := mp.stateTransition(i, false).U io.out := mp.output(i, false).U } } } }
// example from https://en.wikipedia.org/wiki/Mealy_machine val nStates = 3 val s0 = 2 defstateTransition(state: Int, in: Boolean): Int = { if (in) { 1 } else { 0 } } defoutput(state: Int, in: Boolean): Int = { if (state == 2) { return0 } if ((state == 1 && !in) || (state == 0 && in)) { return1 } else { return0 } }
val testParams = BinaryMealyParams(nStates, s0, (state: Int, in: Boolean) => if (in) 1else0, output) // anonymous function println(getVerilog(newBinaryMealy(testParams)))
Build a software Golden Model of a tap configurable FIR.
Redesign our test to use this model, and confirm that it works.
Refactor our My4ElementFir to allow an configurable number of taps.
Test the new circuit using our new test harness.
Golden Model
1 2 3 4 5 6 7 8 9 10 11 12
classScalaFirFilter(taps: Seq[Int]) { var pseudoRegisters = List.fill(taps.length)(0)
defpoke(value: Int): Int = { pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1) var accumulator = 0 for(i <- taps.indices) { accumulator += taps(i) * pseudoRegisters(i) } accumulator } }
mutable.ArrayBuffer[]
We are using a Scala collection type called ArrayBuffer. ArrayBuffer allows you to append elements using the **+= **operator (also insert and delete, but we don’t need this).
classMyManyElementFir(consts: Seq[Int], bitWidth: Int) extendsModule{ val io = IO(newBundle { val in = Input(UInt(bitWidth.W)) val out = Output(UInt(bitWidth.W)) })
val regs = mutable.ArrayBuffer[UInt]() for(i <- 0 until consts.length) { if(i == 0) regs += io.in // not register else regs += RegNext(regs.last, 0.U) // register } val muls = mutable.ArrayBuffer[UInt]() for(i <- 0 until consts.length) { muls += regs(i) * consts(i).U }
classMyManyDynamicElementVecFir(length: Int) extendsModule{ val io = IO(newBundle { val in = Input(UInt(8.W)) val out = Output(UInt(8.W)) val consts = Input(Vec(length, UInt(8.W))) })
Writes will only be performed when wen (write enable) is asserted.
The register at index 0 (the first register) is always zero when you read from it, regardless of what you write to it (it’s often useful to have 0 handy).
io.out := (taps zip io.consts).map { case (a, b) => a * b }.reduce(_ + _)
taps is the list of all samples, with taps(0) = io.in, taps(1) = regs(0), etc.
(taps zip io.consts) or (taps.zip(io.consts)) : combines two lists, taps and io.consts, into one list, where each element is a tuple of the elements at the inputs at the corresponding position. Concretely, its value would be [(taps(0), io.consts(0)), (taps(1), io.consts(1)), ..., (taps(n), io.consts(n))].
.map { case (a, b) => a * b } : applies the anonymous function (takes a tuple of two elements returns their product) to the elements of the list, and returns the result. In this case, the result is equivalent to muls in the verbose example, and has the value [taps(0) * io.consts(0), taps(1) * io.consts(1), ..., taps(n) * io.consts(n)].
.reduce(_ + _) : applies the function (addition of elements) to elements of the list. However, it takes two arguments: the first is the current accumulation, and the second is the list element (in the first iteration, it just adds the first two elements). These are given by the two underscores in the parentheses. The result would then be, assuming left-to-right traversal, (((muls(0) + muls(1)) + muls(2)) + ...) + muls(n), with the result of deeper-nested parentheses evaluated first. This is the output of the convolution.
Functions as Arguments
Different ways of specifying functions
_ + _: For functions where each argument is referred to exactly once, you may be able to use an underscore (_) to refer to each argument. In the example above, the reduce argument function took two arguments and could be specified as _ + _.
(a, b) => a * b: Specifying the inputs argument list explicitly. With the general form of putting the argument list in parentheses, followed by =>, followed by the function body referring to those arguments.
case (a, b) => a * b: When tuple unpacking is needed, using a case statement, as in case (a, b) => a * b. That takes a single argument, a tuple of two elements, and unpacks it into variables a and b, which can then be used in the function body.
Scala Collections API
map
List[A].map type signature: map[B](f: (A) ⇒ B): List[B]
It takes an argument of type (f: (A) ⇒ B), or a function that takes one argument of type A (the same type as the element of the input List) and returns a value of type B (which can be anything). map then returns a new list of type B (the return type of the argument function).
println(0 to 10) // Range 0 to 10 println(0 until 10) // Range 0 until 10
// Those largely behave like lists, and can be useful for generating indices: val myList = List("a", "b", "c", "d") println((0 until 4).map(myList(_))) // Vector(a, b, c, d)
case
1 2 3 4 5 6
val myAnyList = List(1, 2, "3", 4L, myList) myAnyList.map { case (_:Int | _:Long) => "Number" case _:String => "String" case _ => "error" }
zipWithIndex
List.zipWithIndex type signature: zipWithIndex: List[(A, Int)]
It returns a list where each element is a tuple of the original elements, and the index (with the first one being zero). So List(“a”, “b”, “c”, “d”).zipWithIndex would return List((“a”, 0), (“b”, 1), (“c”, 2), (“d”, 3))
List[A].reduce type signature: reduce(op: (A, A) ⇒ A): A
1 2 3 4 5
println(List(1, 2, 3, 4).reduce((a, b) => a + b)) // returns the sum of all the elements println(List(1, 2, 3, 4).reduce(_ * _)) // returns the product of all the elements println(List(1, 2, 3, 4).map(_ + 1).reduce(_ + _)) // you can chain reduce onto the result of a map // Important note: reduce will fail with an empty list println(List[Int]().reduce(_ * _)) // Error
reduceLeftreduceRight : direction, type
1 2
reduceLeft[B >: A](op: (B, A) => B): B reduceRight[B >: A](op: (A, B) => B): B
fold
List[A].fold type signature: fold(z: A)(op: (A, A) ⇒ A): A It is very similar to reduce, except that you can specify the initial accumulation value.
1 2 3
println(List(1, 2, 3, 4).fold(0)(_ + _)) // equivalent to the sum using reduce println(List(1, 2, 3, 4).fold(1)(_ + _)) // like above, but accumulation starts at 1 println(List().fold(1)(_ + _)) // unlike reduce, does not fail on an empty input
scan
(B, B) : same type
1
defscan[B >: A, That](z: B)(op: (B, B) ⇒ B)(implicit cbf: CanBuildFrom[Repr, B, That]): That
(A, B) : can be different type
1
defscanLeft[B, That](z: B)(op: (B, A) ⇒ B)(implicit bf: CanBuildFrom[Repr, B, That]): That
1
defscanRight[B, That](z: B)(op: (A, B) ⇒ B)(implicit bf: CanBuildFrom[Repr, B, That]): That
Exercise: Decoupled Arbiter
Architecturally:
io.out.valid is true if any of the inputs are valid
Consider having an internal wire of the selected channel
Each input’s ready is true if the output is ready, AND that channel is selected (this does combinationally couple ready and valid, but we’ll ignore it for now…)
These constructs may help:
map, especially for returning a Vec of sub-elements, for example io.in.map(_.valid) returns a list of valid signals of the input Bundles
PriorityMux(List[Bool, Bits]), which takes in a list of valid signals and bits, returning the first element that is valid
Dynamic index on a Vec, by indexing with a UInt, for example io.in(0.U)
classMyRoutingArbiter(numChannels: Int) extendsModule{ val io = IO(newBundle { val in = Vec(numChannels, Flipped(Decoupled(UInt(8.W)))) val out = Decoupled(UInt(8.W)) } )
// YOUR CODE BELOW io.out.valid := io.in.map(_.valid).reduce(_ || _) val channel = PriorityMux( io.in.map(_.valid).zipWithIndex.map { case (valid, index) => (valid, index.U) } ) io.out.bits := io.in(channel).bits io.in.map(_.ready).zipWithIndex.foreach { case (ready, index) => ready := io.out.ready && channel === index.U } // for => foreach + collection // for (i <- 0 until numChannels) { // io.in(i).ready := io.out.ready && i.U === selChannel // } }