{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\"Chisel" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Module 2.5: Putting it all Together: An FIR Filter\n", "**Prev: [Sequential Logic](2.4_sequential_logic.ipynb)**
\n", "**Next: [Generators: Parameters](3.1_parameters.ipynb)**\n", "\n", "## Motivation\n", "Now that you've learned the basics of Chisel, let's use that knowledge to build a FIR (finite impulse response) filter module! FIR filters are very common in digital signal processing applications. Also, the FIR filter will reappear frequently in module 3, so it's important that you don't filter out this module by skipping ahead! If you are unfamiliar with FIR filters, head over to the article on [trusty Wikipedia](https://en.wikipedia.org/wiki/Finite_impulse_response) to learn more.\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}" ] }, { "cell_type": "markdown", "metadata": { "nbpresent": { "id": "2e849671-a1e9-48b2-9bba-eb916ac623c6" } }, "source": [ "---\n", "# FIR Filter\n", "\n", "The FIR filter you will design performs the following operation.\n", "\n", "\n", "\n", "Basically, this does a elementwise multiplication of the element of the filter coefficients with the elements of the input signal and outputs the sum (also called a _convolution_).\n", "\n", "Or, a signals definition:\n", "\n", "$y[n] = b_0 x[n] + b_1 x[n-1] + b_2 x[n-2] + ...$\n", " - $y[n]$ is the output signal at time $n$\n", " - $x[n]$ is the input signal\n", " - $b_i$ are the filter coefficients or impulse response\n", " - $n-1$, $n-2$, ... are time $n$ delayed by 1, 2, ... cycles\n", " \n", "## 8-bit Specification\n", "\n", "Build a 4-element FIR filter where the four filter coefficients are parameters. A module skeleton and basic tests are provided for you.\n", "Note that both the input and output are 8-bit unsigned integers. You will need to save necessary state (like delayed signal values) using constructs like shift registers. Use the provided testers to check your implementation.\n", "Registers with constant inputs can be created using a `ShiftRegister` of shift value 1, or by using the `RegNext` construct.\n", "\n", "Note: for the tests to pass, your registers must be initialized to `0.U`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "nbpresent": { "id": "26e4a686-0397-4306-985c-813909256c95" } }, "outputs": [], "source": [ "class My4ElementFir(b0: Int, b1: Int, b2: Int, b3: Int) extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(8.W))\n", " val out = Output(UInt(8.W))\n", " })\n", "\n", " ???\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "nbpresent": { "id": "ddf24b7b-09a2-46f0-b1d8-cb2ca7976b4b" } }, "outputs": [], "source": [ "// Simple sanity check: a element with all zero coefficients should always produce zero\n", "Driver(() => new My4ElementFir(0, 0, 0, 0)) {\n", " c => new PeekPokeTester(c) {\n", " poke(c.io.in, 0)\n", " expect(c.io.out, 0)\n", " step(1)\n", " poke(c.io.in, 4)\n", " expect(c.io.out, 0)\n", " step(1)\n", " poke(c.io.in, 5)\n", " expect(c.io.out, 0)\n", " step(1)\n", " poke(c.io.in, 2)\n", " expect(c.io.out, 0)\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "// Simple 4-point moving average\n", "Driver(() => new My4ElementFir(1, 1, 1, 1)) {\n", " c => new PeekPokeTester(c) {\n", " poke(c.io.in, 1)\n", " expect(c.io.out, 1) // 1, 0, 0, 0\n", " step(1)\n", " poke(c.io.in, 4)\n", " expect(c.io.out, 5) // 4, 1, 0, 0\n", " step(1)\n", " poke(c.io.in, 3)\n", " expect(c.io.out, 8) // 3, 4, 1, 0\n", " step(1)\n", " poke(c.io.in, 2)\n", " expect(c.io.out, 10) // 2, 3, 4, 1\n", " step(1)\n", " poke(c.io.in, 7)\n", " expect(c.io.out, 16) // 7, 2, 3, 4\n", " step(1)\n", " poke(c.io.in, 0)\n", " expect(c.io.out, 12) // 0, 7, 2, 3\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "// Nonsymmetric filter\n", "Driver(() => new My4ElementFir(1, 2, 3, 4)) {\n", " c => new PeekPokeTester(c) {\n", " poke(c.io.in, 1)\n", " expect(c.io.out, 1) // 1*1, 0*2, 0*3, 0*4\n", " step(1)\n", " poke(c.io.in, 4)\n", " expect(c.io.out, 6) // 4*1, 1*2, 0*3, 0*4\n", " step(1)\n", " poke(c.io.in, 3)\n", " expect(c.io.out, 14) // 3*1, 4*2, 1*3, 0*4\n", " step(1)\n", " poke(c.io.in, 2)\n", " expect(c.io.out, 24) // 2*1, 3*2, 4*3, 1*4\n", " step(1)\n", " poke(c.io.in, 7)\n", " expect(c.io.out, 36) // 7*1, 2*2, 3*3, 4*4\n", " step(1)\n", " poke(c.io.in, 0)\n", " expect(c.io.out, 32) // 0*1, 7*2, 2*3, 3*4\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "\n", "
\n", "
\n",
    "  val x_n1 = RegNext(io.in, 0.U)\n",
    "  val x_n2 = RegNext(x_n1, 0.U)\n",
    "  val x_n3 = RegNext(x_n2, 0.U)\n",
    "  io.out := io.in \\* b0.U(8.W) + \n",
    "    x_n1 \\* b1.U(8.W) +\n",
    "    x_n2 \\* b2.U(8.W) + \n",
    "    x_n3 \\* b3.U(8.W)\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# IPXact Example\n", "\n", "Your Chisel generator produces Verilog, but just handing someone else a piece of Verilog is insufficient to verify and integrate the design. What other information is needed to capture a design’s intent? IPXact solves this by providing an XML description of a design and its metadata, including the interfaces, parameters, address mapping, etc. So for this portion of the lab, we'll standardize the FIR interfaces and write out IPXact to ease implementation and verification. You'll need the results of this generator in later labs.\n", "\n", "## Setup\n", "We have compiled the necessary depedencies, which are not all published on Maven, into a jar file. Contact the contributors if you want a copy. If you are not at the bootcamp, you can safely skip this section." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "// depdencies for ipxact\n", "val path = System.getProperty(\"user.dir\") + \"/../rocket-dsp-utils-assembly-1.0.jar\"\n", "interp.load.cp(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# FIR Filter Generator\n", "\n", "For this module, we'll be using a slightly modified example from [Module 3.2: Generators: Collection](3.2_collections.ipynb).\n", "If you haven't started Module 3.2, don't worry.\n", "You'll learn about the details of how `MyManyDynamicElementVecFir` works, but the basic idea is that it is a FIR filter generator.\n", "\n", "The generator has one parameter: length.\n", "That parameter dictates how many taps the filter has, and the taps are inputs to the hardware `Module`.\n", "\n", "The generator has 3 inputs:\n", "* in, the input to the filter\n", "* valid, a boolean that says when the input is valid\n", "* consts, a vector for all the taps\n", "\n", "and 1 output:\n", "* out, the filtered input\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class MyManyDynamicElementVecFir(length: Int) extends Module {\n", " val io = IO(new Bundle {\n", " val in = Input(UInt(8.W))\n", " val valid = Input(Bool())\n", " val out = Output(UInt(8.W))\n", " val consts = Input(Vec(length, UInt(8.W)))\n", " })\n", " \n", " // Such concision! You'll learn what all this means later.\n", " val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))\n", " taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }\n", "\n", " io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _)\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# DspBlock\n", "\n", "Integrating DSP components into a larger system can be challenging and error prone.\n", "The [rocket-dsp-utils](https://github.com/ucb-art/rocket-dsp-utils) repository consists of useful generators that should help with such tasks.\n", "\n", "One of the core abstractions is the notion of a DSPBlock.\n", "A DSPBlock has:\n", "* AXI-4 Stream input and output\n", "* AXI-4 memory-mapped status and control\n", "\n", "\n", "\n", "\n", "The following code wraps the FIR filter in AXI4 interfaces." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import dspblocks._\n", "import freechips.rocketchip.amba.axi4._\n", "import freechips.rocketchip.amba.axi4stream._\n", "import freechips.rocketchip.config._\n", "import freechips.rocketchip.diplomacy._\n", "\n", "case object NumFilters extends CSRField {\n", " val name = \"firQueueDepth\"\n", "}\n", "\n", "case class FIRTap(filterIdx: Int, tapIdx: Int) extends CSRField {\n", " val name = s\"firTap${filterIdx}_$tapIdx\"\n", "}\n", "\n", "class FIRBlock(nFilters: Int, nTaps: Int)(implicit p: Parameters) extends\n", "AXI4DspBlock with AXI4HasCSR with HasIPXactParameters {\n", " outer =>\n", " addStatus(NumFilters)\n", " \n", " for (i <- 0 until nFilters) {\n", " for (j <- 0 until nTaps) {\n", " addControl(FIRTap(i, j))\n", " }\n", " }\n", " makeCSRs()\n", " \n", " val streamNode = AXI4StreamIdentityNode()\n", " \n", " override def ipxactParameters = Map(\n", " \"nFilters\" -> nFilters.toString,\n", " \"nTaps\" -> nTaps.toString\n", " )\n", "\n", " def beatBytes: Int = 8\n", " def csrAddress = freechips.rocketchip.diplomacy.AddressSet(0x0, 0xffffL)\n", " def csrBase: Int = 0\n", " def csrSize: Int = 4096\n", " \n", " lazy val module = new LazyModuleImp(this) {\n", " val (in, _) = streamNode.in(0)\n", " val (out, _) = streamNode.out(0)\n", " val mem = outer.mem.map { m => m.in(0) }\n", " \n", " require(in.params.n >= nFilters,\n", " s\"\"\"AXI-4 Stream port must be big enough for all \n", " |the filters (need $nFilters,, only have ${in.params.n})\"\"\".stripMargin)\n", "\n", " status(NumFilters) := nFilters.U\n", " \n", " \n", " val outs = (0 until nFilters).map(i => {\n", " val fir = Module(new MyManyDynamicElementVecFir(nTaps))\n", " \n", " fir.io.in := in.bits.data((i+1)*8, i*8)\n", " fir.io.valid := in.valid && out.ready\n", " \n", " for (j <- 0 until nTaps) {\n", " fir.io.consts(j) := control(FIRTap(i, j))\n", " }\n", " \n", " fir.io.out\n", " })\n", " \n", " \n", " val output = if (outs.length == 1) {\n", " outs(0)\n", " } else {\n", " outs.reduce((x: UInt, y: UInt) => Cat(y, x))\n", " }\n", " \n", " out.bits.data := output\n", " in.ready := out.ready\n", " out.valid := in.valid\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# Invoking the Generator\n", "\n", "The following code invokes the generator.\n", "It will produce a file called `BlindModule.fir` which contains the firrtl generated by our code, as well as a file called `BlindModule.v` which is the compiled verilog.\n", "`BlindModule` is a wrapper for our `DspBlock`- it is necessary because of how [diplomacy](https://carrv.github.io/papers/cook-diplomacy-carrv2017.pdf) in rocket works.\n", "You'll notice that `BlindModule` instantiates an `FIRBlock`.\n", "\n", "The line\n", "```scala\n", "AXI4StreamBundleParameters(n = 8)\n", "```\n", "sets the AXI-4 streams' data widths to 8 bytes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "object FIRGenerator extends ipxact.IPXactGeneratorApp with App {\n", " override val verilogFilename: String = \"BlindModule.v\"\n", " override val ipxactDir: String = \"./\"\n", " implicit val p: Parameters = Parameters.root((new freechips.rocketchip.coreplex.BaseCoreplexConfig).toInstance)\n", "\n", " val blindNodes = DspBlockBlindNodes.apply(\n", " AXI4StreamBundleParameters(n = 8),\n", " () => AXI4MasterNode(Seq(AXI4MasterPortParameters(Seq(AXI4MasterParameters(\"fir\"))))))\n", " \n", " val dut = () => {\n", " val lazyMod = LazyModule(DspBlock.blindWrapper(() => new FIRBlock(4, 8), blindNodes))\n", " val m = lazyMod.module\n", " IPXactComponents._ipxactComponents += DspIPXact.makeDspBlockComponent(lazyMod.internal)\n", " m\n", " }\n", "\n", " \n", " chisel3.Driver.dumpFirrtl(chisel3.Driver.elaborate(dut), None)\n", " chisel3.Driver.compileFirrtlToVerilog(\"BlindModule\", new java.io.File(System.getProperty(\"user.dir\")))\n", " generateIPXact(IPXactComponents.ipxactComponents())\n", "}\n", "FIRGenerator.main(Array[String]())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "# IPXact\n", "Look in the file browser for a file called `craft_BlindModule_edu.berkeley.cs_1.0.xml`.\n", "This is the ipxact file.\n", "It contains information about:\n", "* Port mappings\n", "* Interfaces\n", "* Memory maps\n", "* Generator parameters\n", "\n", "You'll notice that the parameters from the scala code\n", "```scala\n", "override def ipxactParameters = Map(\n", " \"nFilters\" -> nFilters.toString,\n", " \"nTaps\" -> nTaps.toString\n", ")\n", "```\n", "appears in the IPXact output.\n", "This gives verification generators the information they need to generate appropriate test vectors for the given instance." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "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 }