diff --git a/exercise.org b/exercise.org new file mode 100644 index 0000000..930cbd2 --- /dev/null +++ b/exercise.org @@ -0,0 +1,111 @@ +* Matrix matrix multiplication + For your first foray into chisel you will design a matrix matrix multiplication unit. + Matrix multiplication is fairly straight forward, however on hardware it's a little + trickier than the standard for loops normally employed.. + +** Task 1 - Vector + The first component you should implement is a register bank for storing a vector. + + In Vector.scala you will find the skeleton code for this component. + Unlike the standard Chisel.Vec our custom vector has a read enable which means that + the memory pointed to by idx will only be overWritten when readEnable is true. + + Implement the vector and test that it works by running + ~testOnly Ex0.VectorSpec~ in your sbt console. + +** Task 2 - Matrix + The matrix works just like the vector only in two dimensions. + The skeleton code and associated tests should make the purpose of this module obvious. + Run the tests with ~testOnly Ex0.VectorSpec~ + +** Task 3 - Dot Product + This component differs from the two previous in that it has no explicit control input, + which might at first be rather confusing. + + With only two inputs for data, how do we know when the dotproduct has been calculated? + The answer to this is the ~elements~ argument, which tells the dot product calculator the + size of the input vectors. + Consequently, the resulting hardware can only (at least on its own) compute dotproducts + for one size of vector, which is fine in our circuit. + + To get a better understanding we can model this behavior in regular scala: + + #+begin_src scala + 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){ + (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. + + As with the previous tasks, the dot product calculator must pass the tests with + ~testOnly Ex0.DotProdSpec~ + + +** Task 4 - Matrix Matrix multiplication + With our matrix modules and dot product calculators we have every piece needed to + implement the matrix multiplier. + + When performing matrix multiplication on a computer transposing the second matrix + can help us reduce complexity by quite a lot. To examplify, consider + + #+begin_src + | 2, 5 | + A = | 7, -1 | + | 0, 4 | + + + B = | 1, 1, 2 | + | 0, 4, 0 | + #+end_src + + It would be much simpler to just have two modules with the same dimensions, and we + can do this by transposing B so we get + + #+begin_src + | 2, 5 | + A = | 7, -1 | + | 0, 4 | + + | 1, 0 | + BT = | 1, 4 | + | 2, 0 | + #+end_src + + Now we need to do is calculate the dot products for the final matrix: + + #+begin_src + if A*B = C then + + | A[0] × BT[0], A[0] × BT[1], A[0] × BT[2] | + C = | A[1] × BT[0], ... , ... | + | ... , ... , A[2] × BT[2] | + + where + A[0] × BT[0] is the dot product of [2, 5] and [1, 0] + and + A[0] × BT[1] is the dot product of [2, 5] and [1, 4] + and so forth.. + #+end_src + + Because of this, the input for matrix B will be supplied transposed, thus you do not + have to worry about this. For B the input would be [1, 0, 1, 4, 2, 0] + + 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 + +** 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. + + A common saying is "A few hours of work can save you from several minutes of planning", + and this holds especially true for writing chisel!! diff --git a/introduction.org b/introduction.org new file mode 100644 index 0000000..af2e610 --- /dev/null +++ b/introduction.org @@ -0,0 +1,840 @@ +* Excercise Zero + The goal of this excercise is to gain some familiarity with developing for + FPGAs using chisel. + In this exercise you will implement a circuit capable of performing matrix + matrix multiplication in the chisel hardware description language. + +* Chisel +** Prerequisites + + *You should have some idea of how digital logic circuits work.* + + You should have a basic overview on digital circuits. + It is assumed that you know what a multiplexer is and how it works, and + how a register works. + + + *You must be able to run scala programs.* + + If you can run java then you can run scala. + If not grab the jvm. Remember to curse Larry Page if you pick it up from the + oracle webpage. + + + *Some flavor of GNU/Linux, or at least something UNIX-like.* + + If you use anything other than Ubuntu 16.04 or 18.04 I won't be able to offer + help if something goes wrong. + + + *An editor suited for scala.* + + My personal recommendation is GNU Emacs with emacs-lsp for IDE features along + with the metals language server (which works for any editor with lsp (language + server protocol), such as vim, vscode and atom). + If you prefer an IDE I hear good things about intelliJ, however I haven't tested + it personally, so if odd stuff happens I can't help you. + + + *Optional: sbt* + + You can install the scala build tool on your system, but for convenience I've + included a bootstrap script in sbt.sh. + sbt will select the correct version for you, so you don't have to worry about + getting the wrong version. + + +** Terms + Before delving into code it's necessary to define some terms. + + + *Wire* + + A wire is a bundle of 1 to N condictive wires (yes, that is a recursive + definition, but I think you get what I mean). These wires are connected + either to ground or a voltage source, corresponding to 0 or 1, which + is useful for representing numbers + + We can define a wire consisting of 4 physical wires in chisel like this + #+begin_src scala + val myWire = Wire(UInt(4.W)) + #+end_src + + + *Driving* + + A wire in on itself is rather pointless since it doesn't do anything. + In order for something to happen we need to connect them. + #+begin_src scala + val wireA = Wire(UInt(4.W)) + val wireB = Wire(UInt(4.W)) + wireA := 2.U + wireB := wireA + #+end_src + Here wireA is driven by the signal 2.U, and wireB is driven by wireA. + + For well behaved circuits it does not make sense to let a wire be driven + by multiple sources which would make the resulting signal undefined. + + Similarily a circular dependency is not allowed a la + #+begin_src scala + val wireA = Wire(UInt(4.W)) + val wireB = Wire(UInt(4.W)) + wireA := wireB + wireB := wireA + #+end_src + + Physically it *is* possible to have multiple drivers, but it's not a good idea + as attempting to drive a wire with 0 and 1 simultaneously causes a short circuit + which is definitely not a good thing. + + + *Module* + + In order to make development easier we separate functionality into modules, + defined by its inputs and outputs. + + + *Combinatory circuit* + + A combinatory circuit is a circuit whose output is based only on its + inputs. + + + *Stateful circuit* + + A circuit that will give different results based on its internal state. + In common parlance, a circuit without registers (or memory) is combinatory + while a circuit with registers is stateful. + + + *Chisel Graph* + + A chisel program is a program whose result is a graph which can be synthesized + to a transistor level schematic of a logic circuit. + When connecting wires wireA and wireB we were actually manipulating a graph + (actually, two subgraphs that were eventually combined into one). + The chisel graph is directed, but it does allow cycles so long as they are not + combinatorial. + +** Your first component + The code for the snippets in this subchapter can be found in Example.scala in the test directory. + You can run them using sbt by running ./sbt in your project root which will open + your sbt console. + + This will start a large download, so be patient even if it looks like it's stuck. + + The first component we will consider is a simple combinatorial incrementor: + #+begin_src scala + // Using `val` in a class argument list makes that value public. + class MyIncrement(val 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 + + with incrementBy = 3 we get the following circuit: + TODO: Fig + + +** Testing your chisel component + After creating a module you might wonder how it can be run. + It is not a program, it's a description of a digital circuit, so in order to "run" a chisel model + we have to simulate it. + + This is done by creating a test program where the test runner drives inputs and reads outputs from + the module using what is called a peek poke tester. + + +*** Creating a peek poke tester + #+begin_src scala + class TheTestRunner(c: MyIncrement) extends PeekPokeTester(c) { + for(ii <- 0 until 5){ + poke(c.io.dataIn, ii) + val o = peek(c.io.dataOut) + println(s"At cycle $ii the output of myIncrement was $o") + expect(c.io.dataOut, ii+c.incrementBy) + } + } + #+end_src + + There are three features of the peek poke tester on display here: + + 1. a peek poke tester has the ability to peek at a value, returning its state. + This however is restricted to the modules *io only* + + 2. it has the ability to poke (set) the value of an input signal. + Again, this can be done to *input io only* + + 3. It can expect a signal to have a certain value and throw an exception if not met. + Expect is defined in terms of peek. + + A peek poke tester can also /step/ the circuit, this will be covered when stateful circuits + have been introduced. + +*** Running a peek poke tester + The test runner class we have defined requires a MyIncrement module that can be simulated. + However, by writing ~val inc3 = Module(new MyIncrement(3))~ the return value is a *chisel graph*, + i.e a schematic for a circuit. + In order to interact with a circuit the schematic must be interpreted, resulting in a simulated + circuit which the peek poke tester can interact with. + + If this isn't clear don't worry, in terms of code all we need to do is to invoke a chisel method for + building the circuit: + + #+begin_src scala + chisel3.iotesters.Driver(() => new MyIncrement(3)) { c => + new TheTestRunner(c) + } + #+end_src + + Unfortunately this code might be a little hard to parse if you're new to scala. + Understanding it is not necessary, it is sufficient to simply swap + ~MyIncrement(3)~ in ~(() => MyIncrement(3))~ with the module you want to test, and + ~TheTestRunner(c)~ with the test runner you want to run. + + #+begin_src scala + chisel3.iotesters.Driver(() => new MyIncrement(3)) { c => + new TheTestRunner(c) + } + #+end_src + +*** Putting it together into a runnable test + We want to be able to run our test from sbt. To do this we use the scalatest framework. + A test looks like this: + + #+begin_src scala + class MyTest extends FlatSpec with Matchers { + behavior of "the test that I have written" + + it should "sum two numbers" in { + 2 + 2 + } should be 4 + } + #+end_src + + The tester class introduces a lot of special syntax, but like above it is not necessary + to understand how, simply using the template above is sufficient. + + Applying the tester template we end up with: + + #+begin_src scala + class MyIncrementTest extends FlatSpec with Matchers { + behavior of "my increment" + + it should "increment its input by 3" in { + chisel3.iotesters.Driver(() => new MyIncrement(3)) { c => + new TheTestRunner(c) + } should be(true) + } + } + #+end_src + + By creating this test it is now possible to run it from sbt. + There are two ways to test. By simply writing "test" in the sbt console as so: + ~sbt:chisel-module-template> test~ every single test will be run. + Since this creates a lot of noise it's more useful to run "testOnly": + ~sbt:chisel-module-template> testOnly Examples.MyIncrementTest~ where "Examples" + is the name of the package and MyIncrementTest is the name of the test. + The tests for the exercise itself is located in Ex0, so for instance + ~sbt:chisel-module-template> testOnly Ex0.MatrixSpec~ will run the matrix test. + + Note: by running "test" once you can use tab completion in the sbt shell to find tests with testOnly. + using testOnly + + Running the test should look something like this. + #+begin_src + sbt:chisel-module-template> testOnly Examples.MyIncrementTest + Run starting. Expected test count is: 0 + ... + Circuit state created + [info] [0.001] SEED 1556890076413 + [info] [0.002] At cycle 0 the output of myIncrement was 3 + [info] [0.003] At cycle 1 the output of myIncrement was 4 + [info] [0.003] At cycle 2 the output of myIncrement was 5 + [info] [0.003] At cycle 3 the output of myIncrement was 6 + [info] [0.003] At cycle 4 the output of myIncrement was 7 + test MyIncrementTestMyIncrement Success: 5 tests passed in 5 cycles taking 0.011709 seconds + [info] [0.004] RAN 0 CYCLES PASSED + - should increment its input by 3 + Run completed in 771 milliseconds. + ... + #+end_src + + In the Example.scala file you can find the entire test. + The only difference is that everything is put in the same class. + +** Using modules + Let's see how we can use our module by instantiating it as a submodule: + #+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 + + The RTL diagram now looks like this: + + Note how the modules ~first~ and ~second~ are now drawn as black boxes. + When drawing RTL diagrams we're not interested in the internals of submodules. + + However, what if we want to instantiate an arbitrary amount of incrementors and chain them? + To see how this can be done it is necessary to take a detour: + + +** Scala and chisel + A major stumbling block for learning chisel is understanding the difference between scala and chisel. + To highlight the difference between the two consider how HTML is generated. + + When creating a list we could just write the HTML manually + #+begin_src html + + #+end_src + + However this is rather cumbersome, so we generate HTML programatically. + In scala we might do something (sloppy) like this: + #+begin_src scala + def generateList(politicians: List[String], affiliations: Map[String, String]): String = { + val inner = new ArrayBuffer[String]() + for(ii <- 0 until politicians.size){ + val nameString = politicians(ii) + val affiliationString = affiliations(nameString) + inner.add(s"
  • Name: $nameString, Affiliation: $affiliationString
  • ") + } + "" + } + + // Or if you prefer brevity + def generateList2(politicians: List[String], affiliations: Map[String, String]): String = { + val inner = politicians.map(p => s"
  • Name: $p, Affiliation ${affiliations(p)}
  • ") + "" + } + #+end_src + + Similarily we can use constructs such as for loops to manipulate the chisel graph: + + #+begin_src scala + class MyIncrementN(val incrementBy: Int, val 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.last.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. + + *Important!* + In the HTML examples differentiating the HTML and scala was easy because they're + fundamentally very different. However with hardware and software there is a much + larger overlap. + A big pitfall is vector types and indexing, since these make sense both in software + and in hardware. + + +*** Troubleshooting scala and chisel mixups + Here's a rather silly example highligthing the confusion that can happen when mixing + scala and chisel. + + Some of the code here will cause compiler errors, thus the corresponding code in + Examples/myVector.scala is commented out. + #+begin_src scala + class MyVector() extends Module { + val io = IO( + new Bundle { + val idx = Input(UInt(32.W)) + val out = Output(UInt(32.W)) + } + ) + + val values = List(1, 2, 3, 4) + + io.out := values(io.idx) + } + #+end_src + + If you try to compile this you will get an error. + + #+begin_src scala + sbt:chisel-module-template> compile + ... + [error] found : chisel3.core.UInt + [error] required: Int + [error] io.out := values(io.idx) + [error] ^ + #+end_src + + This error tells you that io.idx was of the wrong type, namely a ~chisel3.core.UInt~. + The List is a scala construct, it only exists when your design is synthesized, thus + attempting to index it with a chisel type does not make sense. + + Let's try again using a chisel Vec which can be indexed by chisel values: + #+begin_src scala + class MyVector() extends Module { + val io = IO( + new Bundle { + val idx = Input(UInt(32.W)) + val out = Output(UInt(32.W)) + } + ) + + // val values: List[Int] = List(1, 2, 3, 4) + val values = Vec(1, 2, 3, 4) + + io.out := values(io.idx) + } + #+end_src + + Now you will get the following error instead: + #+begin_src scala + [error] /home/peteraa/datateknikk/TDT4255_EX0/src/main/scala/Tile.scala:30:16: inferred type arguments [Int] do not conform to macro method apply's type parameter bounds [T <: chisel3.Data] + [error] val values = Vec(1, 2, 3, 4) + [error] ^ + [error] /home/peteraa/datateknikk/TDT4255_EX0/src/main/scala/Tile.scala:30:20: type mismatch; + [error] found : Int(1) + [error] required: T + [error] val values = Vec(1, 2, 3, 4) + ... + #+end_src + + The error states that the type ~Int~ cannot be constrained to a ~type T <: chisel3.Data~ which needs a + little unpacking: + + The ~<:~ symbol means subtype, meaning that the compiler expected the Vec to contain a chisel data type + such as chisel3.Data.UInt or chisel3.Data.Boolean, and Int is not one of them! + + A scala int represent 32 bits in memory, whereas a chisel UInt represents a bundle of wires that we + interpret as an unsigned integer, thus they are not interchangeable although they represent roughly + the same thing. + + #+begin_src scala + class MyVector() extends Module { + val io = IO( + new Bundle { + val idx = Input(UInt(32.W)) + val out = Output(UInt(32.W)) + } + ) + + val values = Vec(1.U, 2.U, 3.U, 4.U) + + // Alternatively + // val values = Vec(List(1, 2, 3, 4).map(scalaInt => UInt(scalaInt))) + + io.out := values(io.idx) + } + #+end_src + Which compiles. + + You might be suprised to see that it is possible to index a Vec with an integer as such: + #+begin_src scala + class MyVector() extends Module { + val io = IO( + new Bundle { + val idx = Input(UInt(32.W)) + val out = Output(UInt(32.W)) + } + ) + + val values = Vec(1.U, 2.U, 3.U, 4.U) + + io.out := values(3) + } + #+end_src + In this case 3 gets automatically changed to 3.U. + It's not a great idea to abuse implicit conversions, so you should refrain from doing this too much. + + + #+begin_src scala + class MyVecSpec extends FlatSpec with Matchers { + behavior of "MyVec" + + it should "Output whatever idx points to" in { + wrapTester( + chisel3.iotesters.Driver(() => new MyVector) { c => + new MyVecTester(c) + } should be(true) + ) + } + } + + class MyVecTester(c: MyVector) extends PeekPokeTester(c) { + for(ii <- 0 until 4){ + poke(c.io.idx, ii) + expect(c.io.out, ii) + } + } + #+end_src + + #+begin_src + sbt:chisel-module-template> testOnly Examples.MyVecSpec + ... + ... + [info] Compiling 1 Scala source to /home/peteraa/datateknikk/TDT4255_EX0/target/scala-2.12/test-classes ... + ... + ... + MyVecSpec: + MyVec + [info] [0.001] Elaborating design... + ... + Circuit state created + [info] [0.001] SEED 1556197694422 + test MyVector Success: 4 tests passed in 5 cycles taking 0.009254 seconds + [info] [0.002] RAN 0 CYCLES PASSED + - should Output whatever idx points to + Run completed in 605 milliseconds. + Total number of tests run: 1 + Suites: completed 1, aborted 0 + Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 + All tests passed. + #+end_src + + Great! + + + In order to get some insight into how a chisel Vec works, let's see how we can implement + myVector without Vec: + #+begin_src scala + class MyVectorAlt() extends Module { + val io = IO( + new Bundle { + val idx = Input(UInt(32.W)) + val out = Output(UInt(32.W)) + } + ) + + val values = Array(0.U, 1.U, 2.U, 3.U) + + io.out := values(0) + for(ii <- 0 until 4){ + when(io.idx(1, 0) === ii.U){ + io.out := values(ii) + } + } + } + #+end_src + + The for-loop creates 4 conditional blocks boiling down to + when 0: output the value in values(0) + when 1: output the value in values(1) + when 2: output the value in values(2) + when 3: output the value in values(3) + otherwise: output 0.U + + The otherwise clause will never occur, chisel is unable to inferr this + (however the synthesizer will likely be able to) + + In the conditional block the following syntax is used: + ~io.idx(1, 0) === ii.U)~ + which indicates that only the two low bits of idx will be used to index, which is + how chisel Vec does it. + + +*** Troubleshooting build time errors + In the HTML example, assume that the the last tag was ommited. This would not + be valid HTML, however the code will happily compile. Likewise, you can easily + create a valid scala program producing an invalid chisel graph: + + #+begin_src scala + class Invalid() extends Module { + val io = IO(new Bundle{}) + + val myVec = Module(new MyVector) + } + #+end_src + + This code will happily compile, however when you attempt to create a simulator from the + schematic chisel will throw an exception. + + #+begin_src scala + class InvalidSpec extends FlatSpec with Matchers { + behavior of "Invalid" + + it should "fail" in { + chisel3.iotesters.Driver(() => new Invalid) { c => + + // chisel tester expects a test here, but we can use ??? + // which is shorthand for throw new NotImplementedException. + // + // This is OK, because it will fail during building. + ??? + } should be(true) + } + } + #+end_src + + Running the test throws an error: + #+begin_src scala + sbt:chisel-module-template> compile + ... + [success] Total time: 3 s, completed Apr 25, 2019 3:15:15 PM + ... + sbt:chisel-module-template> testOnly Examples.InvalidSpec + ... + firrtl.passes.CheckInitialization$RefNotInitializedException: @[Example.scala 25:21:@20.4] : [module Invalid] Reference myVec is not fully initialized. + : myVec.io.idx <= VOID + at firrtl.passes.CheckInitialization$.$anonfun$run$6(CheckInitialization.scala:83) + at firrtl.passes.CheckInitialization$.$anonfun$run$6$adapted(CheckInitialization.scala:78) + at scala.collection.TraversableLike$WithFilter.$anonfun$foreach$1(TraversableLike.scala:789) + at scala.collection.mutable.HashMap.$anonfun$foreach$1(HashMap.scala:138) + at scala.collection.mutable.HashTable.foreachEntry(HashTable.scala:236) + at scala.collection.mutable.HashTable.foreachEntry$(HashTable.scala:229) + at scala.collection.mutable.HashMap.foreachEntry(HashMap.scala:40) + at scala.collection.mutable.HashMap.foreach(HashMap.scala:138) + at scala.collection.TraversableLike$WithFilter.foreach(TraversableLike.scala:788) + at firrtl.passes.CheckInitialization$.checkInitM$1(CheckInitialization.scala:78) + #+end_src + + While scary, the actual error is only this line: + #+begin_src scala + firrtl.passes.CheckInitialization$RefNotInitializedException: @[Example.scala 25:21:@20.4] : [module Invalid] Reference myVec is not fully initialized. + : myVec.io.idx <= VOID + #+end_src + + Which tells you that myVec.io.idx needs a driver. + #+begin_src scala + // Now actually valid... + class Invalid() extends Module { + val io = IO(new Bundle{}) + + val myVec = Module(new MyVector) + myVec.io.idx := 0.U + } + #+end_src + After fixing the invalid circuit and running the test you will insted get a large error + stack trace where you will see that: + ~- should fail *** FAILED ***~ + Which in some respect indicates success. + + +** Stateful circuits + Until now every circuit we have consider has been a combinatory circuit. + Consider the following circuit: + #+begin_src scala + class SimpleDelay() 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 stores its input in delayReg and drives its output with delayRegs output. + Registers are driven by a clock signal in addition to the input value, and it is only + capable of updating its value at a clock pulse. + + In some HDL languages it is necessary to include the clock signal in the modules IO, but + for chisel this happens implicitly. + + When testing we use the ~step(n)~ feature of peek poke tester which runs the clock signal n times. + + Test this by running ~testOnly Examples.DelaySpec~ + #+begin_src scala + class DelaySpec extends FlatSpec with Matchers { + behavior of "SimpleDelay" + + it should "Delay input by one timestep" in { + chisel3.iotesters.Driver(() => new SimpleDelay, verbose = true) { c => + new DelayTester(c) + } should be(true) + } + } + + class DelayTester(c: SimpleDelay) extends PeekPokeTester(c) { + for(ii <- 0 until 10){ + val input = scala.util.Random.nextInt(10) + poke(c.io.dataIn, input) + step(1) + expect(c.io.dataOut, input) + } + } + #+end_src + + In order to make it extra clear the Driver has the optional "verbose" parameter set to true. + This yields the following: + + #+begin_src + DelaySpec: + SimpleDelay + ... + End of dependency graph + Circuit state created + [info] [0.001] SEED 1556898121698 + [info] [0.002] POKE io_dataIn <- 7 + [info] [0.002] STEP 0 -> 1 + [info] [0.002] EXPECT AT 1 io_dataOut got 7 expected 7 PASS + [info] [0.002] POKE io_dataIn <- 8 + [info] [0.002] STEP 1 -> 2 + [info] [0.003] EXPECT AT 2 io_dataOut got 8 expected 8 PASS + [info] [0.003] POKE io_dataIn <- 2 + ... + [info] [0.005] STEP 9 -> 10 + [info] [0.005] EXPECT AT 10 io_dataOut got 7 expected 7 PASS + test SimpleDelay Success: 10 tests passed in 15 cycles taking 0.010393 seconds + [info] [0.005] RAN 10 CYCLES PASSED + #+end_src + + Following the output you can see how at step 0 the input is 7, then at step 1 + the expected (and observed) output is 7. + + +** Debugging + A rather difficult aspect in HDLs, including chisel is debugging. + When debugging it is necessary to inspect how the state of the circuit evolves, which + leaves us with two options, peekPokeTester and printf, however both have flaws. + +*** PeekPoke + The peek poke tester should always give a correct result, if not it's a bug, not a quirk. + Sadly peek poke testing is rather limited in that it cannot be used to access internal state. + Consider the following module: + #+begin_src scala + class Inner() extends Module { + val io = IO( + new Bundle { + val dataIn = Input(UInt(32.W)) + val dataOut = Output(UInt(32.W)) + } + ) + val innerState = RegInit(0.U) + when(io.dataIn % 2.U === 0.U){ + innerState := io.dataIn + } + + io.dataOut := innerState + } + + + class Outer() extends Module { + val io = IO( + new Bundle { + val dataIn = Input(UInt(32.W)) + val dataOut = Output(UInt(32.W)) + } + ) + + val outerState = RegInit(0.U) + val inner = Module(new Inner) + + outerState := io.dataIn + inner.io.dataIn := outerState + io.dataOut := inner.io.dataOut + } + #+end_src + + It would be nice if we could use the peekPokeTester to inspect what goes on inside + Inner, however this information is no longer available once Outer is synthesize into a + runnable circuit. + + To see this, run ~testOnly Example.PeekInternalSpec~ + + In the test an exception is thrown when either of the two peek statements underneath are + run: + #+begin_src scala + class OuterTester(c: Outer) extends PeekPokeTester(c) { + val inner = peek(c.inner.innerState) + val outer = peek(c.outerState) + } + #+end_src + + The only way to deal with this hurdle is to expose the state we are interested in as signals. + An example of this can be seen in + ~/Examples/printing.scala~ + + This approach leads to a lot of annoying clutter in your modules IO, so to separate business-logic + from debug signals it is useful to use a MultiIOModule where debug signals can be put in a separate + io bundle. This approach is used in the skeleton code for the exercises. + +*** printf + ~printf~ and ~println~ must not be mixed! + println behaves as expected in most languages, when executed it simply prints the argument. + In the tests so far it has only printed the value returned by peek. + + a printf statement on the other hand does not immediately print anything to the console. Instead it creates + a special chisel element which only exists during simulation and prints to your console each cycle, + thus helping us peer into the internal state of a circuit! + + Additionally, a printf statement in a conditional block will only execute if the condiditon is met, + allowing us to reduce noise. + #+begin_src scala + class PrintfExample() extends Module { + val io = IO(new Bundle{}) + + val counter = RegInit(0.U(8.W)) + counter := counter + 1.U + + printf("Counter is %d\n", counter) + when(counter % 2.U === 0.U){ + printf("Counter is even\n") + } + } + + class PrintfTest(c: PrintfExample) extends PeekPokeTester(c) { + for(ii <- 0 until 5){ + println(s"At cycle $ii:") + step(1) + } + } + #+end_src + When you run this test with ~testOnly Examples.PrintfExampleSpec~, did you get what you expected? + + As it turns out printf can be rather misleading when using stateful circuits. + To see this in action, try running ~testOnly Examples.EvilPrintfSpec~ which yields the following + #+begin_src + In cycle 0 the output of counter is: 0 + according to printf output is: 0 + [info] [0.003] + In cycle 1 the output of counter is: 0 + according to printf output is: 0 + [info] [0.003] + + + In cycle 2 the output of counter is: 0 + according to printf output is: 1 + ^^^^^^^^ + + [info] [0.004] + In cycle 3 the output of counter is: 1 + according to printf output is: 1 + [info] [0.004] + In cycle 4 the output of counter is: 1 + according to printf output is: 1 + #+end_src + + When looking at the circuits design it is pretty obvious that the peek poke tester is giving the + correct result, whereas the printf statement is printing the updated state of the register which + should not be visible before next cycle. + + In conclusion, do not use printf to debug timing issues, and if you do be extremely methodical. + + (It is possible to use a different simulator, treadle, which from what I have seen gives correct + printf results, it can be used by supplying an extra argument in the peek poke constructor like so: + ~chisel3.iotesters.Driver(() => new Outer, "treadle") { c =>~) diff --git a/oppgavetekst.org b/oppgavetekst.org index 09a8c33..e69de29 100644 --- a/oppgavetekst.org +++ b/oppgavetekst.org @@ -1,954 +0,0 @@ -* Excercise Zero - The goal of this excercise is to gain some familiarity with developing for - FPGAs using chisel. - In this exercise you will implement a circuit capable of performing matrix - matrix multiplication in the chisel hardware description language. - -* Chisel -** Prerequisites - + *You should have some idea of how digital logic circuits work.* - - You should have a basic overview on digital circuits. - It is assumed that you know what a multiplexer is and how it works, and - how a register works. - - + *You must be able to run scala programs.* - - If you can run java then you can run scala. - If not grab the jvm. Remember to curse Larry Page if you pick it up from the - oracle webpage. - - + *Some flavor of GNU/Linux, or at least something UNIX-like.* - - If you use anything other than Ubuntu 16.04 or 18.04 I won't be able to offer - help if something goes wrong. - - + *An editor suited for scala.* - - My personal recommendation is GNU Emacs with emacs-lsp for IDE features along - with the metals language server (which works for any editor with lsp (language - server protocol), such as vim, vscode and atom). - If you prefer an IDE I hear good things about intelliJ, however I haven't tested - it personally, so if odd stuff happens I can't help you. - - + *Optional: sbt* - - You can install the scala build tool on your system, but for convenience I've - included a bootstrap script in sbt.sh. - sbt will select the correct version for you, so you don't have to worry about - getting the wrong version. - - -** Terms - Before delving into code it's necessary to define some terms. - - + *Wire* - - A wire is a bundle of 1 to N condictive wires (yes, that is a recursive - definition, but I think you get what I mean). These wires are connected - either to ground or a voltage source, corresponding to 0 or 1, which - is useful for representing numbers - - We can define a wire consisting of 4 physical wires in chisel like this - #+begin_src scala - val myWire = Wire(UInt(4.W)) - #+end_src - - + *Driving* - - A wire in on itself is rather pointless since it doesn't do anything. - In order for something to happen we need to connect them. - #+begin_src scala - val wireA = Wire(UInt(4.W)) - val wireB = Wire(UInt(4.W)) - wireA := 2.U - wireB := wireA - #+end_src - Here wireA is driven by the signal 2.U, and wireB is driven by wireA. - - For well behaved circuits it does not make sense to let a wire be driven - by multiple sources which would make the resulting signal undefined. - - Similarily a circular dependency is not allowed a la - #+begin_src scala - val wireA = Wire(UInt(4.W)) - val wireB = Wire(UInt(4.W)) - wireA := wireB - wireB := wireA - #+end_src - - Physically it *is* possible to have multiple drivers, but it's not a good idea - as attempting to drive a wire with 0 and 1 simultaneously causes a short circuit - which is definitely not a good thing. - - + *Module* - - In order to make development easier we separate functionality into modules, - defined by its inputs and outputs. - - + *Combinatory circuit* - - A combinatory circuit is a circuit whose output is based only on its - inputs. - - + *Stateful circuit* - - A circuit that will give different results based on its internal state. - In common parlance, a circuit without registers (or memory) is combinatory - while a circuit with registers is stateful. - - + *Chisel Graph* - - A chisel program is a program whose result is a graph which can be synthesized - to a transistor level schematic of a logic circuit. - When connecting wires wireA and wireB we were actually manipulating a graph - (actually, two subgraphs that were eventually combined into one). - The chisel graph is directed, but it does allow cycles so long as they are not - combinatorial. - -** Your first component - The code for the snippets in this subchapter can be found in Example.scala in the test directory. - You can run them using sbt by running ./sbt in your project root which will open - your sbt console. - - This will start a large download, so be patient even if it looks like it's stuck. - - The first component we will consider is a simple combinatorial incrementor: - #+begin_src scala - // Using `val` in a class argument list makes that value public. - class MyIncrement(val 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 - - with incrementBy = 3 we get the following circuit: - TODO: Fig - - -** Testing your chisel component - After creating a module you might wonder how it can be run. - It is not a program, it's a description of a digital circuit, so in order to "run" a chisel model - we have to simulate it. - - This is done by creating a test program where the test runner drives inputs and reads outputs from - the module using what is called a peek poke tester. - - -*** Creating a peek poke tester - #+begin_src scala - class TheTestRunner(c: MyIncrement) extends PeekPokeTester(c) { - for(ii <- 0 until 5){ - poke(c.io.dataIn, ii) - val o = peek(c.io.dataOut) - println(s"At cycle $ii the output of myIncrement was $o") - expect(c.io.dataOut, ii+c.incrementBy) - } - } - #+end_src - - There are three features of the peek poke tester on display here: - - 1. a peek poke tester has the ability to peek at a value, returning its state. - This however is restricted to the modules *io only* - - 2. it has the ability to poke (set) the value of an input signal. - Again, this can be done to *input io only* - - 3. It can expect a signal to have a certain value and throw an exception if not met. - Expect is defined in terms of peek. - - A peek poke tester can also /step/ the circuit, this will be covered when stateful circuits - have been introduced. - -*** Running a peek poke tester - The test runner class we have defined requires a MyIncrement module that can be simulated. - However, by writing ~val inc3 = Module(new MyIncrement(3))~ the return value is a *chisel graph*, - i.e a schematic for a circuit. - In order to interact with a circuit the schematic must be interpreted, resulting in a simulated - circuit which the peek poke tester can interact with. - - If this isn't clear don't worry, in terms of code all we need to do is to invoke a chisel method for - building the circuit: - - #+begin_src scala - chisel3.iotesters.Driver(() => new MyIncrement(3)) { c => - new TheTestRunner(c) - } - #+end_src - - Unfortunately this code might be a little hard to parse if you're new to scala. - Understanding it is not necessary, it is sufficient to simply swap - ~MyIncrement(3)~ in ~(() => MyIncrement(3))~ with the module you want to test, and - ~TheTestRunner(c)~ with the test runner you want to run. - - #+begin_src scala - chisel3.iotesters.Driver(() => new MyIncrement(3)) { c => - new TheTestRunner(c) - } - #+end_src - -*** Putting it together into a runnable test - We want to be able to run our test from sbt. To do this we use the scalatest framework. - A test looks like this: - - #+begin_src scala - class MyTest extends FlatSpec with Matchers { - behavior of "the test that I have written" - - it should "sum two numbers" in { - 2 + 2 - } should be 4 - } - #+end_src - - The tester class introduces a lot of special syntax, but like above it is not necessary - to understand how, simply using the template above is sufficient. - - Applying the tester template we end up with: - - #+begin_src scala - class MyIncrementTest extends FlatSpec with Matchers { - behavior of "my increment" - - it should "increment its input by 3" in { - chisel3.iotesters.Driver(() => new MyIncrement(3)) { c => - new TheTestRunner(c) - } should be(true) - } - } - #+end_src - - By creating this test it is now possible to run it from sbt. - There are two ways to test. By simply writing "test" in the sbt console as so: - ~sbt:chisel-module-template> test~ every single test will be run. - Since this creates a lot of noise it's more useful to run "testOnly": - ~sbt:chisel-module-template> testOnly Examples.MyIncrementTest~ where "Examples" - is the name of the package and MyIncrementTest is the name of the test. - The tests for the exercise itself is located in Ex0, so for instance - ~sbt:chisel-module-template> testOnly Ex0.MatrixSpec~ will run the matrix test. - - Note: by running "test" once you can use tab completion in the sbt shell to find tests with testOnly. - using testOnly - - Running the test should look something like this. - #+begin_src - sbt:chisel-module-template> testOnly Examples.MyIncrementTest - Run starting. Expected test count is: 0 - ... - Circuit state created - [info] [0.001] SEED 1556890076413 - [info] [0.002] At cycle 0 the output of myIncrement was 3 - [info] [0.003] At cycle 1 the output of myIncrement was 4 - [info] [0.003] At cycle 2 the output of myIncrement was 5 - [info] [0.003] At cycle 3 the output of myIncrement was 6 - [info] [0.003] At cycle 4 the output of myIncrement was 7 - test MyIncrementTestMyIncrement Success: 5 tests passed in 5 cycles taking 0.011709 seconds - [info] [0.004] RAN 0 CYCLES PASSED - - should increment its input by 3 - Run completed in 771 milliseconds. - ... - #+end_src - - In the Example.scala file you can find the entire test. - The only difference is that everything is put in the same class. - -** Using modules - Let's see how we can use our module by instantiating it as a submodule: - #+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 - - The RTL diagram now looks like this: - - Note how the modules ~first~ and ~second~ are now drawn as black boxes. - When drawing RTL diagrams we're not interested in the internals of submodules. - - However, what if we want to instantiate an arbitrary amount of incrementors and chain them? - To see how this can be done it is necessary to take a detour: - - -** Scala and chisel - A major stumbling block for learning chisel is understanding the difference between scala and chisel. - To highlight the difference between the two consider how HTML is generated. - - When creating a list we could just write the HTML manually - #+begin_src html - - #+end_src - - However this is rather cumbersome, so we generate HTML programatically. - In scala we might do something (sloppy) like this: - #+begin_src scala - def generateList(politicians: List[String], affiliations: Map[String, String]): String = { - val inner = new ArrayBuffer[String]() - for(ii <- 0 until politicians.size){ - val nameString = politicians(ii) - val affiliationString = affiliations(nameString) - inner.add(s"
  • Name: $nameString, Affiliation: $affiliationString
  • ") - } - "" - } - - // Or if you prefer brevity - def generateList2(politicians: List[String], affiliations: Map[String, String]): String = { - val inner = politicians.map(p => s"
  • Name: $p, Affiliation ${affiliations(p)}
  • ") - "" - } - #+end_src - - Similarily we can use constructs such as for loops to manipulate the chisel graph: - - #+begin_src scala - class MyIncrementN(val incrementBy: Int, val 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.last.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. - - *Important!* - In the HTML examples differentiating the HTML and scala was easy because they're - fundamentally very different. However with hardware and software there is a much - larger overlap. - A big pitfall is vector types and indexing, since these make sense both in software - and in hardware. - - -*** Troubleshooting scala and chisel mixups - Here's a rather silly example highligthing the confusion that can happen when mixing - scala and chisel. - - Some of the code here will cause compiler errors, thus the corresponding code in - Examples/myVector.scala is commented out. - #+begin_src scala - class MyVector() extends Module { - val io = IO( - new Bundle { - val idx = Input(UInt(32.W)) - val out = Output(UInt(32.W)) - } - ) - - val values = List(1, 2, 3, 4) - - io.out := values(io.idx) - } - #+end_src - - If you try to compile this you will get an error. - - #+begin_src scala - sbt:chisel-module-template> compile - ... - [error] found : chisel3.core.UInt - [error] required: Int - [error] io.out := values(io.idx) - [error] ^ - #+end_src - - This error tells you that io.idx was of the wrong type, namely a ~chisel3.core.UInt~. - The List is a scala construct, it only exists when your design is synthesized, thus - attempting to index it with a chisel type does not make sense. - - Let's try again using a chisel Vec which can be indexed by chisel values: - #+begin_src scala - class MyVector() extends Module { - val io = IO( - new Bundle { - val idx = Input(UInt(32.W)) - val out = Output(UInt(32.W)) - } - ) - - // val values: List[Int] = List(1, 2, 3, 4) - val values = Vec(1, 2, 3, 4) - - io.out := values(io.idx) - } - #+end_src - - Now you will get the following error instead: - #+begin_src scala - [error] /home/peteraa/datateknikk/TDT4255_EX0/src/main/scala/Tile.scala:30:16: inferred type arguments [Int] do not conform to macro method apply's type parameter bounds [T <: chisel3.Data] - [error] val values = Vec(1, 2, 3, 4) - [error] ^ - [error] /home/peteraa/datateknikk/TDT4255_EX0/src/main/scala/Tile.scala:30:20: type mismatch; - [error] found : Int(1) - [error] required: T - [error] val values = Vec(1, 2, 3, 4) - ... - #+end_src - - The error states that the type ~Int~ cannot be constrained to a ~type T <: chisel3.Data~ which needs a - little unpacking: - - The ~<:~ symbol means subtype, meaning that the compiler expected the Vec to contain a chisel data type - such as chisel3.Data.UInt or chisel3.Data.Boolean, and Int is not one of them! - - A scala int represent 32 bits in memory, whereas a chisel UInt represents a bundle of wires that we - interpret as an unsigned integer, thus they are not interchangeable although they represent roughly - the same thing. - - #+begin_src scala - class MyVector() extends Module { - val io = IO( - new Bundle { - val idx = Input(UInt(32.W)) - val out = Output(UInt(32.W)) - } - ) - - val values = Vec(1.U, 2.U, 3.U, 4.U) - - // Alternatively - // val values = Vec(List(1, 2, 3, 4).map(scalaInt => UInt(scalaInt))) - - io.out := values(io.idx) - } - #+end_src - Which compiles. - - You might be suprised to see that it is possible to index a Vec with an integer as such: - #+begin_src scala - class MyVector() extends Module { - val io = IO( - new Bundle { - val idx = Input(UInt(32.W)) - val out = Output(UInt(32.W)) - } - ) - - val values = Vec(1.U, 2.U, 3.U, 4.U) - - io.out := values(3) - } - #+end_src - In this case 3 gets automatically changed to 3.U. - It's not a great idea to abuse implicit conversions, so you should refrain from doing this too much. - - - #+begin_src scala - class MyVecSpec extends FlatSpec with Matchers { - behavior of "MyVec" - - it should "Output whatever idx points to" in { - wrapTester( - chisel3.iotesters.Driver(() => new MyVector) { c => - new MyVecTester(c) - } should be(true) - ) - } - } - - class MyVecTester(c: MyVector) extends PeekPokeTester(c) { - for(ii <- 0 until 4){ - poke(c.io.idx, ii) - expect(c.io.out, ii) - } - } - #+end_src - - #+begin_src - sbt:chisel-module-template> testOnly Examples.MyVecSpec - ... - ... - [info] Compiling 1 Scala source to /home/peteraa/datateknikk/TDT4255_EX0/target/scala-2.12/test-classes ... - ... - ... - MyVecSpec: - MyVec - [info] [0.001] Elaborating design... - ... - Circuit state created - [info] [0.001] SEED 1556197694422 - test MyVector Success: 4 tests passed in 5 cycles taking 0.009254 seconds - [info] [0.002] RAN 0 CYCLES PASSED - - should Output whatever idx points to - Run completed in 605 milliseconds. - Total number of tests run: 1 - Suites: completed 1, aborted 0 - Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 - All tests passed. - #+end_src - - Great! - - - In order to get some insight into how a chisel Vec works, let's see how we can implement - myVector without Vec: - #+begin_src scala - class MyVectorAlt() extends Module { - val io = IO( - new Bundle { - val idx = Input(UInt(32.W)) - val out = Output(UInt(32.W)) - } - ) - - val values = Array(0.U, 1.U, 2.U, 3.U) - - io.out := values(0) - for(ii <- 0 until 4){ - when(io.idx(1, 0) === ii.U){ - io.out := values(ii) - } - } - } - #+end_src - - The for-loop creates 4 conditional blocks boiling down to - when 0: output the value in values(0) - when 1: output the value in values(1) - when 2: output the value in values(2) - when 3: output the value in values(3) - otherwise: output 0.U - - The otherwise clause will never occur, chisel is unable to inferr this - (however the synthesizer will likely be able to) - - In the conditional block the following syntax is used: - ~io.idx(1, 0) === ii.U)~ - which indicates that only the two low bits of idx will be used to index, which is - how chisel Vec does it. - - -*** Troubleshooting build time errors - In the HTML example, assume that the the last tag was ommited. This would not - be valid HTML, however the code will happily compile. Likewise, you can easily - create a valid scala program producing an invalid chisel graph: - - #+begin_src scala - class Invalid() extends Module { - val io = IO(new Bundle{}) - - val myVec = Module(new MyVector) - } - #+end_src - - This code will happily compile, however when you attempt to create a simulator from the - schematic chisel will throw an exception. - - #+begin_src scala - class InvalidSpec extends FlatSpec with Matchers { - behavior of "Invalid" - - it should "fail" in { - chisel3.iotesters.Driver(() => new Invalid) { c => - - // chisel tester expects a test here, but we can use ??? - // which is shorthand for throw new NotImplementedException. - // - // This is OK, because it will fail during building. - ??? - } should be(true) - } - } - #+end_src - - Running the test throws an error: - #+begin_src scala - sbt:chisel-module-template> compile - ... - [success] Total time: 3 s, completed Apr 25, 2019 3:15:15 PM - ... - sbt:chisel-module-template> testOnly Examples.InvalidSpec - ... - firrtl.passes.CheckInitialization$RefNotInitializedException: @[Example.scala 25:21:@20.4] : [module Invalid] Reference myVec is not fully initialized. - : myVec.io.idx <= VOID - at firrtl.passes.CheckInitialization$.$anonfun$run$6(CheckInitialization.scala:83) - at firrtl.passes.CheckInitialization$.$anonfun$run$6$adapted(CheckInitialization.scala:78) - at scala.collection.TraversableLike$WithFilter.$anonfun$foreach$1(TraversableLike.scala:789) - at scala.collection.mutable.HashMap.$anonfun$foreach$1(HashMap.scala:138) - at scala.collection.mutable.HashTable.foreachEntry(HashTable.scala:236) - at scala.collection.mutable.HashTable.foreachEntry$(HashTable.scala:229) - at scala.collection.mutable.HashMap.foreachEntry(HashMap.scala:40) - at scala.collection.mutable.HashMap.foreach(HashMap.scala:138) - at scala.collection.TraversableLike$WithFilter.foreach(TraversableLike.scala:788) - at firrtl.passes.CheckInitialization$.checkInitM$1(CheckInitialization.scala:78) - #+end_src - - While scary, the actual error is only this line: - #+begin_src scala - firrtl.passes.CheckInitialization$RefNotInitializedException: @[Example.scala 25:21:@20.4] : [module Invalid] Reference myVec is not fully initialized. - : myVec.io.idx <= VOID - #+end_src - - Which tells you that myVec.io.idx needs a driver. - #+begin_src scala - // Now actually valid... - class Invalid() extends Module { - val io = IO(new Bundle{}) - - val myVec = Module(new MyVector) - myVec.io.idx := 0.U - } - #+end_src - After fixing the invalid circuit and running the test you will insted get a large error - stack trace where you will see that: - ~- should fail *** FAILED ***~ - Which in some respect indicates success. - - -** Stateful circuits - Until now every circuit we have consider has been a combinatory circuit. - Consider the following circuit: - #+begin_src scala - class SimpleDelay() 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 stores its input in delayReg and drives its output with delayRegs output. - Registers are driven by a clock signal in addition to the input value, and it is only - capable of updating its value at a clock pulse. - - In some HDL languages it is necessary to include the clock signal in the modules IO, but - for chisel this happens implicitly. - - When testing we use the ~step(n)~ feature of peek poke tester which runs the clock signal n times. - - Test this by running ~testOnly Examples.DelaySpec~ - #+begin_src scala - class DelaySpec extends FlatSpec with Matchers { - behavior of "SimpleDelay" - - it should "Delay input by one timestep" in { - chisel3.iotesters.Driver(() => new SimpleDelay, verbose = true) { c => - new DelayTester(c) - } should be(true) - } - } - - class DelayTester(c: SimpleDelay) extends PeekPokeTester(c) { - for(ii <- 0 until 10){ - val input = scala.util.Random.nextInt(10) - poke(c.io.dataIn, input) - step(1) - expect(c.io.dataOut, input) - } - } - #+end_src - - In order to make it extra clear the Driver has the optional "verbose" parameter set to true. - This yields the following: - - #+begin_src - DelaySpec: - SimpleDelay - ... - End of dependency graph - Circuit state created - [info] [0.001] SEED 1556898121698 - [info] [0.002] POKE io_dataIn <- 7 - [info] [0.002] STEP 0 -> 1 - [info] [0.002] EXPECT AT 1 io_dataOut got 7 expected 7 PASS - [info] [0.002] POKE io_dataIn <- 8 - [info] [0.002] STEP 1 -> 2 - [info] [0.003] EXPECT AT 2 io_dataOut got 8 expected 8 PASS - [info] [0.003] POKE io_dataIn <- 2 - ... - [info] [0.005] STEP 9 -> 10 - [info] [0.005] EXPECT AT 10 io_dataOut got 7 expected 7 PASS - test SimpleDelay Success: 10 tests passed in 15 cycles taking 0.010393 seconds - [info] [0.005] RAN 10 CYCLES PASSED - #+end_src - - Following the output you can see how at step 0 the input is 7, then at step 1 - the expected (and observed) output is 7. - - -** Debugging - A rather difficult aspect in HDLs, including chisel is debugging. - When debugging it is necessary to inspect how the state of the circuit evolves, which - leaves us with two options, peekPokeTester and printf, however both have flaws. - -*** PeekPoke - The peek poke tester should always give a correct result, if not it's a bug, not a quirk. - Sadly peek poke testing is rather limited in that it cannot be used to access internal state. - Consider the following module: - #+begin_src scala - class Inner() extends Module { - val io = IO( - new Bundle { - val dataIn = Input(UInt(32.W)) - val dataOut = Output(UInt(32.W)) - } - ) - val innerState = RegInit(0.U) - when(io.dataIn % 2.U === 0.U){ - innerState := io.dataIn - } - - io.dataOut := innerState - } - - - class Outer() extends Module { - val io = IO( - new Bundle { - val dataIn = Input(UInt(32.W)) - val dataOut = Output(UInt(32.W)) - } - ) - - val outerState = RegInit(0.U) - val inner = Module(new Inner) - - outerState := io.dataIn - inner.io.dataIn := outerState - io.dataOut := inner.io.dataOut - } - #+end_src - - It would be nice if we could use the peekPokeTester to inspect what goes on inside - Inner, however this information is no longer available once Outer is synthesize into a - runnable circuit. - - To see this, run ~testOnly Example.PeekInternalSpec~ - - In the test an exception is thrown when either of the two peek statements underneath are - run: - #+begin_src scala - class OuterTester(c: Outer) extends PeekPokeTester(c) { - val inner = peek(c.inner.innerState) - val outer = peek(c.outerState) - } - #+end_src - - The only way to deal with this hurdle is to expose the state we are interested in as signals. - An example of this can be seen in - ~/Examples/printing.scala~ - - This approach leads to a lot of annoying clutter in your modules IO, so to separate business-logic - from debug signals it is useful to use a MultiIOModule where debug signals can be put in a separate - io bundle. This approach is used in the skeleton code for the exercises. - -*** printf - ~printf~ and ~println~ must not be mixed! - println behaves as expected in most languages, when executed it simply prints the argument. - In the tests so far it has only printed the value returned by peek. - - a printf statement on the other hand does not immediately print anything to the console. Instead it creates - a special chisel element which only exists during simulation and prints to your console each cycle, - thus helping us peer into the internal state of a circuit! - - Additionally, a printf statement in a conditional block will only execute if the condiditon is met, - allowing us to reduce noise. - #+begin_src scala - class PrintfExample() extends Module { - val io = IO(new Bundle{}) - - val counter = RegInit(0.U(8.W)) - counter := counter + 1.U - - printf("Counter is %d\n", counter) - when(counter % 2.U === 0.U){ - printf("Counter is even\n") - } - } - - class PrintfTest(c: PrintfExample) extends PeekPokeTester(c) { - for(ii <- 0 until 5){ - println(s"At cycle $ii:") - step(1) - } - } - #+end_src - When you run this test with ~testOnly Examples.PrintfExampleSpec~, did you get what you expected? - - As it turns out printf can be rather misleading when using stateful circuits. - To see this in action, try running ~testOnly Examples.EvilPrintfSpec~ which yields the following - #+begin_src - In cycle 0 the output of counter is: 0 - according to printf output is: 0 - [info] [0.003] - In cycle 1 the output of counter is: 0 - according to printf output is: 0 - [info] [0.003] - - - In cycle 2 the output of counter is: 0 - according to printf output is: 1 - ^^^^^^^^ - - [info] [0.004] - In cycle 3 the output of counter is: 1 - according to printf output is: 1 - [info] [0.004] - In cycle 4 the output of counter is: 1 - according to printf output is: 1 - #+end_src - - When looking at the circuits design it is pretty obvious that the peek poke tester is giving the - correct result, whereas the printf statement is printing the updated state of the register which - should not be visible before next cycle. - - In conclusion, do not use printf to debug timing issues, and if you do be extremely methodical. - - (It is possible to use a different simulator, treadle, which from what I have seen gives correct - printf results, it can be used by supplying an extra argument in the peek poke constructor like so: - ~chisel3.iotesters.Driver(() => new Outer, "treadle") { c =>~) - -* Matrix matrix multiplication - For your first foray into chisel you will design a matrix matrix multiplication unit. - Matrix multiplication is fairly straight forward, however on hardware it's a little - trickier than the standard for loops normally employed.. - -** Task 1 - Vector - The first component you should implement is a register bank for storing a vector. - - In Vector.scala you will find the skeleton code for this component. - Unlike the standard Chisel.Vec our custom vector has a read enable which means that - the memory pointed to by idx will only be overWritten when readEnable is true. - (You could argue that writeEnable would be a more fitting name, it's a matter of - perspective) - - Implement the vector and test that it works by running - ~testOnly Ex0.VectorSpec~ in your sbt console. - -** Task 2 - Matrix - The matrix works just like the vector only in two dimensions. - The skeleton code and associated tests should make the purpose of this module obvious. - Run the tests with ~testOnly Ex0.VectorSpec~ - -** Task 3 - Dot Product - This component differs from the two previous in that it has no explicit control input, - which might at first be rather confusing. - - With only two inputs for data, how do we know when the dotproduct has been calculated? - The answer to this is the ~elements~ argument, which tells the dot product calculator the - size of the input vectors. - Consequently, the resulting hardware can only (at least on its own) compute dotproducts - for one size of vector, which is fine in our circuit. - - To get a better understanding we can model this behavior in regular scala: - - #+begin_src scala - 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){ - (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. - - As with the previous tasks, the dot product calculator must pass the tests with - ~testOnly Ex0.DotProdSpec~ - - -** Task 4 - Matrix Matrix multiplication - With our matrix modules and dot product calculators we have every piece needed to - implement the matrix multiplier. - - When performing matrix multiplication on a computer transposing the second matrix - can help us reduce complexity by quite a lot. To examplify, consider - - #+begin_src - | 2, 5 | - A = | 7, -1 | - | 0, 4 | - - - B = | 1, 1, 2 | - | 0, 4, 0 | - #+end_src - - It would be much simpler to just have two modules with the same dimensions, and we - can do this by transposing B so we get - - #+begin_src - | 2, 5 | - A = | 7, -1 | - | 0, 4 | - - | 1, 0 | - BT = | 1, 4 | - | 2, 0 | - #+end_src - - Now we need to do is calculate the dot products for the final matrix: - - #+begin_src - if A*B = C then - - | A[0] × BT[0], A[0] × BT[1], A[0] × BT[2] | - C = | A[1] × BT[0], ... , ... | - | ... , ... , A[2] × BT[2] | - - where - A[0] × BT[0] is the dot product of [2, 5] and [1, 0] - and - A[0] × BT[1] is the dot product of [2, 5] and [1, 4] - and so forth.. - #+end_src - - Because of this, the input for matrix B will be supplied transposed, thus you do not - have to worry about this. For B the input would be [1, 0, 1, 4, 2, 0] - - 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 - -** 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. - - A common saying is "A few hours of work can save you from several minutes of planning", - and this holds especially true for writing chisel!! diff --git a/src/main/scala/Vector.scala b/src/main/scala/Vector.scala index 9a9e5e9..4780815 100644 --- a/src/main/scala/Vector.scala +++ b/src/main/scala/Vector.scala @@ -7,21 +7,29 @@ class Vector(val elements: Int) extends Module { val io = IO( new Bundle { - val idx = Input(UInt(32.W)) - val dataIn = Input(UInt(32.W)) - val readEnable = Input(Bool()) + val idx = Input(UInt(32.W)) + val dataIn = Input(UInt(32.W)) + val writeEnable = Input(Bool()) - val dataOut = Output(UInt(32.W)) + val dataOut = Output(UInt(32.W)) } ) - /** - * Your code here - */ - // Creates a vector of zero-initialized registers - val contents = RegInit(VecInit(List.fill(elements)(0.U(32.W)))) + val internalVector = RegInit(VecInit(List.fill(elements)(0.U(32.W)))) + + + when(writeEnable){ + // TODO: + // When writeEnable is true the content of internalVector at the index specified + // by idx should be set to the value of io.dataIn + } + // In this case we don't want an otherwise block, in writeEnable is low we don't change + // anything + - // placeholder + // TODO: + // io.dataOut should be driven by the contents of internalVector at the index specified + // by idx io.dataOut := 0.U }