FPGA開発日記

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

ChiselとDiplomacyを使ってオリジナルデザインを作成してみる (3. 外部モジュールとの接続方法検討)

前回の続き。Diplomacyを使って実際にいくつかデザインを作ってみようと思った。Diplomacyを使えばデバッグユニットをその場で生成して、さらに外部と接続して制御したい。そんなデザインを作ってみよう。

CoreComplexモジュールの作成

まずはコアの部分を作る、CoreComplexというモジュールを作り、そこにIFUとメモリをLazyModuleとして配置した。

class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
  val ifu    = LazyModule(new ifu("ifu"))
  val xbar   = LazyModule(new TLXbar)
  val memory = LazyModule(new TLRAM(AddressSet(0x020000000, 0x0ffff), beatBytes = ramBeatBytes))

  xbar.node := TLDelayer(0.0001) := ifu.node
  memory.node := xbar.node

上記の記述では、ifuxbarmemoryの3つをLazyModuleとして宣言し、これを接続する。見てわかる通りcore_complexモジュールはLazyModuleとして宣言されており、これはその名の通りDiplomacyによるパス探索で見つからなければ自動的に削除される。またDiplomacyによる「外交」実行中にパラメータの調整が行われる。

さらにこれに対して外部からメモリアクセスするための専用モジュールとしてtest_loaderを宣言し接続した。

class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
  val loader = LazyModule(new loader("loader"))

  val ifu    = LazyModule(new ifu("ifu"))
  val xbar   = LazyModule(new TLXbar)
  val memory = LazyModule(new TLRAM(AddressSet(0x020000000, 0x0ffff), beatBytes = ramBeatBytes))

  xbar.node := loader.node
  xbar.node := TLDelayer(0.0001) := ifu.node
  memory.node := xbar.node

CoreComplexをインスタンス化するTestHarnessモジュールを作成

次にこのCoreComplexをインスタンス化するTestHarnessモジュールを作成する。このTestHarnessモジュールはシミュレーション用で、実際にチップを起こす場合には使用されない。従ってここに外部プログラムローダなどを配置することになる。

class TestHarness()(implicit p: Parameters) extends Module {
  val io = IO(new Bundle {
    val success = Output(Bool())
  })

  val ldut = LazyModule(new core_complex(4, 5000))
  val dut = Module(ldut.module)

  val dtm = Module(new sim_dtm);
  ldut.loader.module.io.req  := dtm.io.req
  ldut.loader.module.io.addr := dtm.io.addr
  ldut.loader.module.io.data := dtm.io.data
  dtm.io.ready := ldut.loader.module.io.ready
}

class DefaultConfig extends Config(new BaseSubsystemConfig)

この時にLazyModuleとしてcore_complexインスタンス化しており、さらにcore_complexの内部インスタンスmoduledutとして引っ張り出している。core_complexにはLazyではないモジュールとしてmoduleを宣言していた。

class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
  val loader = LazyModule(new loader("loader"))
...
  lazy val module = new LazyModuleImp(this) with UnitTestModule {

    // TLPatternPusher
    ifu.module.io.run := true.B

    io.finished := ifu.module.io.done
    when (ifu.module.io.error) {
      printf("Error is detected")
    }
  }

さらに、dtmとして外部との接続用にデバッグユニットをインスタンス化している。このsim_dtmは実体はBlackBoxモジュールでVerilogとして実装されている。DPIを経由してVerilogモジュールと通信をする仕組みになっている。

  • sim_dtm.v
module sim_dtm(
  input clk,
  input reset,

  output        debug_req_valid,
  input         debug_req_ready,
  output [ 6:0] debug_req_bits_addr,
  output [ 1:0] debug_req_bits_op,
  output [31:0] debug_req_bits_data,

  input         debug_resp_valid,
  output        debug_resp_ready,
  input  [ 1:0] debug_resp_bits_resp,
  input  [31:0] debug_resp_bits_data,

  output [31:0] exit
);

  bit r_reset;

  wire __debug_req_ready = debug_req_ready;
  wire __debug_resp_valid = debug_resp_valid;
  wire [31:0] __debug_resp_bits_resp = {30'b0, debug_resp_bits_resp};
...

この状態でVerilogを生成してみたのだが、FIRRTLの段階で失敗してしまった。ldut.loaderが存在しないらしい。確かにLazyModuleなのでタッチしないとインスタンス化されないと思うのだが、そうはいってもどうやってインスタンス化すればいいんだ?

firrtl.passes.CheckHighFormLike$UndeclaredReferenceException:  @[test_harness.scala 25:30]: [module TestHarness] Reference loader is not declared.
firrtl.passes.CheckHighFormLike$UndeclaredReferenceException:  @[test_harness.scala 26:30]: [module TestHarness] Reference loader is not declared.
firrtl.passes.CheckHighFormLike$UndeclaredReferenceException:  @[test_harness.scala 27:30]: [module TestHarness] Reference loader is not declared.
firrtl.passes.CheckHighFormLike$UndeclaredReferenceException:  @[test_harness.scala 28:16]: [module TestHarness] Reference loader is not declared.

これは、一度接続先をLazyModuleの内部にインスタンスされているModuleに接続することで解決できる。以下のように、LazyModule内部のmoduleに対して接続することでモジュール間を接続できる。

f:id:msyksphinz:20201004233440p:plain
class TestHarness()(implicit p: Parameters) extends Module {
  val io = IO(new Bundle {
    val success = Output(Bool())
  })

  val ldut = LazyModule(new core_complex(4, 5000))
  val dut = Module(ldut.module)

  val dtm = Module(new sim_dtm);
  dut.io.req  := dtm.io.req
  dut.io.addr := dtm.io.addr
  dut.io.data := dtm.io.data
  dtm.io.ready := dut.io.ready
}
  module TestHarness :
    input clock : Clock
    input reset : UInt<1>
    output io : {success : UInt<1>}

    clock is invalid
    reset is invalid
    io is invalid
    inst ldut of core_complex @[test_harness.scala 22:19]
    ldut.clock is invalid
    ldut.reset is invalid
    ldut.auto is invalid
    ldut.io is invalid
    ldut.clock <= clock
    ldut.reset <= reset
    inst dtm of sim_dtm @[test_harness.scala 24:19]
    dtm.ready is invalid
    dtm.data is invalid
    dtm.addr is invalid
    dtm.req is invalid
    ldut.io.req <= dtm.req @[test_harness.scala 25:15]
    ldut.io.addr <= dtm.addr @[test_harness.scala 26:15]
    ldut.io.data <= dtm.data @[test_harness.scala 27:15]
    dtm.ready <= ldut.io.ready @[test_harness.scala 28:16]
class core_complex(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
  val loader = LazyModule(new loader("loader"))

  lazy val module = new LazyModuleImp(this) {
    val io = IO(new Bundle {
      val req   = Input(Bool())
      val addr  = Input(UInt(32.W))
      val data  = Input(UInt(32.W))
      val ready = Output(Bool())
    })

    loader.module.io.req  := io.req
    loader.module.io.addr := io.addr
    loader.module.io.data := io.data
    io.ready := loader.module.io.ready

ChiselとDiplomacyを使ってオリジナルデザインを作成してみる (2. Rocket-Chipの確認)

Diplomacyの勉強をしていて、さてRocket-Chipの場合はどのようにして外部インタフェースと接続しているのか、具体的にはどのようにELFファイルなどのテストパタンをロードしているのか気になってきたので調査してみた。

  • Verilatorの場合

VerilatorはシミュレーションのためのTopファイルとしてC++ファイルが必要となる。これはRocket-Chipのディレクトリでは以下に配置してある。

  • rocket-chip/src/main/resources/csrc/emulator.cc
int main(int argc, char** argv)
{
  unsigned random_seed = (unsigned)time(NULL) ^ (unsigned)getpid();
  uint64_t max_cycles = -1;
  int ret = 0;
  bool print_cycles = false;
...
done_processing:
  if (optind == argc) {
    std::cerr << "No binary specified for emulator\n";
    usage(argv[0]);
    return 1;
  }
  int htif_argc = 1 + argc - optind;
  htif_argv = (char **) malloc((htif_argc) * sizeof (char *));
  htif_argv[0] = argv[0];
  for (int i = 1; optind < argc;) htif_argv[i++] = argv[optind++];

  if (verbose)
    fprintf(stderr, "using random seed %u\n", random_seed);
...
  jtag = new remote_bitbang_t(rbb_port);
  dtm = new dtm_t(htif_argc, htif_argv);

で、具体的にはhtif_argvに使用するELFファイルが指定されて、これがdtm_tに引数として指定されインスタンス化される。

このdtmを操作しているのが、SimDTMというモジュールのようだった。

module TestHarness( // @[freechips.rocketchip.system.DefaultConfig.fir 238618:2]
  input   clock, // @[freechips.rocketchip.system.DefaultConfig.fir 238619:4]
  input   reset, // @[freechips.rocketchip.system.DefaultConfig.fir 238620:4]
  output  io_success // @[freechips.rocketchip.system.DefaultConfig.fir 238621:4]
);
...
  SimDTM SimDTM ( // @[Periphery.scala 257:25 freechips.rocketchip.system.DefaultConfig.fir 238700:4]
    .clk(SimDTM_clk),
    .reset(SimDTM_reset),
    .debug_req_ready(SimDTM_debug_req_ready),
    .debug_req_valid(SimDTM_debug_req_valid),
    .debug_req_bits_addr(SimDTM_debug_req_bits_addr),
    .debug_req_bits_data(SimDTM_debug_req_bits_data),
    .debug_req_bits_op(SimDTM_debug_req_bits_op),
    .debug_resp_ready(SimDTM_debug_resp_ready),
    .debug_resp_valid(SimDTM_debug_resp_valid),
    .debug_resp_bits_data(SimDTM_debug_resp_bits_data),
    .debug_resp_bits_resp(SimDTM_debug_resp_bits_resp),
    .exit(SimDTM_exit)
  );

これは、Scalaのモジュールには直接インスタンス化されているようではないらしい。

  • rocket-chip/src/main/scala/system/TestHarness.scala
class TestHarness()(implicit p: Parameters) extends Module {
  val io = IO(new Bundle {
    val success = Output(Bool())
  })

  val ldut = LazyModule(new ExampleRocketSystem)
  val dut = Module(ldut.module)

  // Allow the debug ndreset to reset the dut, but not until the initial reset has completed
  dut.reset := (reset.asBool | dut.debug.map { debug => AsyncResetReg(debug.ndreset) }.getOrElse(false.B)).asBool

  dut.dontTouchPorts()
  dut.tieOffInterrupts()
  SimAXIMem.connectMem(ldut)
  SimAXIMem.connectMMIO(ldut)
  ldut.l2_frontend_bus_axi4.foreach(_.tieoff)
  Debug.connectDebug(dut.debug, dut.resetctrl, dut.psd, clock, reset.asBool, io.success)
}
  • rocket-chip/src/main/scala/devices/debug/Periphery.scala
class SimDTM(implicit p: Parameters) extends BlackBox with HasBlackBoxResource {
  val io = IO(new Bundle {
    val clk = Input(Clock())
    val reset = Input(Bool())
    val debug = new DMIIO
    val exit = Output(UInt(32.W))
  })

  def connect(tbclk: Clock, tbreset: Bool, dutio: ClockedDMIIO, tbsuccess: Bool) = {
    io.clk := tbclk
    io.reset := tbreset
    dutio.dmi <> io.debug
    dutio.dmiClock := tbclk
    dutio.dmiReset := tbreset

    tbsuccess := io.exit === 1.U
    when (io.exit >= 2.U) {
      printf("*** FAILED *** (exit code = %d)\n", io.exit >> 1.U)
      stop(1)
    }
  }

  addResource("/vsrc/SimDTM.v")
  addResource("/csrc/SimDTM.cc")
}

最後のaddResourceVerilogファイルとC言語のDPI関数を追加している。

  • rocket-chip/src/main/resources/vsrc/SimDTM.v
// See LICENSE.SiFive for license details.
//VCS coverage exclude_file

import "DPI-C" function int debug_tick
(
  output bit     debug_req_valid,
  input  bit     debug_req_ready,
  output int     debug_req_bits_addr,
  output int     debug_req_bits_op,
  output int     debug_req_bits_data,

  input  bit        debug_resp_valid,
  output bit        debug_resp_ready,
  input  int        debug_resp_bits_resp,
  input  int        debug_resp_bits_data
);

module SimDTM(
  input clk,
  input reset,

  output        debug_req_valid,
  input         debug_req_ready,
  output [ 6:0] debug_req_bits_addr,
  output [ 1:0] debug_req_bits_op,
  output [31:0] debug_req_bits_data,

  input         debug_resp_valid,
  output        debug_resp_ready,
  input  [ 1:0] debug_resp_bits_resp,
  input  [31:0] debug_resp_bits_data,

  output [31:0] exit
);

つまり、Diplomacyで外部C言語関数とやり取りをしたい場合、このようなラッパーになるような関数を書いて、DPI関数を通じて直接接続しなければならないということかな。

f:id:msyksphinz:20200112022200p:plain

ChiselとDiplomacyを使ってオリジナルデザインを作成してみる (1. 開発環境の構築)

久しぶりにChiselで何か作ってみたくなった。Chiselを使うならDiplomacyを使わないと意味ないだろ!ということで久しぶりにDiplomacyの資料を取り出して読み直している。少し新しいデザインを作りながら、Diplomacyの作り方を復習している。

f:id:msyksphinz:20201003014732p:plain

Diplomacyを使ったTileLinkスレーブノードの作成

Diplomacyを使ったスレーブノードには、以下の2レイヤが必要となる。

  1. LazyModuleを使ったDiplomacyノードを持つクラス
  2. Moduleを使ったDiplomacyノードに接続される実際のクラス

  3. chisel-hw/src/main/scala/TLUnitTest/TLSlaveReg.scala

class TLSlaveReg(
  address: AddressSet,
  parentLogicalTreeNode: Option[LogicalTreeNode] = None,
  beatBytes: Int = 4,
  val devName: Option[String] = None,
  val dtsCompat: Option[Seq[String]] = None
)(implicit p: Parameters) extends LazyModule
...

実際のノードは以下のように作成する。TLManagerNodeなので、スレーブデバイスとして動作させるわけだ。

  • chisel-hw/src/main/scala/TLUnitTest/TLSlaveReg.scala
  val node = TLManagerNode(Seq(TLManagerPortParameters(
    Seq(TLManagerParameters(
      address            = List(address),
      resources          = device.reg("TLSlaveReg"),
      regionType         = RegionType.IDEMPOTENT,
      executable         = false,
      supportsGet        = TransferSizes(1, beatBytes),
...

実際のノードはLazyModuleImplを使って実装される。LazyModuleImplには、nodeからのDiplomacyノードを引き込む形になっている。

  lazy val module = new LazyModuleImp(this) {
    val (in, edge) = node.in(0)

inは実際のノードの配線を示し、edgeはノードのパラメータなどの情報が入っている。

  • inの使い方は、実際にTileLinkのノードを引き込んで制御するために使う。
  lazy val module = new LazyModuleImp(this) {
    val (in, edge) = node.in(0)
...
    val a = in.a
    val d = in.d
...
    val a_read  = a.bits.opcode === TLMessages.Get
    val a_write = a.bits.opcode =/= TLMessages.Get
    val a_extra = Cat(a.bits.source, a.bits.size)

    in.a.ready := true.B

    when (a.fire && a_write) { printf("A.Write Addr = %x, Data = %x", a.bits.address, a.bits.data) }
    when (a.fire && a_read ) { printf("A.Read  Addr = %x", a.bits.address) }

    when (a_write) {
      counter := counter + a.bits.data
    }
  • edgeの使い方は、TileLink接続ノードのパラメータを参照するときに使う。
  lazy val module = new LazyModuleImp(this) {
    val (in, edge) = node.in(0)

    val baseEnd = 0
    val (sizeEnd,   sizeOff)   = (edge.bundle.sizeBits   + baseEnd, baseEnd)
    val (sourceEnd, sourceOff) = (edge.bundle.sourceBits + sizeEnd, sizeEnd)
...
    d.bits := edge.AccessAck(
      toSource    = a_extra(sourceEnd-1, sourceOff),
      lgSize      = a_extra(sizeEnd-1, sizeOff))

実際に接続してみよう。TLOriginalSlave.scalaを作成して接続してみる。

slavereg0slavereg1を作成した。LazyModuleインスタンス化して、AddressSetをそれぞれ0x000, 0x400, 0x400, 0x800を作成した。

  • chisel-hw/src/main/scala/TLUnitTest/TLOriginalSlave.scala
class TLOriginalSlave(ramBeatBytes: Int, txns: Int)(implicit p: Parameters) extends LazyModule {
...
  val xbar      = LazyModule(new TLXbar)
  val slavereg0 = LazyModule(new TLSlaveReg(AddressSet(0x000, 0x3ff), beatBytes = ramBeatBytes))
  val slavereg1 = LazyModule(new TLSlaveReg(AddressSet(0x400, 0x3ff), beatBytes = ramBeatBytes))

  pushers.zip(model).map{ case (pusher, model) =>
    xbar.node := model.node := pusher.node
  }
  slavereg0.node := xbar.node
  slavereg1.node := xbar.node
...

これでVerilogを生成すると、オリジナルのスレーブノードが接続されたVerilogファイルを生成できた。一応シミュレーションも動いている。もう少しデバッグを進めたいかな。

TLSlaveRegs

以下のようにしてMakefileを作成し、自動的にVerilogが生成されるようにした。

PROJECT ?= freechips.rocketchip.unittest
CONFIG  ?= TLOriginalUnitTestConfig
CONFIG_FIR ?= $(PROJECT).$(CONFIG).fir

JAVA_HEAP_SIZE ?= 8G
JAVA_ARGS ?= -Xmx$(JAVA_HEAP_SIZE) -Xss8M -XX:MaxPermSize=256M

export JAVA_ARGS

include ../../Makefrag-verilator

tilelink: TestHarness.sv
    mkdir -p $(generated_dir_debug)/$(long_name)
    $(VERILATOR) $(VERILATOR_FLAGS) -Mdir $(generated_dir_debug)/$(long_name) \
    -o $(abspath $(sim_dir))/$@ $(verilog) $(cppfiles) -LDFLAGS "$(LDFLAGS)" \
    -CFLAGS "-I$(generated_dir_debug) -include $(model_header_debug)"
    $(MAKE) VM_PARALLEL_BUILDS=1 -C $(generated_dir_debug)/$(long_name) -f V$(MODEL).mk
    ./$@
$ make tilelink CONFIG=TLOriginalSlaveTestConfig

ブログを毎日書き続けるためのテクニック

このブログ「FPGA開発日記」は約2100日で2000記事に到達した。特に2019年と2020年は殆どサボることなく記事を書き続けることができている。ブログの品質は置いておいて、毎日日記を書くことは何となく楽しいものだ。2015年にブログを開設したときに「よしこの日記はなるべく毎日書くようにしよう」と決めて以来休まずに書き続けることができている。

元来物書きは嫌いではない。私はエンジニアだが、仕様書を書くのもそこまで嫌いじゃない。自分の実装するものを綺麗にまとめて、「一点の曇りもなく上手くできてるやろ、ドヤ」みたいにするのは楽しいし、バグ発見の報告書とかまとめて、「回避方法は1.と2.あるけどどうする?ニヤニヤ」みたいなのも何となく楽しい。自分がバグを出すのは嫌だけど。

5年以上にわたってブログを書き続けるにあたり、これほどまでに継続することができたのはいくつかの理由がある。この記事ではブログに限らず、物事を継続するために必要なコトについて私の考え方をまとめる。

毎日書くことが目標ではないが、毎日書くことが目標でもある。

矛盾したような話だが、最初にブログを始めるモチベーションは「毎日書くこと」ではない。ブログを書く目的は「技術を学ぶため」なのだ。新しい技術を学んでそれをまとめることで脳内整理する、これが大前提である。勉強だって机に毎日座ることが目的ではない、そうでしょう?

しかし、ここで鶏と卵の問題が発生する。毎日学ぶためには、とりあえず毎日ブログを書かないと始まらないのだ。とりあえず最初は本来の目的は忘れて、毎日ブログを書いてみることだ。そうすることでブログを書くことが習慣になり、ブログを書くために毎日勉強するというポジティブなループバックが回り始める。エンジンをかけるために、まずはあの手この手でエンジンに着火する必要がある、ということだ。

プロジェクトを2~3個決める

毎日記事を書くためには毎日ネタが必要なのだが、そのためにいくつかのプロジェクトを自分で設定する。ある程度長期的なものが良かろう。「~について調査する」「~を実装する」などで自分でもこれまでにやったことがないものを設定すればモチベーションになる。 私の場合は、普段のネットサーフィンで気になったことや、自分のプロジェクトを決めるときはメモ帳(というかGoogle Keep)を使って管理していることが多い。

f:id:msyksphinz:20200903011404p:plain

何故複数のプロジェクトを決めるかというと、(これは私だけかもしれないが)ある程度気分転換が必要だからだ。気分転換に別のプロジェクトをやるの?というとストイックすぎるかもしれないが、今のプロジェクトに少し飽きたら脱線して別のことを調べてみても良いかもしれない。

定量的な目標を決める

これは上記のプロジェクトにも関連するが、プロジェクトというからには目標を決めたい。この目標を明確にすることが大事なのだ。

  • ~までに...を完成させる。
  • ~について纏めて発表する。技術書典向けに本を書く。

とかでも構わない。特にコードリーディングについては明確な目標が設定しにくいため、ブログにざっくりとしたメモ形式でまとめつつ、技術書典などをターゲットにして本を作ってみてもいいかもしれない。

あとは日付を決めることが大事。経験上「キリがいいところまで行ったらそこで纏めよう」とするのは挫折する場合が多い。定量的な目標がないからだ。「キリがいい」なんて言葉を使わずに1週間に1回とかにしてしまえばいい。

継続的にインプットを行う

ブログを続けるためにはネタの仕入れも十分に行わなければならない。ネタ探しはいつもどん欲に。Twitter上での話題の物に焦点を当ててもいいし、自分の興味のあるものを片っ端から調べてみても良い。 理想的には、インプットする対象はいわゆる抽象的な概念ではなく、自分で手を動かして実践できるものが良い。自分で手を動かすことはより深い理解につながる。どうしても抽象的な話題で手を動かすことができないときは、自分の言葉で要約を作ってブログに書き並べていってもいい。

私はインプットの対象は手を動かせるタイプのものが多い。去年や今年で手を付けたもので言えば、

  • LLVMに独自バックエンドを追加するための試行→LLVMソースコードを読むだけでなく、自分で独自実装を追加して理解を深める。
  • Chiselを使ったDiplomacyの改造→TileLinkを使った既存のソースコードを読むだけではなく、OCPなどに移植して理解を深める。
  • ビルドシステムの勉強→ 自分でビルドシステムを作ってみることによって、Makefileがどのようなカラクリで動いているのか理解できるようになった。
  • RISC-V Vector Extensionについての勉強→マニュアルを読むだけでなく、そのマニュアルを翻訳して公開する。自作命令セットシミュレータに命令を実装して理解を深める。
  • ゼロから作るディープラーニング③の写経 → 写経だけでは物足りないので、Python実装をRubyに置き換えて再実装した。かなり良いところまで実装できたと思う。
  • QEMUの勉強→自分でQEMUをRustで作り直してみる。QEMUの構成について理解を深める。

と、列挙するとキリがないが、常に「自分で手を動かして理解する」という方針を貫いた。これが自分にとってアドレナリンを出す方法であり、「次はこれをやってみよう、あれをやってみよう」と継続的なネタをひねり出すための一手法なのだ。

新しいことを恐れない

新しいことはいつも恐ろしいものだ。コンピュータの構成は上手く抽象化が行われていて、自分の知らないテクノロジは魔法の箱のように見えてしまう。その箱を開くのはとても恐ろしいし一目見ただけでは全く理解できない。しかし、ここで恐れてはいけない。ゆっくりと時間をかけていいので、自分で手を動かしながら少しずつ理解を進めていく。こうした小さな努力を続けていけば、やがてその魔法の箱は単なる技術になり、最終的に身に着けることができるようになると思う。

自分の得意な武器を作ろう

これは、「継続的なネタの投下」と共通している。新しい技術を学ぶとき、その技術を、自分の得意な分野から見つめつつ手を動かしていくと、その技術をより効率的に身に着けることができるかもしれない。

自分の場合の柱になる技術は「RISC-Vに関する知識」と「命令セットシミュレータを実装する技術」だ。この2つの柱をベースにして、いろんなツールのRISC-V拡張を行ってみたり、QEMULLVMRISC-V向けに移植したりして、個々の技術について理解を深めることができた。

番外編「無理なら休め」

本当に忙しすぎてブログを書く時間がないならば、その日は休めばいい。 ただし、ブログを書き続けていると、書かない日は体がかゆすぎてゆっくり寝られないようになっているだろう。もうあなたは継続的アウトプットをしたくてたまらない体になってしまっている。

歳を取ったエンジニアとして腕力のある若手にどうやって立ち向かおうか考えた

f:id:msyksphinz:20201017210711p:plain:w600

この記事はFPGA開発日記の祝2,000記事到達の記念に書いているものです。 普段の記事と比べて非常にエモい内容となっております。

FPGA開発日記を始めたのが2015年の1月4日。それからおよそ5年と10か月で2,000記事に到達した。 計算してみると2,115日での2,000記事達成となっていた。我ながらよく頑張った。

ブログを書き始めてもう5年以上経った。5年も経てば周りの状況も変わるし、生活環境も変わる。

私も歳を取り、決して若いとは言えない年齢になった。昔のように徹夜で勉強とか実装はできなくなったし、肩は凝るし集中力は続かない。夜になるとすごく眠たくなる。仕事が終わったらすぐ眠たくなってしまい、趣味やブログを執筆する時間を取るのがとても難しくなってきた。

私が年を取れば取るほど、若い実装力のある、優秀な人たちが参入してきて、私の何倍ものスピードで成果を出していく。 私が持っている武器と言えば、年の功というか、例えばRISC-Vについてはかなり早い段階で参入したこともあり経験値で言うと少しだけ積んでいると思う。でもそんな経験値とか関係なく、若い人はものすごい実装力で私の経験値を追い越していく(東大のCPU実験とか、私が一生掛かっても出来なさそうな猛烈実装を短時間で仕上げてくる)。

で、そんな人たちが社会に出てきて、ある時はライバル企業のエンジニアとして登場し、あるときは頼もしい仲間になる。 しかしそんな若い人達の中で、年の功しかメリットのない私には何ができるのだろう?

マネージャー?エンジニア集団の中で、若い人の技術と知識をわかっていない人がどうやってマネージメントするんだろう? 下手すれば老害化しそうだ。 評論家?技術で置いてきぼりになった老人の評論など誰も聞いてくれるはずがない。 若い人と一緒に実装?もはや力量と腕力ではかないそうもない。

で、結局どういう方向に向いたって技術を学び続けなければ置いてきぼりされるという結論に至った。 でも、どうやって学ぶ?時間はない。集中力も続かない。昔のように、先輩に聞けば教えてくれるという立場ではない。


話は変わるが、昔学会(ISCA 2019)の論文を大量にダウンロードして、一気に読もうとやる気になったことがある。 しかし、いざダウンロードを始めると大量の論文に圧倒されてしまった。60~70本くらいある。 これを全部読み切るとなれば、24時間体制で読み続けても1か月でも全然足りない。

結局全部読むのは諦め、Abstractだけ翻訳サイトに流して読んだ。

例えば翻訳サイト。これを卑怯と考えるのか、時短のための効率化と考えるかは、人それぞれだ。 とにかく時間がない。圧倒的に体力が足りない。だったら、なるべく現代最強の武器を多用して効率的に勉強しよう。でないと若い人について行けない。


学習においてメリハリが重要だということを痛感する。論文を読む、英語の本を読む。しかしこの場合「英語を理解する」という部分は本質的ではないため省略したい。とにかく本質に早くたどり着き、そこに十分な時間を割きたい。

新しい分野を学びたい。なんだか格好良さそうな、分厚い名著が若い人に人気なようだ。 しかしそんな小難しい、正面から立ち向かっても時間がかかるばかりのものから手を出してしまっては、しょっぱなから心が折れてしまう。格好は悪いかもしれないが「サルでもわかる入門書!」みたいな奴からスタートしよう。

いろんな道具を駆使して若い柔軟な脳に立ち向かおう。若い人に比べて資金的にも少し有利に立っていると思う(なんて言うと汚らしいかもしれないが)。必要なところに、しっかり投資しよう。

そして若い人からも十分に学ぼう。教えを乞うことを恥と思わず、彼らの技術をしっかり学び、分からなければ聞こう。

老人には、老人なりの戦い方があるのだと思う。 歳を取ったエンジニアは、腕力ではなく、自分の経験と技術を最大限活用して若いエンジニアと常に共闘できるようにありたい。

Binary Translation型エミュレータを作る(softfloat-wrapper最新版への対応)

少し前に浮動小数点命令対応時に、Rustのクレートであるsoftfloat-wrapperがRISC-V向けコンパイルに対応していなかったのだが、作者の方が対応してくれたようで、RISC-Vコンパイルに対応してくれたようだ。さっそくアップデートしてみる。Cargo.tomlを以下のように変更してみた。

[dependencies]
mmap = "*"
libc = "*"
num = "*"
memmap = "*"
softfloat-wrapper = { version = "0.1.3", default-features = false, features = ["riscv"] }
spike-dasm-wrapper = "*"
clap = "3.0.0-beta.1"
iced-x86 = "1.8.0"

このようにfeaturesに対して["riscv"]を設定することでRISC-V向けにsoftfloatをコンパイルしてくれるようになり、RISC-V向けシミュレータに使えるようになる。これにより亜流のsoftfloatを使用する必要がなくなった。ありがたい。

$ cargo run -- --elf-file riscv/riscv-tools/riscv-tests/build/isa/rv64ud-p-fclass
$ cargo run -- --elf-file riscv/riscv-tools/riscv-tests/build/isa/rv64uf-p-fclass
00007F17B18B0000 48B84000008000000000 movabs    $0x8000_0040,%rax
00007F17B18B000A 48898508020000       mov       %rax,0x208(%rbp)
00007F17B18B0000 E9B0FF0800           jmp       0x0000_7F17_B193_FFB5
<Convert_Virtual_Address. virtual_addr=0000000080001000 : vm_mode = 0, priv_mode = 3>
store32 : converted address: 0000000080001000 --> 0000000080001000
Result: MEM[0x1000] = 00000001

問題無いようだ。

Rustのfeature機能とは何なのか?

feature機能について知らなかったので調べてみた。

qiita.com

なるほど、コンパイル時のオプションとして特定の機能を有効・無効にできるのか。C/C++のdefineマクロみたいなもんかな?今回の場合には、featuresによりincludeするPATHを変更しているのか。

salsa.debian.org

#[cfg(feature = "8086")]
const SPECIALIZED_PATH: &str = "8086";
#[cfg(feature = "8086-sse")]
const SPECIALIZED_PATH: &str = "8086-SSE";
#[cfg(feature = "arm-vfpv2")]
const SPECIALIZED_PATH: &str = "ARM-VFPv2";
#[cfg(feature = "arm-vfpv2-defaultnan")]
const SPECIALIZED_PATH: &str = "ARM-VFPv2-defaultNaN";
#[cfg(feature = "riscv")]
const SPECIALIZED_PATH: &str = "RISCV";

Binary Translation型エミュレータを作る(TCGの途中で例外を取得する方法の検討)

TCGは分岐命令に到達するまで一連のブロックとして変換されるため、1ブロックで複数の命令が実行されます。もしこのブロック実行中に例外が発生した場合どのようにすればよいでしょうか?例えば以下のような命令列をTCGに変換することを考えます。

 add  x10, x10, 1
    ld   x10, 0(x10)
    sret

LD命令で例外が発生すると、MEPCに現在のプログラムカウンタを保存して例外ルーチンにジャンプする必要があります。しかしこのブロックを変換した時点でTCGにプログラムカウンタの情報は保存されていません。例外が発生した場合に、MEPCにどのようにプログラムカウンタを通知すればよいのか問題になります。

これを解決するために、TCG生成時にプログラムカウンタの情報を保存しておくことを考えます。

    pub fn translate_ld(inst: &InstrInfo) -> Vec<TCGOp> {
        let rs1_addr: usize = get_rs1_addr!(inst.inst) as usize;
        let imm_const: u64 = ((inst.inst as i32) >> 20) as u64;
        let rd_addr: usize = get_rd_addr!(inst.inst) as usize;

        let rs1 = Box::new(TCGv::new_reg(rs1_addr as u64));
        let imm = Box::new(TCGv::new_imm(imm_const));
        let rd = Box::new(TCGv::new_reg(rd_addr as u64));

        let tcg_inst_addr = Box::new(TCGv::new_imm(inst.addr));

        let op = TCGOp::new_helper_call_arg4(CALL_HELPER_IDX::CALL_LOAD64_IDX as usize, *rd, *rs1, *imm, *tcg_inst_addr);
        vec![op]

        // Self::translate_rri(TCGOpcode::LOAD_64BIT, inst)
    }

ヘルパー関数の呼び出しですが、引数を増やして*tcg_inst_addrを第4引数に追加しました。tcg_inst_addrは現在のプログラムカウンタinst.addrを保存しており、例外発生時にこの値(TCG変換時に明らかになっているゲストマシンのプログラムカウンタ)が渡されます。

これによりロード命令であれば、ページテーブルウォーク時にページテーブルエラーが発生した際、エラーの発生したアドレスを引数としてもらっているためMEPCに正しい値を設定可能になります。

    pub fn helper_func_load64(
        emu: &mut EmuEnv,
        rd: u64,
        rs1: u64,
        imm: u64,
        guest_pc: u64,
    ) -> usize {
        let rs1_data = emu.m_regs[rs1 as usize];
        let addr = rs1_data.wrapping_add(imm as i32 as u64);

        println!("load64 : converted address: {:016x}", addr);

        #[allow(unused_assignments)]
        let mut guest_phy_addr :u64 = 0;
        match emu.convert_physical_address(guest_pc, addr, MemAccType::Read) {
            Ok(addr) => { 
                guest_phy_addr = addr; 
                println!("load64 : converted address: {:016x} --> {:016x}", addr, guest_phy_addr);
                emu.m_regs[rd as usize] = emu.read_mem_8byte(guest_phy_addr) as u64;
            }
            Err(error) => {
                print!("Read Error: {:?}\n", error);
                emu.generate_exception(guest_pc, ExceptCode::LoadPageFault, addr as i64);
            }
        };

        return 0;
    }
    pub fn generate_exception(&mut self, guest_pc: u64, code: ExceptCode, tval: i64) {
        println!(
            "<Info: Generate Exception Code={}, TVAL={:016x} PC={:016x}>",
            code as u32, tval, guest_pc
        );

        let epc = guest_pc;
...
        if (medeleg & (1 << (code as u32))) != 0 {
            // Delegation
            self.m_csr.csrrw(CsrAddr::Sepc, epc as i64);
            self.m_csr.csrrw(CsrAddr::Scause, code as i64);
            self.m_csr.csrrw(CsrAddr::Stval, tval as i64);

            tvec = self.m_csr.csrrs(CsrAddr::Stvec, 0 as i64);
            next_priv = PrivMode::Supervisor;
        } else {
            self.m_csr.csrrw(CsrAddr::Mepc, epc as i64);
            self.m_csr.csrrw(CsrAddr::Mcause, code as i64);
            self.m_csr.csrrw(CsrAddr::Mtval, tval as i64);

            tvec = self.m_csr.csrrs(CsrAddr::Mtvec, 0 as i64);
            print!("tvec = {:016x}\n", tvec);
        }

この実装でテストプログラムを走らせてみた。

$ cargo run -- --step --dump-gpr --elf-file /home/msyksphinz/work/riscv/riscv-tools/riscv-tests/build/isa/rv64ui-v-lw
========= BLOCK START =========
11999: Guest PC Address = ffffffffffe02258
<Convert_Virtual_Address. virtual_addr=ffffffffffe02258 : vm_mode = 8, priv_mode = 1>
<Info: VAddr = 0xffffffffffe02258 PTEAddr = 0x0000000080004ff8 : PPTE = 0x20001801>
<Info: VAddr = 0xffffffffffe02258 PTEAddr = 0x0000000080006ff8 : PPTE = 0x200000cf>
  converted physical address = 80002258
  0000006f : j       pc + 0x0
tb_address  = 0x7fe8ba390000
00007FE8BA390000 48B85822E0FFFFFFFFFF movabs    $0xFFFF_FFFF_FFE0_2258,%rax
00007FE8BA39000A 48898508020000       mov       %rax,0x208(%rbp)
00007FE8BA390000 E9F9FF0800           jmp       0x0000_7FE8_BA41_FFFE
00007FE8BA390000 E9F4FF0800           jmp       0x0000_7FE8_BA41_FFF9
x00 = 0000000000000000  x01 = ffffffffffe028ac  x02 = ffffffffffe09660  x03 = 0000000000000003
x04 = 0000000000000000  x05 = 0000000000000008  x06 = 0000000000000000  x07 = ffffffffff00ff00
x08 = 000000000003f000  x09 = 00000000000003e0  x10 = 0000000000000007  x11 = ffffffffffe04000
x12 = ffffffffffe04000  x13 = 0000000000004000  x14 = 0000000000000000  x15 = ffffffffffe01250
x16 = 0000000000001000  x17 = 0000000000000000  x18 = 000000000003f000  x19 = 0000000000000007
x20 = ffffffffffe087e0  x21 = ffffffffffe00000  x22 = 0000000000040000  x23 = ffffffffffe04000
x24 = 0000000020010c5f  x25 = 0000000000000000  x26 = ffffffffffe083f0  x27 = ffffffffffe03000
x28 = 0000000000000000  x29 = 0000000000000000  x30 = 0000000000000000  x31 = 0000000000000000

Result: MEM[0x1000] = 00000007

何とか完走するようになったが、まだテストパタンがFailする。例外の処理を正しく実装しないとまだ難しいなあ。