{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\"Chisel" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Module 3.5: Object Oriented Programming\n", "**Prev: [Functional Programming](3.4_functional_programming.ipynb)**
\n", "**Next: [Types](3.6_types.ipynb)**\n", "\n", "## Motivation\n", "Scala and Chisel are object-oriented programming languages, meaning code may be compartmentalized into objects.\n", "Scala, which is built on Java, inherits many of Java's object-oriented features.\n", "However, as we'll see below, there are some differences.\n", "Chisel's hardware modules are similar to Verilog's modules, in that they can be instantiated and wired up as single or multiple instances.\n", "\n", "## Setup" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "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": { "collapsed": false }, "outputs": [], "source": [ "import chisel3._\n", "import chisel3.util._\n", "import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}\n", "import chisel3.experimental._" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Object Oriented Programming\n", "This section outlines how Scala implements the object-oriented programming paradigm. So far you have already seen classes, but Scala also has the following features:\n", "- [Abstract classes](#abstract)\n", "- [Traits](#traits)\n", "- [Objects](#objects)\n", "- [Companion Objects](#compobj)\n", "- [Case Classes](#caseclass)\n", "\n", "## Abstract Classes\n", "Abstract classes are just like other programming language implementations. They can define many unimplemented values that subclasses must implement. Any object can only directly inherit from one parent abstract class.\n", "\n", "**Example: Abstract Class**
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "abstract class MyAbstractClass {\n", " def myFunction(i: Int): Int\n", " val myValue: String\n", "}\n", "class ConcreteClass extends MyAbstractClass {\n", " def myFunction(i: Int): Int = i + 1\n", " val myValue = \"Hello World!\"\n", "}\n", "// Uncomment below to test!\n", "// val abstractClass = new MyAbstractClass() // Illegal! Cannot instantiate an abstract class\n", "val concreteClass = new ConcreteClass() // Legal!\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Traits\n", "Traits are very similar to abstract classes in that they can define unimplemented values. However, they differ in two ways:\n", "- a class can inherit from multiple traits\n", "- a trait cannot have constructor parameters\n", "\n", "**Example: Traits and Multiple Inheritance**
\n", "Traits are how Scala implements multiple inheritance, as shown in the example below. `MyClass` extends from both traits `HasFunction` and `HasValue`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "trait HasFunction {\n", " def myFunction(i: Int): Int\n", "}\n", "trait HasValue {\n", " val myValue: String\n", " val myOtherValue = 100\n", "}\n", "class MyClass extends HasFunction with HasValue {\n", " override def myFunction(i: Int): Int = i + 1\n", " val myValue = \"Hello World!\"\n", "}\n", "// Uncomment below to test!\n", "// val myTraitFunction = new HasFunction() // Illegal! Cannot instantiate a trait\n", "// val myTraitValue = new HasValue() // Illegal! Cannot instantiate a trait\n", "val myClass = new MyClass() // Legal!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To inherit multiple traits, chain them like \n", "\n", "```scala\n", "class MyClass extends HasTrait1 with HasTrait2 with HasTrait3 ...\n", "```\n", "In general, always use traits over abstract classes, unless you are certain you want to enforce the single-inheritance restriction of abstract classes.\n", "\n", "## Objects\n", "Scala has a language feature for these singleton classes, called objects. You cannot instantiate an object **(no need to call `new`)**; you can simply directly reference it. That makes them similar to Java static classes.\n", "\n", "**Example: Objects**
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "object MyObject {\n", " def hi: String = \"Hello World!\"\n", " def apply(msg: String) = msg\n", "}\n", "println(MyObject.hi)\n", "println(MyObject(\"This message is important!\")) // equivalent to MyObject.apply(msg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Companion Objects\n", "\n", "When a class and an object share the same name and defined in the same file, the object is called a **companion object**. When you use `new` before the class/object name, it will instantiate the class. If you don't use `new`, it will reference the object:\n", "\n", "**Example: Companion Object**
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "object Lion {\n", " def roar(): Unit = println(\"I'M AN OBJECT!\")\n", "}\n", "class Lion {\n", " def roar(): Unit = println(\"I'M A CLASS!\")\n", "}\n", "new Lion().roar()\n", "Lion.roar()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Companion objects are usually used for the following reasons:\n", " 1. to contain constants related to the class\n", " 2. to execute code before/after the class constructor\n", " 3. to create multiple constructors for a class\n", "\n", "In the example below, we will instantiate a number of instances of Animal. We want each animal to have a name, and to know its order within all instantiations. Finally, if no name is given, it should get a default name." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "object Animal {\n", " val defaultName = \"Bigfoot\"\n", " private var numberOfAnimals = 0\n", " def apply(name: String): Animal = {\n", " numberOfAnimals += 1\n", " new Animal(name, numberOfAnimals)\n", " }\n", " def apply(): Animal = apply(defaultName)\n", "}\n", "class Animal(name: String, order: Int) {\n", " def info: String = s\"Hi my name is $name, and I'm $order in line!\"\n", "}\n", "\n", "val bunny = Animal.apply(\"Hopper\") // Calls the Animal factory method\n", "println(bunny.info)\n", "val cat = Animal(\"Whiskers\") // Calls the Animal factory method\n", "println(cat.info)\n", "val yeti = Animal() // Calls the Animal factory method\n", "println(yeti.info)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*What's happening here?*\n", "1. Our **Animal companion object** defines a constant relevant to ```class Animal```:\n", "```scala\n", "val defaultName = \"Bigfoot\"\n", "```\n", "1. It also defines a private mutable integer to keep track of the order of Animal instances:\n", "```scala \n", "private var numberOfAnimals = 0\n", "```\n", "1. It defines two **apply** methods, which are known as **factory methods** in that they return instances of the **class Animal**. \n", " 1. The first creates an instance of Animal using only one argument, ```name```, and uses ```numberOfAnimals``` as well to call the Animal class constructor.\n", "```scala\n", "def apply(name: String): Animal = {\n", " numberOfAnimals += 1\n", " new Animal(name, numberOfAnimals)\n", "}\n", "```\n", " 1. The second factory method requires no argument, and instead uses the default name to call the other apply method.\n", "```scala\n", "def apply(): Animal = apply(defaultName)\n", "```\n", "1. These factory methods can be called naively like this\n", "```scala\n", "val bunny = Animal.apply(\"Hopper\")\n", "```\n", "which eliminates the need to use the new keyword, but the real magic is that the compiler assumes the apply method any time it sees parentheses applied to an instance or object:\n", "```scala\n", "val cat = Animal(\"Whiskers\")\n", "```\n", "1. Factory methods, usually provided via companion objects, allow alternative ways to express instance creations, provide additional tests for constructor parameters, conversions, and eliminate the need to use the keyword ```new```. Note that you must call the companion object's `apply` method for `numberOfAnimals` to be incremented.\n", "\n", "**Chisel uses many companion objects, like Module.** When you write the following:\n", "```scala\n", "val myModule = Module(new MyModule)\n", "```\n", "you are calling the **Module companion object**, so Chisel can run background code before and after instantiating \n", "```MyModule```." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Case Classes\n", "Case classes are a special type of Scala class that provides some cool additional features. They are very common in Scala programming, so this section outlines some of their useful features:\n", "- Allows **external access** to the **class parameters**\n", "- **Eliminates** the need to use **`new`** when instantiating the class\n", "- Automatically creates an **unapply method** that supplies access to all of the class Parameters.\n", "- Cannot be subclassed from\n", "\n", "In the following example, we declare three different classes, `Nail`, `Screw`, and `Staple`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class Nail(length: Int) // Regular class\n", "val nail = new Nail(10) // Requires the `new` keyword\n", "// println(nail.length) // Illegal! Class constructor parameters are not by default externally visible\n", "\n", "class Screw(val threadSpace: Int) // By using the `val` keyword, threadSpace is now externally visible\n", "val screw = new Screw(2) // Requires the `new` keyword\n", "println(screw.threadSpace)\n", "\n", "case class Staple(isClosed: Boolean) // Case class constructor parameters are, by default, externally visible\n", "val staple = Staple(false) // No `new` keyword required\n", "println(staple.isClosed)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Nail` is a regular class, and its parameters are not externally visible because we did not use the `val` keyword in the argument list. It also requires the `new` keyword when declaring an instance of `Nail`.\n", "\n", "`Screw` is declared similarly to `Nail`, but includes `val` in the argument list. This allows its parameter, `threadSpace`, to be visible externally.\n", "\n", "By using a case class, `Staple` gets the benefit of all its parameters being externally visible (without needing the `val` keyword).\n", "\n", "In addition, `Staple` does not require using `new` when declaring a case class. This is because the Scala compiler automatically creates a companion object for every case class in your code, which contains an apply method for the case class.\n", "\n", "Case classes are nice containers for generators with lots of parameters.\n", "The constructor gives you a good place to define derived parameters and validate input." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "case class SomeGeneratorParameters(\n", " someWidth: Int,\n", " someOtherWidth: Int = 10,\n", " pipelineMe: Boolean = false\n", ") {\n", " require(someWidth >= 0)\n", " require(someOtherWidth >= 0)\n", " val totalWidth = someWidth + someOtherWidth\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Inheritance with Chisel\n", "You've seen `Module`s and `Bundle`s before, but it's important to realize what's really going on.\n", "Every Chisel module you make is a class extending the base type `Module`.\n", "Every Chisel IO you make is a class extending the base type `Bundle` (or, in some special cases, `Bundle`'s supertype [`Record`](https://github.com/freechipsproject/chisel3/blob/v3.0.0/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala#L415)).\n", "Chisel hardware types like `UInt` or `Bundle` all have `Data` as a supertype.\n", "We'll explore using object oriented programming to create hierarchical hardware blocks and explore object reuse. You'll learn more about types and `Data` in the next Module on type generic generators.\n", "\n", "## Module\n", "Whenever you want to create a hardware object in Chisel, it needs to have `Module` as a superclass.\n", "Inheritance might not always be the right tool for reuse ([composition over inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance) is a common principle), but inheritance is still a powerful tool.\n", "Below is an example of creating a `Module` and connecting multiple instantiations of them together hierarchically." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Example: Gray Encoder and Decoder**
\n", "We'll create a hardware Gray encoder/decoder. The encode or decode operation choice is hardware programmable." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class NoGlitchCounterIO(bitwidth: Int) extends Bundle {\n", " val en = Input(Bool())\n", " val out = Output(UInt(bitwidth.W))\n", "}\n", "\n", "abstract class NoGlitchCounter(val maxCount: Int) extends Module {\n", " val bitwidth: Int\n", " val io = IO(new NoGlitchCounterIO(bitwidth))\n", "}\n", "\n", "abstract class AsyncFIFO(depth: Int) extends Module {\n", " val io = IO(new Bundle{\n", " // write inputs\n", " val write_clock = Input(Clock())\n", " val write_enable = Input(Bool())\n", " val write_data = Input(UInt(32.W))\n", "\n", " // read inputs/outputs\n", " val read_clock = Input(Clock())\n", " val read_enable = Input(Bool())\n", " val read_data = Output(UInt(32.W))\n", "\n", " // FIFO status\n", " val full = Output(Bool())\n", " val empty = Output(Bool())\n", " })\n", " \n", " def makeCounter(maxCount: Int): NoGlitchCounter\n", "\n", " // add extra bit to counter to check for fully/empty status\n", " assert(isPow2(depth), \"AsyncFIFO needs a power-of-two depth!\")\n", " val write_counter = withClock(io.write_clock) {\n", " val count = makeCounter(depth * 2)\n", " count.io.en := io.write_enable && !io.full\n", " count.io.out\n", " }\n", " val read_counter = withClock(io.read_clock) {\n", " val count = makeCounter(depth * 2)\n", " count.io.en := io.read_enable && !io.empty\n", " count.io.out\n", " }\n", "\n", " // synchronize\n", " val sync = withClock(io.read_clock) { ShiftRegister(write_counter, 2) }\n", "\n", " // status logic goes here\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class GrayCounter(val bitwidth: Int) extends NoGlitchCounter(bitwidth) {\n", " // todo\n", "}\n", "\n", "class RingCounter(maxCount: Int) extends NoGlitchCounter(maxCount) {\n", " // todo\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import scala.math.pow\n", "\n", "// create a module\n", "class GrayCoder(bitwidth: Int) extends Module {\n", " val io = IO(new Bundle{\n", " val in = Input(UInt(bitwidth.W))\n", " val out = Output(UInt(bitwidth.W))\n", " val encode = Input(Bool()) // decode on false\n", " })\n", " \n", " when (io.encode) { //encode\n", " io.out := io.in ^ (io.in >> 1.U)\n", " } .otherwise { // decode, much more complicated\n", " io.out := Seq.fill(log2Ceil(bitwidth))(Wire(UInt(bitwidth.W))).zipWithIndex.fold((io.in, 0)){\n", " case ((w1: UInt, i1: Int), (w2: UInt, i2: Int)) => {\n", " w2 := w1 ^ (w1 >> pow(2, log2Ceil(bitwidth)-i2-1).toInt.U)\n", " (w2, i1)\n", " }\n", " }._1\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Give it a whirl!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "// test our gray coder\n", "val bitwidth = 4\n", "Driver(() => new GrayCoder(bitwidth)) {\n", " c => new PeekPokeTester(c) {\n", " \n", " def toBinary(i: Int, digits: Int = 8) =\n", " String.format(\"%\" + digits + \"s\", i.toBinaryString).replace(' ', '0')\n", "\n", " println(\"Encoding:\")\n", " for (i <- 0 until pow(2, bitwidth).toInt) {\n", " poke(c.io.in, i)\n", " poke(c.io.encode, true)\n", " step(1)\n", " println(s\"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(peek(c.io.out).toInt, bitwidth)}\")\n", " }\n", " \n", " println(\"Decoding:\")\n", " for (i <- 0 until pow(2, bitwidth).toInt) {\n", " poke(c.io.in, i)\n", " poke(c.io.encode, false)\n", " step(1)\n", " println(s\"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(peek(c.io.out).toInt, bitwidth)}\")\n", " }\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Gray codes are often used in asynchronous interfaces. Usually Gray counters are used rather than fully-featured encoders/decoders, but we'll the above module to simplify things. Below is an example AsyncFIFO, built using the above Gray coder. The control logic and tester is left as an exercise for later on. For now, look at how the Gray coder is instantiated multiple times and connected." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class AsyncFIFO(depth: Int = 16) extends Module {\n", " val io = IO(new Bundle{\n", " // write inputs\n", " val write_clock = Input(Clock())\n", " val write_enable = Input(Bool())\n", " val write_data = Input(UInt(32.W))\n", " \n", " // read inputs/outputs\n", " val read_clock = Input(Clock())\n", " val read_enable = Input(Bool())\n", " val read_data = Output(UInt(32.W))\n", " \n", " // FIFO status\n", " val full = Output(Bool())\n", " val empty = Output(Bool())\n", " })\n", " \n", " // add extra bit to counter to check for fully/empty status\n", " assert(isPow2(depth), \"AsyncFIFO needs a power-of-two depth!\")\n", " val write_counter = withClock(io.write_clock) { Counter(io.write_enable && !io.full, depth*2)._1 }\n", " val read_counter = withClock(io.read_clock) { Counter(io.read_enable && !io.empty, depth*2)._1 }\n", " \n", " // encode\n", " val encoder = new GrayCoder(write_counter.getWidth)\n", " encoder.io.in := write_counter\n", " encoder.io.encode := true.B\n", " \n", " // synchronize\n", " val sync = withClock(io.read_clock) { ShiftRegister(encoder.io.out, 2) }\n", " \n", " // decode\n", " val decoder = new GrayCoder(read_counter.getWidth)\n", " decoder.io.in := sync\n", " decoder.io.encode := false.B\n", " \n", " // status logic goes here\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": 1 }