diff --git a/Images/DotProd.png b/Images/DotProd.png new file mode 100644 index 0000000..e2d3c38 Binary files /dev/null and b/Images/DotProd.png differ diff --git a/Images/MatMul.png b/Images/MatMul.png new file mode 100644 index 0000000..48fd1b4 Binary files /dev/null and b/Images/MatMul.png differ diff --git a/Images/Source/MatMul.png b/Images/Source/MatMul.png new file mode 100644 index 0000000..48fd1b4 Binary files /dev/null and b/Images/Source/MatMul.png differ diff --git a/Images/Source/waveform.tex b/Images/Source/waveform.tex new file mode 100644 index 0000000..1e61c46 --- /dev/null +++ b/Images/Source/waveform.tex @@ -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} diff --git a/TODOs.org b/TODOs.org index 54a99d6..843014a 100644 --- a/TODOs.org +++ b/TODOs.org @@ -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 diff --git a/circuitRendering.org b/circuitRendering.org index 36187dc..217dad8 100644 --- a/circuitRendering.org +++ b/circuitRendering.org @@ -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 diff --git a/exercise.org b/exercise.org index 446894a..470c650 100644 --- a/exercise.org +++ b/exercise.org @@ -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. diff --git a/introduction.org b/introduction.org index 23d13ec..257b35d 100644 --- a/introduction.org +++ b/introduction.org @@ -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 diff --git a/oppgavetekst.org b/oppgavetekst.org deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/scala/DotProd.scala b/src/main/scala/DotProd.scala index c87b7e3..9a65cc2 100644 --- a/src/main/scala/DotProd.scala +++ b/src/main/scala/DotProd.scala @@ -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 diff --git a/src/main/scala/fileUtils.scala b/src/main/scala/fileUtils.scala deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/scala/main.scala b/src/main/scala/main.scala index 674dc5d..9b0a7c9 100644 --- a/src/main/scala/main.scala +++ b/src/main/scala/main.scala @@ -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)) + } diff --git a/src/test/resources/svgs/SumOrSquare.png b/src/test/resources/svgs/SumOrSquare.png new file mode 100644 index 0000000..26fe8f3 Binary files /dev/null and b/src/test/resources/svgs/SumOrSquare.png differ diff --git a/src/test/scala/DotProdSpec.scala b/src/test/scala/DotProdSpec.scala index ff8030b..d19d1ee 100644 --- a/src/test/scala/DotProdSpec.scala +++ b/src/test/scala/DotProdSpec.scala @@ -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){ diff --git a/src/test/scala/Example.scala b/src/test/scala/Example.scala deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/scala/Examples/softwareDotProd.scala b/src/test/scala/Examples/softwareDotProd.scala new file mode 100644 index 0000000..b33b2c0 --- /dev/null +++ b/src/test/scala/Examples/softwareDotProd.scala @@ -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 + } +} diff --git a/src/test/scala/MatMulSpec.scala b/src/test/scala/MatMulSpec.scala index 1dfecfb..9e1c0a5 100644 --- a/src/test/scala/MatMulSpec.scala +++ b/src/test/scala/MatMulSpec.scala @@ -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) diff --git a/src/test/scala/MatMulTips.org b/src/test/scala/MatMulTips.org index c224fd7..e69de29 100644 --- a/src/test/scala/MatMulTips.org +++ b/src/test/scala/MatMulTips.org @@ -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. diff --git a/src/test/scala/MatrixSpec.scala b/src/test/scala/MatrixSpec.scala index c7ff13b..580881e 100644 --- a/src/test/scala/MatrixSpec.scala +++ b/src/test/scala/MatrixSpec.scala @@ -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) diff --git a/src/test/scala/TestUtils.scala b/src/test/scala/TestUtils.scala index 34a392c..3ded57a 100644 --- a/src/test/scala/TestUtils.scala +++ b/src/test/scala/TestUtils.scala @@ -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 } } diff --git a/src/test/scala/VectorSpec.scala b/src/test/scala/VectorSpec.scala index c830357..355f458 100644 --- a/src/test/scala/VectorSpec.scala +++ b/src/test/scala/VectorSpec.scala @@ -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"