Skip to content

Commit

Permalink
Add explanation for Class and Object, with examples. (#3880)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeurbach authored Feb 29, 2024
1 parent edf9aef commit 036dd5e
Showing 1 changed file with 251 additions and 0 deletions.
251 changes: 251 additions & 0 deletions docs/src/explanations/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,254 @@ on integral `Property` typed values.
| `+` | Perform addition as defined by FIRRTL spec section Integer Add Operation |
| `*` | Perform multiplication as defined by FIRRTL spec section Integer Multiply Operation |
| `>>` | Perform shift right as defined by FIRRTL spec section Integer Shift Right Operation |

### Classes and Objects

Classes and Objects are to `Property` types what modules and instances are to
hardware types. That is, they provide a means to declare hierarchies through
which `Property` typed values flow. `Class` declares a hierarchical container
with input and output `Property` ports, and a body that contains `Property`
connections and `Object`s. `Object`s represent the instantiation of a `Class`,
which requires any input `Property` ports to be assigned, and allows any output
`Property` ports to be read.

This allows domain-specific data models to be built using the basic primitives
of an object-oriented programming language, and embedded directly in the
instance graph Chisel is constructing. Intuitively, inputs to a `Class` are like
constructor arguments, which must be supplied to create an `Object`. Similarly,
outputs from a `Class` are like fields, which may be accessed from an `Object`.
This separation allows `Class` declarations to abstract over any `Object`s
created in their body--from the outside, the inputs must be supplied and only
the outputs may be accessed.

The graphs represented by `Class` declarations and `Object` instantiations
coexist within the hardware instance graph. `Object` instances can exist
within hardware modules, providing domain-specific information, but hardware
instances cannot exist within `Class` declarations.

`Object`s can be referenced, and references to `Object`s are a special kind of
`Property[ClassType]` type. This allows the data model captured by `Class`
declarations and `Object` instances to form arbitrary graphs.

To understand how `Object` graphs are represented, and can ultimately be
queried, consider how the hardware instance graph is elaborated. To build the
`Object` graph, we first pick an entrypoint module to start elaboration. The
elaboration process works according to the Verilog spec's definition of
elaboration--instances of modules and `Object`s are instantiated in-memory,
with connections to their inputs and outputs. Inputs are supplied, and outputs
may be read. After elaboration completes, the `Object` graph is exposed in terms
of the output ports, which may contain any `Property` types, including
references to `Object`s.

To illustrate how these pieces come together, consider the following examples:

```scala mdoc:silent
import chisel3.properties.Class
import chisel3.experimental.hierarchy.{instantiable, public, Definition, Instance}

// An abstract description of a CSR, represented as a Class.
@instantiable
class CSRDescription extends Class {
// An output Property indicating the CSR name.
val identifier = IO(Output(Property[String]()))
// An output Property describing the CSR.
val description = IO(Output(Property[String]()))
// An output Property indicating the CSR width.
val width = IO(Output(Property[Int]()))

// Input Properties to be passed to Objects representing instances of the Class.
@public val identifierIn = IO(Input(Property[String]()))
@public val descriptionIn = IO(Input(Property[String]()))
@public val widthIn = IO(Input(Property[Int]()))

// Simply connect the inputs to the outputs to expose the values.
identifier := identifierIn
description := descriptionIn
width := widthIn
}
```

The `CSRDescription` is a `Class` that represents domain-specific information
about a CSR. This uses `@instantiable` and `@public` so the `Class` can work
with the `Definition` and `Instance` APIs.

The readable fields we want to expose on `Object`s of the `CSRDescription` class
are a string identifier, a string description, and an integer bitwidth, so these
are output `Property` type ports on the `Class`.

To capture concrete values at each `Object` instantiation, we have corresponding
input `Property` type ports, which are connected directly to the outputs. This
is how we would represent something like a Scala `case class` using `Class`.

```scala mdoc:silent
// A hardware module representing a CSR and its description.
class CSRModule(
csrDescDef: Definition[CSRDescription],
width: Int,
identifierStr: String,
descriptionStr: String)
extends Module {
override def desiredName = identifierStr

// Create a hardware port for the CSR value.
val value = IO(Output(UInt(width.W)))

// Create a property port for a reference to the CSR description object.
val description = IO(Output(csrDescDef.getPropertyType))

// Instantiate a CSR description object, and connect its input properties.
val csrDescription = Instance(csrDescDef)
csrDescription.identifierIn := Property(identifierStr)
csrDescription.descriptionIn := Property(descriptionStr)
csrDescription.widthIn := Property(width)

// Create a register for the hardware CSR. A real implementation would be more involved.
val csr = RegInit(0.U(width.W))

// Assign the CSR value to the hardware port.
value := csr

// Assign a reference to the CSR description object to the property port.
description := csrDescription.getPropertyReference
}
```

The `CSRModule` is a `Module` that represents the (dummy) hardware for a CSR, as
well as a `CSRDescription`. Using a `Definition` of a `CSRDescription`, an
`Object` is created and its inputs supplied from the `CSRModule` constructor
arguments. Then, a reference to the `Object` is connected to the `CSRModule`
output, so the reference will be exposed to the outside.

```scala mdoc:silent
// The entrypoint module.
class Top extends Module {
// Create a Definition for the CSRDescription Class.
val csrDescDef = Definition(new CSRDescription)

// Get the CSRDescription ClassType.
val csrDescType = csrDescDef.getClassType

// Create a property port to collect all the CSRDescription object references.
val descriptions = IO(Output(Property[Seq[csrDescType.Type]]()))

// Instantiate a couple CSR modules.
val mcycle = Module(new CSRModule(csrDescDef, 64, "mcycle", "Machine cycle counter."))
val minstret = Module(new CSRModule(csrDescDef, 64, "minstret", "Machine instructions-retired counter."))

// Assign references to the CSR description objects to the property port.
descriptions := Property(Seq(mcycle.description.as(csrDescType), minstret.description.as(csrDescType)))
}
```

The `Top` module represents the entrypoint. It creates the `Definition` of the
`CSRDescription`, and creates some `CSRModule`s. It then takes the description
references, collects them into a list, and outputs the list so it will be
exposed to the outside.

While it is not required to use the `Definition` API to define a `Class`, this
is the "safe" API, with support in Chisel for working with `Definition`s and
`Instance`s of a `Class`. There is also an "unsafe" API. See `DynamicObject` for
more information.

To illustrate what this example generates, here is a listing of the FIRRTL:

```
FIRRTL version 4.0.0
circuit Top :
class CSRDescription :
output identifier : String
output description : String
output width : Integer
input identifierIn : String
input descriptionIn : String
input widthIn : Integer
propassign identifier, identifierIn
propassign description, descriptionIn
propassign width, widthIn
module mcycle :
input clock : Clock
input reset : Reset
output value : UInt<64>
output description : Inst<CSRDescription>
object csrDescription of CSRDescription
propassign csrDescription.identifierIn, String("mcycle")
propassign csrDescription.descriptionIn, String("Machine cycle counter.")
propassign csrDescription.widthIn, Integer(64)
regreset csr : UInt<64>, clock, reset, UInt<64>(0h0)
connect value, csr
propassign description, csrDescription
module minstret :
input clock : Clock
input reset : Reset
output value : UInt<64>
output description : Inst<CSRDescription>
object csrDescription of CSRDescription
propassign csrDescription.identifierIn, String("minstret")
propassign csrDescription.descriptionIn, String("Machine instructions-retired counter.")
propassign csrDescription.widthIn, Integer(64)
regreset csr : UInt<64>, clock, reset, UInt<64>(0h0)
connect value, csr
propassign description, csrDescription
public module Top :
input clock : Clock
input reset : UInt<1>
output descriptions : List<Inst<CSRDescription>>
inst mcycle of mcycle
connect mcycle.clock, clock
connect mcycle.reset, reset
inst minstret of minstret
connect minstret.clock, clock
connect minstret.reset, reset
propassign descriptions, List<Inst<CSRDescription>>(mcycle.description, minstret.description)
```

To understand the `Object` graph that is constructed, we will consider an
entrypoint to elaboration, and then show a hypothetical JSON representation of
the `Object` graph. The details of how we go from IR to an `Object` graph are
outside the scope of this document, and implemented by related tools.

If we elaborate `Top`, the `descriptions` output `Property` is our entrypoint to
the `Object` graph. Within it, there are two `Object`s, the `CSRDescription`s of
the `mcycle` and `minstret` modules:

```json mdoc:silent
{
"descriptions": [
{
"identifier": "mcycle",
"description": "Machine cycle counter.",
"width": 64
},
{
"identifier": "minstret",
"description": "Machine instructions-retired counter.",
"width": 64
}
]
}
```

If instead, we elaborate one of the `CSRModule`s, for example, `minstret`, the
`description` output `Property` is our entrypoint to the `Object` graph, which
contains the single `CSRDescription` object:

```json mdoc:silent
{
"description": {
"identifier": "minstret",
"description": "Machine instructions-retired counter.",
"width": 64
}
}
```

In this way, the output `Property` ports, `Object` references, and choice of
elaboration entrypoint allow us to view the `Object` graph representing the
domain-specific data model from different points in the hierarchy.

0 comments on commit 036dd5e

Please sign in to comment.