FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

ハードウェア記述言語Chiselコンパイラの内部解析(4. Chiselで代入演算子がハードウェアに落ちるまで)

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181105/20181105012126.png

Chiselのコンパイルフローの解析の続き。前の記事は以下。

例えば、Chiselでは以下のようにして入力信号を出力信号につなげる記述が可能になる。

val io = IO(new Bundle {
  val in = Input(Bool())
  val out = Output(Bool())
})
io.out := io.in

この:=という代入演算子は、Scalaで定義されたものではない。Scala演算子オーバーロードを行うことができ、Chisel向けに作られた代入演算子である。 これによりChiselのハードウェアデータ型は、互いに接続することが可能になる。

Chiselのハードウェアデータ型は、UInt, SInt, Boolが定義されており、それぞれのデータはChisel内のBits.scalaに定義がある。

  • chisel3/ChiselFrontend/src/main/scala/chisel3/Bits.scala
sealed class UInt private[chisel3] (width: Width) extends Bits(width) with Num[UInt] {
sealed class SInt private[chisel3] (width: Width) extends Bits(width) with Num[SInt] {
sealed class Bool() extends UInt(1.W) with Reset {

要するに、UInt, SIntBitsクラスを継承しており、BoolUIntを継承している。

f:id:msyksphinz:20190908185610p:plain
Chiselのデータ型のクラス継承関係

Bitsというクラスを継承しているので、その定義を確認してみる。

/** A data type for values represented by a single bitvector. This provides basic bitwise operations.
  *
  * @groupdesc Bitwise Bitwise hardware operators
  * @define coll [[Bits]]
  * @define sumWidthInt    @note The width of the returned $coll is `width of this` + `that`.
  * @define sumWidth       @note The width of the returned $coll is `width of this` + `width of that`.
  * @define unchangedWidth @note The width of the returned $coll is unchanged, i.e., the `width of this`.
  */
sealed abstract class Bits(private[chisel3] val width: Width) extends Element with ToBoolable { //scalastyle:off number.of.methods

Bitsのさらに継承元のクラスであるDataにて、:=演算子が定義されていることが分かる。

  • chisel3/chiselFrontend/src/main/scala/chisel3/Data.scala
  /** Connect this $coll to that $coll mono-directionally and element-wise.
    *
    * This uses the [[MonoConnect]] algorithm.
    *
    * @param that the $coll to connect to
    * @group Connect
    */
  final def := (that: Data)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = this.connect(that)(sourceInfo, connectionCompileOptions) // scalastyle:ignore line.size.limit

さて、実装を見てみると、Dataクラス内のthis.connect()を読んでおり、実体は同じDataクラス内のData::connect()である。

  private[chisel3] def connect(that: Data)(implicit sourceInfo: SourceInfo, connectCompileOptions: CompileOptions): Unit = { // scalastyle:ignore line.size.limit

さらに内部をのぞいてみると、MonoConnect.connect()という関数が呼ばれている。これはつまり一方方向の接続だ。ついでに見てみると、BulkConnect.connect()という関数も存在しており、これはA <> Bのための実装だと思われる。

  • chisel3/chiselFrontend/src/main/scala/chisel3/internal/MonoConnect.scala
private[chisel3] object MonoConnect {
...
  /** This function is what recursively tries to connect a sink and source together
  *
  * There is some cleverness in the use of internal try-catch to catch exceptions
  * during the recursive decent and then rethrow them with extra information added.
  * This gives the user a 'path' to where in the connections things went wrong.
  */
  def connect(  //scalastyle:off cyclomatic.complexity method.length
      sourceInfo: SourceInfo,
      connectCompileOptions: CompileOptions,
      sink: Data,
      source: Data,
      context_mod: RawModule): Unit =
    (sink, source) match {
      // Handle legal element cases, note (Bool, Bool) is caught by the first two, as Bool is a UInt
      case (sink_e: Bool, source_e: UInt) =>
        elemConnect(sourceInfo, connectCompileOptions, sink_e, source_e, context_mod)
      case (sink_e: UInt, source_e: Bool) =>
        elemConnect(sourceInfo, connectCompileOptions, sink_e, source_e, context_mod)
      case (sink_e: UInt, source_e: UInt) =>
...

あとは、変数の型によってどのような接続を行うのか記述してある。elemConnectを見てみると、

  def elemConnect(implicit sourceInfo: SourceInfo, connectCompileOptions: CompileOptions, sink: Element, source: Element, context_mod: RawModule): Unit = { // scalastyle:ignore line.size.limit
    import BindingDirection.{Internal, Input, Output} // Using extensively so import these
...
    // CASE: Context is same module that both left node and right node are in
    if( (context_mod == sink_mod) && (context_mod == source_mod) ) {
      ((sink_direction, source_direction): @unchecked) match {
        //    SINK          SOURCE
        //    CURRENT MOD   CURRENT MOD
        case (Output,       _) => issueConnect(sink, source)
        case (Internal,     _) => issueConnect(sink, source)
        case (Input,        _) => throw UnwritableSinkException
      }
    }

issueConnect()が呼ばれる。最終的にissueConnectpushCommandを呼び出し、この接続をハードウェアコマンドとして登録するわけだ。

  • chisel3/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala
  private def issueConnect(sink: Element, source: Element)(implicit sourceInfo: SourceInfo): Unit = {
    // If the source is a DontCare, generate a DefInvalid for the sink,
    //  otherwise, issue a Connect.
    source.topBinding match {
      case b: DontCareBinding => pushCommand(DefInvalid(sourceInfo, sink.lref))
      case _ => pushCommand(Connect(sourceInfo, sink.lref, source.ref))
    }
  }

まとめると、以下のようになる。

f:id:msyksphinz:20190908185700p:plain