Skip to content

Instantly share code, notes, and snippets.

@jackkoenig
Last active April 13, 2021 00:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jackkoenig/20bffc2e9270386044aba9f00bc82fd5 to your computer and use it in GitHub Desktop.
Save jackkoenig/20bffc2e9270386044aba9f00bc82fd5 to your computer and use it in GitHub Desktop.

Motivation

A common feature request in Chisel is to be able to have more control over the naming of Bundle fields [1, 2, 3, 4]. This request often involves dropping Bundle prefixes, most often to be able to have AXI Bundles that match the AXI naming conventions, while still having Bundle structure for subsets of the signals [1, 2].

For example, AXI has flattened ready-valid structure:

class AXI_AW_Full_Bundle extends Bundle {
  val AWADDR = Output(UInt(32.W))
  val AWVALID = Output(Bool())
  val AWREADY = Input(Bool())
  val AWSIZE = Output(UInt(4.W))
  val AWLEN = Output(UInt(2.W))
}

While a Chisel writer may prefer more composition

class AXI_AW_Bundle extends Bundle {
  val addr = Output(UInt(32.W))
  val size = Output(UInt(4.W))
  val len = Output(UInt(2.W))
}
// Then wrapped in Decoupled(new AXI_AW_Bundle) when you need ready-valid

The latter is easier to use in generators, but is problematic if you need to match a particular interface in Verilog (mainly top-level ports or BlackBox interfaces).

In Chisel2, you could .suggestName your way out of this problem. That doesn’t work in chisel3 though, so it’s an oft-requested feature.

While this could be implemented by some .forceName API (that overrides the final Verilog name in FIRRTL), it suffers from potential composition problems. What if you end up not flattening types the way we currently do? We hope to emit SystemVerilog structs some day, so a “forceName”-style API that ignores aggregate hierarchy is problematic.

EDIT: forceName was introduced in Chisel v3.4.1, but the composition problems remain.

Proposal

Instead, I am proposing a “view” approach where the user can express their Data structure in the way they want it to appear in the Verilog, and then manipulate it using views that match a more Chisel-y structure.

A DataView (or BundleView) is a subtype of chisel3.Data that can be manipulated as normal Data, but manipulations actually affect some other Data. Put another way, the View does not appear in the FIRRTL at all, it is purely a Chisel construct that is providing an interface to manipulate a different underlying Data. Here’s a code example of what a BundleView interface might look like

// Interface sketch
// Simplified to views of Bundle <-> Bundle on an Element-by-Element basis
trait BundleView[A <: Bundle, B <: Bundle] {
  // Combined with flip, can create views of either 
  def of[A](a: A): B
  def flip: BundleView[B, A]
}
object BundleView {
  def apply[A <: Bundle, B <: Bundle](mapping: ((A, B) => (Element, Element))*): BundleView[A, B] = ???
}

And a possible use of it for the previously defined AXI types

class MyModule extends MultiIOModule {
  val AXI = IO(new AXI_AW_Full_Bundle)
  
  // We have the correct names for our AW port,
  // but now we want to use it as if it were a AXI_AW_Bundle
  
  // We can define a view (note this would usually be in an object)
  val myView = BundleView[AXI_AW_Full_Bundle, DecoupledIO[AXI_AW_Bundle]](
    _.AWADDR -> _.bits.addr,
    _.AWVALID -> _.valid,
    _.AWREADY -> _.ready,
    _.AWSIZE -> _.bits.size,
    _.AWLEN -> _.bits.len
  )
  
  // And then binding it to the concrete port
  val axiView: DecoupledIO[AXI_AW_Bundle] = myView.of(AXI)
  
  // Now I can manipulate axiView and actually manipulate the underlying Bundle
  axiView.valid := false.B
  axiView.bits.addr := 0.U
  
  // I can also use the view on the RHS to read
  when (axiView.ready) { // Do something
  }
}

Proof that this type-checks: https://scastie.scala-lang.org/xUDLNryOS7e22pg6wPn8AA

Comparison with Casts

This differs from existing casts in a few important ways:

  1. Casts are 1 way, ie. a given cast is either a read or a write, a view would enable both reads and writes
  2. Casts tend to be structural, casting from 1 Bundle to another is done via casting to a UInt, then just structurally chopping up that UInt. It’s not very safe, eg. https://scastie.scala-lang.org/Ih6zOBUoTQWPQZ5u4qs6Sw. In particular, order and size of the fields matter. Casting between AXI_AW_Full_Bundle and DecoupledIO[AXI_AW_Bundle] would not work properly
  3. Cast writes are only complete connections. You cannot write with a cast to only 1 field of the underlying datastructure at a time.

Another, non-fundamental difference but practical difference in this proposal is subword assignment. Because of the 1-way direction of casts, casts can be used to implement subword assignment. Views could do this as well, but at least the initial proposal does not include support this.

Current Solutions

Custom Connections

As [2] did in https://scastie.scala-lang.org/BAWqXnQVRUyl7aBP76d6Pg, you can create a mapping like the above yourself. Basically just writing two custom connection methods (one for each side as source vs. sink) will do it. This has 2 deficiencies over 1st class view support.

  1. It’s verbose, the mapping is expressed twice
  2. Similarly to casts, it only allows complete connections, you can’t connect to the original thing and then connect to one field of the view and have that only update the corresponding field of the original. Put another way, it doesn’t mesh well with last connect semantics.

Scala Data Structures

In some cases, Scala standard library types can perform a similar function. In particular, Seq[A <: Data] is often used to abstract over several Data when they can be treated as a Seq. It tends to be a bit clunky since you can’t use Seq where Data is required; if read-only is okay, then wrapping the Seq in VecInit will make a Data, but when writing, it has to be kept as a Seq to keep references to the original Data.

Other Applications

Hardware Tuple [5]

There are a lot of convenient things that we could do if we have automatic “tupling” views of groups of Data, for example:

VecInit(vecA.zip(vecB)).indexWhere(x => x._1 === x._2)

The above currently doesn’t work, because the zipped type is (Data, Data) and Vec requires Data. If we had 1st class support for “views”, we could use implicit conversions to create views of these tuples that would satisfy Vecs type parameter requirement that it subclasses Data. In this case, the view is implicit. This example only requires reading from the view.

Another example:

val (a, b) = Mux(cond, (c, d), (e, f))
val (x, y) = Wire(...) 
(x, y) := Mux(cond, (c, d), (e, f))

(a, b) := in particular requires the ability to write to these Views

Connect As Supertype [6]

In Chisel2, <> would connect the fields of the LHS and RHS "by name" (aka "stringly typed"). This is generally considered unsafe, but is really important when using inheritance in Bundle hierarchies. For example:

class Foo extends Bundle {
  val a = UInt(8.W)
}
class Bar extends Foo {
  val b = UInt(8.W)
}

class MyModule extends Module {
  val io = IO(new Bundle {
    val out = Output(new Foo)
    val in  = Input(new Bar)
  })
  io.out := io.in // This is an error due to mismatched fields
}

Generally in Object-Oriented Programming, the subtyping relation between Foo and Bar means that for any function that accepts a Foo, it should also accept a Bar (see [7]). Unfortately, this is not true for inheritance of Bundles in Chisel3, because attempting to connect a Foo and a Bar is an error. While this issue would be pretty difficult to fix, DataView provides a way of dealing with it:

class MyModule extends Module {
  val io = IO(new Bundle {
    val out = Output(new Foo)
    val in  = Input(new Bar)
  })
  io.out := io.in.viewAs[Foo]

The viewAs method could verify that the type being cast to is a superclass of the current type and then we know it's safe to use a stringly mapping.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment