Prev: Generators: Types
Next: FIRRTL AST Traversal
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.
val path = System.getProperty("user.dir") + "/source/load-ivy.sc"
interp.load.module(ammonite.ops.Path(java.nio.file.FileSystems.getDefault().getPath(path)))
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
import 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.
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.
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.
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:
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.
println(stringifyAST(firrtlAST))
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, @[file.fir@2.0] would refer to the FIRRTL file, line 2, column 0.
The following section will outline all of these FIRRTL nodes in detail.
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 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.
Circuit(info: Info, modules: Seq[DefModule], main: String)
circuit Adder:
... //List of modules
Circuit(NoInfo, Seq(...), "Adder")
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.
Module(info: Info, name: String, ports: Seq[Port], body: Stmt) extends DefModule
module Adder:
... // list of ports
... // statements
Module(NoInfo, "Adder", Seq(...), )
A port defines part of a Module's io, and has a name, direction (input or output), and type.
class Port(info: Info, name: String, direction: Direction, tpe: Type)
input x: UInt
Port(NoInfo, "x", INPUT, UIntType(UnknownWidth))
A statement is used to describe the components within a module and how they interact. Below are some commonly used statements:
A group of statements. Commonly used as the body field in a Module declaration.
A wire declaration, containing a name and type. It can be both a source (connected from) and a sink (connected *to").
DefWire(info: Info, name: String, tpe: Type)
wire w: UInt
DefWire(NoInfo, "w", UIntType(UnknownWidth))
A register declaration, containing a name, type, clock signal, reset signal, and reset value.
DefRegister(info: Info, name: String, tpe: Type, clock: Expression, reset: Expression, init: Expression)
Represents a directioned connection from a source to a sink. Note that it abides by last-connect-semantics, as described in Chisel.
Connect(info: Info, loc: Expression, expr: Expression)
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.
Expressions represent references to declared components or logical and arithmetic operations. Below are some commonly used expressions:
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.
Reference(name: String, tpe: Type)
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
.
DoPrim(op: PrimOp, args: Seq[Expression], consts: Seq[BigInt], tpe: Type)
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.
Let's take another look at the FIRRTL AST from our example. Hopefully, the structure of the design makes more sense!
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.