6.13. IOBinders¶
In Chipyard we use a special Parameters
key, IOBinders
to instantiate IO cells in the ChipTop
layer and determine what modules to bind to the IOs of a ChipTop
in the TestHarness
.
// This type describes a function callable on the TestHarness instance. Its return type is unused.
type TestHarnessFunction = (chipyard.TestHarness) => Seq[Any]
// IOBinders will return a Seq of this tuple, which contains three fields:
// 1. A Seq containing all IO ports created by the IOBinder function
// 2. A Seq containing all IO cell modules created by the IOBinder function
// 3. An optional function to call inside the test harness (e.g. to connect the IOs)
type IOBinderTuple = (Seq[Data], Seq[IOCell], Option[TestHarnessFunction])
case object IOBinders extends Field[Map[String, (Any) => Seq[IOBinderTuple]]](
Map[String, (Any) => Seq[IOBinderTuple]]().withDefaultValue((Any) => Nil)
)
// This macro overrides previous matches on some Top mixin. This is useful for
// binders which drive IO, since those typically cannot be composed
class OverrideIOBinder[T](fn: => (T) => Seq[IOBinderTuple])(implicit tag: ClassTag[T]) extends Config((site, here, up) => {
case IOBinders => up(IOBinders, site) + (tag.runtimeClass.toString ->
((t: Any) => {
t match {
case system: T => fn(system)
case _ => Nil
}
})
)
})
// This macro composes with previous matches on some Top mixin. This is useful for
// annotation-like binders, since those can typically be composed
class ComposeIOBinder[T](fn: => (T) => Seq[IOBinderTuple])(implicit tag: ClassTag[T]) extends Config((site, here, up) => {
case IOBinders => up(IOBinders, site) + (tag.runtimeClass.toString ->
((t: Any) => {
t match {
case system: T => (up(IOBinders, site)(tag.runtimeClass.toString)(system)
++ fn(system))
case _ => Nil
}
})
)
})
This special key solves the problem of duplicating test-harnesses for each different System
type.
You could just as well create a custom harness module that attaches IOs explicitly.
Instead, the IOBinders
key provides a map from Scala traits to attachment behaviors.
Each IOBinder
returns a tuple of three values: the list of ChipTop
ports created by the IOBinder
, the list of all IO cell modules instantiated by the IOBinder
, and an optional function to be called inside the test harness.
This function is responsible for instantiating logic inside the TestHarness
to appropriately drive the ChipTop
IO ports created by the IOBinder
.
Conveniently, because the IOBinder
is generating the port, it may also use the port inside this function, which prevents the BaseChipTop
code from ever needing to access the port val
, thus having the IOBinder
house all port specific code.
This scheme prevents the need to have two separate binder functions for each System
trait.
When creating custom IOBinders
it is important to use suggestName
to name ports; otherwise Chisel will raise an exception trying to name the IOs.
The example IOBinders
demonstrate this.
As an example, the WithGPIOTiedOff
IOBinder creates IO cells for the GPIO module(s) instantiated in the System
, then punches out new Analog
ports for each one.
The test harness simply ties these off, but additional logic could be inserted to perform some kind of test in the TestHarness
.
class WithGPIOTiedOff extends OverrideIOBinder({
(system: HasPeripheryGPIOModuleImp) => {
val (ports2d, ioCells2d) = AddIOCells.gpio(system.gpio)
val harnessFn = (th: chipyard.TestHarness) => { ports2d.flatten.foreach(_ <> AnalogConst(0)); Nil }
Seq((ports2d.flatten, ioCells2d.flatten, Some(harnessFn)))
}
})
IOBinders
also do not need to create ports. Some IOBinders
can simply insert circuitry inside the ChipTop
layer.
For example, the WithSimAXIMemTiedOff
IOBinder specifies that any System
which matches CanHaveMasterAXI4MemPortModuleImp
will have a SimAXIMem
connected inside ChipTop
.
class WithSimAXIMem extends OverrideIOBinder({
(system: CanHaveMasterAXI4MemPort with BaseSubsystem) => {
val peiTuples = AddIOCells.axi4(system.mem_axi4, system.memAXI4Node)
// TODO: we are inlining the connectMem method of SimAXIMem because
// it takes in a dut rather than seq of axi4 ports
val harnessFn = (th: chipyard.TestHarness) => {
peiTuples.map { case (port, edge, ios) =>
val mem = LazyModule(new SimAXIMem(edge, size = system.p(ExtMem).get.master.size)(system.p))
Module(mem.module).suggestName("mem")
mem.io_axi4.head <> port
}
Nil
}
Seq((peiTuples.map(_._1), peiTuples.flatMap(_._3), Some(harnessFn)))
}
})
These classes are all Config
objects, which can be mixed into the configs to specify IO connection behaviors.
There are two macros for generating these Config``s. ``OverrideIOBinder
overrides any existing behaviors set for a particular IO in the Config
object. This macro is frequently used because typically top-level IOs drive or are driven by only one source, so a composition of IOBinders
does not make sense. The ComposeIOBinder
macro provides the functionality of not overriding existing behaviors.