6.9. Keys, Traits, and Configs
You have probably seen snippets of Chisel referencing keys, traits, and configs by this point. This section aims to elucidate the interactions between these Chisel/Scala components, and provide best practices for how these should be used to create a parameterized design and configure it.
We will continue to use the GCD example.
6.9.1. Keys
Keys specify some parameter which controls some custom widget. Keys should typically be implemented as Option types, with a default value of None that means no change in the system. In other words, the default behavior when the user does not explicitly set the key should be a no-op.
Keys should be defined and documented in sub-projects, since they generally deal with some specific block, and not system-level integration. (We make an exception for the example GCD widget).
case object GCDKey extends Field[Option[GCDParams]](None)
The object within a key is typically a case class XXXParams, which defines a set of parameters which some block accepts. For example, the GCD widget’s GCDParams parameterizes its address, operand widths, whether the widget should be connected by Tilelink or AXI4, and whether the widget should use the blackbox-Verilog implementation, or the Chisel implementation.
case class GCDParams(
address: BigInt = 0x4000,
width: Int = 32,
useAXI4: Boolean = false,
useBlackBox: Boolean = true,
useHLS: Boolean = false,
externallyClocked: Boolean = false
) {
require(!(useAXI4 && useHLS))
}
Accessing the value stored in the key is easy in Chisel, as long as the implicit p: Parameters object is being passed through to the relevant module. For example, p(GCDKey).get.address returns the address field of GCDParams. Note this only works if GCDKey was not set to None, so your Chisel should check for that case!
6.9.2. Traits
Typically, most custom blocks will need to modify the behavior of some pre-existing block. For example, the GCD widget needs the DigitalTop module to instantiate and connect the widget via Tilelink, generate a top-level gcd_busy port, and connect that to the module as well. Traits let us do this without modifying the existing code for the DigitalTop, and enables compartmentalization of code for different custom blocks.
Top-level traits specify that the DigitalTop has been parameterized to read some custom key and optionally instantiate and connect a widget defined by that key. Traits should not mandate the instantiation of custom logic. In other words, traits should be written with CanHave semantics, where the default behavior when the key is unset is a no-op.
Top-level traits should be defined and documented in subprojects, alongside their corresponding keys. The traits should then be added to the DigitalTop being used by Chipyard.
Below we see the traits for the GCD example. The Lazy trait connects the GCD module to the Diplomacy graph.
trait CanHavePeripheryGCD { this: BaseSubsystem =>
private val portName = "gcd"
private val pbus = locateTLBusWrapper(PBUS)
// Only build if we are using the TL (nonAXI4) version
val (gcd_busy, gcd_clock) = p(GCDKey) match {
case Some(params) => {
// If externallyClocked is true, create an input port for the GCD clock.
// This clock is distinct from the pbus clock or other internal clocks.
// It's defined within InModuleBody as it's a hardware port.
val gcd_clock = Option.when(params.externallyClocked) {
InModuleBody { IO(Input(Clock())).suggestName("gcd_clock_in") }
}
// Define the clock source node for the GCD module.
val gcdClockNode = if (params.externallyClocked) {
// If externally clocked, create a new ClockSourceNode.
// This node acts as the root of the GCD's independent clock domain.
val gcdSourceClockNode = ClockSourceNode(Seq(ClockSourceParameters()))
InModuleBody {
// Connect the ClockSourceNode's output clock to the external gcd_clock input.
gcdSourceClockNode.out(0)._1.clock := gcd_clock.get
// The reset signal for the GCD's clock domain must be synchronous to the gcd_clock.
// ResetCatchAndSync synchronizes the asynchronous pbus reset to the gcd_clock domain.
gcdSourceClockNode.out(0)._1.reset := ResetCatchAndSync(gcd_clock.get, pbus.module.reset.asBool)
}
gcdSourceClockNode
} else {
// If not externally clocked, the GCD runs on the same clock as the pbus.
pbus.fixedClockNode
}
// Define the type of clock crossing required between the pbus and the GCD module.
val gcdCrossing = if (params.externallyClocked) {
// If the GCD has its own clock, an AsynchronousCrossing is necessary
// to safely transfer data between the pbus clock domain and the GCD clock domain.
AsynchronousCrossing()
} else {
// If the GCD uses the pbus clock, a SynchronousCrossing can be used.
SynchronousCrossing()
}
// Instantiate the GCD module (either TL, AXI4, or HLS variant)
val gcd = if (params.useAXI4) {
val gcd = LazyModule(new GCDAXI4(params, pbus.beatBytes)(p))
// Connect the GCD's clock input to our determined gcdClockNode.
gcd.clockNode := gcdClockNode
// Couple the GCD to the pbus, inserting the necessary clock crossing logic.
pbus.coupleTo(portName) {
// AXI4InwardClockCrossingHelper handles crossing details for AXI4.
AXI4InwardClockCrossingHelper("gcd_crossing", gcd, gcd.node)(gcdCrossing) :=
AXI4Buffer () :=
TLToAXI4 () :=
// toVariableWidthSlave doesn't use holdFirstDeny, which TLToAXI4() needs
TLFragmenter(pbus.beatBytes, pbus.blockBytes, holdFirstDeny = true) := _
}
gcd
} else if (params.useHLS) {
val gcd = LazyModule(new HLSGCDAccel(params, pbus.beatBytes)(p))
// Connect the GCD's clock input to our determined gcdClockNode.
gcd.clockNode := gcdClockNode
// Couple the GCD to the pbus, inserting the necessary clock crossing logic.
pbus.coupleTo(portName) {
// TLInwardClockCrossingHelper handles crossing details for TileLink.
TLInwardClockCrossingHelper("gcd_crossing", gcd, gcd.node)(gcdCrossing) :=
TLFragmenter(pbus.beatBytes, pbus.blockBytes) := _
}
gcd
} else {
val gcd = LazyModule(new GCDTL(params, pbus.beatBytes)(p))
// Connect the GCD's clock input to our determined gcdClockNode.
gcd.clockNode := gcdClockNode
// Couple the GCD to the pbus, inserting the necessary clock crossing logic.
pbus.coupleTo(portName) {
// TLInwardClockCrossingHelper handles crossing details for TileLink.
TLInwardClockCrossingHelper("gcd_crossing", gcd, gcd.node)(gcdCrossing) :=
TLFragmenter(pbus.beatBytes, pbus.blockBytes) := _
}
gcd
}
// Expose the GCD's busy signal.
val gcd_busy = InModuleBody {
val busy = IO(Output(Bool())).suggestName("gcd_busy")
busy := gcd.module.io.gcd_busy
busy
}
// Return the busy signal (always needed if GCD exists) and the optional external clock input.
// The Option[Clock] allows the IOBinder (WithGCDIOPunchthrough) to conditionally
// create the top-level clock input only when `externallyClocked` is true.
// The busy signal is Some(busy) because the entire GCD peripheral itself is optional based on GCDKey.
(Some(gcd_busy), gcd_clock)
}
// If GCDKey is None, the GCD peripheral is not instantiated. Return None for both signals.
case None => (None, None)
}
}
These traits are added to the default DigitalTop in Chipyard.
class DigitalTop(implicit p: Parameters) extends ChipyardSystem
with testchipip.tsi.CanHavePeripheryUARTTSI // Enables optional UART-based TSI transport
with testchipip.boot.CanHavePeripheryCustomBootPin // Enables optional custom boot pin
with testchipip.cosim.CanHaveTraceIO // Enables optionally adding trace IO
with testchipip.soc.CanHaveSubsystemInjectors // Enables the subsystem injector API
with testchipip.soc.CanHaveSwitchableOffchipBus // Enables optional off-chip-bus with interface-switch
with testchipip.iceblk.CanHavePeripheryBlockDevice // Enables optionally adding the block device
with testchipip.serdes.CanHavePeripheryTLSerial // Enables optionally adding the tl-serial interface
with testchipip.serdes.old.CanHavePeripheryTLSerial // Enables optionally adding the DEPRECATED tl-serial interface
with testchipip.soc.CanHavePeripheryChipIdPin // Enables optional pin to set chip id for multi-chip configs
with sifive.blocks.devices.i2c.HasPeripheryI2C // Enables optionally adding the sifive I2C
with sifive.blocks.devices.timer.HasPeripheryTimer // Enables optionally adding the timer device
with sifive.blocks.devices.pwm.HasPeripheryPWM // Enables optionally adding the sifive PWM
with sifive.blocks.devices.uart.HasPeripheryUART // Enables optionally adding the sifive UART
with sifive.blocks.devices.gpio.HasPeripheryGPIO // Enables optionally adding the sifive GPIOs
with sifive.blocks.devices.spi.HasPeripherySPIFlash // Enables optionally adding the sifive SPI flash controller
with sifive.blocks.devices.spi.HasPeripherySPI // Enables optionally adding the sifive SPI port
with icenet.CanHavePeripheryIceNIC // Enables optionally adding the IceNIC for FireSim
with chipyard.example.CanHavePeripheryGCD // Enables optionally adding the GCD example widget
with chipyard.clocking.HasChipyardPRCI // Use Chipyard reset/clock distribution
with chipyard.clocking.CanHaveClockTap // Enables optionally adding a clock tap output port
with constellation.soc.CanHaveGlobalNoC // Support instantiating a global NoC interconnect
with rerocc.CanHaveReRoCCTiles // Support tiles that instantiate rerocc-attached accelerators
with testchipip.ctc.CanHavePeripheryCTC // Support optional CTC link
{
override lazy val module = new DigitalTopModule(this)
}
class DigitalTopModule(l: DigitalTop) extends ChipyardSystemModule(l)
with freechips.rocketchip.util.DontTouch
6.9.3. Config Fragments
Config fragments set the keys to a non-default value. Together, the collection of config fragments which define a configuration generate the values for all the keys used by the generator.
For example, the WithGCD config fragment is parameterized by the type of GCD widget you want to instantiate. When this config fragment is added to a config, the GCDKey is set to a instance of GCDParams, informing the previously mentioned traits to instantiate and connect the GCD widget appropriately.
class WithGCD(useAXI4: Boolean = false, useBlackBox: Boolean = false, useHLS: Boolean = false, externallyClocked: Boolean = false) extends Config((site, here, up) => {
case GCDKey => {
// useHLS cannot be used with useAXI4 and useBlackBox
assert(!useHLS || (useHLS && !useAXI4 && !useBlackBox))
Some(GCDParams(useAXI4 = useAXI4, useBlackBox = useBlackBox, useHLS = useHLS, externallyClocked = externallyClocked))
}
})
We can use this config fragment when composing our configs.
class GCDTLRocketConfig extends Config(
new chipyard.example.WithGCD(useAXI4=false, useBlackBox=false) ++ // Use GCD Chisel, connect Tilelink
new freechips.rocketchip.rocket.WithNHugeCores(1) ++
new chipyard.config.AbstractConfig)
Note
Readers who want more information on the configuration system may be interested in reading Context-Dependent-Environments.
6.9.4. Chipyard Config Fragments
For discoverability, users can run make find-configs to list Chipyard Config classes eligible for CONFIG=... (defaults to package chipyard). Only classes whose names end with Config are shown.
To see reusable building blocks, run make find-config-fragments to list config fragments.