Chisel logo

Module 3 Interlude: Chisel Standard Library

Prev: Generators: Collections
Next: Higher-Order Functions

Motivation

Chisel is all about re-use, so it only makes sense to provide a standard library of interfaces (encouraging interoperability of RTL) and generators for commonly-used hardware blocks.

Setup

In [ ]:
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
In [ ]:
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

The Cheatsheet

The Chisel3 cheatsheet contains a summary of all the major hardware construction APIs, including some of the standard library utilities that we'll introduce below.

Decoupled: A Standard Ready-Valid Interface

One of the commonly used interfaces provided by Chisel is DecoupledIO, providing a ready-valid interface for transferring data. The idea is that the source drives the bits signal with the data to be transferred and the valid signal when there is data to be transferred. The sink drives the ready signal when it is ready to accept data, and data is considered transferred when both ready and valid are asserted on a cycle.

This provides a flow control mechanism in both directions for data transfer, including a backpressure mechanism.

Note: ready and valid should not be combinationally coupled, otherwise this may result in unsynthesizable combinational loops. ready should only be dependent on whether the sink is able to receive data, and valid should only be dependent on whether the source has data. Only after the transaction (on the next clock cycle) should the values update.

Any Chisel data can be wrapped in a DecoupledIO (used as the bits field) as follows:

val myChiselData = UInt(8.W)
// or any Chisel data type, such as Bool(), SInt(...), or even custom Bundles
val myDecoupled = Decoupled(myChiselData)

The above creates a new DecoupledIO Bundle with fields

  • valid: Output(Bool)
  • ready: Input(Bool)
  • bits: Output(UInt(8.W))

The rest of the section will be structured somewhat differently from the ones before: instead of giving you coding exercises, we're going to give some code examples and testcases that print the circuit state. Try to predict what will be printed before just running the tests.

Queues

Queue creates a FIFO (first-in, first-out) queue with Decoupled interfaces on both sides, allowing backpressure. Both the data type and number of elements are configurable.

In [ ]:
Driver(() => new Module {
    // Example circuit using a Queue
    val io = IO(new Bundle {
      val in = Flipped(Decoupled(UInt(8.W)))
      val out = Decoupled(UInt(8.W))
    })
    val queue = Queue(io.in, 2)  // 2-element queue
    io.out <> queue
  }) { c => new PeekPokeTester(c) {
    // Example testsequence showing the use and behavior of Queue
    poke(c.io.out.ready, 0)
    poke(c.io.in.valid, 1)  // Enqueue an element
    poke(c.io.in.bits, 42)
    println(s"Starting:")
    println(s"\tio.in: ready=${peek(c.io.in.ready)}")
    println(s"\tio.out: valid=${peek(c.io.out.valid)}, bits=${peek(c.io.out.bits)}")
    step(1)
  
    poke(c.io.in.valid, 1)  // Enqueue another element
    poke(c.io.in.bits, 43)
    // What do you think io.out.valid and io.out.bits will be?
    println(s"After first enqueue:")
    println(s"\tio.in: ready=${peek(c.io.in.ready)}")
    println(s"\tio.out: valid=${peek(c.io.out.valid)}, bits=${peek(c.io.out.bits)}")
    step(1)
  
    poke(c.io.in.valid, 1)  // Read a element, attempt to enqueue
    poke(c.io.in.bits, 44)
    poke(c.io.out.ready, 1)
    // What do you think io.in.ready will be, and will this enqueue succeed, and what will be read?
    println(s"On first read:")
    println(s"\tio.in: ready=${peek(c.io.in.ready)}")
    println(s"\tio.out: valid=${peek(c.io.out.valid)}, bits=${peek(c.io.out.bits)}")
    step(1)
  
    poke(c.io.in.valid, 0)  // Read elements out
    poke(c.io.out.ready, 1)
    // What do you think will be read here?
    println(s"On second read:")
    println(s"\tio.in: ready=${peek(c.io.in.ready)}")
    println(s"\tio.out: valid=${peek(c.io.out.valid)}, bits=${peek(c.io.out.bits)}")
    step(1)
  
    // Will a third read produce anything?
    println(s"On third read:")
    println(s"\tio.in: ready=${peek(c.io.in.ready)}")
    println(s"\tio.out: valid=${peek(c.io.out.valid)}, bits=${peek(c.io.out.bits)}")
    step(1)
} }

Arbiters

Arbiters routes data from n DecoupledIO sources to one DecoupledIO sink, given a prioritization. There are two types included in Chisel:

  • Arbiter: prioritizes lower-index producers
  • RRArbiter: runs in round-robin order

Note that Arbiter routing is implemented in combinational logic.

The below example will demonstrate the use of the priority arbiter (which you will also implement in the next section):

In [ ]:
Driver(() => new Module {
    // Example circuit using a priority arbiter
    val io = IO(new Bundle {
      val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
      val out = Decoupled(UInt(8.W))
    })
    // Arbiter doesn't have a convenience constructor, so it's built like any Module
    val arbiter = Module(new Arbiter(UInt(8.W), 2))  // 2 to 1 Priority Arbiter
    arbiter.io.in <> io.in
    io.out <> arbiter.io.out
  }) { c => new PeekPokeTester(c) {
    poke(c.io.in(0).valid, 0)
    poke(c.io.in(1).valid, 0)
    println(s"Start:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
    poke(c.io.in(1).valid, 1)  // Valid input 1
    poke(c.io.in(1).bits, 42)
    // What do you think the output will be?
    println(s"valid input 1:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
    poke(c.io.in(0).valid, 1)  // Valid inputs 0 and 1
    poke(c.io.in(0).bits, 43)
    // What do you think the output will be? Which inputs will be ready?
    println(s"valid inputs 0 and 1:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
    poke(c.io.in(1).valid, 0)  // Valid input 0
    // What do you think the output will be?
    println(s"valid input 0:")
    println(s"\tin(0).ready=${peek(c.io.in(0).ready)}, in(1).ready=${peek(c.io.in(1).ready)}")
    println(s"\tout.valid=${peek(c.io.out.valid)}, out.bits=${peek(c.io.out.bits)}")
} }

Misc Function Blocks

Chisel Utils has some helpers that perform stateless functions.

Bitwise Utilities

PopCount

PopCount returns the number of high (1) bits in the input as a UInt.

Reverse

Reverse returns the bit-reversed input.

In [ ]:
Driver(() => new Module {
    // Example circuit using Reverse
    val io = IO(new Bundle {
      val in = Input(UInt(8.W))
      val out = Output(UInt(8.W))
    })
    io.out := PopCount(io.in)
  }) { c => new PeekPokeTester(c) {
    // Integer.parseInt is used create an Integer from a binary specification
    poke(c.io.in, Integer.parseInt("00000000", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
  
    poke(c.io.in, Integer.parseInt("00001111", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
  
    poke(c.io.in, Integer.parseInt("11001010", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
  
    poke(c.io.in, Integer.parseInt("11111111", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
} }
In [ ]:
Driver(() => new Module {
    // Example circuit using Reverse
    val io = IO(new Bundle {
      val in = Input(UInt(8.W))
      val out = Output(UInt(8.W))
    })
    io.out := Reverse(io.in)
  }) { c => new PeekPokeTester(c) {
    // Integer.parseInt is used create an Integer from a binary specification
    poke(c.io.in, Integer.parseInt("01010101", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=0b${peek(c.io.out).toInt.toBinaryString}")
  
    poke(c.io.in, Integer.parseInt("00001111", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=0b${peek(c.io.out).toInt.toBinaryString}")
  
    poke(c.io.in, Integer.parseInt("11110000", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=0b${peek(c.io.out).toInt.toBinaryString}")
  
    poke(c.io.in, Integer.parseInt("11001010", 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=0b${peek(c.io.out).toInt.toBinaryString}")
} }

OneHot encoding utilities

OneHot is an encoding of integers where there is one wire for each value, and exactly one wire is high. This allows the efficient creation of some functions, for example muxes. However, behavior may be undefined if the one-wire-high condition is not held.

The below two functions provide conversion between binary (UInt) and OneHot encodings, and are inverses of each other:

  • UInt to OneHot: UIntToOH
  • OneHot to UInt: OHToUInt
In [ ]:
Driver(() => new Module {
    // Example circuit using UIntToOH
    val io = IO(new Bundle {
      val in = Input(UInt(4.W))
      val out = Output(UInt(16.W))
    })
    io.out := UIntToOH(io.in)
  }) { c => new PeekPokeTester(c) {
    poke(c.io.in, 0)
    println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}")

    poke(c.io.in, 1)
    println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}")
  
    poke(c.io.in, 8)
    println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}")
  
    poke(c.io.in, 15)
    println(s"in=${peek(c.io.in)}, out=0b${peek(c.io.out).toInt.toBinaryString}")
} }
In [ ]:
Driver(() => new Module {
    // Example circuit using OHToUInt
    val io = IO(new Bundle {
      val in = Input(UInt(16.W))
      val out = Output(UInt(4.W))
    })
    io.out := OHToUInt(io.in)
  }) { c => new PeekPokeTester(c) {
    poke(c.io.in, Integer.parseInt("0000 0000 0000 0001".replace(" ", ""), 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")

    poke(c.io.in, Integer.parseInt("0000 0000 1000 0000".replace(" ", ""), 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
  
    poke(c.io.in, Integer.parseInt("1000 0000 0000 0001".replace(" ", ""), 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
  
    // Some invalid inputs:
    // None high
    poke(c.io.in, Integer.parseInt("0000 0000 0000 0000".replace(" ", ""), 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
  
    // Multiple high
    poke(c.io.in, Integer.parseInt("0001 0100 0010 0000".replace(" ", ""), 2))
    println(s"in=0b${peek(c.io.in).toInt.toBinaryString}, out=${peek(c.io.out)}")
} }

Muxes

These muxes take in a list of values with select signals, and output the value associated with the lowest-index select signal.

These can either take a list of (select: Bool, value: Data) tuples, or corresponding lists of selects and values as arguments. For simplicity, the examples below only demonstrate the second form.

Priority Mux

A PriorityMux outputs the value associated with the lowest-index asserted select signal.

OneHot Mux

An Mux1H provides an efficient implementation when it is guaranteed that exactly one of the select signals will be high. Behavior is undefined if the assumption is not true.

In [ ]:
Driver(() => new Module {
    // Example circuit using PriorityMux
    val io = IO(new Bundle {
      val in_sels = Input(Vec(2, Bool()))
      val in_bits = Input(Vec(2, UInt(8.W)))
      val out = Output(UInt(8.W))
    })
    io.out := PriorityMux(io.in_sels, io.in_bits)
  }) { c => new PeekPokeTester(c) {
    poke(c.io.in_bits(0), 10)
    poke(c.io.in_bits(1), 20)
  
    // Select higher index only
    poke(c.io.in_sels(0), 0)
    poke(c.io.in_sels(1), 1)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
  
    // Select both - arbitration needed
    poke(c.io.in_sels(0), 1)
    poke(c.io.in_sels(1), 1)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
  
    // Select lower index only
    poke(c.io.in_sels(0), 1)
    poke(c.io.in_sels(1), 0)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
} }
In [ ]:
Driver(() => new Module {
    // Example circuit using Mux1H
    val io = IO(new Bundle {
      val in_sels = Input(Vec(2, Bool()))
      val in_bits = Input(Vec(2, UInt(8.W)))
      val out = Output(UInt(8.W))
    })
    io.out := Mux1H(io.in_sels, io.in_bits)
  }) { c => new PeekPokeTester(c) {
    poke(c.io.in_bits(0), 10)
    poke(c.io.in_bits(1), 20)
  
    // Select index 1
    poke(c.io.in_sels(0), 0)
    poke(c.io.in_sels(1), 1)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
  
    // Select index 0
    poke(c.io.in_sels(0), 1)
    poke(c.io.in_sels(1), 0)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
  
    // Select none (invalid)
    poke(c.io.in_sels(0), 0)
    poke(c.io.in_sels(1), 0)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
  
    // Select both (invalid)
    poke(c.io.in_sels(0), 1)
    poke(c.io.in_sels(1), 1)
    println(s"in_sels=${peek(c.io.in_sels)}, out=${peek(c.io.out)}")
} }

Counter

Counter is a counter that can be incremented once every cycle, up to some specified limit, at which point it overflows. Note that it is not a Module, and its value is accessible.

In [ ]:
Driver(() => new Module {
    // Example circuit using Mux1H
    val io = IO(new Bundle {
      val count = Input(Bool())
      val out = Output(UInt(2.W))
    })
    val counter = Counter(3)  // 3-count Counter (outputs range [0...2])
    when(io.count) {
      counter.inc()
    }
    io.out := counter.value
  }) { c => new PeekPokeTester(c) {
    poke(c.io.count, 1)
    println(s"start: counter value=${peek(c.io.out)}")
  
    step(1)
    println(s"step 1: counter value=${peek(c.io.out)}")
  
    step(1)
    println(s"step 2: counter value=${peek(c.io.out)}")
  
    poke(c.io.count, 0)
    step(1)
    println(s"step without increment: counter value=${peek(c.io.out)}")
  
    poke(c.io.count, 1)
    step(1)
    println(s"step again: counter value=${peek(c.io.out)}")
} }

You're done!

Return to the top.