{ "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", "# 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 section of the dsptools repository](https://github.com/ucb-bar/dsptools/tree/master/rocket) 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", "* Memory-mapped status and control (in this example, AXI4)\n", "\n", "\n", "\n", "`DspBlock`s use diplomatic interfaces from rocket.\n", "[This site](https://www.lowrisc.org/docs/diplomacy/) has a good overview of the basic of diplomacy, but don't worry too much about how it's working for this example.\n", "Diplomacy really shines when you're connecting a lot of different blocks together to form a complex SoC.\n", "In this example, we're just making a single peripheral.\n", "The `StandaloneBlock` traits are mixed in to make diplomatic interfaces work as top-level IOs.\n", "You only need them when the `DspBlock` is being used as a top level interface without any diplomatic connections.\n", "\n", "The following code wraps the FIR filter in AXI4 interfaces.\n" ] }, { "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", "import freechips.rocketchip.regmapper._\n", "\n", "//\n", "// Base class for all FIRBlocks.\n", "// This can be extended to make TileLink, AXI4, APB, AHB, etc. flavors of the FIR filter\n", "//\n", "abstract class FIRBlock[D, U, EO, EI, B <: Data](val nFilters: Int, val nTaps: Int)(implicit p: Parameters)\n", "// HasCSR means that the memory interface will be using the RegMapper API to define status and control registers\n", "extends DspBlock[D, U, EO, EI, B] with HasCSR {\n", " // diplomatic node for the streaming interface\n", " // identity node means the output and input are parameterized to be the same\n", " val streamNode = AXI4StreamIdentityNode()\n", " \n", " // define the what hardware will be elaborated\n", " lazy val module = new LazyModuleImp(this) {\n", " // get streaming input and output wires from diplomatic node\n", " val (in, _) = streamNode.in(0)\n", " val (out, _) = streamNode.out(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", " // make registers to store taps\n", " val taps = Reg(Vec(nFilters, Vec(nTaps, UInt(8.W))))\n", "\n", " // memory map the taps, plus the first address is a read-only field that says how many filter lanes there are\n", " val mmap = Seq(\n", " RegField.r(64, nFilters.U, RegFieldDesc(\"nFilters\", \"Number of filter lanes\"))\n", " ) ++ taps.flatMap(_.map(t => RegField(8, t, RegFieldDesc(\"tap\", \"Tap\"))))\n", "\n", " // generate the hardware for the memory interface\n", " // in this class, regmap is abstract (unimplemented). mixing in something like AXI4HasCSR or TLHasCSR\n", " // will define regmap for the particular memory interface\n", " regmap(mmap.zipWithIndex.map({case (r, i) => i * 8 -> Seq(r)}): _*)\n", "\n", " // make the FIR lanes and connect inputs and taps\n", " val outs = for (i <- 0 until nFilters) yield {\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", " fir.io.consts := taps(i) \n", " fir.io.out\n", " }\n", "\n", " val output = if (outs.length == 1) {\n", " outs.head\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", "}\n", "\n", "// make AXI4 flavor of FIRBlock\n", "abstract class AXI4FIRBlock(nFilters: Int, nTaps: Int)(implicit p: Parameters) extends FIRBlock[AXI4MasterPortParameters, AXI4SlavePortParameters, AXI4EdgeParameters, AXI4EdgeParameters, AXI4Bundle](nFilters, nTaps) with AXI4DspBlock with AXI4HasCSR {\n", " override val mem = Some(AXI4RegisterNode(\n", " AddressSet(0x0, 0xffffL), beatBytes = 8\n", " ))\n", "}\n", "\n", "// running the code below will show what firrtl is generated\n", "// note that LazyModules aren't really chisel modules- you need to call \".module\" on them when invoking the chisel driver\n", "// also note that AXI4StandaloneBlock is mixed in- if you forget it, you will get weird diplomacy errors because the memory\n", "// interface expects a master and the streaming interface expects to be connected. AXI4StandaloneBlock will add top level IOs\n", "// println(chisel3.Driver.emit(() => LazyModule(new AXI4FIRBlock(1, 8)(Parameters.empty) with AXI4StandaloneBlock).module))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Testing\n", "\n", "Testing `DspBlock`s is a little different.\n", "Now we're dealing with memory interfaces and `LazyModule`s.\n", "dsptools has some features that help test `DspBlock`s.\n", "\n", "One important feature is `MemMasterModel`.\n", "The trait defines functions like `memReadWord` and `memWriteWord`- generic functions for generating memory traffic.\n", "This allows you to write one generic test that can be specialized to the memory interface you are using- for example, you write one test and then specialize it for the TileLink and AXI4 interfaces.\n", "\n", "The code below tests the `FIRBlock` this way." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import dsptools.tester.MemMasterModel\n", "import freechips.rocketchip.amba.axi4\n", "\n", "abstract class FIRBlockTester[D, U, EO, EI, B <: Data](c: FIRBlock[D, U, EO, EI, B]) extends PeekPokeTester(c.module) with MemMasterModel {\n", " // check that address 0 is the number of filters\n", " require(memReadWord(0) == c.nFilters)\n", " // write 1 to all the taps\n", " for (i <- 0 until c.nFilters * c.nTaps) {\n", " memWriteWord(8 + i * 8, 1)\n", " }\n", "}\n", "\n", "// specialize the generic tester for axi4\n", "class AXI4FIRBlockTester(c: AXI4FIRBlock with AXI4StandaloneBlock) extends FIRBlockTester(c) with AXI4MasterModel {\n", " def memAXI = c.ioMem.get\n", "}\n", "\n", "// invoking testers on lazymodules is a little strange.\n", "// note that the firblocktester takes a lazymodule, not a module (it calls .module in \"extends PeekPokeTester()\").\n", "val lm = LazyModule(new AXI4FIRBlock(1, 8)(Parameters.empty) with AXI4StandaloneBlock)\n", "chisel3.iotesters.Driver(() => lm.module) { _ => new AXI4FIRBlockTester(lm) }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise: TileLink**
\n", "\n", "Add a version of `FIRBlock` that uses TileLink for its memory interconnect, and extend the `FIRBlockTester` to use TileLink." ] }, { "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": "scala", "nbconvert_exporter": "script", "version": "2.12.8" } }, "nbformat": 4, "nbformat_minor": 2 }