简体   繁体   中英

how to understand the flip in autobundle() and in makeios?

In LazyModule.scala, function AutoBundle() flip the Data(bundleIn) in dangleIn with flipped = true to make autoIO, while in Nodes.scala, function makeIOs() in class sourceNode flip the bundleOut to make IOs, why are they different?

AutoBundle() code in LazyModule.scala:

/** [[AutoBundle]] will construct the [[Bundle]]s for a [[LazyModule]] in [[LazyModuleImpLike.instantiate]],
  *
  * @param elts is a sequence of data containing for each IO port  a tuple of (name, data, flipped), where
  *             name: IO name
  *             data: actual data for connection.
  *             flipped: flip or not in [[makeElements]]
  */
final class AutoBundle(elts: (String, Data, Boolean)*) extends Record {
  // We need to preserve the order of elts, despite grouping by name to disambiguate things.
  val elements: ListMap[String, Data] = ListMap() ++ elts.zipWithIndex.map(makeElements).groupBy(_._1).values.flatMap {
    // If name is unique, it will return a Seq[index -> (name -> data)].
    case Seq((key, element, i)) => Seq(i -> (key -> element))
    // If name is not unique, name will append with j, and return `Seq[index -> (s"${name}_${j}" -> data)]`.
    case seq => seq.zipWithIndex.map { case ((key, element, i), j) => i -> (key + "_" + j -> element) }
  }.toList.sortBy(_._1).map(_._2)
  require(elements.size == elts.size)

  // Trim final "(_[0-9]+)*$" in the name, flip data with flipped.
  private def makeElements(tuple: ((String, Data, Boolean), Int)) = {
    val ((key, data, flip), i) = tuple
    // Trim trailing _0_1_2 stuff so that when we append _# we don't create collisions.
    val regex = new Regex("(_[0-9]+)*$")
    val element = if (flip) data.cloneType.flip() else data.cloneType
    (regex.replaceAllIn(key, ""), element, i)
  }

  override def cloneType: this.type = new AutoBundle(elts: _*).asInstanceOf[this.type]
}

makeIOs() code in Nodes.scala:

/** A node which represents a node in the graph which only has outward edges and no inward edges.
  *
  * A [[SourceNode]] cannot appear left of a `:=`, `:*=`, `:=*, or `:*=*`
  * There are no Mixed [[SourceNode]]s, There are no "Mixed" [[SourceNode]]s because each one only has an outward side.
  */
class SourceNode[D, U, EO, EI, B <: Data](imp: NodeImp[D, U, EO, EI, B])(po: Seq[D])(implicit valName: ValName)
  extends MixedNode(imp, imp)
{
  override def description = "source"
  protected[diplomacy] def resolveStar(iKnown: Int, oKnown: Int, iStars: Int, oStars: Int): (Int, Int) = {
    def resolveStarInfo: String =
      s"""$context
         |$bindingInfo
         |number of known := bindings to inward nodes: $iKnown
         |number of known := bindings to outward nodes: $oKnown
         |number of binding queries from inward nodes: $iStars
         |number of binding queries from outward nodes: $oStars
         |${po.size} outward parameters: [${po.map(_.toString).mkString(",")}]
         |""".stripMargin
    require(oStars <= 1,
      s"""Diplomacy has detected a problem with your graph:
         |The following node appears right of a :=* $oStars times; at most once is allowed.
         |$resolveStarInfo
         |""".stripMargin)
    require(iStars == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node cannot appear left of a :*=
         |$resolveStarInfo
         |""".stripMargin)
    require(iKnown == 0,
      s"""Diplomacy has detected a problem with your graph:
         |The following node cannot appear left of a :=
         |$resolveStarInfo
         |""".stripMargin)
    if (oStars == 0)
      require(po.size == oKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node has $oKnown outward bindings connected to it, but ${po.size} sources were specified to the node constructor.
           |Either the number of outward := bindings should be exactly equal to the number of sources, or connect this node on the right-hand side of a :=*
           |$resolveStarInfo
           |""".stripMargin)
    else
      require(po.size >= oKnown,
        s"""Diplomacy has detected a problem with your graph:
           |The following node has $oKnown outward bindings connected to it, but ${po.size} sources were specified to the node constructor.
           |To resolve :=*, size of outward parameters can not be less than bindings.
           |$resolveStarInfo
           |""".stripMargin
      )
    (0, po.size - oKnown)
  }
  protected[diplomacy] def mapParamsD(n: Int, p: Seq[D]): Seq[D] = po
  protected[diplomacy] def mapParamsU(n: Int, p: Seq[U]): Seq[U] = Seq()

  def makeIOs()(implicit valName: ValName): HeterogeneousBag[B] = {
    val bundles = this.out.map(_._1)
    val ios = IO(Flipped(new HeterogeneousBag(bundles.map(_.cloneType))))
    ios.suggestName(valName.name)
    bundles.zip(ios).foreach { case (bundle, io) => bundle <> io }
    ios
  }
}

This is a pretty complex chunk of code to be looking at (Diplomacy/LazyModule is a very advanced generator), but .flip is the Chisel 2 API equivalent of Flipped(...) in Chisel 3. It's used in rocket-chip via the Chisel 2 compatibility layer ( import Chisel._ as opposed to import chisel3._ ).

Flipped inverts the direction of a type. Note that this is recursive and that types can be bidirectional (which is usually when Flipped is used).

For example:

class Example extends MultiIOModule {
  // .valid and .bits are inputs, .ready is an output
  val in = IO(Flipped(Decoupled(UInt(8.W))))
  // .valid and .bits are outputs, .ready is an input
  val out = IO(Decoupled(UInt(8.W)))
  
  out <> in
}

(Scastie link: https://scastie.scala-lang.org/F91trxakSFSrlVrzk3VxcA )

Basically, when using ready-valid interfaces you often need to "flip" the directions inside your producer or consumer interface.

Let me try if I can answer this question:

  1. AutoBudle part

For the AutoBundle part, it is worth noting that the elts usded by AutoBundle class consturctor is actually coming from Dangles generated by the diplomacy framework. The resolved Dangles of a specific node are actually danglesOut ++ danglesIn . According to the comment, danglesOut are actually Seq of dangles which describe the connections from this node output to other nodes inputs. , while danglesIn means Seq of dangles which describe the connections from this node input from other nodes outputs. . In short words, that means danglesOut is "Dangle abstraction" for an Output port, while danglesIn means Input Port. Also, note that items in DanglesIn Seq has the flipped field to be true, while items in DanglesOut has that field to be false. This is the semantic meaning of flipped field in the Dangles. It strongly implies that flipped means "Input" while "non-flipped" means "Output" under the diplomacy context.

Now, let's circle back to the AutoBundle part. It uses the dangle data to create a chisel Data type. And then use this Data type to construct IOs for the corresponding LazyModuleImp, see code below:

val element = if (flip) data.cloneType.flip() else data.cloneType

This basiclly says if the flipped field inside the Dangle is true, then the direction attached to this Data when constructing IO should be some form of "Input" . Well, now you actually need to delve into the cloneType , binding and directin hot mess in the chisel lib: cloneType is uesd by chisel library to clone/copy an existing Chisel Data object. Why? Maybe it is because of the preference of immutability over mutability in functional programming languages. By creating an exact copy, you dont have to modify the pre-existing object, which may uphold things like thread-safe. So, cloneType is just chisel's way of cloning object. def cloneType: this.type locates in root Data, for all chisel Data subtypes, you have to implment that cloneType thing to tell users how they can clone a fresh object. So, cloneType is highly implementation dependent. Lots of Build-in Data subtypes(Bits(therefore UInt, SInt, Bool), Bundle, DecoupledIO etc.) have built-in cloneType implementations, but if you create new subtypes extending from Record , a user-defined cloneType is still mandatory. Howevevr most subtypes will basically have implementation like (new CusType).asInstanceOf[this.type] . (Bikesheding: I have to say that this.type thing is very wired, and they really should implement the clone stuff using typeclass or F-type. But that may kill compatability.) There is another problem, if you read thru the Data codebase, there are fields like: specifiedDirection , direction and bind . specifiedDirection means the direction of a data specified by the user. While direction means the actual resolved direction combining the specifiedDirection and parent constraints (Input, Output, and also Bidirection if the data type is aggregate). Binding is a little bit tricky, It means wheter this data type has been bounded to a hardware constuctor or not. For example, If you define UInt(32.W), it is just a structural declaration not bounding to any harewire yet(they call this unbound type, or chiselType). However, if you wrap it using Wire() , Reg() or IO() , it will mark the data type you just declared as bounded type(hardware type), there are various forms of binding( PortBinding , OpBinding , MemoryPortBinding , RegBinding , WireBinding , ChildBinding , etc.)

After clarification about the cloneType stuff above, let's go thru this loc again:

val element = if (flip) data.cloneType.flip() else data.cloneType

After data.cloneType , you may want to know whether the cloneType call reserves the direction and binding info? Like I said before, this is a very implementation dependent problem, and you need to check it case by case. However, mostly the implementation will be just one loc: (new CusType).asInstanceOf[this.type] , considering the binding process all happened in the wrapper call( IO(iodef) will return the bounded Type, but it is still just the exactly the same type as iodef), consequently the binding info will be gone after calling cloneType generally. In terms of direction, considering code below:

class TestBundle extends Record{
  val a = Input(UInt(8.W))
  val b =  Output(UInt(8.W))
  val elements =ListMap("a" -> a, "b" -> b)
  override def cloneType: this.type = new TestBundle().asInstanceOf[this.type]
}

class TestModule extends Module{
  val testBundle = new TestBundle
  val flippedTestBundle = Flipped(testBundle)
  val io = IO(flippedTestBundle)
  val cloneIO = io.cloneType
  io.a := io.b
}

io is bounded type for chisel type TestBundle , the direction info has all been resolved(TestBundle->Bidirection, a->Output, b-> Input, note that the resolved direction of a and b are flipped). However, when you call io.cloneType , it will return a brand new TestBundleL: override def cloneType: this.type = new TestBundle().asInstanceOf[this.type] . So, it is clear that all effect of val flippedTestBundle = Flipped(testBundle) and val io = IO(flippedTestBundle) will be removed. The only direction info left in the cloneIO is just specifiedDirection of a(Input) and b(Output). I read somewhere that chisel developers do not suggest applying directionality on Bundle, unless the bundle itself is really Bi-directional. Now, let's review this loc: val element = if (flip) data.cloneType.flip() else data.cloneType . it indicates if the port(resolved in the diplomacy framework) is input, just flip the direction of data attached in this port. And if the port is Output, then just leave it as it is. I think it is fairly straigntforward right? Also worth noting that in Chisel 2 compatibility layer, the user specified direction Flipped are resolved to be Input if parent direction unspecified. (Unspecified-> Output) I hope this could answer your problem about the AutoBundle. Now, let's delve into the makeIOs

  1. makeIOs First thing first, Why makeIOs?Note that only SourceNode and SinkNode have makeIOs , while other node types dont. Inside the dipolomacy infrasturcture the SourceNode and SinkNode are ends of the DAG graph. There will be situations that you want to extend this diplomacy graph using fixed IO(maybe for testing). In diplomacy graph, SourceNode only has output, while SinkNode has only inputs, but if you want to extend the graph, you would have to make input for the Source part of a graph while make Output for the Sink Part. See the picture below:

网络扩展

So, considering that I already clarify what cloneType and flip above, it might be clear to you why the flip in AutoBundle and MakeIos are opposite.

Hope this answer make things a little clearer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM