My First Project using Chisel

chisel_logo

Hello FPGAmigos ! I have always felt uneasy writing VHDL or Verilog code due to their verbose and boilerplate nature. My discomfort has prevented me from training and mastering them, so I cannot confirm whether these languages are truly bad. Consequently, I was thrilled to learn about other open-source HDL options. Today, I am delighted to write my first project in Chisel. I enjoyed it. This article won’t be an in deeps comparison of Chisel vs VHDL/Verilog or Chisel “getting started”, since I don’t fully master them, but rather sharing my noob experience and sharing the little joy I found using this language.

Chisel is an EDSL, or Embedded Domain-Specific Language, in Scala. In other words, it is a language that is specific to the domain of hardware design and is embedded within the GPL (General Purpose Language) of Scala. EDSLs provide a high-level, domain-specific abstraction for a particular problem domain. They are intended to simplify expressing solutions to problems within a specific domain without requiring the user to have an extensive understanding of the underlying implementation details.

Within the context of hardware design, an EDSL can offer a high-level abstraction for describing hardware circuits. Designers can write code that looks and operates like traditional software, yet generates hardware circuits as output through the use of an EDSL. And that what we are going to do !

I made a simple project : a LED chaser. I have 24 LEDs and my alchitry IO shield. We are going to turn them all on with a 24 level of brightness and make the brightness shift from one LED to the next LED at a regular speed.

led_chaser
LED chaser running

In addition to discorver Chisel, this project and article provides an opportunity for beginners to become acquainted with fundamental design elements such as counters, comparators, pulse generators, muxes and PWM.

Prerequisites

If you want to try it at home, the code is available on my github here.

This project require to have docker images for Yosys, NextPnR, Icestorm and chisel you can find them on the my github to :

How to install chisel with docker ?

How to install Yosys with docker ?

How to install Icestorm with docker ?

How to install NextPnR with docker ?

I use my project template from this article that you can find on my article From Chisel to Bitstream.

I. How did I handle LED brightness ?

For LEDs brigthness I have used PWMs (Pulse Width Modulation). Pulse width modulation is a technique used in electronic circuits to control the amount of power that is delivered to a load, such as an LED or a motor. It works by rapidly turning the power on and off at a specific frequency, with the width of the on-time (duty cycle) determining the amount of power delivered to the load.

By adjusting the duty cycle of the PWM signal, we can control the average power delivered to the load, and therefore control its behavior. For example, in the case of an LED, we can adjust the brightness by changing the duty cycle of the PWM signal that controls its power supply. In this project there is 24 LEDs, so i choose to have 24 values of brightness, from 0% to 100%.

For this I needed two basics components : a counter and a comparator.

The counter:

// 1. see explaination below
class UpCounter(maxCount: Int) extends Module{
    // 2.
    val io = IO(new Bundle{
        val enable = Input(UInt(1.W))
        val count = Output(UInt(log2Ceil(maxCount).W))
    })
    // 3.
    val UpCounter = RegInit(0.U(log2Ceil(maxCount).W))
    // 4.
    when(io.enable.asBool){
        UpCounter := Mux(UpCounter===(maxCount-1).U, 0.U, UpCounter + 1.U)
    }
    // 5.
    io.count := UpCounter
  1. The UpCounter module is a class that inherits from the Chisel “Module” class. This means we get all the object-oriented programming features we need, making our job easier.
  2. To define the input and output signals of our module, we use the IO class. We can create bundles of signals, such as AXI and SPI interfaces, and instantiate them easily. This is similar to VHDL’s record. One advantage of Chisel over VHDL is that we have useful features like “Flipped,” which reverses the direction of all inputs and outputs, and the bi-directional connection operator “:<>=” to connect an AXI slave to a master in one line. Another advantage is that we don’t need to explicitly declare clock and reset signals in every module and process.
  3. To declare a register in Chisel, we simply declare it using the “RegInit” function. In our case, we want a register initialized with a vector of log2Ceil(maxCount) zeros, so we write “RegInit(0.U(log2Ceil(maxCount).W)).”
  4. Our UpCounter module has one input signal named “enable,” which turns the counter on and off, and one output signal named “count,” which returns the current count. To determine the number of bits needed to represent the count, we use the “log2Ceil” function, which is scala function provided by chisel3. This example demonstrates how we can leverage the power of Scala and its community to design hardware.
  5. The counter logic itself is very concise, consisting of just one line of code. We use the “Mux” function provided by Chisel3 to create a multiplexer. The first argument is the selector, the second is the output if the selector is true, and the third is the output if the selector is false.

The comparator :

class UnsignedComparator(width: Int) extends Module {
    val io = IO(new Bundle{
        val input1 = Input(UInt(width.W))
        val input2 = Input(UInt(width.W))
        val output = Output(UInt(1.W))
    })
    io.output:= io.input1 < io.input2
}

Really simple, no boilderplate code.

With this two basics modules, I can now design PWMs for handle the LEDs brightness.

class ledBrightnessPwm(numLeds: Int) extends Module {
    val io = IO(new Bundle{
        val ledsBrightness = Output(Vec(numLeds, UInt(1.W)))
    })
    // 1.
    val ledBrightnessCounter = Module(new UpCounter(numLeds))
    ledBrightnessCounter.io.enable := 1.U
    
    // 2.
    val ledBrightnessThresholds = List.fill(numLeds)(Module(new UnsignedComparator(log2Ceil(numLeds))))

    // 3.
    for(led <- 0 until numLeds){
        ledBrightnessThresholds(led).io.input1 := ledBrightnessCounter.io.count
        ledBrightnessThresholds(led).io.input2 := led.U
        io.ledsBrightness(led) := ledBrightnessThresholds(led).io.output
    }
}

To control the brightness of 24 LEDs in a PWM (Pulse Width Modulation) scheme, we can define a class called ledBrightnessPwm. This class has a constructor that takes in the number of LEDs as a parameter, and creates an object that can control the brightness of all the LEDs.

To achieve this, we use three main steps:

  1. First, we instantiate an UpCounter module as a counter for the LEDs’ brightness levels. We set the enable input of the counter to 1 to ensure that it counts continuously.
  2. Next, we create a list of UnsignedComparator modules that will compare the counter value to a threshold value for each LED. We use “List.fill” to instantiate all 24 comparators at once. Since chisel modules are scala objects, we can handle them in a scala list and use every feature provide by scala for list.
  3. Finally, we connect the counter output to the input of each comparator, and set the threshold input to the LED’s index number. This ensures that each LED’s brightness level is proportional to its index number, with LED_1 having a duty cycle of 1/24, LED_2 having a duty cycle of 2/24, and so on, up to LED_24 with a duty cycle of 1. We then assign the output of each comparator to the corresponding LED’s brightness level output.

By following these steps, we can control the brightness of multiple LEDs by easily using our previously defined modules.

II. Handling the LEDs shifting

To control the timing of the LED shifting, we require a signal that pulses at a specific rate. This is achieved through the use of a pulse generator.

class PulseGenerator(pulseThreshold:Int) extends Module {
    val io = IO(new Bundle{
        val pulse = Output(UInt(1.W))
    })
    // Free UpCounter
    val pulseCounter = Module(new UpCounter(pulseThreshold))
    pulseCounter.io.enable := 1.U // always enable
    
    // compare the pulseCounter.count with the pulseThreshold
    val pulse_comparator = Module(new UnsignedComparator(log2Ceil(pulseThreshold)))
    pulse_comparator.io.input2 := (pulseThreshold-1).U
    pulse_comparator.io.input1 := pulseCounter.io.count

    // Detect the cycle when the UpCounter is equal to pulseThreshold
    io.pulse := Util.detectFallingEdge(pulse_comparator.io.output.asBool)
}

To detect when the count surpasses the threshold, I reused both my comparator and counter. The counter continuously counts and its output is compared to the threshold value using the comparator. I use a rising edge detection method to identify when the threshold is surpassed, which I handle in the following manner:

object Util{
    def detectFallingEdge(x: Bool) = !x && RegNext(x)
}

I am sure that it is not a good practice to define such function in the top of the file, but it is a quick and dirty discovery. I guess it would be better to create a “Util” package.

My FPGA have a 100Mhz clock so if I set pulseThreshold to 100,000,000 it will shift every second. I also did a bad practice by creating a Conf object in the to of the file:

object Conf{
    val numLeds = 24 // Number of LEDs
    val fpgaFrequency = 100000000 // Mhz
    val ledShiftDuration = (0.1 * fpgaFrequency).toInt  // The duration between a shift of leds
}

By doing this, I’m able to set the duration between two shifts. With this accomplished, I can define the module responsible for handling the LED shift. This module takes in the 24 signals for the LEDs’ brightness and outputs them to the appropriate LEDs.

class LedShifter(numLeds:Int, ledShiftDuration:Int) extends Module{
    val io = IO(new Bundle{
        val ledsBrightness = Input(Vec(numLeds, UInt(1.W)))
        val associotedLeds = Output(Vec(numLeds, UInt(1.W)))
    })
    // 1
    val shiftPulse = Module(new PulseGenerator(ledShiftDuration))
    val ledShiftCounter = Module(new UpCounter(numLeds))
    ledShiftCounter.io.enable := shiftPulse.io.pulse

    // 2.
    for(led <- 0 until numLeds){
        val cases = List.tabulate(numLeds)(idx => (idx.U -> io.ledsBrightness((idx+led)%numLeds))).toSeq
        io.associotedLeds(led) := MuxLookup(ledShiftCounter.io.count, 0.U(1.W),cases)
    }
}

In this code block, I’m defining a class called “LedShifter” that extends Module. Here’s a breakdown of the code:

  1. I’m instantiating the PulseGenerator and reusing my counter. The counter counts from 0 to 23, which corresponds to the 24 LED positions. However, this time it’s not a free counter, as it’s the pulse from the pulse generator that enables it. Every time there is a pulse, the counter increments by one.
  2. The following lines instantiate 24 MUXes, each with 24 inputs (for the 24 levels of brightness). All the MUXes share the same selector, “ledShiftCounter.io.count,” which increments with every pulse of the pulse generator. Consequently, every MUX selects the next input. Every set of 24 inputs is shifted by one position, with each MUX outputting the corresponding brightness level to the appropriate LED.

I’m happy to have achieved this behavior with so few lines of code, but I wonder if it’s really readable for someone else. Would I be able to remember what it means in the near future?

MuxLookup is a standard Chisel feature that can be seen here.

Mux Numbermux input 1mux input 2mux input 3… mux input 24
1Brightness1Brightness2Brightness3Brightness24
2Brightness24Brightness1Brightness2Brightness23
3Brightness23Brightness24Brightness1Brightness22
.
.
.
24Brightness2Brightness3Brightness4Brightness1
24 set of brightness as input for the 24 for muxes

III. LedChaser module and top level

Now that we have the two main modules, one that handles brightness and the other that handles signal shifting, we can connect them together.

class LedChaser(numLeds: Int, ledShiftDuration: Int) extends Module{
    val io = IO(new Bundle{
        val LEDs = Output(Vec(numLeds, UInt(1.W)))
    })

    val ledsBrightnessModule = Module(new ledBrightnessPwm(numLeds))
    val ledShifterModule     = Module(new LedShifter(numLeds, ledShiftDuration))
   
    for(led <- 0 until numLeds){
        ledShifterModule.io.ledsBrightness(led) := ledsBrightnessModule.io.ledsBrightness(led)
        io.LEDs(led) := ledShifterModule.io.associotedLeds(led)
    }
}

The LedChaser module is designed to be clock and reset agnostic, allowing for easy reuse in other FPGAs or clock domains. The top level of this project has only one purpose: to instantiate the LedChaser module with the active low reset of the Alchitry CU board.

class AlchitryCUTop extends Module {
    val io = IO(new Bundle{
        val ledPins = Output(Vec(Conf.numLeds, UInt(1.W)))
    })
    // the alchitry CU board has an active low reset
    val reset_n = !reset.asBool

    withReset(reset_n){
        val my_led_chaser  = Module(new LedChaser(Conf.numLeds, Conf.ledShiftDuration))
        io.ledPins := my_led_chaser.io.LEDs
    }
}

Since Chisel is embedded in scala your design is a scala program.

object Main extends App{
    (new chisel3.stage.ChiselStage).emitVerilog(new AlchitryCUTop, Array("--target-dir", "build/artifacts/netlist/"))
}

This is the main of your scala program that will generated the Top in the indicated directory.

Should you use Chisel ? Will I continue using it ?

The question of whether or not to use Chisel is a tough one for me to answer since this is my first project using it. I don’t yet feel confident enough to recommend that others switch to Chisel, but I think it’s definitely worth studying for fun. However, if you’re seeking a job, I’d recommend focusing on VHDL, Verilog, or SystemVerilog.

As for whether or not I’ll continue using Chisel, I can’t say for sure yet. One small project isn’t enough for me to form a final opinion, and while Chisel may be great for design, I’ve heard it can be a challenge for verification. I definitly enjoy the design part, more than VHDL or Verilog. This experience and the many cool features I saw in the documentation really motivate me to continue studying it.

If you want a real-world feedback on using Chisel in industry, I recommend watching this Google conference where they discuss their experience designing an ASIC using Chisel. It’s a four-year-old video, so things may have changed since then. One issue they faced during verification was that their verification was against generated Verilog, not the Chisel design. To me, it sounds like testing the assembly code of your C++ program rather than the C++ code itself. Chisel was apparently not mature enough at the time concerning verification, and professional features like coverage and formal verification still only target VHDL/Verilog or SystemVerilog. However, as open-source tools in FPGA become more mature, I’m confident this obstacle will vanish.

Will I try other open-source HDL on this blog? Of course! Which one should I explore next? Let me know in the comments.

I admit that I didn’t do any verification on this project, which is a bad practice. However, since I was just learning and writing the project with the Chisel documentation on the side, I hope you’ll forgive me. Testing Chisel with ChiselTest and/or verifying the generated Verilog with Cocotb to experience the verification process myself is a good candidate for future articles. Stay tuned!

Thank you for reading ! If you like, please bookmark, share and comment !

Leave a Reply

Your email address will not be published. Required fields are marked *

My First Project using Chisel

dark blue dot

Summary

Share it !

Get my Ebook ?

ebook_hero-home

Jumpstart you FPGA journey by

• Understanding the place of FPGA in industry
• Learn about internal composition of an FPGA
• A simple beginner friendly project
• An overview of the FPGA workflow
ebook_banner_11