FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

ハードウェア記述言語Chiselコンパイラの内部解析(5. Chiselに新しい型と演算子を追加してみる)

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

Chiselを勉強するためにはとりあえず自分専用の拡張Chiselを作ってみるのが良いと思う。 とりあえずは、まずは手っ取り早く新しいデータ型でも追加してみたい。Chiselには以下の基本データ型が存在する。

  • UInt
  • SInt
  • Bool

そこで、これに追加して今度はTIntという型を追加してみる。TIntは特に何もできない新しいデータ型で、役割はUIntと大して変わらない。 そして、TIntに追加する新たな演算子として、代入演算子::=というものを追加してみる。 この演算子も限りなく意味のないものだが、A ::= BBをビット反転してAに格納する。

  • TInt 型を追加する

まず、UInt, SIntなどが定義されているのは./chiselFrontend/src/main/scala/chisel3/Data.scalaで定義されている。 これに新たにTIntクラスとTIntFactoryを追加する。 TIntクラスはUIntクラスをベースにして派生する。

/** A data type for unsigned integers, represented as a binary bitvector. Defines arithmetic operations between other
  * integer types.
  *
  * @define coll [[UInt]]
  * @define numType $coll
  * @define expandingWidth @note The width of the returned $coll is `width of this` + `1`.
  * @define constantWidth  @note The width of the returned $coll is unchanged, i.e., `width of this`.
  */
sealed class TInt private[chisel3] (width: Width) extends UInt(width) with Num[UInt] {
  /** 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
    */
  def ::= (that: TInt)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = {
    this.connect(~that)(sourceInfo, connectionCompileOptions) // scalastyle:ignore line.size.limit
  }
}


// This is currently a factory because both Bits and TInt inherit it.
trait TIntFactory {
  /** Create a UInt type with inferred width. */
  def apply(): UInt = apply(Width())
  /** Create a UInt port with specified width. */
  def apply(width: Width): UInt = new UInt(width)

   /** Create a UInt literal with specified width. */
  protected[chisel3] def Lit(value: BigInt, width: Width): UInt = {
    val lit = ULit(value, width)
    val result = new UInt(lit.width)
    // Bind result to being an Literal
    lit.bindLitArg(result)
  }

  /** Create a UInt with the specified range */
  def apply(range: Range): UInt = {
    apply(range.getWidth)
  }
  /** Create a UInt with the specified range */
  def apply(range: (NumericBound[Int], NumericBound[Int])): UInt = {
    apply(KnownUIntRange(range._1, range._2))
  }
}

このTInt型にはTIntFactoryが追加されている。これはUIntFactoryと同一である。

そして、TInt型には::=型を追加した。これは:=の実装をコピーしているのだが、一点だけ、connectをする前にビット反転の演算子を挿入している。

  def ::= (that: TInt)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = {
    this.connect(~that)(sourceInfo, connectionCompileOptions) // scalastyle:ignore line.size.limit
  }

ついでに、派生元のUIntにも::=演算子がないとコンパイル中に怒られたので追加する。

diff --git a/chiselFrontend/src/main/scala/chisel3/Bits.scala b/chiselFrontend/src/main/scala/chisel3/Bits.scala
index 5a6db7c1..571a3100 100644
--- a/chiselFrontend/src/main/scala/chisel3/Bits.scala
+++ b/chiselFrontend/src/main/scala/chisel3/Bits.scala
@@ -902,6 +902,10 @@ sealed class UInt private[chisel3] (width: Width) extends Bits(width) with Num[U

   private def subtractAsSInt(that: UInt)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): SInt =
     binop(sourceInfo, SInt((this.width max that.width) + 1), SubOp, that)
+
+  def ::= (that: Bits)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = {
+    this.connect(~that)(sourceInfo, connectionCompileOptions) // scalastyle:ignore line.size.limit
+  }
 }

 // This is currently a factory because both Bits and UInt inherit it.

この::=演算子は、実行される場合に呼ばれるのはMonoConnectクラスのメソッドだ。MonoConnectクラスにTInt同士の接続を行うパタンを追加する。

diff --git a/chiselFrontend/src/main/scala/chisel3/internal/MonoConnect.scala b/chiselFrontend/src/main/scala/chisel3/internal/MonoConnect.scala
index ace7be20..833106e7 100644
--- a/chiselFrontend/src/main/scala/chisel3/internal/MonoConnect.scala
+++ b/chiselFrontend/src/main/scala/chisel3/internal/MonoConnect.scala
@@ -81,6 +81,8 @@ private[chisel3] object MonoConnect {
         elemConnect(sourceInfo, connectCompileOptions, sink_e, source_e, context_mod)
       case (sink_e: UInt, source_e: UInt) =>
         elemConnect(sourceInfo, connectCompileOptions, sink_e, source_e, context_mod)
+      case (sink_e: TInt, source_e: TInt) =>
+        elemConnect(sourceInfo, connectCompileOptions, sink_e, source_e, context_mod)
       case (sink_e: SInt, source_e: SInt) =>
         elemConnect(sourceInfo, connectCompileOptions, sink_e, source_e, context_mod)
       case (sink_e: FixedPoint, source_e: FixedPoint) =>
  • テストをする

まず、上記の改造を行ったChiselをコンパイルし、ローカル環境にデプロイする。

sbt publishLocal

終了すると、以下のテストコードを作成した。

  • test_tint.scala
package tint_test

import chisel3._

class tint_test extends Module {
  val io = IO(new Bundle {
    val in  = Input(TInt(8.W))
    val out = Output(TInt(8.W))
  })

  io.out ::= io.in
}

object main extends App {
  chisel3.Driver.emitVerilog(new tint_test())
}

TInt型と、::=演算子を使用して配線の接続を行っている。ではさっそくコンパイルしてみよう。

sbt 'runMain tint_test.main`

コンパイルの結果、以下のVerilogファイルが生成された。

module tint_test(
  input        clock,
  input        reset,
  input  [7:0] io_in,
  output [7:0] io_out
);
  assign io_out = ~ io_in; // @[tint_test.scala 11:10]
endmodule

正しく生成されているようだ。

ハードウェア記述言語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

Chiselでtypedef(のようなもの)を実現する方法

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

ハードウェア記述言語ChiselはScalaをベースにした言語であり、Scalaの言語単位をベースにして設計してある。

それでは、ハードウェアの配線や変数を表すためにどのような型が用意してあるかというと、例えば、以下のような型が用意されている。

  • UInt : 任意の幅を定義できる符号なしの整数を格納するための型
  • SInt : 任意の幅を定義できる符号ありの整数を格納するための型
  • Bool : 1ビットのTrue/Falseを格納するための型

例えば、配線とレジスタを定義するためには以下のようにして宣言する。WireRegというクラスに型の情報を与え、実体化させるという訳だ。

val w_a = Wire(UInt(32.W))
val r_a = Reg(Bool())

ところで、Chiselの型の中で主に使うのは上記の3つくらいしかない。しかし、System Verilogにはtypedefにより様々な型を定義できる。 ハードウェアの設計に当たり、どの信号がどの型を持っているのか正確に区別できた方が設計がはかどりやすい。

そこで、Chiselの型をさらに増やすにはどのようにすればよいか、つまりtypedefのようなことを実現するためにはどうすればよいか、調査した。

ScalaにはtypeというTypedefに似たような機能がある。

  type new_type = UInt

例えば、上記のnew_typeを使って以下の3つの値を宣言する。

  type new_type = UInt
  val ADD: new_type = 0.U(2.W)
  val SUB: new_type = 1.U(2.W)
  val AND: new_type = 2.U(2.W)

しかしこれだけではだめで、Chiselで実際に活用するためにはObjectも宣言する。この時、UIntを定義するために使用しているUIntFactoryを継承して使用する。 そしてnew_typeを呼び出すときに型の幅も固定してしまう。例えば、下記だとnew_typeは2ビットに固定される。

  type new_type = UInt
  val ADD: new_type = 0.U(2.W)
  val SUB: new_type = 1.U(2.W)
  val AND: new_type = 2.U(2.W)
  object new_type extends UIntFactory {
    override def apply(): UInt = apply(2.W)
  }

これで型が作れた。さっそく使ってみる。

import pkg._

class typedef_test extends Module
{
  val io = IO(new Bundle {
    val op  = Input(new_type())
    val in0 = Input(UInt(32.W))
    val in1 = Input(UInt(32.W))
    val out = Output(UInt(32.W))
  })

  io.out := 0.U(32.W)
  switch(io.op) {
    is(ADD) { io.out := io.in0 + io.in1 }
    is(SUB) { io.out := io.in0 - io.in1 }
    is(AND) { io.out := io.in0 & io.in1 }
  }
}

入力信号opの型をnew_typeに設定した。実体はUInt(2.W)だが、これだけで少し見栄えが良くなる。

ハードウェア記述言語Chiselコンパイラの内部解析(3. Chiselコンパイルのフローを追いかける)

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

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

どこでChiselからFIRへの変換が行われているのかというと、それはChiselStageEmitterが変換処理を行っているらしい。

  • chisel3/src/main/scala/chisel3/internal/firrtl/Emitter.scala
  private def emit(e: Command, ctx: Component): String = { // scalastyle:ignore cyclomatic.complexity
    val firrtlLine = e match {
      case e: DefPrim[_] => s"node ${e.name} = ${e.op.name}(${e.args.map(_.fullName(ctx)).mkString(", ")})"
      case e: DefWire => s"wire ${e.name} : ${emitType(e.id)}"
      case e: DefReg => s"reg ${e.name} : ${emitType(e.id)}, ${e.clock.fullName(ctx)}"
      case e: DefRegInit => s"reg ${e.name} : ${emitType(e.id)}, ${e.clock.fullName(ctx)} with : (reset => (${e.reset.fullName(ctx)}, ${e.init.fullName(ctx)}))" // scalastyle:ignore line.size.limit
      case e: DefMemory => s"cmem ${e.name} : ${emitType(e.t)}[${e.size}]"
      case e: DefSeqMemory => s"smem ${e.name} : ${emitType(e.t)}[${e.size}]"
      case e: DefMemPort[_] => s"${e.dir} mport ${e.name} = ${e.source.fullName(ctx)}[${e.index.fullName(ctx)}], ${e.clock.fullName(ctx)}" // scalastyle:ignore line.size.limit
      case e: Connect => s"${e.loc.fullName(ctx)} <= ${e.exp.fullName(ctx)}"
      case e: BulkConnect => s"${e.loc1.fullName(ctx)} <- ${e.loc2.fullName(ctx)}"
      case e: Attach => e.locs.map(_.fullName(ctx)).mkString("attach (", ", ", ")")
      case e: Stop => s"stop(${e.clock.fullName(ctx)}, UInt<1>(1), ${e.ret})"
...

ステップを突き詰めていくと、まずChisel3のemitVerilogが呼び出されると、各種コンパイルのステージを通っていきChiselStageというフェーズに入る。この中でEmitterが呼び出され、FIRへの変換が行われるらしい。

f:id:msyksphinz:20190830235936p:plain
Chisel3のemitVerilogからのコンパイルフロー(一部)

ここでDefPrimやらDefMemoryやらで挙動が変わるような仕組みになっているが、これはどこで宣言されているのだろうか?

Chiselでは、例えばレジスタを宣言する場合にDefRegInitなどのオブジェクトが宣言され、これによりオブジェクトの階層が構成されるようになっている。

  • chisel3/chiselFrontend/src/main/scala/chisel3/Reg.scala
object RegInit {
  /** Construct a [[Reg]] from a type template initialized to the specified value on reset
    * @param t The type template used to construct this [[Reg]]
    * @param init The value the [[Reg]] is initialized to on reset
    */
  def apply[T <: Data](t: T, init: T)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): T = {
    if (compileOptions.declaredTypeMustBeUnbound) {
      requireIsChiselType(t, "reg type")
    }
    val reg = t.cloneTypeFull
    val clock = Builder.forcedClock
    val reset = Builder.forcedReset

    reg.bind(RegBinding(Builder.forcedUserModule))
    requireIsHardware(init, "reg initializer")
    pushCommand(DefRegInit(sourceInfo, reg, clock.ref, reset.ref, init.ref))
    reg
  }
...

上記の場合は、DefRegInitというコマンドを登録している。DefRegInitの宣言は以下だ。

  • chisel3/chiselFrontend/src/main/scala/chisel3/internal/firrtl/IR.scala
case class DefRegInit(sourceInfo: SourceInfo, id: Data, clock: Arg, reset: Arg, init: Arg) extends Definition

Moduleの場合は、DefInstanceが呼び出される。

  • chisel3/chiselFrontend/src/main/scala/chisel3/Module.scala
object Module extends SourceInfoDoc {
  /** A wrapper method that all Module instantiations must be wrapped in
    * (necessary to help Chisel track internal state).
...
  */
  /** @group SourceInfoTransformMacro */
  def do_apply[T <: BaseModule](bc: => T)
                               (implicit sourceInfo: SourceInfo,
                                         compileOptions: CompileOptions): T = {
...
    // Handle connections at enclosing scope
    if(!Builder.currentModule.isEmpty) {
      pushCommand(DefInstance(sourceInfo, module, component.ports))
      module.initializeInParent(compileOptions)
    }
    module
  }  
                                

Chisel3で生成されたVerilogのデバッグを捗らせるためのいくつかの技法

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

ChiselはScalaをベースとしたDSLからVerilog-HDLを生成することのできるハードウェア記述言語であるが、ちょっと複雑な記法をすると生成されたVerilogが全く読めなくなる。

例えば、ChiselはModuleの中に関数を定義することができる。Scalaなので以下のような記法は当然可能である。

class pmp_simple extends Module
{
  val io = IO(new Bundle {
    val vld  = Input(Bool())
    val addr = Input(UInt(32.W))
    val en   = Output(Bool())
  })

  def check_region(addr: UInt, base: Int, length: Int) : Bool = {
    val cond = addr >= base.U(64.W) && addr < (base + length).U(64.W)
    return Mux(cond, true.B, false.B)
  }

  io.en := Mux(io.vld, check_region(io.addr, 0x3000, 0x1000), false.B)
}

しかし生成されるVerilogは非常に煩雑で分かりにくい。特にcheck_region()内で宣言された変数condはいったいどこに行ったのだ、となる。

  • 上記のChiselコードをコンパイルした場合の生成されたVerilog。変数condは消え、どこに行ったのか分からない。
  assign _GEN_0 = {{32'd0}, io_addr}; 
  assign _T_12 = _GEN_0 >= 64'h3000; 
  assign _T_14 = _GEN_0 < 64'h4000; 
  assign _T_15 = _T_12 & _T_14; 
  assign io_en = io_vld & _T_15; 

そこで、Chiselには最適化を抑制したり、デバッグのために信号名をキープするためのマクロがある。 これらについて調査した。調査にあたり以下の資料をしっかりと読んだ。

www.youtube.com

あとは以下の記事。非常に分かりやすくて感心した。

jijing.site

  • 方法1. @chiselNameを使う

@chiselNameマクロを使うと、上記の関数内に配置された変数の名前を生成されたVerilogの中でキープする。例えば、Chiselのコードを以下のように変更する。

import chisel3.experimental.chiselName

@chiselName
class pmp_simple(cfg: AddressCfgBase) extends Module
{
  val io = IO(new Bundle {
    val vld  = Input(Bool())
    val addr = Input(UInt(32.W))
    val en   = Output(Bool())
  })

  def check_region(addr: UInt, base: Int, length: Int) : Bool = {
    val cond = addr >= base.U(64.W) && addr < (base + length).U(64.W)
    return Mux(cond, true.B, false.B)
  }

  io.en := Mux(io.vld, check_region(io.addr, 0x3000, 0x1000), false.B)

こうすると、condという変数名を残してくれる。これはデバッグが行いやすい。

  wire  cond;    // 変数condを残す。
  assign _GEN_0 = {{32'd0}, io_addr}; 
  assign _T_12 = _GEN_0 >= 64'h3000; 
  assign _T_14 = _GEN_0 < 64'h4000; 
  assign cond = _T_12 & _T_14;  // condを使用する。
  assign io_en = io_vld & cond; 
  • 方法2. suggestNameを使う

suggestName@chiselNameよりもさらに柔軟性がある。特定の信号に対して、変数名を消さずに残しておくように依頼する。例えば、Chiselのコードを以下のように変更する。

class pmp_simple(cfg: AddressCfgBase) extends Module
{
  val io = IO(new Bundle {
    val vld  = Input(Bool())
    val addr = Input(UInt(32.W))
    val en   = Output(Bool())
  })

  def check_region(addr: UInt, base: Int, length: Int) : Bool = {
    val cond = addr >= base.U(64.W) && addr < (base + length).U(64.W)
    cond.suggestName("special_cond")    // suggestNameを追加する。
    return Mux(cond, true.B, false.B)
  }

  io.en := Mux(io.vld, check_region(io.addr, 0x3000, 0x1000), false.B)

suggestNameによって指定された変数名に書き換えられる。

  wire  special_cond; 
  assign _GEN_0 = {{32'd0}, io_addr}; 
  assign _T_12 = _GEN_0 >= 64'h3000; 
  assign _T_14 = _GEN_0 < 64'h4000; 
  assign special_cond = _T_12 & _T_14; 
  assign io_en = io_vld & special_cond; 
  • 方法3. 変数のdontTouchを使う

変数に対してdontTouchを付加すると、その変数の最適化による除去を抑制する。 これはマニュアルにも出ているので使えるはずなのだが、FIRまで生成された時点で落ちてしまったのでまだ検証が十分ではない? (.firにはdontTouchが効いていたのでChiselでは有効なのかも) 例えば、以下のようなChiselコードでは変数in_dataが除去されるが、

class pmp_simple extends Module
{
  val io = IO(new Bundle {
    val vld  = Input(Bool())
    val addr = Input(UInt(32.W))
    val en   = Output(Bool())
  })

  def check_region(addr: UInt, base: Int, length: Int) : Bool = {
    val cond = addr >= base.U(64.W) && addr < (base + length).U(64.W)
    return Mux(cond, true.B, false.B)
  }

  val in_data = Wire(Bool())
  in_data := Mux(io.vld, check_region(io.addr, 0x3000, 0x1000), false.B))))
  io.en := in_data

これにdontTouchを付加する。

import chisel3.experimental.dontTouch
class pmp_simple extends Module
{
  val io = IO(new Bundle {
    val vld  = Input(Bool())
    val addr = Input(UInt(32.W))
    val en   = Output(Bool())
  })

  def check_region(addr: UInt, base: Int, length: Int) : Bool = {
    val cond = addr >= base.U(64.W) && addr < (base + length).U(64.W)
    return Mux(cond, true.B, false.B)
  }

  val in_data = dontTouch(Wire(Bool())
  in_data := Mux(io.vld, check_region(io.addr, 0x3000, 0x1000), false.B))))
  io.en := in_data

が、FIRまでは出てきたがVerilogの生成に失敗している。

AWSの共有ファイルシステムEFSを試す

f:id:msyksphinz:20190818215623p:plain

AWSで複数のEC2でディスクを共有し、プロジェクトファイルのコピーの無駄を省こうといろいろ試行したのだが、AWSのEBSはどうも複数のEC2にアタッチメントをすることができないらしい。これは初めて知った。

その代わりに、複数のEC2インスタンスで同じファイルを参照するためにはいくつかの方法がある。 S3を使ってSynchronizeする方法もあるが、EFS(Elastic File System)を試してみることにした。

aws.amazon.com

EFSは、EBSと異なり複数のインスタンスでマウントすることができる。 これにより、EFS上にLLVMなどのファイルを配置しておき、ビルドの時はビルドサーバを立ち上げたうえで共有ファイルシステムをアタッチし、ファイルにアクセスすることでS3を介した転送に必要なファイルアクセスを削減する。

とりあえずは、EFSを作成した。EFSを使用するEC2インスタンスとは、同じセキュリティグループを設定する必要があるらしい。 また、セキュリティグループの設定で2049番のポート(つまりNFSのポート)を開けておく必要があった。

f:id:msyksphinz:20190828001913p:plain
EFS作成状況

これをEC2インスタンスからマウントするためには、efsのツールセットをインストールした上でmountコマンドを試す。

sudo yum install -y amazon-efs-utils
sudo apt-get install nfs-common
sudo mkdir efs
sudo mount -t efs fs-[efsのファイルシステムID]:/ efs

これで、マウントできた。

Chisel3で外部モジュールをロードする場合に必要なbuild.sbtの変更点

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

Chisel3を使っていて、どうもメインディレクトリの外部にあるソースコードを参照したい、Mavenに登録してあるわけではないのだけれども、ローカルに作ってあるクラスを参照して作りたいなと思ったとき。

例えば、以下のようなモジュールを開発しており、これを別のプロジェクトから参照したい。

  • other_resource.scala
package other_resource

import chisel3._
import chisel3.util._
import chisel3.Bool

class other_resource extends Module
{
  val io = IO(new Bundle {
    val in1 = Input(UInt(32.W))
    val in2 = Input(UInt(32.W))
    val out = Output(UInt(32.W))
  })

  io.out := io.in1 + io.in2
}
  • ref_other_resource.scala
package ref_other_resource

import chisel3._
import chisel3.util._
import chisel3.Bool

import other_resource._

class ref_other_resource extends Module
{
  val io = IO(new Bundle {
    val in1 = Input(UInt(32.W))
    val in2 = Input(UInt(32.W))
    val out = Output(UInt(32.W))
  })

  val i_ref = Module(new other_resource())
  i_ref.io.in1 := io.in1
  i_ref.io.in2 := io.in2
  io.out := i_ref.io.out
}


object main extends App {
  chisel3.Driver.emitVerilog(new ref_other_resource())
}

それぞれ、ディレクトリの位置関係は以下のようになる。

.
├── build.sbt
├── other_resource
│   ├── build.sbt
│   └── src
│       └── main
│           └── scala
│               └── other_resource
│                   └── other_resource.scala
└── src
     └── main
        └── scala
            └── ref_other_resource
                └── ref_other_resource.scala

bulid.sbtに特に何も細工をしないと、ref_other_resource.scalaからはother_resourceのモジュールは参照できない。 そこで、build.sbtに依存関係を加える。

lazy val other_resource = project
lazy val root = (project in file("."))
  .dependsOn(other_resource)
  .aggregate(other_resource)

詳細な解説は以下のsbt公式マニュアルを参照してほしいが、まずはother_resourceのプロジェクトを登録する(val other_resource = projectというのは簡易的な書き方で、実際にはlazy val other_resource = (project in file("other_resource"))と書かなければならないらしい。変数名とプロジェクトのディレクトリが同一の場合は上記の省略記法が有効となる。

rootはルートディレクトリのプロジェクト。つまり、ref_other_resourceを参照している。ここで、ref_other_resourceプロジェクトに対してother_resourceプロジェクトの依存関係を加えている。DependsOnがそれだ。

この記述により、ref_other_resourceother_resourceを参照することができ、回路を生成することができる。

sbt 'runMain ref_other_resource.main`
[info] Loading settings for project root from build.sbt ...
[info] Loading settings for project other_resource from build.sbt ...
...
[info] Running ref_other_resource.main
[info] [0.004] Elaborating design...
[info] [2.709] Done elaborating.
...
Total FIRRTL Compile Time: 636.1 ms
[success] Total time: 16 s, completed 2019/08/27 3:10:41