FPGA開発日記

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

Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (2. シミュレーションモデルの作成)

f:id:msyksphinz:20180702001454p:plain

ハードウェア記述言語であるChiselのチュートリアルを試してみている。 ChiselはScalaをベースにしているDSL(ドメイン特化言語)だ。 したがって、Scalaを勉強しながら進めていく必要がある。

Chiselの理解のために、簡単なCPUを作ってChiselを使いシミュレーションをしてみることにした。 だいたい考えているのはおおよそこんな形だ。CPUとメモリをつなぐインタフェースを持っており、CPUが命令をフェッチして実行する。

f:id:msyksphinz:20180703020306p:plain

まずはCPUから命令をフェッチする部分を考えてみることにしたが、命令をフェッチするためにはメモリにあらかじめ値を書き込んでおかなければならない。 Verilogを使う場合はreadmemhなどを使えば一発なのだが、そうもいかないのでいろいろと調査して、結局テストベンチの外からデータを流し込むことにした。

とりあえずメモリに外部との接続ポートを設けて(本当はCPUのバスと共有すべきなんだけど)、そこからデータを流し込むことにした。 以下のようなChiselのテストパタンを記述した。

  • src/test/scala/cpu/CpuTests.scala
  var mem_init = Array (
    0x041b0010,
    0x141301f4,
    0x2573f140,
    0x05970000,
    0x85930745,
...
    0x00000000,
    0x00000000
  )
...
  for (addr <- 0 to mem_init.length-1) {
    poke (cpu_tb.io.i_memReq, 1)
    poke (cpu_tb.io.i_memAddr, addr)
    poke (cpu_tb.io.i_memData, mem_init(addr) & 0x0000FFFFFFFFL)

    step(1)
  }

最後のpokeを使って、外部からメモリへとデータを書き込んでいる。 ちなみに、mem_init(addr) & 0x0000FFFFFFFFLという「そんなのいらんやろ」みたいな記述は、Scalaは符号なし型が存在しない?という制約から、無理やり符号拡張を防ぐための処理だ。 これがないとChiselでコンパイルエラーになる。

メモリに値を書き込んだら、外部ピンをAssertしてCPUのフェッチを開始する。

  • src/test/scala/cpu/CpuTests.scala
  step(1)
  step(1)
  poke (cpu_tb.io.i_run, 1)
  step(1)

するとフェッチが始まるので、とりあえずその値をキャプチャする。とりあえずまっすぐフェッチしていくだけなので、フェッチアドレスはサイクル毎に+4で上がっていくはずだ。

  for (step_idx <- 0 to 10 by 1) {
    val hexwidth = 8

    expect(cpu_tb.io.o_DebugInstReq,  1)
    expect(cpu_tb.io.o_DebugInstAddr, step_idx * 4)

    printf(s"<Info: Step %02d Instruction %0${hexwidth}x is fetched>\n", step_idx, peek(cpu_tb.io.o_DebugInstData))

    step(1)
  }

(まあここまで来るのに何回もエラーだらけでやり直したんだけど、)実行した結果、以下のようになった。

$ sbt 'testOnly cpu.CpuTopTester -- -z Basic'
...
[info] [0.001] SEED 1530550195363
<Info : Address 0 : Write 41b0010>
<Info : Address 1 : Write 141301f4>
<Info : Address 2 : Write 2573f140>
<Info : Address 3 : Write 5970000>
<Info : Address 4 : Write 85930745>
<Info : Address 5 : Write 84020000>
<Info : Address 6 : Write 0>
<Info : Address 7 : Write 0>
<Info : Address 8 : Write 0>
<Info : Address 9 : Write 0>
<Info : Address a : Write 0>
<Info : Address b : Write 0>
<Info : Address c : Write 0>
<Info : Address d : Write 0>
<Info : Address e : Write 0>
<Info : Address f : Write 0>
<Info : Address 10 : Write 2573f140>
<Info : Address 11 : Write 5970000>
<Info : Address 12 : Write 859303c5>
<Info : Address 13 : Write 731050>
<Info : Address 14 : Write bff50000>
<Info : Address 15 : Write 0>
<Info : Address 16 : Write 0>
<Info : Address 17 : Write 0>
<Info : Address 18 : Write 0>
<Info : Address 19 : Write 0>
<Info : Address 1a : Write 0>
<Info : Address 1b : Write 0>
<Info: Step 00 Instruction 041b0010 is fetched>
<Info: Step 01 Instruction 141301f4 is fetched>
<Info: Step 02 Instruction 2573f140 is fetched>
<Info: Step 03 Instruction 05970000 is fetched>
<Info: Step 04 Instruction 85930745 is fetched>
<Info: Step 05 Instruction 84020000 is fetched>
<Info: Step 06 Instruction 00000000 is fetched>
<Info: Step 07 Instruction 00000000 is fetched>
<Info: Step 08 Instruction 00000000 is fetched>
<Info: Step 09 Instruction 00000000 is fetched>
<Info: Step 10 Instruction 00000000 is fetched>
test CpuTop Success: 24 tests passed in 48 cycles taking 0.083588 seconds
[info] [0.064] RAN 43 CYCLES PASSED
[info] CpuTopTester:
[info] CPU
[info] Basic test using Driver.execute
[info] - should be used as an alternative way to run specification
[info] ScalaTest
[info] Run completed in 1 second, 963 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 3 s, completed Jul 3, 2018 1:49:57 AM

アドレスが順調に伸びていって、期待通りのフェッチができていることが分かる。とりあえず成功だ。

f:id:msyksphinz:20180703015121p:plain

ちなみに失敗すると以下のようになる。デバッグきつい。

f:id:msyksphinz:20180703015320p:plain

こうしてデバッグしていて気がついたのは、外部のテストベンチからテストするとき、デバッグピンは必ず外に出さないといけないらしい?

つまり、今回は内部のCPUのフェッチピントフェッチアドレスを見たわけだけど、これをテストするためにはわざわざDUTの外に波形を持ってこなければならないのかもしれない。 これは面倒くさい。。。

  • src/main/scala/cpu/cpu.scala
class CpuTop extends Module {
  val io = IO (new Bundle {
    val i_run     = Input(Bool())
...  // アサーションを打ちたいときは、必ず外にピンを出してこなければならない?
    val o_DebugInstReq  = Output(Bool())
    val o_DebugInstAddr = Output(UInt(8.W))
    val o_DebugInstData = Output(UInt(32.W))
  })
...

- `src/test/scala/cpu/CpuTests.scala`

// 参照するときも必ずテストベンチの外部ポートに出してく必要がある? expect(cpu_tb.io.o_DebugInstReq, 1) expect(cpu_tb.io.o_DebugInstAddr, 0)

Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (1. デザインの作成)

f:id:msyksphinz:20180702001454p:plain

RISC-Vの実装であるRocket-ChipはChiselと呼ばれるDSLで記述されているのだが、この書き方やテクニックについてはあまり知られていない(まあ知る必要もないが...)

ただし、AWSで動作するFireSimを改良したりだとか、Rocket-Coreを改造する場合にはこの知識は必須になるので、Chiselのチュートリアルを試して、理解してみることにした。

参考にしたのは以下の2つのリポジトリだが、このリポジトリを見ながら考えること以外に、参考になる文献が全くないのでかなりきつい。

github.com

github.com

とりあえず簡単なCPUを作ってみたいと思い、メモリから命令をフェッチするだけの簡単なステートマシンを作ってみたのだが、、、

  • src/main/scala/cpu/cpu.scala
class Cpu extends Module {
  val io = IO (new Bundle {
    val o_instAddr = Output(UInt(8.W))
    val o_instReq  = Output(Bool())

    val i_instAck  = Input(Bool())
    val i_instData = Input(UInt(32.W))
  })

  val r_inst_addr = RegInit(0.U(8.W))
  val r_inst_en   = RegInit(true.B)

  when(io.i_instAck) {
    r_inst_addr := r_inst_addr + 4.U(8.W)
  }

  io.o_instAddr := r_inst_addr
  io.o_instReq  := r_inst_en
}
  • src/main/scala/cpu/memory.scala
class Memory extends Module {
  val io = IO(new Bundle {
    val i_wen    = Input(Bool())
    val i_wrAddr = Input(UInt(8.W))
    val i_wrData = Input(UInt(32.W))

    val i_ren    = Input(Bool())
    val i_rdAddr = Input(UInt(8.W))
    val o_rdData = Output(UInt(32.W))
    val o_rdEn   = Output(Bool())
  })

  val memory = Mem(256, UInt(32.W))
  val r_en   = Reg(Bool())

  when (io.i_wen) {
    memory(io.i_wrAddr) := io.i_wrData
  }

  r_en := io.i_ren

  io.o_rdData := memory(io.i_rdAddr)
  io.o_rdEn  := r_en
}

Topモジュールは以下のようにして定義した。CPUとメモリを接続しただけだ。

  • src/main/scala/cpu/cpu.scala
class CpuTop extends Module {
  val io = IO (new Bundle)

  val memory = Module(new Memory)
  val cpu    = Module(new Cpu)

  // Connect CPU and Memory
  memory.io.i_ren    := cpu.io.o_instReq
  memory.io.i_rdAddr := cpu.io.o_instAddr

  cpu.io.i_instAck   := memory.io.o_rdEn
  cpu.io.i_instData  := memory.io.o_rdData
}

これでコンパイルして実行してみたのだが、"No implicit clock and reset."となりうまく行かない。

[info] CpuTopTester:
[info] Basic test using Driver.execute
[info] - should be used as an alternative way to run specification *** FAILED ***
[info]   chisel3.internal.ChiselException: Error: No implicit clock and reset.
[info]   at chisel3.internal.throwException$.apply(Error.scala:13)
[info]   at chisel3.internal.Builder$.forcedClockAndReset(Builder.scala:209)
[info]   at chisel3.internal.Builder$.forcedReset(Builder.scala:212)
[info]   at chisel3.core.Module$.reset(Module.scala:75)
[info]   at chisel3.core.printf$$anonfun$apply$2.apply(Printf.scala:89)
[info]   at chisel3.core.printf$$anonfun$apply$2.apply(Printf.scala:89)
[info]   at chisel3.core.WhenContext$$anonfun$1.apply(When.scala:72)
[info]   at chisel3.core.WhenContext$$anonfun$1.apply(When.scala:72)
[info]   at scala.Option.foreach(Option.scala:257)
[info]   at chisel3.core.WhenContext.<init>(When.scala:72)

なんとか修正していく。

「量子コンピュータ・超並列計算のからくり」を購入

師匠のブログでおすすめとのことで、すごく分かりやすいよとアピールを受けたので購入。

なるほど、このサイズで横書きというのはなかなか珍しい。これから時間を見つけて読みます。

RISC-Vにも対応できるJTAG Debugger ARM-USB-TINY-Hを買った

f:id:msyksphinz:20180630131007p:plain

Freedom SoCのデバッグをしたいので、ARMのJTAG Debuggerを購入した。だいたい5000円くらい。

Strawberry Linuxで購入できるとのことだけど、クレジットカードに対応していなくて代引きのみ。 そんなの仕事してる最中にどうやって払えっていうんだよ!

というわけでDigi-Keyで購入。到着まで3~4日くらい。

あとは接続用のケーブルを買っていないので秋葉原に行って買ってこなきゃ...

高速C++コンパイラZapccの試行(2. GCC / LLVM / zapcc でのコンパイル速度比較)

https://www.zapcc.com/wp-content/uploads/2018/06/xcopy-zapcc-logo.png.pagespeed.ic.HTEiBd-jW-.webp

zapccは先日発表されたClangをベースとしたコンパイラである。基本的な高速化の手法についてはいろんなところで公開されているのでそちらをチェックして欲しい。

サーバクライアント方式にすることで高速化を図っているということなのだが、実際にはどの程度の速度が出るのか挑戦してみたい。

ということで、自分で管理しているRISC-VシミュレータをGCC/LLVM/zapccでコンパイルして、コンパイル速度を比較してみたい。

調査したのは以下のコンパイラについて自作RISC-Vシミュレータをコンパイルしてその速度を比較する。 ビルド環境はCMakeで作ってあるのでCC, CXXを変更してコンパイル速度を見る。 ここではtimeコマンドを使って、user時間のみを計測した。

  • 使用バージョン
$ clang++ --version
clang version 7.0.0 (http://llvm.org/git/clang.git 7d013cb1a231fb165b41120e8aa786b044b0b8bd) (http://llvm.org/git/llvm.git 04d15315b2afc0be0839c9d9045644841c384455)
Target: x86_64-unknown-linux-gnu
Thread model: posix

$ zapcc++ --version
clang version 6.0.0 (trunk) (https://github.com/yrnkrn/zapcc.git 2c88635dd66151adad5119c4929a81d8d428c739)
Target: x86_64-unknown-linux-gnu
Thread model: posix

$ gcc --version
gcc (GCC) 6.2.0
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ CC=zapcc CXX=zapcc++ cmake . && make   # zapccによるビルド
$ CC=clang CXX=clang++ cmake . && make # LLVMによるビルド
$ CC=gcc CXX=c++ cmake . && make # gccによるビルド

それぞれ結果は以下のようになった。

  • zapcc : 0m13.323s
  • LLVM : 0m39.539s
  • gcc : 0m47.468s

なるほど、zapcc、高速だ。っていうかClangもgccより速いんですね。LLVM系列にビルド環境は移行しようかな。

f:id:msyksphinz:20180706230639p:plain

AWSで動作するRISC-VデザインFireSimのカスタマイズ : オリジナルデバイスをVerilog付きでシミュレーションする

前回の記事で、どうにかFixedInputStreamが実行できるようになったが、これはテストベンチまですべてScalaで記述して常に同じ動作を繰り返す環境になっている。 これは嫌なので、Verilogなどを接続して外部から引数を渡し、自由にシミュレーションを実行できるようにしたい。

というわけで、以下のチュートリアルを進めて、さらにFireChipのオリジナルデザインを拡張していく。

f:id:msyksphinz:20180628023357p:plain

Creating Simulation Model — FireSim documentation

まずは、ChiselのデザインとVerilogのデザインを接続するインタフェースを作らなければならない。 そこで、「実際にはVerilogのデザイン」なのだけれども、まずはScalaで型を作る。これにはBlackBoxというクラスを使用する。

class SimInputStream(w: Int) extends BlackBox(Map("DATA_BITS" -> IntParam(w))) {
  val io = IO(new Bundle {
    val clock = Input(Clock())
    val reset = Input(Bool())
    val out = Decoupled(UInt(w.W))
  })
}

このインタフェースを接続するためにSimInputStreamConfigコンフィグレーションを作成する。 さらにconnectSimInputを定義し、VerilogScalaをインタフェースするためのしくみを作る。

  • src/main/scala/example/Config.scala
class WithSimInputStream extends Config((site, here, up) => {
  case BuildTop => (clock: Clock, reset: Bool, p: Parameters) => {
    val top = Module(LazyModule(new ExampleTopWithInputStream()(p)).module)
    top.connectSimInput(clock, reset)
    top
  }
})

class SimInputStreamConfig extends Config(
  new WithSimInputStream ++ new BaseExampleConfig)
  • src/main/scala/example/InputStream.scala
 trait HasPeripheryInputStream { this: BaseSubsystem =>
   private val portName = "input-stream"
@@ -30,6 +41,13 @@ trait HasPeripheryInputStreamModuleImp extends LazyModuleImp {
     val fixed = Module(new FixedInputStream(data, outer.streamWidth))
     stream_in <> fixed.io.out
   }
+
+  def connectSimInput(clock: Clock, reset: Bool) {
+    val sim = Module(new SimInputStream(outer.streamWidth))
+    sim.io.clock := clock
+    sim.io.reset := reset
+    stream_in <> sim.io.out
+  }
 }

そしてVerilog/C++のインタフェースを作成して接続していく。これは長いしCopy&Pasteなので割愛。

要点としては、Verilogで定義されたSimInputStreamScalaで定義されたSimInputStreamconnectSimInputで接続する、ということ。

これができればあとはデータをC++インタフェース側からひたすら流し込むだけである。

  • src/main/resources/vsrc/SimInputStream.v
module SimInputStream #(DATA_BITS=64) 
   (
    input                    clock,
    input                    reset,
    output                   out_valid,
    input                    out_ready,
    output [DATA_BITS-1:0] out_bits
    );
   
   bit                     __out_valid;
...

これでコンパイルを行い、シミュレーションを行った。

$ make CONFIG=SimInputStreamConfig
$ dd if=/dev/urandom of=instream.img bs=32 count=1
1+0 records in
1+0 records out
32 bytes copied, 0.000306216 s, 105 kB/s
$ hexdump instream.img
0000000 1d2b e182 6629 006e 46be 3532 06c6 f1f2
0000010 2d00 aece 4ef7 d17b 4500 513a fd1e ca96
0000020
$ ./simulator-example-SimInputStreamConfig +instream=instream.img ../tests/input-stream.riscv
006e6629e1821d2b
f1f206c6353246be
d17b4ef7aece2d00
ca96fd1e513a4500

SiFive社から発表されたRISC-Vコアまとめ

f:id:msyksphinz:20180628004733p:plain

2018/11/01追記。だいぶ増えてきた。表更新。

f:id:msyksphinz:20181101230035p:plain
  • U7, S7, E7 Series
U74-MC U74-MC U74 Standard S76-MC S76 Standard E76-MC(Main-Core) E76-MC(Sub-core) E76 Standard
Core Type U74 RV64C S7 RV64IMAC RV64GC RV64GC RV64GC RV32IMAFC RV32IMAFC
L1 Icache 32kB 16kB 32kB 32kB, ITIM option 32kB, ITIM option 32kB, ITIM option 32kB, ITIM option
L1 Dcache 32kB 8kB DTIM 32kB 32kB, FIO RAM option 32kB, FIO RAM option 32kB, FIO RAM 32kB, FIO RAM
Memory Protection 8 8 8
Local Interruption Per Core
Core Local Interrupt CLIC - CLIC
Virtual Memory Support Sv39 - Sv39
L2 ECC 2MB 128kB
DMIPS/MHz 2.5 2.5 2.5 2.3 2.3
Coremark/MHz 4.9 4.9 4.9 4.9 4.9
Pipeline In-order, 8-stage In-order, 8-stage 8-stage 8-stage
  • U5, E5 Series
U54‑MC(Main-Core) U54‑MC(Sub-Core) E54 Standard Core E51 Standard Core
Core Type U54 RV64GC E51 RV64IMAC RV64IMAFDC RV64IMAC
L1 Icache 32kB 16kB 16kB, ITIM option 16kB, ITIM option
L1 Dcache 32kB 8kB DTIM 64kB DTIM 64kB DTIM
Memory Protection 8 8 8 8
Local Interruption Per Core 48 48 16 16
Core Local Interrupt - - 1 timer, 1SW 1 timer, 1 SW
Virtual Memory Support Sv39 - Up to 40 physical address -
L2 ECC 2MB - -
DMIPS/MHz 1.7 1.61 1.7 1.7
Coremark/MHz 2.75 2.73 3.01 3.01
Pipeline In-order, 5-6stage In-order, 5-6stage
  • E3, E2 Series
E34 Standard Core E31 Standard Core E24 Standard Core E21 Standard Core E20 Standard Core
Core Type RV32IMAFC RV32IMAC RV32IMAFC RV32IMAC RV32IMC
L1 Icache 16kB, ITIM option 16kB, ITIM option - - -
L1 Dcache 64kB, DTIM support 64kB, DTIM support - - -
Memory Protection 8 8 8 2 -
Local Interruption Per Core 16 16 ? 127 -
Core Local Interrupt 1 timer, 1 SW 1 timer, 1 SW 1024 Interrupts 127 Interrupts 32 Interrupts
Virtual Memory Support - - - - -
L2 ECC - - - - -
DMIPS/MHz 1.61 1.61 1.38 1.38 1.1
Coremark/MHz 3.01 3.01 3.1 3.1 2.4
Pipeline In-order, 5-6stage In-order, 5-6stage 3-stage 3-stage 2-stage