diff options
| author | Jack Koenig | 2021-09-17 21:01:26 -0700 |
|---|---|---|
| committer | Jack Koenig | 2021-09-17 21:01:26 -0700 |
| commit | 5c8c19345e6711279594cf1f9ddab33623c8eba7 (patch) | |
| tree | d9d6ced3934aa4a8be3dec19ddcefe50a7a93d5a /docs/src/explanations/dataview.md | |
| parent | e63b9667d89768e0ec6dc8a9153335cb48a213a7 (diff) | |
| parent | 958904cb2f2f65d02b2ab3ec6d9ec2e06d04e482 (diff) | |
Merge branch 'master' into 3.5-release
Diffstat (limited to 'docs/src/explanations/dataview.md')
| -rw-r--r-- | docs/src/explanations/dataview.md | 520 |
1 files changed, 520 insertions, 0 deletions
diff --git a/docs/src/explanations/dataview.md b/docs/src/explanations/dataview.md new file mode 100644 index 00000000..2f229bfc --- /dev/null +++ b/docs/src/explanations/dataview.md @@ -0,0 +1,520 @@ +--- +layout: docs +title: "DataView" +section: "chisel3" +--- + +# DataView + +_New in Chisel 3.5_ + +```scala mdoc:invisible +import chisel3._ +import chisel3.stage.ChiselStage.emitVerilog +``` + +## Introduction + +DataView is a mechanism for "viewing" Scala objects as a subtype of `chisel3.Data`. +Often, this is useful for viewing one subtype of `chisel3.Data`, as another. +One can think about a `DataView` as a mapping from a _Target_ type `T` to a _View_ type `V`. +This is similar to a cast (eg. `.asTypeOf`) with a few differences: +1. Views are _connectable_—connections to the view will occur on the target +2. Whereas casts are _structural_ (a reinterpretation of the underlying bits), a DataView is a customizable mapping +3. Views can be _partial_—not every field in the target must be included in the mapping + +## A Motivating Example (AXI4) + +[AXI4](https://en.wikipedia.org/wiki/Advanced_eXtensible_Interface) is a common interface in digital +design. +A typical Verilog peripheral using AXI4 will define a write channel as something like: +```verilog +module my_module( + // Write Channel + input AXI_AWVALID, + output AXI_AWREADY, + input [3:0] AXI_AWID, + input [19:0] AXI_AWADDR, + input [1:0] AXI_AWLEN, + input [1:0] AXI_AWSIZE, + // ... +); +``` + +This would correspond to the following Chisel Bundle: + +```scala mdoc +class VerilogAXIBundle(val addrWidth: Int) extends Bundle { + val AWVALID = Output(Bool()) + val AWREADY = Input(Bool()) + val AWID = Output(UInt(4.W)) + val AWADDR = Output(UInt(addrWidth.W)) + val AWLEN = Output(UInt(2.W)) + val AWSIZE = Output(UInt(2.W)) + // The rest of AW and other AXI channels here +} + +// Instantiated as +class my_module extends RawModule { + val AXI = IO(new VerilogAXIBundle(20)) +} +``` + +Expressing something that matches a standard Verilog interface is important when instantiating Verilog +modules in a Chisel design as `BlackBoxes`. +Generally though, Chisel developers prefer to use composition via utilities like `Decoupled` rather +than a flat handling of `ready` and `valid` as in the above. +A more "Chisel-y" implementation of this interface might look like: + +```scala mdoc +// Note that both the AW and AR channels look similar and could use the same Bundle definition +class AXIAddressChannel(val addrWidth: Int) extends Bundle { + val id = UInt(4.W) + val addr = UInt(addrWidth.W) + val len = UInt(2.W) + val size = UInt(2.W) + // ... +} +import chisel3.util.Decoupled +// We can compose the various AXI channels together +class AXIBundle(val addrWidth: Int) extends Bundle { + val aw = Decoupled(new AXIAddressChannel(addrWidth)) + // val ar = new AXIAddressChannel + // ... Other channels here ... +} +// Instantiated as +class MyModule extends RawModule { + val axi = IO(new AXIBundle(20)) +} +``` + +Of course, this would result in very different looking Verilog: + +```scala mdoc:verilog +emitVerilog(new MyModule { + override def desiredName = "MyModule" + axi := DontCare // Just to generate Verilog in this stub +}) +``` + +So how can we use our more structured types while maintaining expected Verilog interfaces? +Meet DataView: + +```scala mdoc +import chisel3.experimental.dataview._ + +// We recommend putting DataViews in a companion object of one of the involved types +object AXIBundle { + // Don't be afraid of the use of implicits, we will discuss this pattern in more detail later + implicit val axiView = DataView[VerilogAXIBundle, AXIBundle]( + // The first argument is a function constructing an object of View type (AXIBundle) + // from an object of the Target type (VerilogAXIBundle) + vab => new AXIBundle(vab.addrWidth), + // The remaining arguments are a mapping of the corresponding fields of the two types + _.AWVALID -> _.aw.valid, + _.AWREADY -> _.aw.ready, + _.AWID -> _.aw.bits.id, + _.AWADDR -> _.aw.bits.addr, + _.AWLEN -> _.aw.bits.len, + _.AWSIZE -> _.aw.bits.size, + // ... + ) +} +``` + +This `DataView` is a mapping between our flat, Verilog-style AXI Bundle to our more compositional, +Chisel-style AXI Bundle. +It allows us to define our ports to match the expected Verilog interface, while manipulating it as if +it were the more structured type: + +```scala mdoc +class AXIStub extends RawModule { + val AXI = IO(new VerilogAXIBundle(20)) + val view = AXI.viewAs[AXIBundle] + + // We can now manipulate `AXI` via `view` + view.aw.bits := 0.U.asTypeOf(new AXIAddressChannel(20)) // zero everything out by default + view.aw.valid := true.B + when (view.aw.ready) { + view.aw.bits.id := 5.U + view.aw.bits.addr := 1234.U + // We can still manipulate AXI as well + AXI.AWLEN := 1.U + } +} +``` + +This will generate Verilog that matches the standard naming convention: + +```scala mdoc:verilog +emitVerilog(new AXIStub) +``` + +Note that if both the _Target_ and the _View_ types are subtypes of `Data` (as they are in this example), +the `DataView` is _invertible_. +This means that we can easily create a `DataView[AXIBundle, VerilogAXIBundle]` from our existing +`DataView[VerilogAXIBundle, AXIBundle]`, all we need to do is provide a function to construct +a `VerilogAXIBundle` from an instance of an `AXIBundle`: + +```scala mdoc:silent +// Note that typically you should define these together (eg. inside object AXIBundle) +implicit val axiView2 = AXIBundle.axiView.invert(ab => new VerilogAXIBundle(ab.addrWidth)) +``` + +The following example shows this and illustrates another use case of `DataView`—connecting unrelated +types: + +```scala mdoc +class ConnectionExample extends RawModule { + val in = IO(new AXIBundle(20)) + val out = IO(Flipped(new VerilogAXIBundle(20))) + out.viewAs[AXIBundle] <> in +} +``` + +This results in the corresponding fields being connected in the emitted Verilog: + +```scala mdoc:verilog +emitVerilog(new ConnectionExample) +``` + +## Other Use Cases + +While the ability to map between `Bundle` types as in the AXI4 example is pretty compelling, +DataView has many other applications. +Importantly, because the _Target_ of the `DataView` need not be a `Data`, it provides a way to use +`non-Data` objects with APIs that require `Data`. + +### Tuples + +Perhaps the most helpful use of `DataView` for a non-`Data` type is viewing Scala tuples as `Bundles`. +For example, in Chisel prior to the introduction of `DataView`, one might try to `Mux` tuples and +see an error like the following: + +<!-- Todo will need to ensure built-in code for Tuples is suppressed once added to stdlib --> + +```scala mdoc:fail +class TupleExample extends RawModule { + val a, b, c, d = IO(Input(UInt(8.W))) + val cond = IO(Input(Bool())) + val x, y = IO(Output(UInt(8.W))) + (x, y) := Mux(cond, (a, b), (c, d)) +} +``` + +The issue, is that Chisel primitives like `Mux` and `:=` only operate on subtypes of `Data` and +Tuples (as members of the Scala standard library), are not subclasses of `Data`. +`DataView` provides a mechanism to _view_ a `Tuple` as if it were a `Data`: + +<!-- TODO replace this with stdlib import --> + +```scala mdoc:invisible +// ProductDataProduct +implicit val productDataProduct: DataProduct[Product] = new DataProduct[Product] { + def dataIterator(a: Product, path: String): Iterator[(Data, String)] = { + a.productIterator.zipWithIndex.collect { case (d: Data, i) => d -> s"$path._$i" } + } +} +``` + +```scala mdoc +// We need a type to represent the Tuple +class HWTuple2[A <: Data, B <: Data](val _1: A, val _2: B) extends Bundle + +// Provide DataView between Tuple and HWTuple +implicit def view[A <: Data, B <: Data]: DataView[(A, B), HWTuple2[A, B]] = + DataView(tup => new HWTuple2(tup._1.cloneType, tup._2.cloneType), + _._1 -> _._1, _._2 -> _._2) +``` + +Now, we can use `.viewAs` to view Tuples as if they were subtypes of `Data`: + +```scala mdoc +class TupleVerboseExample extends RawModule { + val a, b, c, d = IO(Input(UInt(8.W))) + val cond = IO(Input(Bool())) + val x, y = IO(Output(UInt(8.W))) + (x, y).viewAs[HWTuple2[UInt, UInt]] := Mux(cond, (a, b).viewAs[HWTuple2[UInt, UInt]], (c, d).viewAs[HWTuple2[UInt, UInt]]) +} +``` + +This is much more verbose than the original idea of just using the Tuples directly as if they were `Data`. +We can make this better by providing an implicit conversion that views a `Tuple` as a `HWTuple2`: + +```scala mdoc +implicit def tuple2hwtuple[A <: Data, B <: Data](tup: (A, B)): HWTuple2[A, B] = + tup.viewAs[HWTuple2[A, B]] +``` + +Now, the original code just works! + +```scala mdoc +class TupleExample extends RawModule { + val a, b, c, d = IO(Input(UInt(8.W))) + val cond = IO(Input(Bool())) + val x, y = IO(Output(UInt(8.W))) + (x, y) := Mux(cond, (a, b), (c, d)) +} +``` + +```scala mdoc:invisible +// Always emit Verilog to make sure it actually works +emitVerilog(new TupleExample) +``` + +Note that this example ignored `DataProduct` which is another required piece (see [the documentation +about it below](#dataproduct)). + +All of this is slated to be included the Chisel standard library. + +## Totality and PartialDataView + +A `DataView` is _total_ if all fields of the _Target_ type and all fields of the _View_ type are +included in the mapping. +Chisel will error if a field is accidentally left out from a `DataView`. +For example: + +```scala mdoc +class BundleA extends Bundle { + val foo = UInt(8.W) + val bar = UInt(8.W) +} +class BundleB extends Bundle { + val fizz = UInt(8.W) +} +``` + +```scala mdoc:crash +{ // Using an extra scope here to avoid a bug in mdoc (documentation generation) +// We forgot BundleA.foo in the mapping! +implicit val myView = DataView[BundleA, BundleB](_ => new BundleB, _.bar -> _.fizz) +class BadMapping extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] +} +// We must run Chisel to see the error +emitVerilog(new BadMapping) +} +``` + +As that error suggests, if we *want* the view to be non-total, we can use a `PartialDataView`: + +```scala mdoc +// A PartialDataView does not have to be total for the Target +implicit val myView = PartialDataView[BundleA, BundleB](_ => new BundleB, _.bar -> _.fizz) +class PartialDataViewModule extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] +} +``` + +```scala mdoc:verilog +emitVerilog(new PartialDataViewModule) +``` + +While `PartialDataViews` need not be total for the _Target_, both `PartialDataViews` and `DataViews` +must always be total for the _View_. +This has the consequence that `PartialDataViews` are **not** invertible in the same way as `DataViews`. + +For example: + +```scala mdoc:crash +{ // Using an extra scope here to avoid a bug in mdoc (documentation generation) +implicit val myView2 = myView.invert(_ => new BundleA) +class PartialDataViewModule2 extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + // Using the inverted version of the mapping + out.viewAs[BundleA] := in +} +// We must run Chisel to see the error +emitVerilog(new PartialDataViewModule2) +} +``` + +As noted, the mapping must **always** be total for the `View`. + +## Advanced Details + +`DataView` takes advantage of features of Scala that may be new to many users of Chisel—in particular +[Type Classes](#type-classes). + +### Type Classes + +[Type classes](https://en.wikipedia.org/wiki/Type_class) are powerful language feature for writing +polymorphic code. +They are a common feature in "modern programming languages" like +Scala, +Swift (see [protocols](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html)), +and Rust (see [traits](https://doc.rust-lang.org/book/ch10-02-traits.html)). +Type classes may appear similar to inheritance in object-oriented programming but there are some +important differences: + +1. You can provide a type class for a type you don't own (eg. one defined in a 3rd party library, + the Scala standard library, or Chisel itself) +2. You can write a single type class for many types that do not have a sub-typing relationship +3. You can provide multiple different type classes for the same type + +For `DataView`, (1) is crucial because we want to be able to implement `DataViews` of built-in Scala +types like tuples and `Seqs`. Furthermore, `DataView` has two type parameters (the _Target_ and the +_View_ types) so inheritance does not really make sense—which type would `extend` `DataView`? + +In Scala 2, type classes are not a built-in language feature, but rather are implemented using implicits. +There are great resources out there for interested readers: +* [Basic Tutorial](https://scalac.io/blog/typeclasses-in-scala/) +* [Fantastic Explanation on StackOverflow](https://stackoverflow.com/a/5598107/2483329) + +Note that Scala 3 has added built-in syntax for type classes that does not apply to Chisel 3 which +currently only supports Scala 2. + +### Implicit Resolution + +Given that `DataView` is implemented using implicits, it is important to understand implicit +resolution. +Whenever the compiler sees an implicit argument is required, it first looks in _current scope_ +before looking in the _implicit scope_. + +1. Current scope + * Values defined in the current scope + * Explicit imports + * Wildcard imports +2. Implicit scope + * Companion object of a type + * Implicit scope of an argument's type + * Implicit scope of type parameters + +If at either stage, multiple implicits are found, then the static overloading rule is used to resolve +it. +Put simply, if one implicit applies to a more-specific type than the other, the more-specific one +will be selected. +If multiple implicits apply within a given stage, then the compiler throws an ambiguous implicit +resolution error. + + +This section draws heavily from [[1]](https://stackoverflow.com/a/5598107/2483329) and +[[2]](https://stackoverflow.com/a/5598107/2483329). +In particular, see [1] for examples. + +#### Implicit Resolution Example + +To help clarify a bit, let us consider how implicit resolution works for `DataView`. +Consider the definition of `viewAs`: + +```scala +def viewAs[V <: Data](implicit dataView: DataView[T, V]): V +``` + +Armed with the knowledge from the previous section, we know that whenever we call `.viewAs`, the +Scala compiler will first look for a `DataView[T, V]` in the current scope (defined in, or imported), +then it will look in the companion objects of `DataView`, `T`, and `V`. +This enables a fairly powerful pattern, namely that default or typical implementations of a `DataView` +should be defined in the companion object for one of the two types. +We can think about `DataViews` defined in this way as "low priority defaults". +They can then be overruled by a specific import if a given user ever wants different behavior. +For example: + +Given the following types: + +```scala mdoc +class Foo extends Bundle { + val a = UInt(8.W) + val b = UInt(8.W) +} +class Bar extends Bundle { + val c = UInt(8.W) + val d = UInt(8.W) +} +object Foo { + implicit val f2b = DataView[Foo, Bar](_ => new Bar, _.a -> _.c, _.b -> _.d) + implicit val b2f = f2b.invert(_ => new Foo) +} +``` + +This provides an implementation of `DataView` in the _implicit scope_ as a "default" mapping between +`Foo` and `Bar` (and it doesn't even require an import!): + +```scala mdoc +class FooToBar extends Module { + val foo = IO(Input(new Foo)) + val bar = IO(Output(new Bar)) + bar := foo.viewAs[Bar] +} +``` + +```scala mdoc:verilog +emitVerilog(new FooToBar) +``` + +However, it's possible that some user of `Foo` and `Bar` wants different behavior, +perhaps they would prefer more of "swizzling" behavior rather than a direct mapping: + +```scala mdoc +object Swizzle { + implicit val swizzle = DataView[Foo, Bar](_ => new Bar, _.a -> _.d, _.b -> _.c) +} +// Current scope always wins over implicit scope +import Swizzle._ +class FooToBarSwizzled extends Module { + val foo = IO(Input(new Foo)) + val bar = IO(Output(new Bar)) + bar := foo.viewAs[Bar] +} +``` + +```scala mdoc:verilog +emitVerilog(new FooToBarSwizzled) +``` + +### DataProduct + +`DataProduct` is a type class used by `DataView` to validate the correctness of a user-provided mapping. +In order for a type to be "viewable" (ie. the `Target` type of a `DataView`), it must have an +implementation of `DataProduct`. + +For example, say we have some non-Bundle type: +```scala mdoc +// Loosely based on chisel3.util.Counter +class MyCounter(val width: Int) { + /** Indicates if the Counter is incrementing this cycle */ + val active = WireDefault(false.B) + val value = RegInit(0.U(width.W)) + def inc(): Unit = { + active := true.B + value := value + 1.U + } + def reset(): Unit = { + value := 0.U + } +} +``` + +Say we want to view `MyCounter` as a `Valid[UInt]`: + +```scala mdoc:fail +import chisel3.util.Valid +implicit val counterView = DataView[MyCounter, Valid[UInt]](c => Valid(UInt(c.width.W)), _.value -> _.bits, _.active -> _.valid) +``` + +As you can see, this fails Scala compliation. +We need to provide an implementation of `DataProduct[MyCounter]` which provides Chisel a way to access +the objects of type `Data` within `MyCounter`: + +```scala mdoc:silent +import chisel3.util.Valid +implicit val counterProduct = new DataProduct[MyCounter] { + // The String part of the tuple is a String path to the object to help in debugging + def dataIterator(a: MyCounter, path: String): Iterator[(Data, String)] = + List(a.value -> s"$path.value", a.active -> s"$path.active").iterator +} +// Now this works +implicit val counterView = DataView[MyCounter, Valid[UInt]](c => Valid(UInt(c.width.W)), _.value -> _.bits, _.active -> _.valid) +``` + +Why is this useful? +This is how Chisel is able to check for totality as [described above](#totality-and-partialdataview). +In addition to checking if a user has left a field out of the mapping, it also allows Chisel to check +if the user has included a `Data` in the mapping that isn't actually a part of the _target_ nor the +_view_. + |
