Showing posts with label Chisel. Show all posts
Showing posts with label Chisel. Show all posts

Thursday, September 6, 2018

Register a bundle in Chisel3

A quick update to a previous post that I made: http://blog.edmondcote.com/2017/03/register-bundle-in-chisel.html.  Here is how I now create a register (sequential storage element) of a bundle in Chisel3.  The benefit is the elimination of the previously-necessary wire declaration.


val pageReg: Page = RegInit({
  val page = Wire(new Page())
  page.initialize()
  page
})

Friday, January 12, 2018

Address Masking for RAM Devices

I found the problem of generating the correct address for a memory operation confusing and decided that it would be of value to me (and others) to take notes.

Assume we have an implementation of a slave device on a TileLink2 bus. It can receive either read (Get) or write (PutFullData) messages on channel A and correctly return the response on channel D.  More information about this in the spec.

The following covers the implementation of the module, specifically on the mask generation, nothing more.

Assumptions. The slave device covers address range 0x2000000, size 0x1FFF (or 8KB).  The system bus uses 8 bytes per beat (matching the 64 bit bus).  The memory width matches the bus width.  Number of memory words is 1024 and address width is 10 bits.

val address = in.a.bits.address // receive the full 64-bit physical address
val size = 0x1FFF // 8KB size, get using addressSet.mask
val size = in.a.size  // log2(bytes) of total amount of the data requested, e.g., 8B cache line == 1

Problem.  To compute the word-aligned address, you need a mask.  Here is an example worked out by hand.

2000_1000 ^ (0x2000_1000 & ~mask-1) = 0x1000 (byte aligned) // where mask = 0x2000
1000 / 8 = 0x200 (8B word aligned)
addr[9:0] = 0x200

That was the easy part.  I got confused by the operation of the circuit generator.  Isn't the following great ;)

val a_address   = Cat((mask zip (in.haddr >> log2Ceil(beatBytes)).toBools).filter(_._1).map(_._2).reverse)

This is definitely one the least appealing parts of functional programming.  It would take me a few hours to reverse engineer this.  How this should work is any non-trivial function should be placed in its own container and unit tested.  The unit tests are commented and serve as a description of the functional algorithm.  But, I digress ...

There is a helper object in the util package.  Moral of the story.  Use it.

// This gets used everywhere, so make the smallest circuit possible ...
// Given an address and size, create a mask of beatBytes size
// eg: (0x3, 0, 4) => 0001, (0x3, 1, 4) => 0011, (0x3, 2, 4) => 1111
// groupBy applies an interleaved OR reduction; groupBy=2 take 0010 => 01
object MaskGen {
  def apply(addr_lo: UInt, lgSize: UInt, beatBytes: Int, groupBy: Int = 1): UInt = {
[..]




Saturday, October 21, 2017

Script to generate Verilog ports from Chisel source

Here's a script I wrote that will (naively) parse a Verilog file and output it's corresponding ports in Chisel format.  This is useful to import a Verilog module as a blackbox in a Chisel design.


#!/bin/sh
exec scala "$0" "$@"
!#

import scala.util.{Success, Try}
import scala.util.matching.Regex

val port = raw"\s*(input|output|inout)\s+(wire\s*|reg\s*)?\s*(\[(\d+):(\d+)\])?\s*(\w+)?\s*;?".r

val p = scala.io.Source.fromFile(args(0)).getLines.foreach { f: String =>
  val b = f match {
    case port(d, tzpe, _, msb, lsb, name) =>
      val dir = d match {
        case "input" => "Input"
        case "output" => "Output"
        case "inout" => "Analog"
        case _ =>
      }
      val w = Try({msb.toInt - lsb.toInt + 1}) match {
        case Success(s) => s"%d".W.format(s)
        case _ => "1.W"
      }
      s"val $name = $dir(UInt($w))"
    case _ => ""
  }
  if (b.nonEmpty) println(b)
}

The chosen regular expression was testing using regex101.com, a populate online regular expression tester.

Monday, October 16, 2017

My Clocking and Reset Strategy with RawModule

This post is originally a reply to: https://groups.google.com/forum/#!topic/chisel-users/LTujWW6DtI4

The following snippet of code illustrates my top level clocking and reset strategy.  I opt to use LazyRawModuleImp and explicitly specify the clocks and reset.

class LazyTop(implicit p: Parameters) extends LazyModule {
  lazy val module = new LazyRawModuleImp(this) {
    val io = IO(new Bundle {      
      val ref_clk_p = Input(Clock())
      val rst_sw_n = Input(Bool()) // async reset, debounced on board?
      val led_n = Output(Bits(8.W))
    })

    val GSR_INST = Module(new GSR()) // this is blackbox module
    GSR_INST.io.GSR := io.rst_sw_n

    val global_reset = Wire(Bool())
    val global_reset_n = Wire(Bool())
    global_reset_n := GSR_INST.io.GSR // appears GSR is active low
    global_reset := !GSR_INST.io.GSR

    // use active high clock
    withClockAndReset(ref_clk_p, global_reset) {
      io.led_n := RegNext("h5A".asUInt(8.W), init = 0.U)
    }
  }
}

Module GSR is a Chisel BlackBox for my FPGA's GSR (global set reset) library cell.


class GSR extends BlackBox {
  val io = IO(new Bundle {
    val GSR = Input(Bool())
  })
}

Wednesday, October 4, 2017

To link Chisel annotations and circuit info

Here's another snippet of Chisel code.  It was well worth the (ashamed to admit) 4+ hrs to write.  Admittedly, I am still getting a hand of functional programming.  I am particularly proud of the recursive function call to populate an immutable val.  The function's use case is to return Chisel circuit level information from an Annotation object.  It is necessary because Annotation does not contain (do best of my knowledge) a reference to a Data object.


  private def getDirection(circuit: Circuit, a: Annotation): ActualDirection = {
    // get net info from annotation object
    val thisModName = a.target.asInstanceOf[ComponentName].module.name
    val thisPortName = a.target.name.replace("io.", "") // FIXME: this is a HACK ..

    // find matching module in Chisel circuit
    val module = circuit.components.flatMap {
      case m: DefModule => Some(m)
      case b: DefBlackBox => None
    }.find {
      _.name == thisModName
    }.head

    // recursively return Element objects
    def elemsFromData(d: Data): Seq[Element] = d match {
      case e: Element => Seq(e)
      case r: Record =>
        r.elements.flatMap((e: (String, Data)) => {
          elemsFromData(e._2)
        }).toSeq
    }

    // all elements for all module port
    val elems: Seq[Element] = module.ports.flatMap((p: Port) => {
      elemsFromData(p.id)
    })

    // use DataMirror to find direction, have yet to test whether fully accurate
    // FIXME: obviously not optimal, should filter above
    val dir = DataMirror.directionOf(elems.find(_.name == thisPortName).head)
    dir
  }

Tuesday, September 26, 2017

Generate custom Chisel bundle using meta programming

I found the example to extend a Bundle's functionality limiting.  You need to pass a varargs of (String,Data) tuple to the function.  Here's a snippet:

 final class CustomBundle(elts: (String, Data)*) extends Record {
    val elements = ListMap(elts map { case (field, elt) => field -> elt.chiselCloneType }: _*)
    def apply(elt: String): Data = elements(elt)
    override def cloneType = (new CustomBundle(elements.toList: _*)).asInstanceOf[this.type]
  }

I could have found a solution using the above (not sure, is it possible to convert ListMap to varargs?), but I dug into Scala meta programming for the purpose of ramp.  Behold!  Here's an alternative (though incomplete) method of generating a bundle.

object MetaBundle {
  import reflect.runtime.currentMirror
  import tools.reflect.ToolBox
  val toolbox = currentMirror.mkToolBox()
  import toolbox.u._

  def apply(): Bundle = {
    val tree =
      q"""
          import chisel3.core._
          val b = new Bundle {
            // placeholder for your interface
            val i = Input(UInt(1.W))
            val o = Output(UInt(1.W))
          }
          b
       """    
     toolbox.eval(tree).asInstanceOf[Bundle]
  }
}

The code above returns an instance of a typeless bundle.  The result isn't equivalent to above, but serves my purpose.  Have yet to try, but suspect the technique can be extended to other cases.  There's likely implications wrt. statically typed nature of Scala.  Use at your own risk.  I am only posting this because it's a neat hack.

Monday, September 25, 2017

Driving random values on all elements of a bundle

Quick addition to the previous post.  This technique is useful when prototyping hardware.  Use LFSRs to drive pins/ports such that no logic pruning occurs its cone of logic during synthesis.

  final def prng(bundle: Bundle): Unit = {
    bundle.elements.filter(_._2.dir == OUTPUT).foreach {
      case (n, d) => {
        val r = Module(new LFSR(d.getWidth))
        when(true.B) {
          d := r.io.y
  } } } }

Example function to enhance Chisel bundle connection

Here's a real world example that demonstrates the flexibility of Chisel (or DSLs in general) compared to traditional languages for describing hardware.

It is my opinion that SystemVerilog's promise of making module connection more user (and verification) friendly has not been realized.  The interface construct was a good first step, so were interface modports.  The .* connection operator or parameterizable interfaces, not so much.  Traditional teams send their best (aka. quickest) Perl monkey to the rescue and, just like that, a new target for feature creep and "I'm busy, fix the script yourself" is created.

You have access to an API when using an internal DSL approach (such as Chisel).  If you wanted to write your own function to connect to arbitrary hardware interfaces - you do it.  This doesn't directly eliminate feature creep, but provides a better framework for development than Perl/RegEx can offer.  Consider this tradeoff.  How many lines of parsing, data structure definition, testing and software regression strategies are needed compared to a few lines of Scala.

Here's an example from my private repo.  It takes two Bundles as input and connects fields with matching names.

final def connect(left: Bundle, right: Bundle)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = {
  (left.elements.toSeq ++ right.elements.toSeq) // Map to Seq, then combine lhs and rhs
    .groupBy(_._1) // group by name of
    .filter(_._2.length == 2) // filter out non matches
    .foreach {
      case (k, v) => {
        (lhs.dir, rhs.dir) match {
          case (NODIR, NODIR) => attach(lhs.asInstanceOf[Analog], rhs.asInstanceOf[Analog]) // to support INOUT ports
          case (INPUT, OUTPUT) => rhs := lhs
          case (OUTPUT, INPUT) => lhs := rhs
          case (INPUT, INPUT) => rhs := lhs // TODO: verify more
          case (OUTPUT, OUTPUT) => lhs := rhs // TODO: verify mode
        }
      }
    }

PS. Slowly, but surely, I'm becoming functional.


Its difficult for the time being, especially without a IDE.  IntelliJ IDEA is terrific.  Still can't properly explain a Monad.  The ability to highlight a val, then hit "Alt-Enter", select "Add Type annotation to value definition", provides immeasurable value.  Here's an example.  By breaking up different parts of the above function, I can better determine (before compile time) the type of data structure that will be output.


    val a: Map[String, Data] = left.elements
    val b: Map[String, Data] = right.elements
    val c: Seq[(String, Data)] = a.toSeq
    val d: Seq[(String, Data)] = a.toSeq ++ b.toSeq
    val e: Map[String, Seq[(String, Data)]] = d.groupBy(_._1)

Friday, August 25, 2017

Decomposing TileLink2 in RocketChip: A Barebones System

Quick post.  I spent some time yesterday/today hacking at the TileLink2 implementation in RocketChip.

(As a matter of coincidence, SiFive recently released a draft spec of the protocol.)

My preferred method of learning is first to understand the engineer's original intent before enhancing or leveraging code.  This means taking things apart.

The most difficult aspect in getting something up was understanding the inner workings of SystemBus class.  I kept hitting run time assertions and it was helpful to manually sketch out the connections between the TL objects:


Here is a summary of my findings.

I identified the minimal set of configuration knobs for this experiment.


// Barebones system configuration
class BarebonesSystemConfig extends Config((site, here, up) => {
  case XLen => 64
  case BankedL2Params => BankedL2Params(nMemoryChannels = 0,
    nBanksPerChannel = 1,
    coherenceManager = { // FIXME: not really a coherence manager
      case (q, _) =>
        implicit val p = q
        val cork = LazyModule(new TLCacheCork(unsafe = true))
        (cork.node, cork.node)
    })
  case SystemBusParams => SystemBusParams(beatBytes = site(XLen) / 8, blockBytes = site(CacheBlockBytes))
  case PeripheryBusParams => PeripheryBusParams(beatBytes = site(XLen) / 8, blockBytes = site(CacheBlockBytes))
  case MemoryBusParams => MemoryBusParams(beatBytes = 8, blockBytes = site(CacheBlockBytes))
  case CacheBlockBytes => 64
  case DTSTimebase => BigInt(1000000) // 1 MHz
  case DTSModel => "bboneschip"
  case DTSCompat => Nil
  case TLMonitorBuilder => (args: TLMonitorArgs) => Some(LazyModule(new TLMonitor(args)))
  case TLCombinationalCheck => false
  case TLBusDelayProbability => 0.0
})

This is the system itself.  It extends BaseComplex.  Not RocketComplex.


class BarebonesSystem(implicit p: Parameters) extends BaseCoreplex {
  // connect single master device (fuzzer) to system bus (sbus)
  sbus.fromSyncPorts() :=* LazyModule(new TLFuzzer(1)).node

  // connect single slave device (memory) to the periphery bus (pbus)
  LazyModule(new TLTestRAM(AddressSet(0x0, 0xfff))).node :=* pbus.toFixedWidthSingleBeatSlave(4)

  override lazy val module = new BarebonesSystemModule(this)
}
class BarebonesSystemModule[T <: BarebonesSystem](_outer: T) extends BaseCoreplexModule(_outer) {}

This should be familiar.  It is your top level Chisel3 module.


// Chisel top module
class Top(implicit val p: Parameters) extends Module {
  val io = new Bundle {
    val success = Bool(OUTPUT)
  }
  val dut = Module(LazyModule(new BarebonesSystem).module)
  dut.reset := reset
}

Here are the important parts of the system generator.  This will generate your firrtl and verilog output.


// System generator
class System extends HasGeneratorUtilities {
  val names = new ParsedInputNames(
    targetDir = ".",
    topModuleProject = "com.bbones",
    topModuleClass = "Top",
    configProject = "",
    configs = "system"
  )

  val config = new BarebonesSystemConfig
  val params = Parameters.root(config.toInstance)
  val circuit = elaborate(names, params)

  def generateArtefacts: Unit =
    ElaborationArtefacts.files.foreach {
      case (extension, contents) =>
        writeOutputFile(names.targetDir, s"${names.configs}.${extension}", contents())
    }

  def generateFirrtl: Unit =
    Driver.dumpFirrtl(circuit, Some(new File(names.targetDir, s"${names.configs}.fir")))

  def generateVerilog: Unit = {
    import sys.process._
    val firrtl = "/home/edc/gitrepos/rocket-chip/firrtl/utils/bin/firrtl"
    val path = s"${names.targetDir}/${names.configs}"
    val bin = s"$firrtl -i $path.fir -o $path.v -X verilog"
    bin.!
  }

  generateArtefacts
  generateFirrtl
  generateVerilog
}

Finally, one should never forget unit testing!


// Unit test
class SystemSpec extends FlatSpec with Matchers {
  "An instance of the system generator " should "compile" in {
    val s = new System
  }
}

Monday, May 8, 2017

How to test a parameterized module in Chisel

Quick update today.  Busy with real work.  This quick post is written because how to test these modules in not adequately covered in the documentation for chisel-testers.

Consider the case for a parameterizable LSFR module.


class LSFR[T <: Bundle](gen: T = UInt(4.W)) extends Module {
  val io = IO(new Bundle {
    val rand = Output(gen)
  })
  val width = gen.getWidth
  val regLFSR = Reg(UInt(width.W), init = UInt(0))
  when(true.B) {
    regLFSR := Cat(regLFSR(0) ^ regLFSR(1), regLFSR(width - 1, 1))
  }
  io.rand := gen.fromBits(regLFSR)
}

Below I show a test containing two examples.  First using PokeTester and the second using PeekPokeTester.  See if you can spot the difference ...


class LSFRPeekPokeTester[T <: Bundle](gen: T, c: LSFR[T]) extends PeekPokeTester(c) {
  println("peekpoke test")
}

class LSFRSpec extends ChiselFlatSpec with PokeTester {
  behavior of "LSFR"

  it should "elaborate with peek test" in {
    val options = new TesterOptionsManager
    options.setTargetDirName("test_run_dir/LSFRSpec")
    test(new LSFR(new DDRCommand), testerBackend = FirrtlInterpreterBackend, options = options) {
      (t, c) => {
        println("poke test")
      }
    }
  }

  it should "elaborate with peekpoke test" in {
    Driver(() => new LSFR(new DDRCommand)) {
      c => new LSFRPeekPokeTester(new DDRCommand, c)
    }
  }
}

In the PokeTester case, the first argument to test is new LSFR(new DDRCommand).  For PeekPokeTester, you pass a function to generate the module () => new LSFR(new DDRCommand).  Not immediately obvious.  Now, look at the function prototypes for PokeTester and PeekPokeTester respectively ...


// PokeTester source
def test[T <: Module](dutGen: => T, testerBackend: TesterBackend, options: TesterOptionsManager)(block: (InnerTester, T) => Unit) { //[..]

// PeekPokeTester source
def apply[T <: Module](
      dutGen: () => T,
      backendType: String = "firrtl",
      verbose: Boolean = false,
      testerSeed: Long = System.currentTimeMillis())(
      testerGen: T => PeekPokeTester[T]): Boolean = { //[..]

The definition of dutGen is different.

Wise to keep in mind that PokeTester is under iotesters.experimental.

Saturday, May 6, 2017

Use of HellaQueue in Chisel, secondary constructor

This post is an expanded update to an entry in my personal "NOTES" file.  At first glance, I found the following code perplxing, therefore I decided to break it down.

class HellaQueue[T <: Data](val entries: Int)(data: => T) extends Module {
  val io = new QueueIO(data, entries)

  val fq = Module(new HellaFlowQueue(entries)(data))
  fq.io.enq <> io.enq
  io.deq <> Queue(fq.io.deq, 1, pipe = true)
}

object HellaQueue {
  def apply[T <: Data](enq: DecoupledIO[T], entries: Int) = {
    val q = Module((new HellaQueue(entries)) { enq.bits })
    q.io.enq.valid := enq.valid // not using <> so that override is allowed
    q.io.enq.bits := enq.bits
    enq.ready := q.io.enq.ready
    q.io.deq
  }
}

What does the :< operator do in [T <: Data>]?

It is obvious (to me at least) that this is for type parameterization.  The <: operator indicates that the type supplied to the class instance must have chisel3.core.Data as an ancestor.

Why does (data: => T) do?

No suprise that (val entries: Int) is the primary constructor.  The fat arrow (=>) implies a function.  The first argument to the constructor lets you specific the number of entries.  The second argument data helps to parameterize the class.


// example usage
val xy: Data
val hq1 = (new HellaQueue(entries = 90)){xy}
// equivalent to above, without (data: => T) in the class constructor
val hq2 = (new HellaQueue[SInt](entries = 90))

Why is a class definion and a object definition of the same name?

The object is known as a companion class, consult stackoverflow.com for more info

What does the apply function do in the object?

The apply function acts as an auxillary constructor.  Allows alternative syntax for creating HellaQueue.

Wednesday, May 3, 2017

Using IDEA Scala debugger for Chisel

This a quick follow up on an older post where I set up IntelliJ IDEA and Scala.  I wrote this to make sure my steps for getting the debugger up and running were reproducable.  The steps assume you have a working Chisel project that compiles using sbt.

Check if sbt compile works from the command line for your Chisel project ...


[plus]:~/gitrepos/plus/arbiterdemo$ sbt compile
[info] Loading global plugins from /home/edc/.sbt/0.13/plugins
[info] Loading project definition from /home/edc/gitrepos/plus/arbiterdemo/project
[info] Set current project to memctrldemo (in build file:/home/edc/gitrepos/plus/arbiterdemo/)
[info] Updating {file:/home/edc/gitrepos/plus/arbiterdemo/}arbiterdemo...
[..]
[info] Done updating.
[info] Compiling 3 Scala sources to /home/edc/gitrepos/plus/arbiterdemo/target/scala-2.11/classes...
[success] Total time: 4 s, completed May 3, 2017 8:26:36 PM

Import project from SBT, select the relevant build.sbt file ...



Use the default project settings ...


Right click on your class that contains call to chisel3.Driver.execute, hit Run ...


Follow up by checking that that Verilog code is generated as expected (yes)

Finally, try setting a breakpoint by right clicking to the left of the source window and hit shift-F9 to begin debug.  I was 100% successfull following only these steps on IDEA 2017.1 with the project-specific IDEA settings (i.e. .idea/ diretory) wiped out prior.