| @@ -5,7 +5,7 @@ | |||
| Finn ut hvordan bundles, defs etc burde fungere. | |||
| * Now | |||
| ** TODO Port Babby to chisel3 | |||
| ** DONE Port Babby to chisel3 | |||
| *** DONE compile and run | |||
| *** DONE fix deprecation | |||
| @@ -6,12 +6,9 @@ import chisel3.iotesters.PeekPokeTester | |||
| object CoreMain { | |||
| def main(args: Array[String]): Unit = { | |||
| // chiselMainTest(args, () => Module(new daisyVector(4, 32))) { c => new daisyVectorTest(c) } | |||
| // chiselMainTest(args, () => Module(new daisyGrid(4, 3, 32))) { c => new daisyGridTest(c) } | |||
| // chiselMainTest(args, () => Module(new daisyMultiplier(3, 2, 2, 3, 32))) { c => new daisyMultiplierTest(c) } | |||
| iotesters.Driver.execute(args, () => new daisyMultiplier(3, 2, 2, 3, 32)) { | |||
| c => new daisyMultiplierTest(c) | |||
| iotesters.Driver.execute(args, () => new mySelector(10)){ | |||
| c => new mySelectorTest(c) | |||
| } | |||
| } | |||
| } | |||
| @@ -59,7 +56,7 @@ object Extras { | |||
| val vecA = List(1, 2, 4) | |||
| val vecB = List(2, -3, 1) | |||
| val dotProductForLoop = { | |||
| def dotProductForLoop(vecA: List[Int], vecB: List[Int]) = { | |||
| var dotProduct = 0 | |||
| for(i <- 0 until vecA.length){ | |||
| dotProduct = dotProduct + (vecA(i) * vecB(i)) | |||
| @@ -81,4 +78,25 @@ object Extras { | |||
| // This is not good code!!! | |||
| val tooFancyDotProduct = | |||
| (0 /: (vecA zip vecB)){ case(acc, ab) => acc + (ab._1 * ab._2) } | |||
| type Matrix[A] = List[List[A]] | |||
| def vectorMatrixMultiply(vec: List[Int], matrix: Matrix[Int]): List[Int] = { | |||
| val transposed = matrix.transpose | |||
| val outputVector = Array.ofDim[Int](vec.length) | |||
| for(ii <- 0 until matrix.length){ | |||
| outputVector(ii) = dotProductForLoop(vec, transposed(ii)) | |||
| } | |||
| outputVector.toList | |||
| } | |||
| val vec = List(1, 0, 1) | |||
| val matrix = List( | |||
| List(2, 1, 2), | |||
| List(3, 2, 3), | |||
| List(4, 1, 1) | |||
| ) | |||
| println(vectorMatrixMultiply(vec, matrix)) | |||
| } | |||
| @@ -0,0 +1,157 @@ | |||
| package Core | |||
| import chisel3._ | |||
| import chisel3.core.Input | |||
| import chisel3.iotesters.PeekPokeTester | |||
| class myIncrement(incrementBy: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| io.dataOut := io.dataIn + incrementBy.U | |||
| } | |||
| class myIncrementTwice(incrementBy: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val first = Module(new myIncrement(incrementBy)) | |||
| val second = Module(new myIncrement(incrementBy)) | |||
| first.io.dataIn := io.dataIn | |||
| second.io.dataIn := first.io.dataOut | |||
| io.dataOut := second.io.dataOut | |||
| } | |||
| class myIncrementN(incrementBy: Int, numIncrementors: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val incrementors = Array.fill(numIncrementors){ Module(new myIncrement(incrementBy)) } | |||
| for(ii <- 1 until numIncrementors){ | |||
| incrementors(ii).io.dataIn := incrementors(ii - 1).io.dataOut | |||
| } | |||
| incrementors(0).io.dataIn := io.dataIn | |||
| io.dataOut := incrementors(numIncrementors).io.dataOut | |||
| } | |||
| class myDelay() extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val reg = RegInit(UInt(32.W), 0.U) | |||
| reg := io.dataIn | |||
| io.dataOut := reg | |||
| } | |||
| class myDelayN(steps: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val delayers = Array.fill(steps){ Module(new myDelay()) } | |||
| for(ii <- 1 until steps){ | |||
| delayers(ii).io.dataIn := delayers(ii - 1).io.dataOut | |||
| } | |||
| delayers(0).io.dataIn := io.dataIn | |||
| io.dataOut := delayers(steps).io.dataOut | |||
| } | |||
| class mySelector(numValues: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val next = Input(Bool()) | |||
| val dataOut = Output(UInt(32.W)) | |||
| val newOutput = Output(Bool()) | |||
| } | |||
| ) | |||
| val counter = RegInit(UInt(Chisel.log2Up(numValues).W), 0.U) | |||
| val nextOutputIsFresh = RegInit(Bool(), true.B) | |||
| // Generate random values. Using the when keyword we choose which random | |||
| // value should drive the dataOut signal | |||
| io.dataOut := 0.U | |||
| List.fill(numValues)(scala.util.Random.nextInt(100)).zipWithIndex.foreach { | |||
| case(rand, idx) => | |||
| when(counter === idx.U){ | |||
| if(rand < 50) | |||
| io.dataOut := rand.U | |||
| else | |||
| io.dataOut := (rand + 100).U | |||
| } | |||
| } | |||
| // While chisel comes with an inbuilt Counter, we implement ours the old fashion way | |||
| // There are far more elegant ways of implementing this, read the chisel docs, discuss | |||
| // best practice among yourselves and experiment! | |||
| nextOutputIsFresh := true.B | |||
| when(io.next === true.B){ | |||
| when(counter < (numValues - 1).U){ | |||
| counter := counter + 1.U | |||
| }.otherwise { | |||
| counter := 0.U | |||
| } | |||
| }.otherwise { | |||
| nextOutputIsFresh := false.B | |||
| } | |||
| io.newOutput := nextOutputIsFresh | |||
| } | |||
| class mySelectorTest(c: mySelector) extends PeekPokeTester(c) { | |||
| poke(c.io.next, true.B) | |||
| for(ii <- 0 until 10){ | |||
| val wasStale = peek(c.io.newOutput) == 0 | |||
| val output = peek(c.io.dataOut).toString() | |||
| println(s"at step $ii:") | |||
| println(s"data out is $output") | |||
| println(s"was the output fresh? ${!wasStale}") | |||
| println() | |||
| step(1) | |||
| } | |||
| poke(c.io.next, false.B) | |||
| for(ii <- 0 until 3){ | |||
| val wasStale = peek(c.io.newOutput) == 0 | |||
| val output = peek(c.io.dataOut).toString() | |||
| println(s"at step $ii:") | |||
| println(s"data out is $output") | |||
| println(s"was the output fresh? ${!wasStale}") | |||
| println() | |||
| step(1) | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| package Core | |||
| import chisel3._ | |||
| import chisel3.core.Input | |||
| import chisel3.iotesters.PeekPokeTester | |||
| import chisel3.util.Counter | |||
| /** | |||
| DaisyVectors are not indexed. They have no control inputs or outputs, only data. | |||
| */ | |||
| class daisyDot(elements: Int, dataWidth: Int) extends Module{ | |||
| val io = IO(new Bundle { | |||
| val dataInA = Input(UInt(dataWidth.W)) | |||
| val dataInB = Input(UInt(dataWidth.W)) | |||
| val dataOut = Output(UInt(dataWidth.W)) | |||
| val outputValid = Output(Bool()) | |||
| }) | |||
| val counter = Counter(elements) | |||
| val accumulator = RegInit(UInt(dataWidth.W), 0.U) | |||
| /** | |||
| Your implementation here | |||
| */ | |||
| /** | |||
| LF | |||
| */ | |||
| val product = io.dataInA * io.dataInB | |||
| when(counter.inc()){ | |||
| io.outputValid := true.B | |||
| accumulator := 0.U | |||
| }.otherwise{ | |||
| io.outputValid := false.B | |||
| accumulator := accumulator + product | |||
| } | |||
| io.dataOut := accumulator + product | |||
| } | |||
| @@ -13,7 +13,7 @@ class daisyGrid(rows: Int, cols: Int, dataWidth: Int) extends Module{ | |||
| val readEnable = Input(Bool()) | |||
| val dataIn = Input(UInt(dataWidth.W)) | |||
| val readRow = Input(UInt(8.W)) | |||
| val rowSelect = Input(UInt(8.W)) | |||
| val dataOut = Output(UInt(dataWidth.W)) | |||
| }) | |||
| @@ -25,6 +25,13 @@ class daisyGrid(rows: Int, cols: Int, dataWidth: Int) extends Module{ | |||
| val elements = rows*cols | |||
| /** | |||
| Your implementation here | |||
| */ | |||
| /** | |||
| LF | |||
| */ | |||
| io.dataOut := 0.U | |||
| for(ii <- 0 until rows){ | |||
| @@ -32,27 +39,9 @@ class daisyGrid(rows: Int, cols: Int, dataWidth: Int) extends Module{ | |||
| memRows(ii).readEnable := 0.U | |||
| memRows(ii).dataIn := io.dataIn | |||
| when(io.readRow === ii.U ){ | |||
| when(io.rowSelect === ii.U ){ | |||
| memRows(ii).readEnable := io.readEnable | |||
| io.dataOut := memRows(ii).dataOut | |||
| } | |||
| } | |||
| } | |||
| class daisyGridTest(c: daisyGrid) extends PeekPokeTester(c) { | |||
| poke(c.io.readEnable, 1) | |||
| for(ii <- 0 until 12){ | |||
| poke(c.io.dataIn, ii) | |||
| poke(c.io.readRow, ii/3) | |||
| step(1) | |||
| println("////////////////////") | |||
| } | |||
| poke(c.io.readEnable, 0) | |||
| for(ii <- 0 until 12){ | |||
| peek(c.io.dataOut) | |||
| poke(c.io.readRow, ii/3) | |||
| step(1) | |||
| println("////////////////////") | |||
| } | |||
| } | |||
| @@ -32,7 +32,6 @@ class daisyMultiplier(val rowsA: Int, val colsA: Int, val rowsB: Int, val colsB: | |||
| val resultReady = RegInit(Bool(), false.B) | |||
| println(s"rowsA: $rowsA, colsA: $colsA, rowsB: $rowsB, colsB: $colsB") | |||
| //////////////////////////////////////// | |||
| //////////////////////////////////////// | |||
| @@ -85,12 +84,12 @@ class daisyMultiplier(val rowsA: Int, val colsA: Int, val rowsB: Int, val colsB: | |||
| //////////////////////////////////////// | |||
| /// set up reading patterns depending on if we are in calculating state or not | |||
| when(calculating === true.B){ | |||
| matrixA.readRow := rowOutputCounter | |||
| matrixA.rowSelect := rowOutputCounter | |||
| }.otherwise{ | |||
| matrixA.readRow := rowCounter | |||
| matrixA.rowSelect := rowCounter | |||
| } | |||
| matrixB.readRow := rowCounter | |||
| matrixB.rowSelect := rowCounter | |||
| @@ -17,7 +17,7 @@ class daisyVector(elements: Int, dataWidth: Int) extends Module{ | |||
| val currentIndex = RegInit(UInt(8.W), 0.U) | |||
| val memory = Array.fill(elements)(Reg(UInt(dataWidth.W))) | |||
| val memory = Array.fill(elements)(RegInit(UInt(dataWidth.W), 0.U)) | |||
| when(currentIndex === (elements - 1).U ){ | |||
| currentIndex := 0.U | |||
| @@ -37,21 +37,3 @@ class daisyVector(elements: Int, dataWidth: Int) extends Module{ | |||
| } | |||
| } | |||
| } | |||
| class daisyVectorTest(c: daisyVector) extends PeekPokeTester(c) { | |||
| poke(c.io.readEnable, 1) | |||
| step(1) | |||
| for(ii <- 0 until 4){ | |||
| poke(c.io.dataIn, ii) | |||
| println("////////////////////") | |||
| step(1) | |||
| } | |||
| poke(c.io.readEnable, 0) | |||
| for(ii <- 0 until 4){ | |||
| peek(c.io.dataOut) | |||
| step(1) | |||
| } | |||
| } | |||
| @@ -0,0 +1,117 @@ | |||
| package Core | |||
| import Core.daisyVector | |||
| import chisel3._ | |||
| import chisel3.core.Input | |||
| import chisel3.iotesters.PeekPokeTester | |||
| import chisel3.util.Counter | |||
| /** | |||
| The daisy multiplier creates two daisy grids, one transposed, and multiplies them. | |||
| */ | |||
| class daisyVecMat(val lengthA: Int, val rowsB: Int, val colsB: Int, val dataWidth: Int) extends Module { | |||
| val io = IO(new Bundle { | |||
| val dataInA = Input(UInt(dataWidth.W)) | |||
| val readEnableA = Input(Bool()) | |||
| val dataInB = Input(UInt(dataWidth.W)) | |||
| val readEnableB = Input(Bool()) | |||
| val dataOut = Output(UInt(dataWidth.W)) | |||
| val dataValid = Output(Bool()) | |||
| val done = Output(Bool()) | |||
| }) | |||
| // How many cycles does it take to fill the matrices with data? | |||
| //////////////////////////////////////// | |||
| //////////////////////////////////////// | |||
| /// We transpose matrix B. | |||
| val vecA = Module(new daisyVector(lengthA, dataWidth)).io | |||
| val matrixB = Module(new daisyGrid(colsB, rowsB, dataWidth)).io | |||
| val dotProductCalculator = Module(new daisyDot(lengthA, dataWidth)).io | |||
| val dataIsLoaded = RegInit(Bool(), false.B) | |||
| /** | |||
| Your implementation here | |||
| */ | |||
| /** | |||
| LF | |||
| */ | |||
| val dataValid = Wire(Bool()) | |||
| //////////////////////////////////////// | |||
| //////////////////////////////////////// | |||
| /// Wire components | |||
| vecA.dataIn := io.dataInA | |||
| vecA.readEnable := io.readEnableA | |||
| matrixB.dataIn := io.dataInB | |||
| matrixB.readEnable := io.readEnableB | |||
| io.dataOut := dotProductCalculator.dataOut | |||
| // allows us to use dataValid internally | |||
| io.dataValid := dataValid | |||
| dotProductCalculator.dataInA := vecA.dataOut | |||
| dotProductCalculator.dataInB := matrixB.dataOut | |||
| dataValid := dotProductCalculator.outputValid & dataIsLoaded | |||
| //////////////////////////////////////// | |||
| //////////////////////////////////////// | |||
| /// Select the correct row | |||
| val (currentCol, colDone) = Counter(true.B, colsB) | |||
| val (rowSel, _) = Counter(colDone, rowsB) | |||
| matrixB.rowSelect := rowSel | |||
| //////////////////////////////////////// | |||
| //////////////////////////////////////// | |||
| /// Check if data is loaded | |||
| val aReady = RegInit(Bool(), false.B) | |||
| val bReady = RegInit(Bool(), false.B) | |||
| val (inputCounterA, counterAWrapped) = Counter(io.readEnableA, lengthA - 1) | |||
| when(counterAWrapped){ aReady := true.B } | |||
| val (inputCounterB, counterBWrapped) = Counter(io.readEnableB, colsB*rowsB) | |||
| when(counterBWrapped){ bReady := true.B } | |||
| dataIsLoaded := aReady & bReady | |||
| //////////////////////////////////////// | |||
| //////////////////////////////////////// | |||
| /// Check if we're done | |||
| val isDone = RegInit(Bool(), false.B) | |||
| val (numOutputted, numOutputtedWrapped) = Counter(dataValid, lengthA) | |||
| when(numOutputtedWrapped){ isDone := true.B } | |||
| // printf(p"dataInA = ${io.dataInA}\n") | |||
| // printf(p"validA = ${io.readEnableA}\n") | |||
| // printf(p"dataInB = ${io.dataInB}\n") | |||
| // printf(p"validB = ${io.readEnableB}\n") | |||
| // printf(p"validOut = ${io.dataValid}\n") | |||
| // printf(p"data loaded = ${dataIsLoaded}\n") | |||
| // printf(p"aReady = ${aReady}\n") | |||
| // printf(p"bReady = ${bReady}\n") | |||
| // printf(p"counter A = ${inputCounterA}\n") | |||
| // printf(p"counter B = ${inputCounterB}\n") | |||
| // printf(p"out = ${dotProductCalculator.dataOut}\n\n") | |||
| io.done := isDone | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| package Core | |||
| import chisel3._ | |||
| import chisel3.core.Input | |||
| import chisel3.iotesters.PeekPokeTester | |||
| import chisel3.util.Counter | |||
| /** | |||
| DaisyVectors are not indexed. They have no control inputs or outputs, only data. | |||
| */ | |||
| class daisyVecVec(elements: Int, dataWidth: Int) extends Module{ | |||
| val io = IO(new Bundle { | |||
| val dataInA = Input(UInt(dataWidth.W)) | |||
| val dataInB = Input(UInt(dataWidth.W)) | |||
| val dataOut = Output(UInt(dataWidth.W)) | |||
| val outputValid = Output(Bool()) | |||
| }) | |||
| val counter = Counter(elements) | |||
| val accumulator = RegInit(UInt(dataWidth.W), 0.U) | |||
| /** | |||
| Your implementation here | |||
| */ | |||
| /** | |||
| LF | |||
| */ | |||
| val product = io.dataInA * io.dataInB | |||
| when(counter.inc()){ | |||
| io.outputValid := true.B | |||
| accumulator := 0.U | |||
| }.otherwise{ | |||
| io.outputValid := false.B | |||
| accumulator := accumulator + product | |||
| } | |||
| io.dataOut := accumulator + product | |||
| } | |||
| @@ -4,12 +4,142 @@ | |||
| In this exercise you will implement a circuit capable of performing matrix | |||
| matrix multiplication in the chisel hardware description language. | |||
| * Your first component | |||
| There are two types of digital components: Combinatorial and stateful. | |||
| The first component we will consider is a simple combinatorial incrementor: | |||
| #+begin_src scala | |||
| class myIncrement(incrementBy: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| io.dataOut := io.dataIn + incrementBy.U | |||
| #+end_src | |||
| Let's break the code down down. First, myIncrement is a Module, meaning that | |||
| this class can be instantiated as a hardware circuit. | |||
| Figure [rm3] shows the model that you have just declared. | |||
| A 32 bit signal, data_in goes in, and another 32 bit signal goes out. | |||
| Apart from the IO, there is only one statement, assigning dataOut to dataIn + | |||
| incrementBy. | |||
| In RTL the component looks like fig [rm4] | |||
| Let's see how we can use our module: | |||
| #+begin_src scala | |||
| class myIncrementTwice(incrementBy: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val first = Module(new myIncrement(incrementBy)) | |||
| val second = Module(new myIncrement(incrementBy)) | |||
| first.io.dataIn := io.dataIn | |||
| second.io.dataIn := first.io.dataOut | |||
| io.dataOut := second.io.dataOut | |||
| } | |||
| #+end_src | |||
| Fig [rm5] shows the RTL design, as expected it's just two incrementors | |||
| chained. | |||
| The following code shows off how you can use for loops to instantiate an | |||
| arbitrary amount of modules. | |||
| #+begin_src scala | |||
| class myIncrementN(incrementBy: Int, numIncrementors: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val incrementors = Array.fill(numIncrementors){ Module(new myIncrement(incrementBy)) } | |||
| for(ii <- 1 until numIncrementors){ | |||
| incrementors(ii).io.dataIn := incrementors(ii - 1).io.dataOut | |||
| } | |||
| incrementors(0).io.dataIn := io.dataIn | |||
| io.dataOut := incrementors(numIncrementors).io.dataOut | |||
| } | |||
| #+end_src | |||
| Keep in mind that the for-loop only exists at design time, just like a for loop | |||
| generating a table in HTML will not be part of the finished HTML. | |||
| So, what does combinatorial mean? | |||
| To answer that, let's create a stateful circuit first. | |||
| #+begin_src scala | |||
| class myDelay() extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| val delayReg = RegInit(UInt(32.W), 0.U) | |||
| delayReg := io.dataIn | |||
| io.dataOut := delayReg | |||
| } | |||
| #+end_src | |||
| This circuit seems rather pointless, it simply assigns the input to the output. | |||
| However, the register has another input, as seen in the RTL: fig [rm6]. | |||
| The register can only change value during rising edges on the clock! | |||
| To examplify, assume at step 0 data in is 0x45. | |||
| delayReg will now have 0x45 as its data in, but data out will still be 0. | |||
| Only when the clock ticks will delayReg.dataOut take on the value 0x45. | |||
| You should now be able to implement myDelayN following the same principles as | |||
| myIncrementN | |||
| #+begin_src scala | |||
| class myDelayN(delay: Int) extends Module { | |||
| val io = IO( | |||
| new Bundle { | |||
| val dataIn = Input(UInt(32.W)) | |||
| val dataOut = Output(UInt(32.W)) | |||
| } | |||
| ) | |||
| ??? | |||
| } | |||
| #+end_src | |||
| This should answer the initial question of combinatorial vs stateful: | |||
| The output of a combinatorial circuit will be available instantly, while | |||
| a stateful circuit will only update its output during rising edges on the | |||
| clock. | |||
| Before you continue it is recommended that you check out the chisel3 | |||
| tutorials. | |||
| In the basics.scala there is one more module, a basic selector. | |||
| At compile time this component builds n random numbers, to see which we can | |||
| cycle through them. | |||
| The component comes with a test, this test will be run when you do sbt.run | |||
| You should study this component. What is the difference between if/else and | |||
| when/otherwise? | |||
| * Matrix matrix multiplication | |||
| When designing digital logic you should always start with decomposition. | |||
| Your first task is therefore to implement a dot product calculator, since | |||
| a matrix matrix multiplication is essentially a series of these. | |||
| * Dot Product calculator | |||
| * Dot Prod | |||
| First, let's consider how a dot product calculator would look like in regular | |||
| scala: | |||
| @@ -25,20 +155,69 @@ | |||
| } | |||
| dotProduct | |||
| } | |||
| // Scala has rich support for functional programming | |||
| val dotProductFP = (vecA zip vecB) | |||
| .map{ case(a, b) => a*b } | |||
| .sum | |||
| #+end_src | |||
| In the for loop version you can see how the dot product is sequentially | |||
| In the for loop you can see how the dot product is sequentially | |||
| calculated by multiplying vector values of the same indice and summing the | |||
| result. | |||
| The dot product for loop works in a similar fashion to your first design, but | |||
| before you get there some basics are in order. | |||
| * Your first component | |||
| There are two types of digital components: Combinatorial and stateful. | |||
| To implement this logic in hardware the first thing you need is some way to | |||
| represent a vector which is your first task. | |||
| ** Task 1 - Vector | |||
| The first component you should implement is a register bank for storing a vector. | |||
| This module works as follows: | |||
| let dataOut(T) = if (T - vectorLength) < 0 then 0 else | |||
| if enableIn(T - vectorLength) | |||
| then dataIn(T - vectorLength) | |||
| else | |||
| dataOut(T - vectorLength) | |||
| From the figure the principle of operation becomes clearer [inkskape drawing, rm sketch] | |||
| To test your implementation you can run testOnly Core.daisyVecSpec in sbt | |||
| ** Task 2 - Dot Product | |||
| Your next task is to implement daisyDot. | |||
| daisyDot should calculate the dot product of two vectors, inA and inB. Ensure that validOut | |||
| is only asserted when you have a result. Ensure that your accumulator gets flushed after | |||
| calculating your dot product. | |||
| ** Task 3 - Vector Matrix multiplication | |||
| Having implemented a dot product calculator, a vector matrix multiplier is not that different. | |||
| In imperative code we get something like this: | |||
| #+begin_src scala | |||
| type Matrix[A] = List[List[A]] | |||
| def vectorMatrixMultiply(vec: List[Int], matrix: Matrix[Int]): List[Int] = { | |||
| val transposed = matrix.transpose | |||
| val outputVector = Array.ofDim[Int](vec.length) | |||
| for(ii <- 0 until matrix.length){ | |||
| outputVector(ii) = dotProductForLoop(vec, transposed(ii)) | |||
| } | |||
| outputVector.toList | |||
| } | |||
| #+end_src scala | |||
| This is just repeated application of dotProduct. | |||
| Since vector matrix multiplication is the dotproduct of the vector and the rows of the matrix, | |||
| the matrix must be transposed. | |||
| *** Subtask 1 - representing a matrix | |||
| Like the dot product calculator, the first step is to implement a register bank for storing a matrix. | |||
| This can be done by creating n vectors from Task 1 and then select which row is the 'current' row. | |||
| The matrix representation you have created in this task allows you to select which row to read, but | |||
| not which column. This isn't very efficient when you want to read an entire column since you would have | |||
| to wait a full cycle for each row. | |||
| The way we deal with this is noticing that when multiplying two matrices we work on a row basis in | |||
| matrix A, and column basis on matrix B. If we simply transpose matrix B, then accessing its rows is | |||
| the same as accessing the columns of matrix B. | |||
| A consequence of this is that the API exposed by your matrix multiplier requires matrix B to be transposed. | |||
| *** Subtask 2 - vector matrix multiplication | |||
| @@ -0,0 +1,317 @@ | |||
| package Core | |||
| import chisel3._ | |||
| import chisel3.util._ | |||
| import chisel3.core.Input | |||
| import chisel3.iotesters._ | |||
| import org.scalatest.{Matchers, FlatSpec} | |||
| class daisyVectorTest(c: daisyVector, inputs: List[(Int, Int, Int)]) extends PeekPokeTester(c) { | |||
| (inputs).foreach { | |||
| case(enIn, dataIn, dataOut) => { | |||
| poke(c.io.readEnable, enIn) | |||
| poke(c.io.dataIn, dataIn) | |||
| expect(c.io.dataOut, dataOut) | |||
| step(1) | |||
| } | |||
| } | |||
| } | |||
| class daisyDotTest(c: daisyDot, inputs: List[(Int, Int, Option[Int], Int)]) extends PeekPokeTester(c) { | |||
| (inputs).foreach { | |||
| case(inA, inB, dataOut, dataValid) => { | |||
| poke(c.io.dataInA, inA) | |||
| poke(c.io.dataInB, inB) | |||
| dataOut.foreach { expect(c.io.dataOut, _) } | |||
| expect(c.io.outputValid, dataValid) | |||
| step(1) | |||
| } | |||
| } | |||
| } | |||
| class daisyGridTest(c: daisyGrid, inputs: List[(Int, Int, Int, Int)]) extends PeekPokeTester(c) { | |||
| (inputs).foreach { | |||
| case(readEnable, dataIn, readRow, dataOut) => { | |||
| poke(c.io.readEnable, readEnable) | |||
| poke(c.io.dataIn, dataIn) | |||
| poke(c.io.rowSelect, readRow) | |||
| expect(c.io.dataOut, dataOut) | |||
| step(1) | |||
| } | |||
| } | |||
| } | |||
| class daisyVecMatTest(c: daisyVecMat, inputs: List[(Int,Int,Int,Int,Option[Int],Int,Int)]) extends PeekPokeTester(c) { | |||
| (inputs).foreach { | |||
| case(dataInA, readEnableA, dataInB, readEnableB, dataOutExpect, dataValidExpect, doneExpect) => { | |||
| poke(c.io.dataInA, dataInA) | |||
| poke(c.io.dataInB, dataInB) | |||
| poke(c.io.readEnableA, readEnableA) | |||
| poke(c.io.readEnableB, readEnableB) | |||
| expect(c.io.dataValid, dataValidExpect) | |||
| expect(c.io.done, doneExpect) | |||
| dataOutExpect.foreach { expect(c.io.dataOut, _) } | |||
| step(1) | |||
| } | |||
| } | |||
| } | |||
| class daisyVecSpec extends FlatSpec with Matchers { | |||
| val input1 = List.fill(10)((0, 0x45, 0)) | |||
| val input2 = input1 ++ List( | |||
| // enableIn, dataIn, expected | |||
| (1, 2, 0), | |||
| (1, 2, 0), | |||
| (1, 2, 0), | |||
| (1, 2, 0), | |||
| (0, 0, 2), | |||
| (0, 0, 2), | |||
| (0, 0, 2), | |||
| (0, 0, 2), | |||
| (0, 0, 2), | |||
| (0, 0, 2), | |||
| (0, 0, 2), | |||
| (0, 0, 2)) | |||
| val input3 = { | |||
| val inputs = List.fill(100)(scala.util.Random.nextInt(10000)) | |||
| val withExpected = (List.fill(4)(0) ++ inputs) zip inputs | |||
| val withEnabled = withExpected.map{ case(expected, in) => (1, in, expected) } | |||
| withEnabled | |||
| } | |||
| behavior of "daisy vector" | |||
| it should "not read when read enable is low" in { | |||
| iotesters.Driver.execute(() => new daisyVector(4, 32), new TesterOptionsManager) { c => | |||
| new daisyVectorTest(c, input1) | |||
| } should be(true) | |||
| } | |||
| it should "read only when read enable is asserted" in { | |||
| iotesters.Driver.execute(() => new daisyVector(4, 32), new TesterOptionsManager) { c => | |||
| new daisyVectorTest(c, input2) | |||
| } should be(true) | |||
| } | |||
| it should "Work in general" in { | |||
| iotesters.Driver.execute(() => new daisyVector(4, 32), new TesterOptionsManager) { c => | |||
| new daisyVectorTest(c, input3) | |||
| } should be(true) | |||
| } | |||
| } | |||
| class daisyDotSpec extends FlatSpec with Matchers { | |||
| behavior of "daisy vector" | |||
| val input1 = List( | |||
| (0, 0, None, 0), | |||
| (0, 0, None, 0), | |||
| (0, 0, None, 1), | |||
| (0, 0, None, 0), | |||
| (0, 0, None, 0), | |||
| (0, 0, None, 1), | |||
| (0, 0, None, 0), | |||
| (0, 0, None, 0), | |||
| (0, 0, None, 1)) | |||
| it should "Only signal valid output at end of calculation" in { | |||
| iotesters.Driver.execute(() => new daisyDot(3, 32), new TesterOptionsManager) { c => | |||
| new daisyDotTest(c, input1) | |||
| } should be(true) | |||
| } | |||
| val input2 = List( | |||
| (1, 0, None, 0), | |||
| (1, 0, None, 0), | |||
| (1, 0, Some(3), 1), | |||
| (1, 0, None, 0), | |||
| (1, 0, None, 0), | |||
| (1, 0, Some(3), 1), | |||
| (1, 0, None, 0), | |||
| (1, 0, None, 0), | |||
| (1, 0, Some(3), 1)) | |||
| it should "Be able to count to 3" in { | |||
| iotesters.Driver.execute(() => new daisyDot(3, 32), new TesterOptionsManager) { c => | |||
| new daisyDotTest(c, input1) | |||
| } should be(true) | |||
| } | |||
| def createProblem(vecLen: Int): List[(Int, Int, Option[Int], Int)] = { | |||
| val in1 = List.fill(vecLen)(scala.util.Random.nextInt(10)) | |||
| val in2 = List.fill(vecLen)(scala.util.Random.nextInt(10)) | |||
| val dotProduct = (in1, in2).zipped.map(_*_).sum | |||
| (in1, in2, (0 to vecLen)).zipped.map{ | |||
| case(a, b, idx) => | |||
| val dpExpect = if(idx == (vecLen - 1)) Some(dotProduct) else None | |||
| val outExpect = if(idx == (vecLen - 1)) 1 else 0 | |||
| (a, b, dpExpect, outExpect) | |||
| } | |||
| } | |||
| def createProblems(vecLen: Int): List[(Int, Int, Option[Int], Int)] = | |||
| List.fill(10)(createProblem(vecLen)).flatten | |||
| it should "Be able to calculate dot products" in { | |||
| iotesters.Driver.execute(() => new daisyDot(10, 32), new TesterOptionsManager) { c => | |||
| new daisyDotTest(c, createProblems(10)) | |||
| } should be(true) | |||
| } | |||
| } | |||
| class daisyGridSpec extends FlatSpec with Matchers { | |||
| type Matrix[A] = List[List[A]] | |||
| behavior of "daisy grid" | |||
| def genMatrix(dims: (Int,Int)): Matrix[Int] = | |||
| List.fill(dims._1)( | |||
| List.fill(dims._2)(scala.util.Random.nextInt(100)) | |||
| ) | |||
| def readRowCheck(dims: (Int,Int)): List[(Int,Int,Int,Int)] = { | |||
| // readEn, dataIn, readRow, expected dataOut | |||
| List.fill(dims._1 - 1)(( 1, 1, 0, 0)) ++ | |||
| List.fill(dims._1 - 1)((0, 0, 0, 1)) ++ | |||
| List.fill(dims._1 - 1)((0, 0, 0, 1)) | |||
| } | |||
| def readRow2Check(dims: (Int,Int)): List[(Int,Int,Int,Int)] = { | |||
| // readEn, dataIn, readRow, expected dataOut | |||
| List.fill(dims._1 - 1)(( 1, 1, 1, 0)) ++ | |||
| List.fill(dims._1 - 1)((0, 0, 1, 1)) ++ | |||
| List.fill(dims._1 - 1)((0, 0, 1, 1)) | |||
| } | |||
| def readMatrix(dims: (Int,Int)): List[(Int,Int,Int,Int)] = { | |||
| val m = genMatrix(dims) | |||
| val input = m.zipWithIndex.map{ case(row, rowIdx) => | |||
| row.zipWithIndex.map{ case(a, colIdx) => | |||
| // readEn, dataIn, readRow, expected dataOut | |||
| ( 1, a, rowIdx, 0) | |||
| } | |||
| }.flatten | |||
| val output = m.zipWithIndex.map{ case(row, rowIdx) => | |||
| row.zipWithIndex.map{ case(a, colIdx) => | |||
| // readEn, dataIn, readRow, expected dataOut | |||
| ( 0, 0, rowIdx, a) | |||
| } | |||
| }.flatten | |||
| input ++ output | |||
| } | |||
| it should "work like a regular daisyVec when row select is fixed to 0" in { | |||
| iotesters.Driver.execute(() => new daisyGrid(5, 4, 32), new TesterOptionsManager) { c => | |||
| new daisyGridTest(c, readRowCheck((5,4))) | |||
| } should be(true) | |||
| } | |||
| it should "work like a regular daisyVec when row select is fixed to 1" in { | |||
| iotesters.Driver.execute(() => new daisyGrid(5, 4, 32), new TesterOptionsManager) { c => | |||
| new daisyGridTest(c, readRow2Check((5,4))) | |||
| } should be(true) | |||
| } | |||
| it should "be able to read a matrix" in { | |||
| iotesters.Driver.execute(() => new daisyGrid(5, 4, 32), new TesterOptionsManager) { c => | |||
| new daisyGridTest(c, readMatrix((5,4))) | |||
| } should be(true) | |||
| } | |||
| } | |||
| class daisyVecMatSpec extends FlatSpec with Matchers { | |||
| type Matrix[A] = List[List[A]] | |||
| def genMatrix(dims: (Int,Int)): Matrix[Int] = | |||
| List.fill(dims._1)( | |||
| List.fill(dims._2)(scala.util.Random.nextInt(4)) | |||
| ) | |||
| def generateInputs(dims: (Int,Int)): List[(Int,Int,Int,Int,Option[Int],Int,Int)] = { | |||
| val matrixB = genMatrix(dims) | |||
| val vecA = genMatrix((1, (dims._1))).head | |||
| println("multiplying: ") | |||
| println(vecA.mkString("[","\t","]")) | |||
| println("matrix:") | |||
| matrixB.foreach { row => | |||
| println(row.mkString("[","\t","]")) | |||
| } | |||
| def answers: List[Int] = matrixB.transpose.map( col => | |||
| (col, vecA).zipped.map(_*_).sum | |||
| ) | |||
| println("should equal") | |||
| println(answers.mkString("[","\t","]")) | |||
| val vecAndMatrixInput = (matrixB.head zip vecA).map{ | |||
| case(m, v) => | |||
| (v, 1, m, 1, None, 0, 0) | |||
| } | |||
| val matrixInput = matrixB.tail.flatten.map{ m => | |||
| (0, 0, m, 1, None, 0, 0) | |||
| } | |||
| val checkOutput = answers.map( a => | |||
| { | |||
| val filler = List.fill(dims._2 - 1)((0, 0, 0, 0, None, 0, 0)) | |||
| val check = List((0, 0, 0, 0, Some(a), 0, 0)) | |||
| filler ++ check | |||
| }).flatten | |||
| vecAndMatrixInput ::: matrixInput ::: checkOutput | |||
| } | |||
| behavior of "vec mat multiplier" | |||
| it should "compile" in { | |||
| iotesters.Driver.execute(() => new daisyVecMat(5, 4, 5, 32), new TesterOptionsManager) { c => | |||
| new daisyVecMatTest(c, Nil) | |||
| } should be(true) | |||
| } | |||
| it should "Not assert valid output when loading data" in { | |||
| iotesters.Driver.execute(() => new daisyVecMat(5, 4, 5, 32), new TesterOptionsManager) { c => | |||
| new daisyVecMatTest(c, generateInputs((5,4))) | |||
| } should be(true) | |||
| } | |||
| } | |||