{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\"Chisel" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "# Module 2.2: Combinational Logic\n", "**Prev: [Your First Chisel Module](2.1_first_module.ipynb)**
\n", "**Next: [Control Flow](2.3_control_flow.ipynb)**\n", "\n", "## Motivation\n", "In this section you will see how to use Chisel components to implement combinational logic.\n", "We will demonstrate how three of the basic Chisel types: `UInt `- unsigned integer; `SInt` - signed integer, and `Bool` - true or false may be connected and operated upon.\n", "Notice how all Chisel variables are declared as Scala `val`s.\n", "Never use a Scala `var` for a hardware construct, since the construct itself may never change once defined; only its value may change when running the hardware.\n", "Wires may be used for parameterized types.\n", "\n", "## Setup" ] }, { "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": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import chisel3._\n", "import chisel3.util._\n", "import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Common Operators\n", "Now that you understand how `Module`s are constructed, let's make some hardware! Take a look at the empty module below." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MyModule extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out = Output(UInt(4.W))\n", " })\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We've called our class `MyModule`, and it extends `Module`. This means it gets mapped to a hardware module in Verilog. Our `MyModule` module has one input and one output. The input is a 4-bit unsigned integer (`UInt`), and so is the output. \n", "\n", "**Example: Scala and Chisel Operators Look the Same**
\n", "Let's look at different operations we can perform on data." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MyModule extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out = Output(UInt(4.W))\n", " })\n", "\n", " val two = 1 + 1\n", " println(two)\n", " val utwo = 1.U + 1.U\n", " println(utwo)\n", " \n", " io.out := io.in\n", "}\n", "println(getVerilog(new MyModule))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We create two `val`s. The first adds two Scala `Int`s, so `println` prints out the integer 2. The second `val` adds two *Chisel* `UInt`s together, so `println` sees this as a hardware node and prints out the type name and pointer (`chisel3.core.UInt@d`). Note that `1.U` is a type cast from a Scala `Int` (1) to a Chisel `UInt` literal.\n", "\n", "We need to drive the output to something, so we just connect it to the input for now, as with the passthrough module in the previous tutorial.\n", "\n", "**Example: Incompatible Operation**
\n", "What happens if we add a Chisel `1.U` to the literal `1`? These types are incompatible, as the former is a hardware wire of value 1, while the latter is a Scala value of 1. So Chisel will give a type mismatch error." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MyModuleTwo extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out = Output(UInt(4.W))\n", " })\n", "\n", " val twotwo = 1.U + 1\n", " println(twotwo)\n", " \n", " io.out := io.in\n", "}\n", "println(getVerilog(new MyModule))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's important to remember the distinction between types when performing operations. Scala is a strongly typed language, so any type casting must be explicit.\n", "\n", "**Example: More Chisel Operators**
\n", "Other common operations are subtraction and multiplication. These are handled on unsigned integers as expected. Let's see these in action. We show the Verilog, though there's some underlying Chisel features that obfuscate the simple code we would expect." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MyOperators extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out_add = Output(UInt(4.W))\n", " val out_sub = Output(UInt(4.W))\n", " val out_mul = Output(UInt(4.W))\n", " })\n", "\n", " io.out_add := 1.U + 4.U\n", " io.out_sub := 2.U - 1.U\n", " io.out_mul := 4.U * 2.U\n", "}\n", "println(getVerilog(new MyOperators))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And here's a sample tester for the above operations. Instead of using an anonymous tester class like in the previous tutorial, we'll create an explicit tester class. This is just an alternative way of writing a tester." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MyOperatorsTester(c: MyOperators) extends PeekPokeTester(c) {\n", " expect(c.io.out_add, 5)\n", " expect(c.io.out_sub, 1)\n", " expect(c.io.out_mul, 8)\n", "}\n", "assert(Driver(() => new MyOperators) {c => new MyOperatorsTester(c)})\n", "println(\"SUCCESS!!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Example: Mux and Concatenation**
\n", "In addition to addition, subtraction, and multplication, Chisel has mux and concatenation operators. These are shown below. The `Mux` operates like a traditional ternary operator, with the order (select, value if true, value if false). Note that `true.B` and `false.B` are the preferred ways to create Chisel Bool literals. The `Cat` ordering is MSB then LSB (where B refers to bit or bits), and only takes two arguments. Concatenating more than two values requires multiple `Cat` calls or advanced Chisel and Scala features covered in later sections." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MyOperatorsTwo extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(4.W))\n", " val out_mux = Output(UInt(4.W))\n", " val out_cat = Output(UInt(4.W))\n", " })\n", "\n", " val s = true.B\n", " io.out_mux := Mux(s, 3.U, 0.U) // should return 3.U, since s is true\n", " io.out_cat := Cat(2.U, 1.U) // concatenates 2 (b10) with 1 (b1) to give 5 (101)\n", "}\n", "\n", "println(getVerilog(new MyOperatorsTwo))\n", "class MyOperatorsTwoTester(c: MyOperatorsTwo) extends PeekPokeTester(c) {\n", " expect(c.io.out_mux, 3)\n", " expect(c.io.out_cat, 5)\n", "}\n", "assert(Driver(() => new MyOperatorsTwo) {c => new MyOperatorsTwoTester(c)})\n", "println(\"SUCCESS!!\")" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Notice how the Verilog contains constants instead of actual mux or concatenation logic. This is because FIRRTL transformations have simplified the circuit, eliminating obvious logic. \n", "\n", "For a more complete list of Chisel operators, see the [Chisel cheatsheet](https://chisel.eecs.berkeley.edu/documentation.html). For the most complete list of operators and their implementation details, look through the [Chisel API](https://chisel.eecs.berkeley.edu/api/#package)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Exercises\n", "To complete these exercises, you may need to look through the [Chisel cheatsheet](https://chisel.eecs.berkeley.edu/documentation.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise: MAC**
\n", "Create a Chisel module that implements the multiply accumulate function, `(A*B)+C`, and passes the testbench." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MAC extends Module {\n", " val io = IO(new Bundle {\n", " val in_a = Input(UInt(4.W))\n", " val in_b = Input(UInt(4.W))\n", " val in_c = Input(UInt(4.W))\n", " val out = Output(UInt(8.W))\n", " })\n", "\n", " ???\n", "}\n", "class MACTester(c: MAC) extends PeekPokeTester(c) {\n", " val cycles = 100\n", " import scala.util.Random\n", " for (i <- 0 until cycles) {\n", " val in_a = Random.nextInt(16)\n", " val in_b = Random.nextInt(16)\n", " val in_c = Random.nextInt(16)\n", " poke(c.io.in_a, in_a)\n", " poke(c.io.in_b, in_b)\n", " poke(c.io.in_c, in_c)\n", " expect(c.io.out, in_a*in_b+in_c)\n", " }\n", "}\n", "assert(Driver(() => new MAC) {c => new MACTester(c)})\n", "println(\"SUCCESS!!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "\n", "
\n", "
\n",
    "class MAC extends Module {\n",
    "  val io = IO(new Bundle {\n",
    "    val in_a = Input(UInt(4.W))\n",
    "    val in_b = Input(UInt(4.W))\n",
    "    val in_c = Input(UInt(4.W))\n",
    "    val out  = Output(UInt(8.W))\n",
    "  })\n",
    "\n",
    "  io.out := (io.in_a * io.in_b) + io.in_c\n",
    "}\n",
    "
" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "**Exercise: Arbiter**
\n", "The following circuit arbitrates data coming from a FIFO into two parallel processing units. The FIFO and processing elements (PEs) communicate with ready-valid interfaces. Construct the arbiter to send data to whichever PE is ready to receive data, prioritizing PE0 if both are ready to receive data. Remember that the arbiter should tell the FIFO that it's ready to receive data when at least one of the PEs can receive data. Also, wait for a PE to assert that it's ready before asserting that the data are valid. You will likely need binary operators to complete this exercise.\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Arbiter extends Module {\n", " val io = IO(new Bundle {\n", " // FIFO\n", " val fifo_valid = Input(Bool())\n", " val fifo_ready = Output(Bool())\n", " val fifo_data = Input(UInt(16.W))\n", " \n", " // PE0\n", " val pe0_valid = Output(Bool())\n", " val pe0_ready = Input(Bool())\n", " val pe0_data = Output(UInt(16.W))\n", " \n", " // PE1\n", " val pe1_valid = Output(Bool())\n", " val pe1_ready = Input(Bool())\n", " val pe1_data = Output(UInt(16.W))\n", " })\n", "\n", " ??? \n", "}\n", "class ArbiterTester(c: Arbiter) extends PeekPokeTester(c) {\n", " import scala.util.Random\n", " val data = Random.nextInt(65536)\n", " poke(c.io.fifo_data, data)\n", " \n", " for (i <- 0 until 8) {\n", " poke(c.io.fifo_valid, (i>>0)%2)\n", " poke(c.io.pe0_ready, (i>>1)%2)\n", " poke(c.io.pe1_ready, (i>>2)%2)\n", "\n", " expect(c.io.fifo_ready, i>1)\n", " expect(c.io.pe0_valid, i==3 || i==7)\n", " expect(c.io.pe1_valid, i==5)\n", " \n", " if (i == 3 || i ==7) {\n", " expect(c.io.pe0_data, data)\n", " } else if (i == 5) {\n", " expect(c.io.pe1_data, data)\n", " }\n", " }\n", "}\n", "assert(Driver(() => new Arbiter) {c => new ArbiterTester(c)})\n", "println(\"SUCCESS!!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "\n", "
\n", "
\n",
    "  io.fifo_ready := io.pe0_ready || io.pe1_ready\n",
    "  io.pe0_valid := io.fifo_valid && io.pe0_ready\n",
    "  io.pe1_valid := io.fifo_valid && io.pe1_ready && !io.pe0_ready\n",
    "  io.pe0_data := io.fifo_data\n",
    "  io.pe1_data := io.fifo_data\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise: Parameterized Adder (Optional)**
\n", "This optional exercise exposes you to one of the most powerful features of Chisel, it's parameterization capabilities. To demonstrate this, we'll construct a parameterized adder that can either saturate the output when overflow occurs, or truncate the results (i.e. wrap around).\n", "\n", "First, look at the `Module` below. The parameter we pass into it is called `saturate` and has type *Scala* `Boolean`. This is not a Chisel `Bool`. So, we're not creating a single hardware adder that can either saturate or truncate, but rather we're creating a *generator* that produces either a saturating hardware adder *or* a truncating hardware adder. The decision is made at compile time.\n", "\n", "Next, notice the inputs and outputs are all 4-bit `UInt`s. Chisel has built-in width inferencing, and if you look at the [cheatsheet](https://chisel.eecs.berkeley.edu/doc/chisel-cheatsheet3.pdf), you'll see that the bitwidth of a normal summation is equal to the maximum bitwidth of the two inputs. This means that\n", "\n", "```scala\n", "val sum = io.in_a + io.in_b\n", "```\n", "\n", "will make `sum` a 4-bit wire, and the value will be the truncated result for 4-bit inputs. To check if the summation should saturate, you need to place the result in a 5-bit wire. This can be done with the `+&` summation, as seen on the cheatsheet.\n", "\n", "```scala\n", "val sum = io.in_a +& io.in_b\n", "```\n", "\n", "Finally, note that connecting a 4-bit `UInt` wire to a 5-bit `UInt` wire will truncate the MSB by default. You can use this to easily truncate the 5-bit sum for the non-saturating adder." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class ParameterizedAdder(saturate: Boolean) extends Module {\n", " val io = IO(new Bundle {\n", " val in_a = Input(UInt(4.W))\n", " val in_b = Input(UInt(4.W))\n", " val out = Output(UInt(4.W))\n", " })\n", "\n", " ???\n", "}\n", "class ParameterizedAdderTester(c: ParameterizedAdder, saturate: Boolean) extends PeekPokeTester(c) {\n", " // 100 random tests\n", " val cycles = 100\n", " import scala.util.Random\n", " import scala.math.min\n", " for (i <- 0 until cycles) {\n", " val in_a = Random.nextInt(16)\n", " val in_b = Random.nextInt(16)\n", " poke(c.io.in_a, in_a)\n", " poke(c.io.in_b, in_b)\n", " if (saturate) {\n", " expect(c.io.out, min(in_a+in_b, 15))\n", " } else {\n", " expect(c.io.out, (in_a+in_b)%16)\n", " }\n", " }\n", " \n", " // ensure we test saturation vs. truncation\n", " poke(c.io.in_a, 15)\n", " poke(c.io.in_b, 15)\n", " if (saturate) {\n", " expect(c.io.out, 15)\n", " } else {\n", " expect(c.io.out, 14)\n", " }\n", "}\n", "for (saturate <- Seq(true, false)) {\n", " assert(Driver(() => new ParameterizedAdder(saturate)) {c => new ParameterizedAdderTester(c, saturate)})\n", "}\n", "println(\"SUCCESS!!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "\n", "
\n", "
\n",
    "  val sum = io.in_a +& io.in_b\n",
    "  if (saturate) {\n",
    "    io.out := Mux(sum > 15.U, 15.U, sum)\n",
    "  } else {\n",
    "    io.out := sum\n",
    "  }\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# You're done!\n", "\n", "[Return to the top.](#top)" ] } ], "metadata": { "kernelspec": { "display_name": "Scala", "language": "scala", "name": "scala" }, "language_info": { "codemirror_mode": "text/x-scala", "file_extension": ".scala", "mimetype": "text/x-scala", "name": "scala211", "nbconvert_exporter": "script", "pygments_lexer": "scala", "version": "2.11.11" } }, "nbformat": 4, "nbformat_minor": 2 }