Chisel logo

Module 4.1: Introduction to FIRRTL

Prev: Generators: Types
Next: FIRRTL AST Traversal

Motivation

You've learned some Scala and written some Chisel, and for 90% of users, that should be enough to become a Chisel aficionado.

However, some use cases are better expressed as a programmatic transformation of a Chisel design, rather than as a generator.

For example, suppose we want to count the number of registers in a design. This would be difficult to do as a generator, so instead, we can write a FIRRTL pass to do it for us.

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}
import firrtl._

What is FIRRTL?

As you've probably become aware, when you execute a Chisel design, it elaborates (executes the surrounding Scala code) to construct an instance of your generator, with all Scala parameters resolved.

Instead of directly emitting Verilog, Chisel emits an intermediate representation called FIRRTL, which represents the elaborated (parameter-resolved) RTL instance. It can be serialized (converted to a String for writing to a file), and this serialized syntax is human readable. Internally, however, it is not represented as a long string. Instead, it is a datastructure organized as a tree of nodes, called an abstract-syntax-tree (AST).

Let's take a look! We will take a simple Chisel design, elaborate it, and inspect what FIRRTL it generates!

First, we define a Chisel module, which delays its input signal by two cycles.

In [ ]:
class DelayBy2(width: Int) extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(width.W))
    val out = Output(UInt(width.W))
  })
  val r0 = RegNext(io.in)
  val r1 = RegNext(r0)
  io.out := r1
}

Next, let's elaborate it, serialize, and print out the FIRRTL it generates.

In [ ]:
println(chisel3.Driver.emit(() => new DelayBy2(4)))

As you can see, the serialized FIRRTL looks very similar to what our Chisel design would look like, with all generator parameters resolved.

The FIRRTL AST

As mentioned earlier, the FIRRTL representation can be serialized as a String, but internally, it is a datastructure called an AST (abstract syntax tree). This data structure is a tree of nodes, where one node can contain children nodes. There are no cycles in this datastructure.

Let's take a look at what the internal datastructure looks like:

In [ ]:
val firrtlSerialization = chisel3.Driver.emit(() => new DelayBy2(4))
val firrtlAST = firrtl.Parser.parse(firrtlSerialization.split("\n").toIterator, Parser.GenInfo("file.fir"))

println(firrtlAST)

Obviously, the serialization of a datastructure isn't as pretty, but you can see some of the classes and such that internally represent the RTL design. Let's try to pretty that up a bit to make it understandable.

In [ ]:
println(stringifyAST(firrtlAST))

This is the internal datastructure that holds the FIRRTL AST. It is a tree structure whose root node is Circuit, which has 3 children: @[[email protected]], ArrayBuffer, and cmd5WrapperHelperDelayBy2. The following is the definition of Circuit's actual Scala class that was serialized:Circuit case class

As you can see, it has three children nodes: info: Info, Modules: Seq[DefModule], and main: String. It extends FirrtlNode, of which all FIRRTL AST nodes must do. Ignore the def mapXXXX functions for now.

Many FIRRTL nodes contain an info: Info field, which the parser can either insert file information like line number and column number, or insert a NoInfo token. In this example, @[[email protected]] would refer to the FIRRTL file, line 2, column 0.

The following section will outline all of these FIRRTL nodes in detail.

FIRRTL Node Descriptions

This section describes common FirrtlNodes found in firrtl/src/main/scala/firrtl/ir/IR.scala.

For more detail on components not mentioned here, please refer to The FIRRTL Specification.

Circuit

Circuit is the root node of any Firrtl datastructure. There is only ever one Circuit, and that Circuit contains a list of module definitions and the name of the top-level module.

FirrtlNode Declaration

Circuit(info: Info, modules: Seq[DefModule], main: String)

Concrete Syntax

circuit Adder:
  ... //List of modules

In-memory Representation

Circuit(NoInfo, Seq(...), "Adder")

Module

Modules are the unit of modularity within Firrtl and are never directly nested (declaring an instance of a module has its own concrete syntax and AST representation). Each Module has a name, and a list of ports, and a body containing its implementation.

FirrtlNode declaration

Module(info: Info, name: String, ports: Seq[Port], body: Stmt) extends DefModule

Concrete Syntax

module Adder:
  ... // list of ports
  ... // statements

In-memory representation

Module(NoInfo, "Adder", Seq(...), )

Port

A port defines part of a Module's io, and has a name, direction (input or output), and type.

FirrtlNode Declaration

class Port(info: Info, name: String, direction: Direction, tpe: Type)

Concrete Syntax

input x: UInt

In-memory representation

Port(NoInfo, "x", INPUT, UIntType(UnknownWidth))

Statement

A statement is used to describe the components within a module and how they interact. Below are some commonly used statements:

Block of Statements

A group of statements. Commonly used as the body field in a Module declaration.

Wire Declaration

A wire declaration, containing a name and type. It can be both a source (connected from) and a sink (connected *to").

FirrtlNode declaration

DefWire(info: Info, name: String, tpe: Type)

Concrete syntax

wire w: UInt

In-memory Representation

DefWire(NoInfo, "w", UIntType(UnknownWidth))

Register Declaration

A register declaration, containing a name, type, clock signal, reset signal, and reset value.

FirrtlNode declaration

DefRegister(info: Info, name: String, tpe: Type, clock: Expression, reset: Expression, init: Expression)

Connection

Represents a directioned connection from a source to a sink. Note that it abides by last-connect-semantics, as described in Chisel.

FirrtlNode declaration

Connect(info: Info, loc: Expression, expr: Expression)

Other Statements

Other statement types like DefMemory, DefNode, IsInvalid, Conditionally, and others are omitted here; please refer to firrtl/src/main/scala/firrtl/ir/IR.scala for more detail.

Expression

Expressions represent references to declared components or logical and arithmetic operations. Below are some commonly used expressions:

Reference

A reference to a declared component, such as a wire, register, or port. It has a name and type field. Note that it does not contain a pointer to the actual declaration, but instead just contains the name as a String.

FirrtlNode declaration

Reference(name: String, tpe: Type)

DoPrim

An anonymous primitive operation, such as Add, Sub, or And, Or, or subword-selection (Bits). The type of operation is indicated by the op: PrimOp field. Note that the number of required arguments and constants are determined by the op.

FirrtlNode declaration

DoPrim(op: PrimOp, args: Seq[Expression], consts: Seq[BigInt], tpe: Type)

Other Expressions

Other expressions including SubField, SubIndex, SubAccess, Mux, ValidIf etc. are described in more detail in firrtl/src/main/scala/firrtl/ir/IR.scala and The FIRRTL Specification.

Back to our example

Let's take another look at the FIRRTL AST from our example. Hopefully, the structure of the design makes more sense!

In [ ]:
println(stringifyAST(firrtlAST))

That's it for this section! In the next section, we will look at how a FIRRTL transformation walks this AST and modifies it.