1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
|
---
layout: docs
title: "DataView"
section: "chisel3"
---
# DataView
_New in Chisel 3.5_
```scala mdoc:invisible
import chisel3._
```
## 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
getVerilogString(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
getVerilogString(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
getVerilogString(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`:
```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
getVerilogString(new TupleExample)
```
Note that this example ignored `DataProduct` which is another required piece (see [the documentation
about it below](#dataproduct)).
All of this is available to users via a single import:
```scala mdoc:reset
import chisel3.experimental.conversions._
```
## Totality and PartialDataView
```scala mdoc:reset:invisible
import chisel3._
import chisel3.experimental.dataview._
```
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
// 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
getVerilogString(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
getVerilogString(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
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
getVerilogString(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/8694558/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
getVerilogString(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
getVerilogString(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_.
|