Chisel - (not quite) new approach to the development of digital logic

With the development of microelectronics, rtl designs have become more and more. Reusability of the verilog code delivers a lot of inconvenience, even using generate, macros and system verilog chips. Chisel, on the other hand, makes it possible to apply all the power of object and functional programming to the development of rtl, which is quite a welcome step that can fill the light of ASIC and FPGA developers with fresh air.

This article will give a brief overview of the main functionality and consider some use cases, we will also talk about the shortcomings of this language. In the future, if the topic is interesting, we will continue the article in more detailed tutorials.

System requirements

  • scala baseline
  • verilog and the basic principles of digital design.
  • keep chisel documentation handy

I will try to make out the basics of chisel with simple examples, but if something is not clear, you can peek here .

As for scala for quick immersion, this cheat list can help .

Similar is for chisel .

The full article code (in the form of a scala sbt project) can be found here .

Simple counter

As can be understood from the name 'Constructing Hardware In a scala' Embedded Language 'chisel is a hardware description language built on over scala.

Briefly about how everything works, then: a hardware graph is built from the rtl description on chisel, which, in turn, turns into an intermediate description in the firrtl language, and after the built-in backend interpreter generates from firrtl verilog.

Let's look at two implementations of a simple counter.


module SimpleCounter #(
  parameter WIDTH = 8
  input clk,
  input wire enable,
  output wire [WIDTH-1:0] out
  reg [WIDTH-1:0] counter;
  assign out = counter;
  always @(posedge clk)
    if (reset) begin
      counter <= {(WIDTH){1'b0}};
    end else if (enable) begin
      counter <= counter + 1;


classSimpleCounter(width: Int = 32) extendsModule{
  val io = IO(newBundle {
    val enable = Input(Bool())
    val out = Output(UInt(width.W))
  val counter = RegInit(0.U(width.W))
  io.out <> counter
  when(io.enable) {
    counter := counter + 1.U

A little bit about chisel:

  • Module - container for rtl module description
  • Bundle - The data structure in chisel is mainly used to define interfaces.
  • io - variable for defining ports
  • Bool - data type, simple one-bit signal
  • UInt(width: Width) - unsigned integer, the constructor accepts a signal width at the input.
  • RegInit[T <: Data](init: T) - the constructor of the register, at the input it takes a reset value and has the same data type.
  • <> - universal signal connection operator
  • when(cond: => Bool) { /*...*/ }- analogue ifin verilog

About what verilog generates chisel talk a little later. Now just compare these two designs. As you can see, in chisel there is no mention of signals clkand reset. The point is that chisel adds these signals to the module by default. The reset value for the register counteris passed to the register designer with the reset RegInit. Support for modules with multiple clock signals in chisel is, but about it, too, a little later.

The counter is a bit more complicated

Let's go ahead and make the task a bit more complicated, for example, let's make a multi-channel counter with an input parameter in the form of a sequence of digits for each channel.

Let's start now with the version on chisel

classMultiChannelCounter(width: Seq[Int] = Seq(32, 16, 8, 4)) extendsModule{
  val io = IO(newBundle {
    val enable = Input(Vec(width.length, Bool()))
    val out = Output(UInt(width.sum.W))
    defgetOut(i: Int): UInt = {
      val right = width.dropRight(width.length - i).sum
      this.out(right + width(i) - 1, right)
  val counters: Seq[SimpleCounter] = =>
  io.out <> util.Cat(
  width.indices.foreach { i =>
    counters(i).io.enable <> io.enable(i)

A little bit about scala:

  • width: Seq[Int]- input parameter for the class constructor MultiChannelCounter; it has a type Seq[Int]- a sequence with integer elements.
  • Seq - one of the types of collections in scala with a clearly defined sequence of elements.
  • .map- for all the familiar function on collections, capable of converting one collection to another due to the same operation on each element, in our case the sequence of integer values ​​turns into a sequence of SimpleCounter's with a corresponding bit depth.

A little bit about chisel:

  • Vec[T <: Data](gen: T, n: Int): Vec[T] - data type chisel, is an analogue of the array.
  • Module[T <: BaseModule](bc: => T): T - Mandatory wrapper method for instantiated modules.
  • util.Cat[T <: Bits](r: Seq[T]): UInt- concatenation function, analogue {1'b1, 2'b01, 4'h0}in verilog

Let's pay attention to the ports:
enable- I turned around already in Vec[Bool]*, roughly speaking, into an array of one-bit signals, one for each channel, it was possible to make and UInt(width.length.W).
out- expanded to the sum of the widths of all our channels.

A variable countersis an array of our counters. We connect the enablesignal of each counter to the corresponding input port, and outcombine all signals into one using the built-in util.Catfunction and forward it to the output.

Note also the function getOut(i: Int)- this function calculates and returns the range of bits in the signal outfor the i'th channel. It will be very useful in further work with such a counter. Implement something similar in verilog will not work

* Vecnot to be confused with Vector, the first is the data array in chisel, the second is the collection in scala.

Let's now try to write this module on verilog, for convenience even on systemVerilog.

After sitting thinking, I came to this option (most likely it is not the only correct and most optimal, but you can always offer your implementation in the comments).

module MultiChannelCounter #(
  parameter TOTAL = 4,
  parameter integer WIDTH_SEQ [TOTAL] = {32, 16, 8, 4}
)(clk, reset, enable, out);
  localparam OUT_WIDTH = get_sum(TOTAL, WIDTH_SEQ);
  input  clk;
  input wire [TOTAL - 1  : 0] enable;
  output wire [OUT_WIDTH - 1 :0] out;
  genvar j;
    for(j = 0; j < TOTAL; j = j + 1) begin : counter_generation
      localparam OUT_INDEX = get_sum(j, WIDTH_SEQ);
      SimpleCounter #( WIDTH_SEQ[j] ) SimpleCounter_unit (
        .out(out[OUT_INDEX + WIDTH_SEQ[j] - 1: OUT_INDEX])
  function automatic integer get_sum;
    inputinteger array_width;
    inputintegerarray [TOTAL];
    integer counter = 0;
    integer i;
  beginfor(i = 0; i < array_width; i = i + 1)
      counter = counter + array[i];
    get_sum = counter;  

It looks much more impressive already. But what if, we go ahead and attach to this popular wishbone interface with register access.

Bundle interfaces

Wishbone is a small bus like AMBA APB, used mainly for ip kernels with open source.

A little more on the wiki:

Because chisel provides us with data containers of the type Bundleit makes sense to wrap the tire in a container that can later be used in any projects on chisel.

    addrWidth: Int = 32,
    dataWidth: Int = 32,
    gotTag: Boolean = false)extendsBundle {
  val adr = Output(UInt(addrWidth.W))
  val dat_master = Output(UInt(dataWidth.W))
  val dat_slave = Input(UInt(dataWidth.W))
  val stb = Output(Bool())
  val we = Output(Bool())
  val cyc = Output(Bool())
  val sel = Output(UInt((dataWidth / 8).W))
  val ack_master = Output(Bool())
  val ack_slave = Input(Bool())
  val tag_master: Option[UInt] = if(gotTag) Some(Output(Bool())) elseNoneval tag_slave: Option[UInt] = if(gotTag) Some(Input(Bool())) elseNonedefwbTransaction: Bool = cyc && stb
  defwbWrite: Bool = wbTransaction && we
  defwbRead: Bool = wbTransaction && !we
  overridedefcloneType: wishboneMasterSignals.this.type =
    new wishboneMasterSignals(addrWidth, dataWidth, gotTag).asInstanceOf[this.type]

A little bit about scala:

  • Option- The optional data wrapper in scala, which can be either a member either None, Option[UInt]is either, Some(UInt(/*...*/))or Noneis useful for parameterizing signals.

It seems nothing unusual. A simple description of the interface from the wizard, with the exception of a few signals and methods:

tag_masterand tag_slave- optional general signals in the wishbone protocol, in our case they will appear if the parameter gotTagis equal to true.

wbTransaction, wbWrite, wbRead- functions to simplify working with the bus.

cloneType- mandatory type cloning method for all parameterized [T <: Bundle]classes

But we also need a slave interface, let's see how it can be implemented.

    addrWidth: Int = 32,
    dataWidth: Int = 32,
    tagWidht: Int = 0)extendsBundle {
  val wb = Flipped(new wishboneMasterSignals(addrWidth , dataWidth, tagWidht))
  overridedefcloneType: wishboneSlave.this.type =
    new wishboneSlave(addrWidth, dataWidth, tagWidht).asInstanceOf[this.type]

The method Flipped, as it was possible to guess from the name, turns the interface over, and now our master interface has become a slave, we will add the same class but for the master.

    addrWidth: Int = 32,
    dataWidth: Int = 32,
    tagWidht: Int = 0)extendsBundle {
  val wb = new wishboneMasterSignals(addrWidth , dataWidth, tagWidht)
  overridedefcloneType: wishboneMaster.this.type =
    new wishboneMaster(addrWidth, dataWidth, tagWidht).asInstanceOf[this.type]

Well, that's all, the interface is ready. But before writing a handler, let's see how you can use these interfaces if we need to make a switch or something with a large set of wishbone interfaces.

classWishboneCrossbarIo(n: Int, addrWidth: Int, dataWidth: Int) extendsBundle{
  val slaves = Vec(n, new wishboneSlave(addrWidth, dataWidth, 0))
  val master = new wishboneMaster(addrWidth, dataWidth, 0)
  val io = IO(newWishboneCrossbarIo(1, 32, 32))
  io.master <> io.slaves(0)
  // ...

This is a small piece under the switch. It is convenient to declare an interface of the type Vec[wishboneSlave], and you can connect the interfaces with the same operator <>. Chisel chips are quite useful when it comes to managing a large set of signals.

Universal Bus Controller

As mentioned earlier about functional and object programming, let's try to apply it. Then we will discuss the implementation of the wishbone universal bus controller in the form trait, it will be a mixin for any module with a bus wishboneSlave, for the module you just need to define a memory card and mix it trait- the controller to it when it is generated.


For those who are still enthusiastic

Перейдем к реализации обработчика. Он будет простым и сразу отвечать на одиночные транзакции, в случае выпадения из пула адресов выдавать ноль.

Разеберем по частям:

  • на каждую транзакцию нужно отвечать acknowlege-ом

    val io : wishboneSlave = /* ... */val wb_ack = RegInit(false.B)
    when(io.wb.wbTransaction) {
    wb_ack := true.B
    }.otherwise {
    wb_ack := false.B
    wb_ack <> io.wb.ack_slave

  • На чтение отвечаем данными
    val wb_dat = RegInit(0.U(io.wb.dat_slave.getWidth.W)) // getWidth возращает разрядность
    when(io.wb.wbRead) {
    wb_dat := MuxCase(default = 0.U, Seq(
      (io.wb.addr === ADDR_1) -> data_1,
      (io.wb.addr === ADDR_3) -> data_2,
      (io.wb.addr === ADDR_3) -> data_2
    wb_dat <> io.wb.dat_slave

    • MuxCase[T <: Data] (default: T, mapping: Seq[(Bool, T)]): T — встроенная кобинационная схема типа case в verilog*.

Как примерно выглядело бы в verilog:

always @(posedge clock)
      wb_dat_o <= 0;
      case (wb_adr_i) 
        `ADDR_1  : wb_dat_o <= data_1;
        `ADDR_2  : wb_dat_o <= data_2;
        `ADDR_3  : wb_dat_o <= data_3;
        default  : wb_dat_o <= 0;

*Вообще в данном случае это небольшой хак ради параметризируемости, в chisel есть стандартная конструкция которую лучше использовать если, пишите что-то более простое.

switch(x) {
  is(value1) {
    // ...
  is(value2) {
    // ...

Ну и запись

  when(io.wb.wbWrite) {
    data_4 := Mux(io.wb.addr === ADDR_4, io.wb.dat_master, data_4)

  • Mux[T <: Data](cond: Bool, con: T, alt: T): T — обычный мультиплексор

Встраиваем нечто подобное к нашему мультиканальному счетчику, вешаем регистры на управление каналами и дело в шляпе. Но тут уже рукой подать до универсального контроллер шины WB которому мы будем передавать карту памяти такого вида:

val readMemMap = Map(
    ADDR_1 -> DATA_1,
    ADDR_2 -> DATA_2/*...*/
  val writeMemMap = Map(
    ADDR_1 -> DATA_1,
    ADDR_2 -> DATA_2/*...*/

Для такой задачи нам помогут trait — что-то вроде mixin-ов в Sala. Основной задачей будет привести readMemMap: [Int, Data] к виду Seq(условие -> данные), а еще было бы неплохо если бы можно было передавать внутри карты памяти базовый адрес и массив данных

val readMemMap = Map(
    ADDR_2 -> DATA_2/*...*/

Что будет раскрываться с в нечто подобное, где WB_DAT_WIDTH ширина данных в байтах

val readMemMap = Map(
    ADDR_1_BASE + 0 * (WB_DAT_WIDHT)-> DATA_SEQ_0,
    ADDR_1_BASE + 1 * (WB_DAT_WIDHT)-> DATA_SEQ_1,
    ADDR_1_BASE + 2 * (WB_DAT_WIDHT)-> DATA_SEQ_2,
    ADDR_1_BASE + 3 * (WB_DAT_WIDHT)-> DATA_SEQ_3/*...*/ADDR_2 -> DATA_2/*...*/

Для реализации этого, напишем функцию конвертор из Map[Int, Any] в Seq[(Bool, UInt)]. Придется задействовать scala pattern mathcing.

defparseMemMap(memMap: Map[Int, Any]): Seq[(Bool, UInt)] = memMap.flatMap { case(addr, data) =>
    data match {
      case a: UInt => Seq((io.wb.adr === addr.U) -> a)
      case a: Seq[UInt] => => (io.wb.adr === (addr + io.wb.dat_slave.getWidth / 8).U) -> x)
      case _ => thrownewException("WRONG MEM MAP!!!")

Окончательно наш трейт будет выглядеть так :

  val io : wishboneSlave
  val readMemMap: Map[Int, Any]
  val writeMemMap: Map[Int, Any]
  val parsedReadMap: Seq[(Bool, UInt)] = parseMemMap(readMemMap)
  val parsedWriteMap: Seq[(Bool, UInt)] = parseMemMap(writeMemMap)
  val wb_ack = RegInit(false.B)
  val wb_dat = RegInit(0.U(io.wb.dat_slave.getWidth.W))
  when(io.wb.wbTransaction) {
    wb_ack := true.B
  }.otherwise {
    wb_ack := false.B
  when(io.wb.wbRead) {
    wb_dat := MuxCase(default = 0.U, parsedReadMap)
  when(io.wb.wbWrite) {
    parsedWriteMap.foreach { case(addrMatched, data) =>
      data := Mux(addrMatched, io.wb.dat_master, data)
  wb_dat <> io.wb.dat_slave
  wb_ack <> io.wb.ack_slave
  defparseMemMap(memMap: Map[Int, Any]): Seq[(Bool, UInt)] = { /*...*/}

Немного о scala :

  • io , readMemMap, writeMemMap — абстрактные поля нашего trait'a, которые должны быть определены в классе в который мы будем его замешивать.

How to use it

To mix ours traitwith the module, you need to meet several conditions:

  • io must inherit from class wishboneSlave
  • need to declare two memory cards readMemMapandwriteMemMap

  valBASE = 0x11A00000valOUT  = 0x00000100valS_EN = 0x00000200valH_EN = 0x00000300val wbAddrWidth = 32val wbDataWidth = 32val wbTagWidth = 0val width = Seq(32, 16, 8, 4)
  val io = IO(new wishboneSlave(wbAddrWidth, wbDataWidth, wbTagWidth) {
    val hardwareEnable: Vec[Bool] = Input(Vec(width.length, Bool()))
  val counter = Module(newMultiChannelCounter(width))
  val softwareEnable = RegInit(0.U(width.length.W))
  width.indices.foreach(i => := io.hardwareEnable(i) && softwareEnable(i))
  val readMemMap = Map(
    BASE + OUT  ->,
    BASE + S_EN -> softwareEnable,
    BASE + H_EN -> io.hardwareEnable.asUInt
  val writeMemMap = Map(
    BASE + S_EN -> softwareEnable

Create a register. softwareEnableIt is added to the 'and' with the input signal hardwareEnableand comes to enable counter[MultiChannelCounter].

We declare two memory cards for reading and writing: readMemMapwriteMemMapmore details about the structure can be found in the chapter above.
We transfer to the reading memory card the value of each channel's counter *, softwareEnableand hardwareEnable. And on record we give only the softwareEnableregister.

* strange construction, we will analyze in parts.

  • width.indices- returns an array with indices of elements, i.e. if width.length == 4thenwidth.indices = {0, 1, 2, 3}
  • {0, 1, 2, 3}.map( - gives about the following:
    {,, /*...*/ }

Now, for any module on chisel with, we can declare memory cards for reading and writing and simply connect our universal wishbone bus controller when generating something like this:

  Driver.execute(Array("-td", "./src/generated"), () =>
    new wishbone_multicahnnel_counter

wishboneSlaveDriver - just that trait mix that we described under the spoiler.

Of course, this version of the universal controller is far from final, but rather, on the contrary, crude. His main goal is to demonstrate one of the possible approaches to the development of rtl on chisel. With all the possibilities of scala, such approaches can be much more, so that each developer has his own field of creativity. It’s true that there’s nowhere else to get inspired, except:

  • the native chisel utils library, about which a little further, there you can look at the inheritance of modules and interfaces
  • - risc-v kernel entirely implemented in chisel, provided that you know scala very well, for newbies, without a liter, as they say, you will understand for a very long time. There is no official documentation on the internal structure of the project.


What if we want to manually manage the clock and reset signals in the chisel. Until recently, it was impossible to do this, but support appeared with one of the latest releases withClock {}, withReset {}and withClockAndReset {}. Let's look at an example:

  val io = IO(newBundle {
    val clockB = Input(Clock())
    val in = Input(Bool())
    val out = Output(Bool())
    val outB = Output(Bool())
  val regClock = RegNext(, false.B)
  regClock <> io.out
  val regClockB = withClock(io.clockB) {
    RegNext(, false.B)
  regClockB <> io.outB

  • regClock- the register that will be clocked by the standard signal clockand reset by the standardreset
  • regClockB- the same register is clocked, you guessed it, with a signal io.clockB, but the reset will be used standard.

If we want to remove the standard signals clockand resetcompletely, then we can use an experimental feature for now - RawModule(the module without standard clocking and reset signals, everyone will have to be managed manually). Example:

  val io = IO(newBundle {
    val clockA = Input(Clock())
    val clockB = Input(Clock())
    val resetA = Input(Bool())
    val resetB = Input(Bool())
    val in = Input(Bool())
    val outA = Output(Bool())
    val outB = Output(Bool())
  val regClockA = withClockAndReset(io.clockA, io.resetA) {
    RegNext(, false.B)
  regClockA <> io.outA
  val regClockB = withClockAndReset (io.clockB, io.resetB) {
     RegNext(, false.B)
  regClockB <> io.outB

Utils library

This does not end pleasant chisel bonuses. Its creators worked and wrote a small but very useful library of small, interfaces, modules, functions. Oddly enough, there is no description of the library on the wiki, but you can see the cheat list, the link to which is at the very beginning (the last two sections are there)


  • DecoupledIO- General frequently used ready / valid interface.
    DecoupledIO(UInt(32.W))- will contain signals:
    val ready = Input(Bool())
    val valid = Output(Bool())
    val data = Output(UInt(32.W))
  • ValidIO- same as DecoupledIOwithoutready


  • Queue- the synchronous FIFO module is a very useful thing. The interface looks like
    val enq: DecoupledIO[T]- inverted DecoupledIO
    val deq: DecoupledIO[T]- normal DecoupledIO
    val count: UInt- the amount of data in the queue
  • Pipe - delay module, inserts the n-th number of register slices
  • Arbiter- the arbiter on the DecoupledIOinterfaces has many subspecies differing in the type of arbitration
    val in: Vec[DecoupledIO[T]]- an array of input interfaces
    val out: DecoupledIO[T]
    val chosen: UInt- shows the selected channel

As far as can be understood from the discussion on github, the global plans have a significant expansion of this library: modules such as asynchronous FIFO, LSFSR, frequency dividers, PLL templates for FPGA; various interfaces; controllers for them and much more.

Chisel io-teseters

It should be mentioned, and the possibility of testing in chisel, at the moment there are two ways to test this:

  • peekPokeTesters - purely simulation tests that verify the logic of your design
  • hardwareIOTeseters- this is more interesting, because With this approach, you will get a generated teset bench with tests that you wrote on chisel, and with verilator you will even get a timeline.

    But so far, the testing approach has not been finalized, and the discussion is still underway. In the future, a universal tool will most likely appear, for testing and tests it will also be possible to write on chisel. But for now, you can look at what is already there and how to use it here .

Disadvantages chisel

This is not to say that chisel is a universal tool, and that everyone should switch to it. Like all projects at the development stage, it has its drawbacks, which are worth mentioning for completeness.

The first and perhaps the most important drawback is the lack of asynchronous dumps. It is quite significant, but it can be solved in several ways, and one of them is scripts over verilog, which turn a synchronous reset into asynchronous. This is easy to do, because all constructions in the generated verilog are alwaysfairly uniform.

The second drawback is, according to many, in the unreadability of the generated verilog and, as a result, the complexity of debugging. But let's take a look at the generated code from the example with a simple counter.

generated verilog
module SimpleCounter(
  input        clock,
  input        reset,
  input        io_enable,
  output [7:0] io_out
  reg [7:0] counter;
  reg [31:0] _RAND_0;
  wire [8:0] _T_7;
  wire [7:0] _T_8;
  wire [7:0] _GEN_0;
  assign _T_7 = counter + 8'h1;
  assign _T_8 = _T_7[7:0];
  assign _GEN_0 = io_enable ? _T_8 : counter;
  assign io_out = counter;
  integer initvar;
  initial begin
    `ifndef verilator
      #0.002 begin end`endif
  _RAND_0 = {1{$random}};
  counter = _RAND_0[7:0];
`endif // RANDOMIZE
  always @(posedge clock) beginif (reset) begin
      counter <= 8'h0;
    endelsebeginif (io_enable) begin
        counter <= _T_8;

At first glance, the generated verilog can push away, even in a medium-sized design, but let's see a little.

  • RANDOMIZE defines - (may be useful when testing with chisel-testers) - generally useless, but do not interfere
  • As we see the name of our ports, and the register are preserved
  • _GEN_0 is a useless variable for us, but necessary for the firrtl interpreter to generate verilog. We also do not pay attention to it.
  • The _T_7 and _T_8 remain, the entire combinational logic in the generated verilog will be represented step by step in the form of _T variables.

Most importantly, all the ports, registers, and wires that are necessary for debugging retain their names from chisel. And if you look not only at verilog but also at chisel, then soon the debugging process will go as easily as with pure verilog.


In modern reality, the development of RTL, whether asic or fpga outside of the academic environment, has long since gone from using only pure handwritten verilog code to various kinds of generation scripts, be it a small tcl script or a whole IDE with lots of possibilities.

Chisel, in turn, is a logical development of languages ​​for developing and testing digital logic. Suppose that at this stage he is far from perfect, but he is already able to provide opportunities for the sake of which one can put up with his shortcomings. It is important that the project is alive and developing, and there is a high probability that in the foreseeable future there will be very little such flaws and a lot of functionalities.

Also popular now: