Skip to content

ccelio/chisel-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 

Repository files navigation

chisel-style-guide

A Style Guide for the Chisel Hardware Construction Language.

Overall goal

Code is meant to be read, not written. You will spend more time searching for bugs, adding features to existing code bases, and trying to learn what other people have done, than you will writing your own code from scratch. Code should strive to be easy to understand and easy to maintain.

As style can be a deeply personal preference, and because Chisel is still a very young language, this guide will eschew making hard edicts on DOs and DONTs. Instead, this guide will strive to provide guidance to newcomers to Chisel through a discussion on best practices.

Prelude

Chisel is a DSL embedded in Scala. However, it is still a distinct language, and so it may not follow all of Scala's conventions.

For examples of Chisel style, I recommend reading the source code to the Hwacha vector unit, the rocket-chip uncore, and the BOOM out-of-order processor. Although they are all Berkeley-originated projects, each explores some very interesting (and different!) techniques to describing hardware generators.

Feedback requested!

Table of Contents

Spacing

Spaces, not tabs. Never tabs.

Follow the indent level of the existing code.

Naming

Variable names should tend to be descriptive and not overly abbreviated. The smaller the scope (and the more used it is), the more abbreviated the name can be.

Bundles used for I/Os should be named SomethingIO. IO comes last. The name is Camel-cased. Example: (FreeListIO).

Any variable that is used for debugging purposes should be begin with the prefix debug_ (i.e., things that you ideally would not synthesize).

Constants/parameters should be named in all caps, demonstrating their global nature. While most things in Scala are immutable, Chisel isn’t Scala.

Constants should be all uppercase and should be put in a companion object:

object ALU {
   val SZ_ALU_FN = 4
   FN_ADD = UInt(0, SZ_ALU_FN)
  ...
}

Or trait (if you want a Module or Bundle to extend the trait):

trait RISCVConstants {
   val RD_MSB  = 11
   val RD_LSB  = 7
}

Registers

Registers (and their type) should be specified as follows:

Reg(UInt())               // good!
Reg(UInt(8.W))            // also good!

Reg(chiselTypeOf(io.my_signal) // good!

This construct Reg(x) tells the Reg to be of type x. It does NOT tell Reg what initial value it should be, nor does it add a Delay to a signal!

Reg(UInt(0))      // bad!  Returns a Reg of type UInt and unknown width.
Reg(UInt(0,15))   // bad!  Returns a Reg of type UInt with width 15.

Reg(io.my_signal) // bad. This makes a Reg of type io.my_signal, but the intention is not clear!
                  // It can be easily misread as Reg(next=io.my_signal).

Registers should be initialized as follows:

RegInit(0.U(15.W))   // good, initialized register to value 0 with width 15

Reg(0.U(15.W))       // WRONG! This will be an elaboration error

Delaying a Node (i.e., piping it into a register) should be performed as follows:

RegNext(io.my_signal)  // good

Reg(io.my_signal)      // WRONG! Creates a Reg of the same type as io.a, this will be an elaboration error

And commit the following to memory. It's Reg of Vec, not Vec of Reg, for example

val lotsOfRegs = RegInit(VecInit(10, UInt(32.W)))

Bundles

Consider providing def functions in your Bundles. It provides a clearer level of intention to the user of how to interact with the Bundle.

// simplified example
class DecoupledIO extends Bundle {
   val ready = Input(Bool())
   val valid = Output(Bool())
   def fire: Bool = ready && valid
   ....

In some older chisel code you may see def fire(dummy: Int = 0): Bool the dummy argument is no longer necessary

Users of the DecoupledIO can now do something like when(io.deq.fire())! (note: the dummy: Int = 0 argument must be provided to functions with no arguments placed within Bundles, as Chisel is (currently) unable to differentiate between fields that are wires and fields that are functions with no arguments).

Or this example, which performs a query against a TLB address translation structure:

class TLBIO extends VMUBundle {
   val req = Decoupled(new rocket.TLBReq)
   val resp = new rocket.TLBRespNoHitIndex().flip

   def query(vpn: UInt, store: Bool): Bool = {
      this.req.bits.vpn := vpn
      this.req.bits.asid := 0.U
      this.req.bits.passthrough := false.B
      this.req.bits.instruction := false.B
      this.req.bits.store := store

      this.req.ready && !this.resp.miss
   }
}

The particular example is quite interesting - the query function provides a clearer interface to the user, it automatically sets up the request signals, and it provides a combinational return value to the caller!

Conditional I/O Fields

Consider breaking off Conditional I/O fields into a separate Bundles (FreeListRollbackIO and FreeListSingleCycleIO).

Literals

Be careful of using Scala Ints to describe Chisel literals. 0xffffffff is a 32-bit signed integer with value -1, and thus will throw an error when used as UInt(0xffffffff, 32). Instead, use Strings to describe large literals:

UInt("hffffffff", 32)

Parameters

When instantiating an object from another package, explicitly name the arguments:

val s2d = Module(new hardfloat.RecFNToRecFN(inExpWidth = 8, inSigWidth = 24, outExpWidth = 11, outSigWidth = 53))

This safe-guards against the order (or the name) of parameters changing in an external package without your knowledge.

Ready-Valid Interfaces

A ready signal denotes a resource is available/is ready to be utilized.

A valid signal denotes something is valid and can commit a state update (it will commit a state update if the corresponding ready signal is high).

Performance tip: a valid signal may often be a late arriving signal. Try to avoid using valid signals to drive datapath logic, and instead use valid signals to gate off state updates.

A valid signal should not depend on the ready signal (unless you really know what you are doing). This hurts the critical path and can create combinational loops if both sides get coupled.

Vector of Modules

Static Indexing

An array of modules can be instantiated as follows:

val my_args = Seq(1,2,3,4)
val exe_units = for (i <- 0 until num_units) yield {
   val exe_unit = Module(new AluExeUnit(args = my_args(i)))
   // any wiring or other logic can go here
   exe_unit
}

You can provide different input parameters to each constructor as required (the above toy example shows different elements of my_args being provided to each AluExeUnit).

The disadvantage is you cannot index the collection using Chisel nodes (aka, you can not dynamically index the collection during hardware execution). If you must use a Scala collection (for the first advantage), you can still use dynamic indexing by grabbing a Vec of the IOs:

val exe_units_io = Vec(exe_units.map(_.io))

Vec

If you need to index the vector of Modules using a Chisel node, you can also use the following structure:

val table = Vec.fill(num_elements) { Module(new TableElement()).io }

val idx = Wire(UInt())
table(idx).wen := true.B // indexed by a Chisel node!

Note that table is actually a Vec of TableElement I/O bundles.

Val versus Var

Only use val, unless you are an experienced Chisel programmer. Even then, only use var in constrained situations (try to abstract it within a function). The use of the var can make it difficult to reason about your design.

For context, a bit more background is needed. A hardware design described in Chisel is quite literally a Scala program that, when executed, generates a hardware graph composed of Chisel Nodes that is then passed to a back-end which generates a cycle-exact replica in either C++ or Verilog (or whatever other formats supported by the backend).

Thus, val and var denote Scala variables (well more exactly, val is an immutable value and var is a mutable variable).

val my_node = Wire(UInt())

This is a Scala value called my_node, which points to a Chisel Node in the hardware graph that is a Wire of type UInt. The my_node value can only ever point to this particular Chisel Node in the graph.

var node_ptr = Wire(UInt())

Uh oh. The Scala variable node_ptr is pointing to a Chisel node in the graph, but it can later be changed to point to a new Chisel node!

var node_ptr = io.a
node_ptr := true.B
node_ptr = io.b
node_ptr := false.B

In the above (scary!) code,

  • node_ptr first points to io.a,
  • then uses the Chisel assignment operator := to set io.a to true.B
  • node_ptr is then changed to point to io.b
  • and finally, io.b is set to false.B!

We get the following Verilog output:

module Hello(
    output io_a,
    output io_b
);

  assign io_b = 1'h0;
  assign io_a = 1'h1;
endmodule

Using var can make it difficult to reason about the circuit. And be CAREFUL when mixing = and :=! The = is a Scala assignment, and sets a var variable to point to a new Node in the graph. Meanwhile, := is a Chisel assignment and performs a new assignment to the Chisel Node. This distinction is important! For example, Chisel conditional when statements are for conditionally assigning values to Chisel Nodes - the scala = operator is invisible to when statements!!

var my_node = io.a
my_node := true.B // this sets io.a to "true"
my_node = true.B  // this sets the Scala variable my_node to point to a Chisel node that is a literal true

Consider the incorrect code below, which tries to mix when, var, and = to perform an OR reduction:

val my_bits = Wire(UInt(n.W))
var temp = false.B
for (i <- 0 until n) {
   when (my_bits(i)) {
      temp = true.B // wrong! always returns true.
      temp := true.B // compiler error!
   }
}

For the first statement temp = true.B, the Scala variable temp points to the Chisel node true.B, ignoring the when() statement.

For the second statement temp := true.B, a Chisel compiler error is thrown because the code is trying to reassign the node false.B to be true.B, which is nonsensical.

Conclusion: don't mix when and var's!

For completness sake, the proper code for an OR reduction would be my_bits.orR (and no need to use var or when!). If you don’t understand why var and when() don’t mix, then for the love of god AVOID var.

Valid uses of Vars

There are a few valid uses of var. One would be to generate cascading logic. For example, this locking arbiter from ChiselUtil:

var choose = (n-1).U
for (i <- n-2 to 0 by -1) {
   choose = Mux(io.in(i).valid, i.U, choose)
}
chosen := Mux(locked, lockIdx, choose)

After each iteration of the Scala for loop, choose is pointing to a new node in the cascading Mux tree.

Another use is forward declaring Modules that are conditionally instantiated later.

var fpu: FPUUnit = null
if (has_fpu) {
   fpu = Module(new FPUUnit())
   ...

Imports

Try to avoid wildcard imports. They make code more obfuscated and fragile.

import rocket.{UseFPU, XLen}
import cde.{Parameters, Field}

AVOID using import statements for bringing in new Module and Bundle definitions. Instead, explicitly invoke the namespace when instantiating the Module or Bundle. It makes the origin of the object clear.

//bad
import rocket._
...
val tlb = Module(new TLB())

//good
val tlb = Module(new rocket.TLB())

There are exceptions to this rule. It is usually best to use the wild card with chisel3, as in

import chisel3._

There are many useful things in chisel such as the support for the .U and .S constructs. It's easier to just make them all available, and it is likely that the import suggestion mechanism in an IDE will not suggest correctly.

Though you may see import Chisel with various endings in some examples (or sometimes suggested by an IDE), we recommend that you not use it. Chisel with a capital 'C' is a backward compatibility mode for Chisel2,

Private versus Public

By default, all vals and defs are public in Scala. Label all defs private if the scope is meant to stay internal to the current object. This makes intention clearer.

Comments

Consider commenting the use of the I/O fields (especially if there are unintuitive timings!). Chisel I/Os aren’t functions - it isn’t obvious how to interface with a Module to other programmers.

class CpuReq extends Bundle {
   val addr = UInt(someWidth.W)
   val cmd  = UInt(someWidth.W)
   val data = UInt(someWidth.W) // is sent the cycle after the request is valid

In fact, you may prefer to codify timings in the names of the signals themselves:

val io = new Bundle {
   // send read addr on cycle 0, get data out on cycle 2.
   val s0_r_idx = Input(UInt(index_sz.W))
   val s2_r_out = Output(UInt(fetch_width.W))

If your code required cleverness to write, you should probably describe why it does what it does. The reader is never as smart as the writer. Especially when it’s yourself.

Assertions

If you solve a bug, strongly contemplate what assert() could have caught this bug and then add it.

If you are using a one-hot encoding, guard it with asserts! Especially calls to OHToUInt.

assert(PopCount(updates_oh) == 1.U, "[MyModuleName] ...")

Note which Module the assert resides in when authoring the failure string.

Requires

Scala provides a require() function that will throw a Scala run-time error when compiling your Chisel hardware if the condition is not met.

Use require() statements to guard against unsupported parameter values in your hardware generators.

Use require() statements to codify your assumptions in your code (e.g., require(isPow2(num_entries)) for logic that only works when num_entries is a power of 2).

Additional Best Practices

If you ever write +N, ask yourself if the number will ever be NonPow2 (and then you should write a require statement if the logic depends on Pow2 properties!). For example, wrap-around logic will be needed to guard incrementing pointers to queues with NonPow2 number of elements. Just like in software, overflows and array bounds overruns are scary!

Consider restraining any undefined hardware behavior. It makes writing asserts easier. Undefined behavior may provide for a more efficient circuit, but a circuit that works is even more efficient!

About

A Style Guide for the Chisel Hardware Construction Language

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published