{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\"Chisel" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "# Module 2.1: Your First Chisel Module\n", "**Prev: [Introduction to Scala](1_intro_to_scala.ipynb)**
\n", "**Next: [Combinational Logic](2.2_comb_logic.ipynb)**\n", "\n", "## Motivation\n", "Now that you are familiar with Scala, let's start carving out some hardware! Chisel stands for **C**onstructing **H**ardware **I**n a **S**cala **E**mbedded **L**anguage. That means it is a DSL in Scala, allowing you to take advantage of both Scala and Chisel programming within the same code. It is important to understand which code is \"Scala\" and which code is \"Chisel\", but we will discuss that more later. For now, think of Chisel and the code in Module 2 as a better way to write Verilog. This module throws an entire Chisel `Module` and tester at you. Just get the gist of it for now. You'll see plenty more examples later." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup\n", "The following cell downloads the dependencies needed for Chisel. You will see it in all future notebooks. **Run this cell now**." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "val path = System.getProperty(\"user.dir\") + \"/source/load-ivy.sc\"\n", "interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As mentioned in the last module, these statements are needed to import Chisel. **Run this cell now** before running any future code blocks." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import chisel3._\n", "import chisel3.util._\n", "import chisel3.tester._\n", "import chisel3.tester.RawTester.test\n", "import dotvisualizer._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Your First Module\n", "This section will present your first hardware module, a test case, and how to run it. It will contain many things that you will not understand, and that is OK. We want you to take away the broad strokes, so you can continually return to this complete and working example to reinforce what you've learned.\n", "\n", "**Example: A Module**
\n", "Like Verilog, we can declare module definitions in Chisel. The following example is a Chisel `Module`, `Passthrough`, that has one 4-bit input, `in`, and one 4-bit output, `out`. The module combinationally connects `in` and `out`, so `in` drives `out`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Chisel Code: Declare a new module definition\n", "class Passthrough extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out = Output(UInt(4.W))\n", " })\n", " io.out := io.in\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There's a lot here! The following explains how to think of each line in terms of the hardware we are describing.\n", "\n", "```scala\n", "class Passthrough extends Module {\n", "```\n", "We declare a new module called `Passthrough`. `Module` is a built-in Chisel class that all hardware modules must extend.\n", "\n", "```scala \n", "val io = IO(...)\n", "```\n", "We declare all our input and output ports in a special `io` `val`. It must be called `io` and be an `IO` object or instance, which requires something of the form `IO(_instantiated_bundle_)`.\n", "\n", "```scala\n", "new Bundle {\n", " val in = Input(...)\n", " val out = Output(...)\n", "}\n", "```\n", "We declare a new hardware struct type (Bundle) that contains some named signals `in` and `out` with directions Input and Output, respectively.\n", "\n", "```scala\n", "UInt(4.W)\n", "```\n", "We declare a signal's hardware type. In this case, it is an unsigned integer of width 4.\n", "\n", "```scala\n", "io.out := io.in\n", "```\n", "We connect our input port to our output port, such that `io.in` *drives* `io.out`. Note that the `:=` operator is a ***Chisel*** operator that indicates that the right-hand signal drives the left-hand signal. It is a directioned operator.\n", "\n", "The neat thing about hardware construction languages (HCLs) is that we can use the underlying programming language as a scripting language. For example, after declaring our Chisel module, we then use Scala to call the Chisel compiler to translate Chisel `Passthrough` into Verilog `Passthrough`. This process is called ***elaboration***." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Scala Code: Elaborate our Chisel design by translating it to Verilog\n", "// Don't worry about understanding this code; it is very complicated Scala\n", "println(getVerilog(new Passthrough))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Example: A Module Generator**
\n", "If we apply what we learned about Scala to this example, we can see that a Chisel module is implemented as a Scala class. Just like any other Scala class, we could make a Chisel module take some construction parameters. In this case, we make a new class `PassthroughGenerator` which will accept an integer `width` that dictates the widths of its input and output ports:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Chisel Code, but pass in a parameter to set widths of ports\n", "class PassthroughGenerator(width: Int) extends Module { \n", " val io = IO(new Bundle {\n", " val in = Input(UInt(width.W))\n", " val out = Output(UInt(width.W))\n", " })\n", " io.out := io.in\n", "}\n", "\n", "// Let's now generate modules with different widths\n", "println(getVerilog(new PassthroughGenerator(10)))\n", "println(getVerilog(new PassthroughGenerator(20)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that the generated Verilog uses different bitwidths for the input/output depending on the value assigned to the `width` parameter. Let's dig into how this works. Because Chisel Modules are normal Scala classes, we can use the power of Scala's class constructors to parameterize the elaboration of our design.\n", "\n", "You may notice that this parameterization is enabled by *Scala*, not *Chisel*; Chisel has no extra APIs for parameterization, but a designer can simply leverage Scala features to parameterize his/her designs.\n", "\n", "Because `PassthroughGenerator` no longer describes a single Module, but instead describes a family of modules parameterized by `width`, we refer to this `Passthrough` as a ***generator***." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Testing Your Hardware\n", "\n", "No hardware module or generator should be complete without a tester. Chisel has built-in test features that you will explore throughout this bootcamp. The following example is a Chisel test harness that passes values to an instance of `Passthrough`'s input port `in`, and checks that the same value is seen on the output port `out`.\n", "\n", "**Example: A Tester**
\n", "There is some advanced Scala going on here. However, there is no need for you to understand anything except the `poke` and `expect` commands. You can think of the rest of the code as simply boilerplate to write these simple tests." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Scala Code: `test` runs the unit test. \n", "// test takes a user Module and has a code block that applies pokes and expects to the \n", "// circuit under test (c)\n", "test(new Passthrough()) { c =>\n", " c.io.in.poke(0.U) // Set our input to value 0\n", " c.io.out.expect(0.U) // Assert that the output correctly has 0\n", " c.io.in.poke(1.U) // Set our input to value 1\n", " c.io.out.expect(1.U) // Assert that the output correctly has 1\n", " c.io.in.poke(2.U) // Set our input to value 2\n", " c.io.out.expect(2.U) // Assert that the output correctly has 2\n", "}\n", "println(\"SUCCESS!!\") // Scala Code: if we get here, our tests passed!\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "What's going on? The test accepts a `Passthrough` module, assigns values to the module's inputs, and checks its outputs. To set an input, we call `poke`. To check an output, we call `expect`. If we don't want to compare the output to an expected value (no assertion), we can `peek` the output instead.\n", "\n", "If all `expect` statements are true, then our boilerplate code will return pass.\n", "\n", ">Note that the `poke` and `expect` use chisel hardware literal notation. Both operations expect literals of the correct type.\n", "If `poke`ing a `UInt()` you must supply a `UInt` literal (example: `c.io.in.poke(10.U)`, likewise if the input is a `Bool()` the `poke` would expect either `true.B` or `false.B`.\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise: Writing Your Own Testers**
\n", "Write and execute two tests, one that tests `PassthroughGenerator` for a width of 10 and a second that tests `PassthroughGenerator` for a width of 20. Check at least two values for each: zero and the maximum value supported by the specified width. Note that the triple question mark has a special meaning in Scala. You may see it frequently in these bootcamp exercises. Running code with the `???` will produce the `NotImplementedError`. Replace `???` with your own code." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Test with width 10\n", "\n", "test(???) { c =>\n", " ???\n", "}\n", "\n", "// Test with width 20\n", "\n", "test(???) { c =>\n", " ???\n", "}\n", "\n", "println(\"SUCCESS!!\") // Scala Code: if we get here, our tests passed!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "\n", "
\n", "
\n",
    "test(new PassthroughGenerator(10)) { c =>\n",
    "    c.io.in.poke(0.U)\n",
    "    c.io.out.expect(0.U)\n",
    "    c.io.in.poke(1023.U)\n",
    "    c.io.out.expect(1023.U)\n",
    "}\n",
    "\n",
    "test(new PassthroughGenerator(20)) { c =>\n",
    "    c.io.in.poke(0.U)\n",
    "    c.io.out.expect(0.U)\n",
    "    c.io.in.poke(1048575.U)\n",
    "    c.io.out.expect(1048575.U)\n",
    "}\n",
    "\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Looking at Generated Verilog/FIRRTL\n", "\n", "If you are having trouble understanding the generated hardware and are comfortable with reading structural Verilog and/or FIRRTL (Chisel's IR which is comparable to a synthesis-only subset of Verilog), then you can try looking at the generated Verilog to see the result of Chisel execution.\n", "\n", "Here is an example of generating the Verilog (which you've seen already) and the FIRRTL." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Viewing the Verilog for debugging\n", "println(getVerilog(new Passthrough))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "// Viewing the firrtl for debugging\n", "println(getFirrtl(new Passthrough))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# You're done!\n", "\n", "[Return to the top.](#top)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Appendix: A Note on \"printf\" Debugging\n", "[Debugging with print statements](https://stackoverflow.com/a/189570) is not always the best way to debug, but is often an easy first step to see what's going on when something doesn't work the way you expect.\n", "Because Chisel generators are programs generating hardware, there are some extra subtleties about printing generator and circuit state.\n", "It is important to remember when your print statement executes and what is being printed.\n", "The three common scenarios where you might want to print have some important differences:\n", "* Chisel generator prints during circuit generation\n", "* Circuit prints during circuit simulation\n", "* Tester prints during testing\n", "\n", "`println` is a built-in Scala function that prints to the console. It **cannot** be used to print during circuit simulation because the generated circuit is FIRRTL or Verilog- not Scala.\n", "\n", "The following code block shows different styles of printing." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class PrintingModule extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out = Output(UInt(4.W))\n", " })\n", " io.out := io.in\n", "\n", " printf(\"Print during simulation: Input is %d\\n\", io.in)\n", " // chisel printf has its own string interpolator too\n", " printf(p\"Print during simulation: IO is $io\\n\")\n", "\n", " println(s\"Print during generation: Input is ${io.in}\")\n", "}\n", "\n", "test(new PrintingModule ) { c =>\n", " c.io.in.poke(3.U)\n", " c.clock.step(5) // circuit will print\n", " \n", " println(s\"Print during testing: Input is ${c.io.in.peek()}\")\n", "}" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Scala", "language": "scala", "name": "scala" }, "language_info": { "codemirror_mode": "text/x-scala", "file_extension": ".scala", "mimetype": "text/x-scala", "name": "scala", "nbconvert_exporter": "script", "version": "2.12.10" } }, "nbformat": 4, "nbformat_minor": 2 }