Chisel logo

WARNING. THIS FILE IS NOT PART OF THE BOOTCAMP

TEACHING STEPS. IT IS A MID TO END LEVEL EXAMPLE

OF THE THINGS YOU WILL LEARN. IF YOU ARE DOING THE

BOOTCAMP TO LEARN CHISEL DON'T START HERE

START AT Introduction to Scala

Chisel Demo

Next: Introduction to Scala

Welcome! Perhaps you're an interested student who heard the name "Chisel" tossed about, or maybe you're a seasoned hardware design veteran who has been tasked by your manager to explore Chisel as a new HDL alternative. Either way if you are new to Chisel, you want to figure out as fast as possible what all the fuss is about. Look no futher - let's see what Chisel has to offer!

Setup

Before we start, we need to download and imports the dependencies needed for the demo.

Please run the following two cell blocks by either pressing SHIFT+ENTER on your keyboard or the Run button in the menu.

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}

Hardware Generators: Type-Safe Meta-Programming for RTL

All hardware description languages support writing single instances of an RTL design - Chisel is no different. In fact, most Verilog/VHDL digital logic designs can be directly transcribed into Chisel! While Chisel provides other awesome features that we will get to, we want to emphasize that users switching to Chisel will retain the exact same degree of control over their design as any other hardware language.

Take the following example of a 3-point moving average implemented in the style of a FIR filter.

Chisel provides similar base primitives as synthesizable Verilog and can be used as such! Run next cell to declare our Chisel module.

In [ ]:
// 3-point moving average implemented in the style of a FIR filter
class MovingAverage3(bitWidth: Int) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(bitWidth.W))
    val out = Output(UInt(bitWidth.W))
  })

  val z1 = RegNext(io.in) // Create a register whose input is connected to the argument io.in
  val z2 = RegNext(z1)    // Create a register whose input is connected to the argument z1

  io.out := (io.in * 1.U) + (z1 * 1.U) + (z2 * 1.U) // `1.U` is an unsigned literal with value 1
}

After defining class MovingAverage3, let's instantiate it and take a look at its structure:

In [ ]:
// same 3-point moving average filter as before
visualize(() => new MovingAverage3(8))

In this visualization of the Chisel instance, the inputs on the left, and the z1 and z2 registers in gold. Both registers and io_in are multiplied by their coefficients and which are then added successively. The tail and bits elements are used to keep the additions from growing.

You may now ask: "Oh well and good - you can do stuff in Verilog in Chisel, but then why would I want to use Chisel?"

We are so glad you asked! The real power of Chisel comes from the ability to create generators, not instances. Suppose instead of only a MovingAverage3 module, we wanted to create a generic FIRFilter module that is parameterized by a list of coefficients.

Below we have rewritten MovingAverage3 to accept into a sequence of coefficients. The number of coefficients will determine the size of the filter.

In [ ]:
// Generalized FIR filter parameterized by the convolution coefficients
class FirFilter(bitWidth: Int, coeffs: Seq[UInt]) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(bitWidth.W))
    val out = Output(UInt())
  })
  // Create the serial-in, parallel-out shift register
  val zs = Reg(Vec(coeffs.length, UInt(bitWidth.W)))
  zs(0) := io.in
  for (i <- 1 until coeffs.length) {
    zs(i) := zs(i-1)
  }

  // Do the multiplies
  val products = VecInit.tabulate(coeffs.length)(i => zs(i) * coeffs(i))

  // Sum up the products
  io.out := products.reduce(_ +& _)
}

Now by changing our coeffs parameters during instantiation, our FIRFilter module can be used to instantiate an endless number of different hardware modules! Below we create three different instances of FIRFiler

In [ ]:
// same 3-point moving average filter as before
visualize(() => new FirFilter(8, Seq(1.U, 1.U, 1.U)))
In [ ]:
// 1-cycle delay as a FIR filter
visualize(() => new FirFilter(8, Seq(0.U, 1.U)))
In [ ]:
// 5-point FIR filter with a triangle impulse response
visualize(() => new FirFilter(8, Seq(1.U, 2.U, 3.U, 2.U, 1.U)))

Without this powerful parameterization, we would need many more module definitions, likely one for each of these FIR filters. Ideally, we want our generators to be (1) composable, (2) powerful, and (3) enable fine-grained control over the generated design.

The benefits of Chisel are in how you use it, not in the language itself. If you decide to write instances instead of generators, you will see fewer advantages of Chisel over Verilog. But if you take the time to learn how to write generators, then the power of Chisel will become apparent and you will realize you can never go back to writing Verilog. Learning to write generators is difficult, but we hope this tutorial will pave the way for you to become a better hardware designer, programmer, and thinker!