| @@ -5,7 +5,7 @@ | |||||
| Finn ut hvordan bundles, defs etc burde fungere. | Finn ut hvordan bundles, defs etc burde fungere. | ||||
| * Now | * Now | ||||
| ** TODO Port Babby to chisel3 | |||||
| ** DONE Port Babby to chisel3 | |||||
| *** DONE compile and run | *** DONE compile and run | ||||
| *** DONE fix deprecation | *** DONE fix deprecation | ||||
| @@ -6,12 +6,9 @@ import chisel3.iotesters.PeekPokeTester | |||||
| object CoreMain { | object CoreMain { | ||||
| def main(args: Array[String]): Unit = { | 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 vecA = List(1, 2, 4) | ||||
| val vecB = List(2, -3, 1) | val vecB = List(2, -3, 1) | ||||
| val dotProductForLoop = { | |||||
| def dotProductForLoop(vecA: List[Int], vecB: List[Int]) = { | |||||
| var dotProduct = 0 | var dotProduct = 0 | ||||
| for(i <- 0 until vecA.length){ | for(i <- 0 until vecA.length){ | ||||
| dotProduct = dotProduct + (vecA(i) * vecB(i)) | dotProduct = dotProduct + (vecA(i) * vecB(i)) | ||||
| @@ -81,4 +78,25 @@ object Extras { | |||||
| // This is not good code!!! | // This is not good code!!! | ||||
| val tooFancyDotProduct = | val tooFancyDotProduct = | ||||
| (0 /: (vecA zip vecB)){ case(acc, ab) => acc + (ab._1 * ab._2) } | (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 readEnable = Input(Bool()) | ||||
| val dataIn = Input(UInt(dataWidth.W)) | val dataIn = Input(UInt(dataWidth.W)) | ||||
| val readRow = Input(UInt(8.W)) | |||||
| val rowSelect = Input(UInt(8.W)) | |||||
| val dataOut = Output(UInt(dataWidth.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 | val elements = rows*cols | ||||
| /** | |||||
| Your implementation here | |||||
| */ | |||||
| /** | |||||
| LF | |||||
| */ | |||||
| io.dataOut := 0.U | io.dataOut := 0.U | ||||
| for(ii <- 0 until rows){ | 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).readEnable := 0.U | ||||
| memRows(ii).dataIn := io.dataIn | memRows(ii).dataIn := io.dataIn | ||||
| when(io.readRow === ii.U ){ | |||||
| when(io.rowSelect === ii.U ){ | |||||
| memRows(ii).readEnable := io.readEnable | memRows(ii).readEnable := io.readEnable | ||||
| io.dataOut := memRows(ii).dataOut | 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) | 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 | /// set up reading patterns depending on if we are in calculating state or not | ||||
| when(calculating === true.B){ | when(calculating === true.B){ | ||||
| matrixA.readRow := rowOutputCounter | |||||
| matrixA.rowSelect := rowOutputCounter | |||||
| }.otherwise{ | }.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 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 ){ | when(currentIndex === (elements - 1).U ){ | ||||
| currentIndex := 0.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 | In this exercise you will implement a circuit capable of performing matrix | ||||
| matrix multiplication in the chisel hardware description language. | 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 | * Matrix matrix multiplication | ||||
| When designing digital logic you should always start with decomposition. | When designing digital logic you should always start with decomposition. | ||||
| Your first task is therefore to implement a dot product calculator, since | Your first task is therefore to implement a dot product calculator, since | ||||
| a matrix matrix multiplication is essentially a series of these. | 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 | First, let's consider how a dot product calculator would look like in regular | ||||
| scala: | scala: | ||||
| @@ -25,20 +155,69 @@ | |||||
| } | } | ||||
| dotProduct | dotProduct | ||||
| } | } | ||||
| // Scala has rich support for functional programming | |||||
| val dotProductFP = (vecA zip vecB) | |||||
| .map{ case(a, b) => a*b } | |||||
| .sum | |||||
| #+end_src | #+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 | calculated by multiplying vector values of the same indice and summing the | ||||
| result. | 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) | |||||
| } | |||||
| } | |||||