ソースを参照

split into two files

master
peteraa 6年前
コミット
74fe9b7065
4個のファイルの変更969行の追加964行の削除
  1. +111
    -0
      exercise.org
  2. +840
    -0
      introduction.org
  3. +0
    -954
      oppgavetekst.org
  4. +18
    -10
      src/main/scala/Vector.scala

+ 111
- 0
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!!

+ 840
- 0
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 <TAB>
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
<ul>
<li>Name: Siv Jensen, Affiliation: FrP</li>
<li>Name: Jonas Gahr Støre, Affiliation: AP</li>
<li>Name: Bjørnar Moxnes, Affiliation: Rødt</li>
<li>Name: Malcolm Tucker, Affiliation: DOSAC</li>
</ul>
#+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"<li>Name: $nameString, Affiliation: $affiliationString</li>")
}
"<ul>\n" + inner.mkString("\n") + "</ul>"
}
// Or if you prefer brevity
def generateList2(politicians: List[String], affiliations: Map[String, String]): String = {
val inner = politicians.map(p => s"<li>Name: $p, Affiliation ${affiliations(p)}</li>")
"<ul>\n" + inner.mkString("\n") + "</ul>"
}
#+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 </ul> 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 =>~)

+ 0
- 954
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 <TAB>
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
<ul>
<li>Name: Siv Jensen, Affiliation: FrP</li>
<li>Name: Jonas Gahr Støre, Affiliation: AP</li>
<li>Name: Bjørnar Moxnes, Affiliation: Rødt</li>
<li>Name: Malcolm Tucker, Affiliation: DOSAC</li>
</ul>
#+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"<li>Name: $nameString, Affiliation: $affiliationString</li>")
}
"<ul>\n" + inner.mkString("\n") + "</ul>"
}
// Or if you prefer brevity
def generateList2(politicians: List[String], affiliations: Map[String, String]): String = {
val inner = politicians.map(p => s"<li>Name: $p, Affiliation ${affiliations(p)}</li>")
"<ul>\n" + inner.mkString("\n") + "</ul>"
}
#+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 </ul> 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!!

+ 18
- 10
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
}

読み込み中…
キャンセル
保存