| @@ -0,0 +1,51 @@ | |||
| \documentclass{article} | |||
| \usepackage[utf8]{inputenc} | |||
| \usepackage{tikz-timing} | |||
| \usetikztiminglibrary[rising arrows]{clockarrows} | |||
| \usepackage{xparse} % NewDocumentCommand, IfValueTF, IFBooleanTF | |||
| % Reference a bus. | |||
| % | |||
| % Usage: | |||
| % | |||
| % \busref[3::0]{C/BE} -> C/BE[3::0] | |||
| % \busref*{AD} -> AD# | |||
| % \busref*[3::0]{C/BE} -> C/BE[3::0]# | |||
| % | |||
| \NewDocumentCommand{\busref}{som}{\texttt{% | |||
| #3% | |||
| \IfValueTF{#2}{[#2]}{}% | |||
| \IfBooleanTF{#1}{\#}{}% | |||
| }} | |||
| \title{Waveform drawing} | |||
| \author{peteraa } | |||
| \date{June 2019} | |||
| \begin{document} | |||
| \begin{tikztimingtable}[% | |||
| timing/dslope=0.1, | |||
| timing/.style={x=5ex,y=2ex}, | |||
| x=5ex, | |||
| timing/rowdist=3ex, | |||
| timing/name/.style={font=\sffamily\scriptsize} | |||
| ] | |||
| \busref{CLK} & 32{c} \\ | |||
| \busref{A} & 1D{5} 1D{0} 1D{4} 1D{8} 1D{1} 1D{6} 1D{6} 1D{2} 1D{9} 1D{0} 1D{8} 1D{3} 1D{9} 1D{3} 2U \\ | |||
| \busref{B} & 1D{8} 1D{3} 1D{3} 1D{2} 1D{7} 1D{6} 1D{7} 1D{8} 1D{5} 1D{2} 1D{7} 1D{1} 1D{8} 1D{2} 2U \\ | |||
| \busref{VALID} & 6U 1H 6U 1H 2U\\ | |||
| \busref{Output} & 1D{40} 1D{40} 1D{52} 1D{68} 1D{75} 1D{111} 1D{153} 1D{16} 1D{61} 1D{61} 1D{117} 1D{120} 1D{192} 1D{198} 2U\\ | |||
| \extracode | |||
| \begin{pgfonlayer}{background} | |||
| \begin{scope}[semitransparent ,semithick] | |||
| \vertlines[darkgray,dotted]{0.5,1.5 ,...,8.0} | |||
| \end{scope} | |||
| \end{pgfonlayer} | |||
| \end{tikztimingtable} | |||
| \maketitle | |||
| \section{Introduction} | |||
| \end{document} | |||
| @@ -49,3 +49,14 @@ | |||
| bi-directional busses. Otherwise, the value is simply outputted and | |||
| the module receiving the output simply ignores it if it didn’t ask | |||
| for anything. | |||
| * Jahre comments | |||
| ** DONE Block diagrams | |||
| ** DONE Specify timing constraints | |||
| *** DONE For dot prod | |||
| *** DONE -ish For mat mul | |||
| ** DONE Make dot prod sim easier to find | |||
| It was gone. Oops | |||
| ** DONE Direct link to chisel docs | |||
| ** DONE Modularize MatMul | |||
| ** DONE Unrandomize tests | |||
| @@ -6,7 +6,7 @@ | |||
| drawing programs. | |||
| As an example we will use a very simple circuit: | |||
| [[./Images/inkscape.jpg]] | |||
| [[./Images/inkscape.png]] | |||
| This circuit has a register, and we want to see how its state evolves, thus we add a label. | |||
| The name of the register is "Reg_A", which will be replaced by the actual value as the circuit | |||
| @@ -82,7 +82,7 @@ | |||
| ** Draw the circuit in inkscape | |||
| You can add as much detail as you want here, the only thing the parser looks for is | |||
| text fields that are postfixed with "_field" | |||
| text fields that are postfixed with "_field". | |||
| The fields I would be interested in are the row and column counters, and the dot product | |||
| accumulator state. | |||
| Save the svg as | |||
| @@ -40,19 +40,59 @@ | |||
| case class DotProdCalculator(vectorLen: Int, timeStep: Int, accumulator: Int){ | |||
| def update(inputA: Int, inputB: Int): (Int, Boolean, DotProdCalculator) = { | |||
| val product = inputA * inputB | |||
| if(((timeStep + 1) % vectorLen) == 0){ | |||
| if(((timeStep + 1) % vectorLen) == 0) | |||
| (accumulator + product, true, this.copy(timeStep = 0, accumulator = 0)) | |||
| else | |||
| (accumulator + product, false, this.copy(timeStep = this.timeStep + 1, accumulator = accumulator + product)) | |||
| } | |||
| } | |||
| } | |||
| #+end_src | |||
| To see it in action run ~testOnly Ex0.DPCsimulatorSpec~ in your sbt console. | |||
| To see it in action run ~testOnly Examples.SoftwareDotProdSpec~ in your sbt console. | |||
| As with the previous tasks, the dot product calculator must pass the tests with | |||
| ~testOnly Ex0.DotProdSpec~ | |||
| *** Timing | |||
| As shown in the timing diagram below, the dot product calculator should deliver the result as | |||
| soon as possible. | |||
| This means you will have to drive the output with the sum of the accumulator and the product of | |||
| the two inputs. | |||
| If you choose to drive the output only by the value of the accumulator your circuit will | |||
| lag behind by one cycle, which while good for pipelining purposes is not good for passing the test | |||
| purposes. | |||
| #+CAPTION: The expected output of the dot product calculator | |||
| [[./Images/counter.png]] | |||
| *** Chisel counters and a short detour on scala documentation | |||
| Doing an action for a set amount of timesteps is a very common task in hardware design, so this | |||
| functionality is included in chisel via the Counter class. | |||
| In order to understand how this counter works you can google "chisel counter" and get the following | |||
| https://chisel.eecs.berkeley.edu/api/3.0.1/chisel3/util/Counter.html as first result. | |||
| However, this is for the counter Class, what you actually want is the counter Object: | |||
| https://chisel.eecs.berkeley.edu/api/3.0.1/chisel3/util/Counter$.html | |||
| This can be very confusing when new to scala, but it is simply convention: | |||
| When a class and an object share name this is just a convenience for keeping static methods, such | |||
| as constructors, separated from the non-static methods. | |||
| In the Counter object (the second link) there is an apply method: | |||
| #+begin_src scala | |||
| def apply(cond: Bool, n: Int): (UInt, Bool) | |||
| #+end_src | |||
| The type signature tells you that the input is a regular scala integer, and a chisel boolean | |||
| (scala booleans are of type ~Boolean~, rather than ~Bool~) and the output is a UInt and a chisel | |||
| boolean. | |||
| This means that upon instantiating a Counter with its apply method you only get the outputs from | |||
| the counter, not the object itself. | |||
| The result is a convenient method of making a counter, simply supply how many ticks it takes for the | |||
| counter to roll over, as well as an input signal for enabling the clock, and receive a tuple with the | |||
| signal for the counters value, as well as a boolean signal that toggles whenever the clock rolls over. | |||
| A special property of apply methods are that they can be called directly on the object. | |||
| ~Counter.apply(cCond, 10)~ does the same as ~Counter(cCond, 10)~. | |||
| To call the class constructor, use the ~new~ keyword. | |||
| ** Task 4 - Matrix Matrix multiplication | |||
| @@ -106,9 +146,43 @@ | |||
| The skeleton code for the matrix multiplier is less detailed, with only one test. | |||
| You're encouraged to write your own tests to make this easier. | |||
| Additionally, if you feel like you're getting stuck you can take a look at | |||
| MatMulTips.org | |||
| *** Structuring your circuit | |||
| It is very easy to get bogged down with details in this exercise, so it's useful to take | |||
| a few moments to plan ahead. | |||
| A natural way to break down the task is to split it into two phases: setup and execution. | |||
| For setup you simply want to shuffle data from the input signals to your two matrix modules. | |||
| The next task is to actually perform the calculation. | |||
| This is a little more complex, seeing as the read patterns are different from matrix A and B. | |||
| To make this simpler a good idea is to introduce a control module. | |||
| This module should keep track of which state the multiplier is in, setup or execution, and | |||
| provide the appropriate row and column select signals. | |||
| You may also choose to split the control module into an init controller and an execution | |||
| controller if you see fit. | |||
| A suggested design is shown underneath: | |||
| [[./Images/MatMul.png]] | |||
| *** Timing | |||
| The timing for your matrix multiplier is straight forward. For a 3x4 matrix it takes | |||
| 12 cycles to input data (cycles 0 to 11), and execution should proceed on cycle 12. | |||
| While you can technically start execution sooner than this the tests expect you to | |||
| not start executing before all data is loaded. | |||
| As long as you start executing just as data has been loaded your dot prod design will | |||
| take care of the rest. | |||
| *** Testing | |||
| In order to make testing easier, consider testing your row and column select signals | |||
| first. | |||
| The actual values stored in the matrixes are just noise, the important part is that | |||
| you select the correct rows and columns at the correct times for the correct matrixes, | |||
| and if you do this the rest is comparatively easy. | |||
| ** Bonus exercise - Introspection on code quality and design choices | |||
| This last exercise has no deliverable, but you should spend some time thinking about | |||
| where you spent most of your efforts. | |||
| @@ -852,9 +852,18 @@ | |||
| Just don't bank your money on the correctness, it might fail in rare circumstances making debugging | |||
| a nightmare) | |||
| ** Visualizing generated circuits | |||
| While limited, it is possible to visualize your generated circuit using [[https://github.com/freechipsproject/diagrammer][diagrammer]]. | |||
| The necessary code to generate .fir file is in the main.scala file, just comment it out to generate | |||
| these. | |||
| I encourage you to give it a fair shake to see if you find it useful or not. | |||
| ** Visualizing circuit state (Optional) | |||
| In order to make debugging easier it is helpful to render the state of the circuit to see where | |||
| errors happen. | |||
| A prototype for this is included in this project, read more about it here | |||
| [[./circuitRendering.org][Here]] | |||
| ** Resources | |||
| Chisel cheat sheet | |||
| https://chisel.eecs.berkeley.edu/doc/chisel-cheatsheet3.pdf | |||
| @@ -21,6 +21,8 @@ class DotProd(val elements: Int) extends Module { | |||
| */ | |||
| val counter = Counter(elements) | |||
| val accumulator = RegInit(UInt(32.W), 0.U) | |||
| // Please don't manually implement product! | |||
| val product = io.dataInA * io.dataInB | |||
| // placeholder | |||
| @@ -1,12 +1,22 @@ | |||
| package Ex0 | |||
| import chisel3._ | |||
| import java.io.File | |||
| object main { | |||
| def main(args: Array[String]): Unit = { | |||
| val s = """ | |||
| | Attempting to "run" a chisel program is rather meaningless. | |||
| | Instead, try running the tests, for instance with "test" or "testOnly Examples.MyIncrementTest | |||
| | | |||
| | If you want to create chisel graphs, simply remove this message and comment in the code underneath | |||
| | to generate the modules you're interested in. | |||
| """.stripMargin | |||
| println(s) | |||
| } | |||
| // Uncomment to dump .fir file | |||
| // val f = new File("MatMul.fir") | |||
| // chisel3.Driver.dumpFirrtl(chisel3.Driver.elaborate(() => new MatMul(5, 4)), Option(f)) | |||
| } | |||
| @@ -8,7 +8,8 @@ import TestUtils._ | |||
| class DotProdSpec extends FlatSpec with Matchers { | |||
| import DotProdTests._ | |||
| val elements = scala.util.Random.nextInt(5) + 2 | |||
| val rand = new scala.util.Random(100) | |||
| val elements = 7 | |||
| behavior of "DotProd" | |||
| @@ -41,6 +42,8 @@ class DotProdSpec extends FlatSpec with Matchers { | |||
| object DotProdTests { | |||
| val rand = new scala.util.Random(100) | |||
| class SignalsWhenDone(c: DotProd) extends PeekPokeTester(c) { | |||
| for(ii <- 0 until c.elements - 1){ | |||
| @@ -61,8 +64,11 @@ object DotProdTests { | |||
| class CalculatesCorrectResult(c: DotProd) extends PeekPokeTester(c) { | |||
| val inputsA = List.fill(c.elements)(scala.util.Random.nextInt(10)) | |||
| val inputsB = List.fill(c.elements)(scala.util.Random.nextInt(10)) | |||
| val inputsA = List.fill(c.elements)(rand.nextInt(10)) | |||
| val inputsB = List.fill(c.elements)(rand.nextInt(10)) | |||
| println("runnign dot prod calc with inputs:") | |||
| println(inputsA.mkString("[", "] [", "]")) | |||
| println(inputsB.mkString("[", "] [", "]")) | |||
| val expectedOutput = (for ((a, b) <- inputsA zip inputsB) yield a * b) sum | |||
| for(ii <- 0 until c.elements){ | |||
| @@ -77,8 +83,11 @@ object DotProdTests { | |||
| class CalculatesCorrectResultAndSignals(c: DotProd) extends PeekPokeTester(c) { | |||
| val inputsA = List.fill(c.elements)(scala.util.Random.nextInt(10)) | |||
| val inputsB = List.fill(c.elements)(scala.util.Random.nextInt(10)) | |||
| val inputsA = List.fill(c.elements)(rand.nextInt(10)) | |||
| val inputsB = List.fill(c.elements)(rand.nextInt(10)) | |||
| println("runnign dot prod calc with inputs:") | |||
| println(inputsA.mkString("[", "] [", "]")) | |||
| println(inputsB.mkString("[", "] [", "]")) | |||
| val expectedOutput = (for ((a, b) <- inputsA zip inputsB) yield a * b) sum | |||
| for(ii <- 0 until c.elements){ | |||
| @@ -0,0 +1,42 @@ | |||
| /** | |||
| * This code supplements instructions.org | |||
| */ | |||
| package Examples | |||
| import Ex0._ | |||
| import org.scalatest.{Matchers, FlatSpec} | |||
| case class DotProdCalculator(vectorLen: Int, timeStep: Int = 0, accumulator: Int = 0){ | |||
| def update(inputA: Int, inputB: Int): (Int, Boolean, DotProdCalculator) = { | |||
| val product = inputA * inputB | |||
| if(((timeStep + 1) % vectorLen) == 0) | |||
| (accumulator + product, true, this.copy(timeStep = 0, accumulator = 0)) | |||
| else | |||
| (accumulator + product, false, this.copy(timeStep = this.timeStep + 1, accumulator = accumulator + product)) | |||
| } | |||
| } | |||
| class SoftwareDotProdSpec extends FlatSpec with Matchers { | |||
| import DotProdTests._ | |||
| val elements = scala.util.Random.nextInt(5) + 2 | |||
| behavior of "DotProdSim" | |||
| it should "Simulate dot product calculation" in { | |||
| println("Running a simulated dot product calculator with input vector size = 5") | |||
| println("Note how output is only valid on cycles divisible by 5, and how the accumulator flushes\n\n") | |||
| ((0 to 15).map(_ => util.Random.nextInt(8)) zip | |||
| (0 to 15).map(_ => util.Random.nextInt(8))) | |||
| .foldLeft(DotProdCalculator(5)){ case(dotProd, (inputA, inputB)) => | |||
| val (output, valid, nextDotProd) = dotProd.update(inputA, inputB) | |||
| val inputString = s"At timestep ${dotProd.timeStep} inputs A: " + Console.YELLOW + s"$inputA " + Console.RESET + "B: " + Console.YELLOW + s"$inputB" + Console.RESET | |||
| val outputString = s"the output was: output: " + Console.YELLOW + s"$output" + Console.RESET + ", valid: " + (if(valid) Console.GREEN else Console.RED) + s"$valid\n" + Console.RESET | |||
| println(inputString) | |||
| println(outputString) | |||
| nextDotProd | |||
| } | |||
| true | |||
| } | |||
| } | |||
| @@ -8,8 +8,8 @@ import TestUtils._ | |||
| class MatMulSpec extends FlatSpec with Matchers { | |||
| import MatMulTests._ | |||
| val rowDims = scala.util.Random.nextInt(5) + 3 | |||
| val colDims = scala.util.Random.nextInt(5) + 3 | |||
| val rowDims = 3 | |||
| val colDims = 7 | |||
| behavior of "MatMul" | |||
| @@ -25,6 +25,8 @@ class MatMulSpec extends FlatSpec with Matchers { | |||
| object MatMulTests { | |||
| val rand = new scala.util.Random(100) | |||
| class TestExample(c: MatMul) extends PeekPokeTester(c) { | |||
| val mA = genMatrix(c.rowDimsA, c.colDimsA) | |||
| val mB = genMatrix(c.rowDimsA, c.colDimsA) | |||
| @@ -1,58 +0,0 @@ | |||
| The most important part of this task is keeping track of which row and column should be | |||
| accessed in each matrix. | |||
| In short, there are three values you need to have control over: | |||
| Column select, Row select A and Row select B | |||
| Further, it is useful to separate input phase and calculation phase. | |||
| In the input phase Row select A should be the same as Row select B since | |||
| you're simply inputting values. | |||
| For a 3x2 matrix you want to first input the first vector (that is, row 0), | |||
| so for cycle 0, 1 and 2 rowSelect should be 0, whereas column select should be | |||
| the same as the (cycle % columns) | |||
| After Inputting data all three values should be reset to 0. | |||
| This is a fairly typical task, so chisel.util has you covered with the Counter class | |||
| https://chisel.eecs.berkeley.edu/api/3.0.1/chisel3/util/Counter.html | |||
| https://chisel.eecs.berkeley.edu/api/3.0.1/chisel3/util/Counter$.html | |||
| The second link links to the Counter object rather than the class. Typically we put | |||
| constructors and static methods in the companion object of a class. | |||
| Object methods named apply can be called directly from the object, which means | |||
| ~MyObject.apply(123)~ is the same as ~MyObject(123)~ | |||
| Very convenient once you're used to it, but a little odd first time you see it. | |||
| In the Counter object there is an apply method: | |||
| #+begin_src scala | |||
| def apply(cond: Bool, n: Int): (UInt, Bool) | |||
| #+end_src | |||
| From the signature we see (well, I see because I happen to know that scala bools are called Boolean) | |||
| that the input is a chisel.data.Bool and a scala Int, and the output is a tuple of | |||
| chisel.data.UInt and chisel.data.Bool | |||
| The return values for the call to Counter.apply is a wire containing the current value of the counter, | |||
| and a wire that is toggled whenever the clock rolls over. | |||
| The arguments are a scala int, specifying how many ticks it takes for the clock to roll over, and a | |||
| wire whose signal toggles the clock on or off. | |||
| In our matrix multiplier this is pretty handy: | |||
| #+begin_src scala | |||
| val (colCounter, colCounterWrap) = Counter(true.B, colDimsA) | |||
| val (rowSelA, rowSelAWrap) = Counter(colCounterWrap, ???) | |||
| #+end_src | |||
| Here we have defined two counters. The column counter always ticks, wrapping around when it reaches colDimsA. | |||
| When the column counter wraps, the colCounterWrap wire is toggled, and the rowSelect clock makes a single tick. | |||
| If you can get row select A, B and col select to work in both phases you're very close to done, so these | |||
| should be your priority. | |||
| To test them, write your own test for MatMul that simply runs your MatMul unit and peeks at these values each | |||
| timestep until you're confident that they're correct. | |||
| As described in the debugging section, you should make a separate debug IO port with debug signals enabling you | |||
| to read out these values. Attempting to read them directly will throw an error. | |||
| The best tip of course is to actually show up during lab hours. | |||
| @@ -12,12 +12,22 @@ class MatrixSpec extends FlatSpec with Matchers { | |||
| behavior of "Matrix" | |||
| val rowDims = scala.util.Random.nextInt(5) + 3 | |||
| val colDims = scala.util.Random.nextInt(5) + 3 | |||
| val rand = new scala.util.Random(100) | |||
| val rowDims = 5 | |||
| val colDims = 3 | |||
| it should "Update its contents" in { | |||
| it should "Update its contents with a square shape" in { | |||
| wrapTester( | |||
| chisel3.iotesters.Driver(() => new Matrix(10,10)) { c => | |||
| chisel3.iotesters.Driver(() => new Matrix(rowDims,rowDims)) { c => | |||
| new UpdatesData(c) | |||
| } should be(true) | |||
| ) | |||
| } | |||
| it should "Update its contents with a rectangular shape" in { | |||
| wrapTester( | |||
| chisel3.iotesters.Driver(() => new Matrix(rowDims,colDims)) { c => | |||
| new UpdatesData(c) | |||
| } should be(true) | |||
| ) | |||
| @@ -26,7 +36,7 @@ class MatrixSpec extends FlatSpec with Matchers { | |||
| it should "Retain its contents when writeEnable is low" in { | |||
| wrapTester( | |||
| chisel3.iotesters.Driver(() => new Matrix(10,10)) { c => | |||
| chisel3.iotesters.Driver(() => new Matrix(rowDims, colDims)) { c => | |||
| new UpdatesData(c) | |||
| } should be(true) | |||
| ) | |||
| @@ -35,10 +45,12 @@ class MatrixSpec extends FlatSpec with Matchers { | |||
| object MatrixTests { | |||
| val rand = new scala.util.Random(100) | |||
| class UpdatesData(c: Matrix) extends PeekPokeTester(c) { | |||
| val inputs = List.fill(c.colsDim){ | |||
| List.fill(c.rowsDim)(scala.util.Random.nextInt(20) + 1) | |||
| List.fill(c.rowsDim)(rand.nextInt(20) + 1) | |||
| } | |||
| poke(c.io.writeEnable, true) | |||
| @@ -65,7 +77,7 @@ object MatrixTests { | |||
| class RetainsData(c: Matrix) extends PeekPokeTester(c) { | |||
| val inputs = List.fill(c.colsDim){ | |||
| List.fill(c.rowsDim)(scala.util.Random.nextInt(20) + 1) | |||
| List.fill(c.rowsDim)(rand.nextInt(20) + 1) | |||
| } | |||
| poke(c.io.writeEnable, true) | |||
| @@ -7,8 +7,10 @@ import org.scalatest.{Matchers, FlatSpec} | |||
| object TestUtils { | |||
| val rand = new scala.util.Random(100) | |||
| def genMatrix(rows: Int, cols: Int) = List.fill(rows)( | |||
| List.fill(cols)(scala.util.Random.nextInt(5)) | |||
| List.fill(cols)(rand.nextInt(5)) | |||
| ) | |||
| def printVector(v: List[Int]): String = | |||
| @@ -40,6 +42,30 @@ object TestUtils { | |||
| println("##########################################################") | |||
| println("##########################################################") | |||
| } | |||
| case e: chisel3.core.Binding.ExpectedHardwareException => { | |||
| println("##########################################################") | |||
| println("##########################################################") | |||
| println("##########################################################") | |||
| println("Your design is using raw chisel types!") | |||
| println("error:\n") | |||
| println(e.getMessage) | |||
| println("") | |||
| println("") | |||
| println("This typically occurs when you forget to wrap a module") | |||
| println("e.g") | |||
| println(""" | |||
| class MyBundle extends Bundle { | |||
| val signal = UInt(32.W) | |||
| } | |||
| val mySignal = new MyBundle | |||
| ^^^^ Wrong! | |||
| should be | |||
| val mySignal = Wire(new MyBundle) | |||
| """) | |||
| println("##########################################################") | |||
| println("##########################################################") | |||
| println("##########################################################") | |||
| } | |||
| case e: Exception => throw e | |||
| } | |||
| } | |||
| @@ -11,7 +11,7 @@ import scala.collection.immutable.{ Vector => _ } | |||
| class VectorSpec extends FlatSpec with Matchers { | |||
| import VectorTests._ | |||
| val elements = scala.util.Random.nextInt(5) + 2 | |||
| val elements = 7 | |||
| behavior of "Vector" | |||