FPGA開発日記

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

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(2. Cpu0の最初のビルド)

f:id:msyksphinz:20181123225150p:plain

Cpu0のインポートを実施したLLVMの最初のビルドを実施してみる。参考にしたのは以下だ。

Cpu0 architecture and LLVM structure — Tutorial: Creating an LLVM Backend for the Cpu0 Architecture

チュートリアルLLVMのバージョンはどうも古いようで、きちんとパッチが当たらない。 いろいろ改造したので、GitHubに7.0.0に対応したものを構築した。

github.com

一番驚いたのは、llvm/Support/ELFRelocs/Cpu0.definclude/llvm/BinaryFormat/ELFRelocs/Cpu0.defに移動されていたこと。 かなり手こずった。

ここから先はLLVM 7.0.0をベースに構築している。

git clone https://github.com/msyksphinz/llvm.git --branch cpu0_chapter2
cd llvm/tools
git clone https://git.llvm.org/git/clang.git -b release_70
cd ../../
mkdir build-cpu0
cmake -G Ninja -DCMAKE_BUILD_TYPE="Debug" --DLLVM_TARGETS_TO_BUILD="Cpu0" ../llvm
cmake --build . -- -j32

無事にビルドが完了する。次に、llcのバージョンを確認する。

./bin/llc --version
$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM version 7.0.1
  DEBUG build with assertions.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: haswell

  Registered Targets:
...
    bpfel      - BPF (little endian)
    cpu0       - CPU0 (32-bit big endian)
    cpu0el     - CPU0 (32-bit little endian)
    hexagon    - Hexagon

cpu0が入っていることを確認できた。

次に、以下のプログラムをコンパイルしてみる。LLVM-IRに相当するもの?をllvm-disで生成してみる。

$ cat ch3.cpp
int main()
{
  return 0;
}
./bin/clang -target mips-unknown-linux-gnu -c ch3.cpp -emit-llvm -o ch3.bc
$ ./bin/llvm-dis ch3.bc -o -
; ModuleID = 'ch3.bc'
source_filename = "ch3.cpp"
target datalayout = "E-m:m-p:32:32-i8:8:32-i16:16:32-i64:64-n32-S64"
target triple = "mips-unknown-linux-gnu"

; Function Attrs: noinline norecurse nounwind optnone
define dso_local i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  ret i32 0
}

attributes #0 = { noinline norecurse nounwind optnone "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="mips32r2" "target-features"="+mips32r2,-noabicalls" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 7.0.1 (https://git.llvm.org/git/clang.git 64485e37ac4b73bdf0b1834164dee90f7cae2eea) (https://github.com/msyksphinz/llvm.git 5e092d55ef0917451923bc25e05e0b9b50cacff6)"}

とりあえず、それっぽいものが生成できていることは確認できた。

次に、llcコンパイルを行う。ターゲットとしてCpu0を指定した。

./bin/llc -march=cpu0 -relocation-model=pic -filetype=asm ch3.bc -o ch3.cpu0.s

llc: /home/masayuki/others/riscv/llvm/llvm-msyksphinz/tools/llc/llc.cpp:458: int compileModule(char**, llvm::LLVMContext&): Assertion `Target && "Could not allocate target machine!"' failed.
Stack dump:
0.      Program arguments: ./bin/llc -march=cpu0 -relocation-model=pic -filetype=asm ch3.bc -o ch3.cpu0.s
#0 0x00000000038e3a77 llvm::sys::PrintStackTrace(llvm::raw_ostream&) /home/masayuki/others/riscv/llvm/llvm-msyksphinz/lib/Support/Unix/Signals.inc:490:0
#1 0x00000000038e3b08 PrintStackTraceSignalHandler(void*) /home/masayuki/others/riscv/llvm/llvm-msyksphinz/lib/Support/Unix/Signals.inc:554:0
#2 0x00000000038e1b59 llvm::sys::RunSignalHandlers() /home/masayuki/others/riscv/llvm/llvm-msyksphinz/lib/Support/Signals.cpp:67:0
#3 0x00000000038e351c SignalHandler(int) /home/masayuki/others/riscv/llvm/llvm-msyksphinz/lib/Support/Unix/Signals.inc:353:0

"Could not allocate target machine!"というエラーが出てきたが、これは想定内のようだ。 とりあえず、先に進むことにしよう。

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(1. Cpu0のアーキテクチャ)

f:id:msyksphinz:20181123225150p:plain

なんかLLVMのバックエンドの資料を読み漁るの、浮気ばっかりしているが面白そうな資料を見つけたのでこっちに浮気してしまった。

もともとはこっちをやろうとしたのだが、Step by Stepじゃないので根気が続かなくなってしまった。

Writing an LLVM Backend — LLVM 8 documentation

以下の資料は、Cpu0という独自のアーキテクチャ向けのLLVMバックエンドを作成するためのチュートリアルだ。

Cpu0は命令長32-bit、独自のアーキテクチャで、整数命令を備えている。 命令のフォーマットとしては以下の3種類を備えており、割り込みの定義も一応備わっている。

f:id:msyksphinz:20181121010447p:plain

LLVMのバックエンドを組み立てるためには以下のような情報が必要になるらしい。

  • 命令の情報 (InstInfo.td)
  • レジスタの情報 (RegisterInfo.td)
  • Calling Conventionの情報 (CallingConv.td)

が必要になるらしい。

で、命令フォーマットのベースになるのはCpu0InsntrFormats.tdに記述してある。

以下がベースになるCpu0Instクラス。

// Generic Cpu0 Format
class Cpu0Inst<dag outs, dag ins, string asmstr, list<dag> pattern,
               InstrItinClass itin, Format f>: Instruction
{
  // Inst and Size: for tablegen(... -gen-emitter) and 
  // tablegen(... -gen-disassembler) in CMakeLists.txt
  field bits<32> Inst;
  Format Form = f;

  let Namespace = "Cpu0";

  let Size = 4;

  bits<8> Opcode = 0;

  // Top 8 bits are the 'opcode' field
  let Inst{31-24} = Opcode;

  let OutOperandList = outs;
  let InOperandList  = ins;

  let AsmString   = asmstr;
  let Pattern     = pattern;
  let Itinerary   = itin;

  //
  // Attributes specific to Cpu0 instructions...
  //
  bits<4> FormBits = Form.Value;

  // TSFlags layout should be kept in sync with Cpu0InstrInfo.h.
  let TSFlags{3-0}   = FormBits;

  let DecoderNamespace = "Cpu0";

  field bits<32> SoftFail = 0;
}

このクラスを継承する形で、3つのフォーマットを構成する、という理解でいいのかな?

class FL<bits<8> op, dag outs, dag ins, string asmstr, list<dag> pattern,
         InstrItinClass itin>: Cpu0Inst<outs, ins, asmstr, pattern, itin, FrmL>
{
  bits<4>  ra;
  bits<4>  rb;
  bits<16> imm16;

  let Opcode = op;

  let Inst{23-20} = ra;
  let Inst{19-16} = rb;
  let Inst{15-0}  = imm16;
}

さらに、FLを継承する形でArithmetcLogicIクラス(これは1レジスタと1即値による演算命令)を作っており、最終的にADDiu命令が形成される。

// Arithmetic and logical instructions with 2 register operands.
class ArithLogicI<bits<8> op, string instr_asm, SDNode OpNode,
                  Operand Od, PatLeaf imm_type, RegisterClass RC> :
  FL<op, (outs GPROut:$ra), (ins RC:$rb, Od:$imm16),
     !strconcat(instr_asm, "\t$ra, $rb, $imm16"),
     [(set GPROut:$ra, (OpNode RC:$rb, imm_type:$imm16))], IIAlu> {
  let isReMaterializable = 1;
}
// IR "add" defined in include/llvm/Target/TargetSelectionDAG.td, line 315 (def add).
def ADDiu   : ArithLogicI<0x09, "addiu", add, simm16, immSExt16, CPURegs>;

TileLinkはどのようにしてコヒーレントを維持するのか(1. TileLinkのコマンド)

f:id:msyksphinz:20181124195957p:plain

RISC-Vの多くの実装で採用されているTileLinkには、大きく分けて3つのグレードが存在している。

  • TL-UL (TileLink Uncached Lightweight)
  • TL-UH (TileLink Uncached Heavyweight)
  • TL-C (TileLink Cached)

この中で、最も上位のTL-Cはマスター間でのコヒーレントを維持することができる構成になっている。 TileLinkの内部でどのようなプロトコルが動作してコヒーレントを維持するのか、調査した。

参考にしているのは、SiFiveが公開しているTileLink Spefication 1.7だ。

TileLinkには以下の5つのチャネルが用意されている。 この辺りはAXIなどと似ていると思う。

  • Channel A : キャッシュブロックの取得とブロックの読み込み・書き込み権限の取得に用いる。
  • Channel B : キャッシュブロックのProbe処理(後述)などを行うためのチャネルである。
  • Channel C : キャッシュブロックの操作、開放処理などを行うためのチャネルである。
  • Channel D : キャッシュブロックの取得要求に対してデータを返す。
  • Channel E : エラー応答などを取り扱う。

TL-Cで扱えるプロトコルは以下のようになっている。その際に仕様されるChannelの種類も一緒に示している。 (SiFive TileLink Specification 1.7 より抜粋)

f:id:msyksphinz:20181124193031p:plain

ブロックのパーミッションについて。

TL-Cにおいて扱われるブロックには、パーミッションが存在している。 それぞれ、そのブロックは持っているノードがどのような状態なのか、どのような操作が許されるのかを示している。

  • Nothing : 現在、データのコピーをキャッシュしていない。
  • Trunk : キャッシュされたコピーを持っている。書き込みによりデータがアップデートする権限を持っている。データがアップデートされている可能性がある。
  • Hint : メモリアクセスのシリアライズポイントとして機能する。キャッシュされたコピーを持っており、そのコピーに対してRWする権限を持っている。
  • Branch : 読み取り専用のキャッシュコピーを持っているノード。

これに対して、TL-Cのコマンドで以下のような操作を実施する。

  • Prune:権限をダウングレードする。前の権限と現在の権限を記録する。
  • Grow : 権限をアップグレードする。前の権限と現在の権限を記録する。
  • Report : 現在のアクセス権限を報告する。
  • Cap : 元のアクセス許可の内容に関係なく、アクセス許可の変更を行う。

以下も資料から抜粋。コンテンツに対して、Cap, Grow, Prune, Reportでどのように権限が変更されるか。

  • Capは前の権限に依存しないので、問答無用でT(Trunk), B(Branch), N(None)へ変更される。
  • Growは前の権限に依存し、常に上位の権限に移動する(None to Branch, None to Trunk, Branch to Trunk)
  • Pruneは前の権限に依存し、常に下位の権限に移動する(Trunk to Branch, Trunk to None, Branch to None)
  • Reportは単純に権限の報告をするだけで、権限の移動はない。
f:id:msyksphinz:20181124194316p:plain

各コマンドで発行される権限の移動メッセージは以下となる。

  • Acquire(データブロックの取得) : Grow(NtoB, NtoT, BtoT)
  • Probe(データブロックの権限の変更) : Cap(toN, toB, toT)
  • ProbeAck(Probeの応答) : Shrink or Report(TtoB, TtoN, BtoN, TtoT, BtoB, NtoN)
  • ProbeAckData(Probeの応答) : Shrink or Report(TtoB, TtoN, BtoN, TtoT, BtoB, NtoN)
  • Grant (データの応答) : Cap(toN, toB, toT)
  • GrantData(データの応答) : Cap(toN, toB, toT)
  • Release(権限の解放) : Shrink or Report(TtoB, TtoN, BtoN, TtoT, BtoB, NtoN)
  • ReleaseData(権限とデータの解放) : Shrink or Report(TtoB, TtoN, BtoN, TtoT, BtoB, NtoN)

これらのコマンドにより、データの権限の移動が行われるという訳だ。

Chiselを使ってCPUを作ろう(9. テストベンチを走らせる)

Chiselを使って、非常にシンプルなCPUを作ってみるプロジェクト、算術演算命令、ロード・ストア命令、CSR命令が多少動くようになってきたので、RISC-Vのテストパタンを動かしてみてもよさそうな気がしてくる。

f:id:msyksphinz:20181123005953p:plain

riscv-testesに乗っているテストパタンを動かしてどのようになるかチェックしてみたい。そのための環境を構築した。

riscv-testsのパタンはデフォルトだと0x80000000番地に構築されるので、テストパタンを修正して0x00000000番地から始まるように修正する。

diff --git a/isa/Makefile b/isa/Makefile
index 4e1ba20..ca329b6 100644
--- a/isa/Makefile
+++ b/isa/Makefile
@@ -35,6 +35,7 @@ RISCV_PREFIX ?= riscv$(XLEN)-unknown-elf-
 RISCV_GCC ?= $(RISCV_PREFIX)gcc
 RISCV_GCC_OPTS ?= -static -mcmodel=medany -fvisibility=hidden -nostdlib -nostartfiles
 RISCV_OBJDUMP ?= $(RISCV_PREFIX)objdump --disassemble-all --disassemble-zeroes --section=.text --section=.text.startup --section=.text.init --section=.data
+RISCV_OBJCOPY ?= riscv64-unknown-elf-objcopy --gap-fill 0 -O binary
 RISCV_SIM ?= spike

 vpath %.S $(src_dir)
@@ -45,6 +46,10 @@ vpath %.S $(src_dir)
 %.dump: %
        $(RISCV_OBJDUMP) $< > $@

+%.hex: %
+       $(RISCV_OBJCOPY) $< $<.bin
+       od -tx4 -v -w4 -Ax $<.bin | sed 's/^/0x/g' | gawk -F ' ' '{printf "@%08x %s\n", rshift(strtonum($$1), 2), $$2}' > $@
+
 %.out: %
        $(RISCV_SIM) --isa=rv64gc $< 2> $@

@@ -102,7 +107,7 @@ junk += $(tests) $(tests_dump) $(tests_hex) $(tests_out) $(tests32_out)
 #------------------------------------------------------------
 # Default

-all: $(tests_dump)
+all: $(tests_dump) $(tests_hex)

これでテストを流してみると、トレースとしては以下のようになる。何となく流れているようだが、途中で止まっている。

        31 : x05<=0x00000000000000c0                                : 0x000000c0 : INST(0x00000297) : auipc   t0, 0x0
        32 : x05<=0x0000000000000000                                : 0x000000c4 : INST(0xf4028293) : addi    t0, t0, -192
        33 :                                                        : 0x000000c8 : INST(0x00028e63) : beqz    t0, pc + 28
        35 :                                                        : 0x000000e4 : INST(0x30005073) : csrwi   mstatus, 0
        36 : x05<=0x00000000000000e8                                : 0x000000e8 : INST(0x00000297) : auipc   t0, 0x0
        37 : x05<=0x00000000000000fc                                : 0x000000ec : INST(0x01428293) : addi    t0, t0, 20
        38 :                                                        : 0x000000f0 : INST(0x34129073) : csrw    mepc, t0
        39 : x10<=0x0000000000000000                                : 0x000000f4 : INST(0xf1402573) : csrr    a0, mhartid
        40 :                                                        : 0x000000f8 : INST(0x30200073) : mret
        42 : x01<=0x0000000000000000                                : 0x000000fc : INST(0x00000093) : li      ra, 0
        43 : x02<=0x0000000000000000                                : 0x00000100 : INST(0x00000113) : li      sp, 0
        44 : x30<=0x0000000000000000                                : 0x00000104 : INST(0x00208f33) : add     t5, ra, sp
        45 : x29<=0x0000000000000000                                : 0x00000108 : INST(0x00000e93) : li      t4, 0
        46 : x03<=0x0000000000000002                                : 0x0000010c : INST(0x00200193) : li      gp, 2
        47 :                                                        : 0x00000110 : INST(0x4fdf1063) : bne     t5, t4, pc + 1248
        48 : x01<=0x0000000000000001                                : 0x00000114 : INST(0x00100093) : li      ra, 1
        49 : x02<=0x0000000000000001                                : 0x00000118 : INST(0x00100113) : li      sp, 1
        50 : x30<=0x0000000000000002                                : 0x0000011c : INST(0x00208f33) : add     t5, ra, sp
        51 : x29<=0x0000000000000002                                : 0x00000120 : INST(0x00200e93) : li      t4, 2
        52 : x03<=0x0000000000000003                                : 0x00000124 : INST(0x00300193) : li      gp, 3
        53 :                                                        : 0x00000128 : INST(0x4ddf1463) : bne     t5, t4, pc + 1224
        54 : x01<=0x0000000000000003                                : 0x0000012c : INST(0x00300093) : li      ra, 3
        55 : x02<=0x0000000000000007                                : 0x00000130 : INST(0x00700113) : li      sp, 7...

この時に、mretなどの基本的な命令はサポートしておく必要がある。mretはあまり考えなければ単純で、次のPCをMEPCに設定するだけだ(権限の概念が存在しないCPUであれば)。

  when(if_inst_en & dec_jalr_en) {
    if_inst_addr := dec_reg_op0.asUInt
  } .elsewhen (if_inst_en & dec_jal_en) {
    if_inst_addr := dec_inst_addr + dec_imm_j
  } .elsewhen (if_inst_en & dec_br_en) {
    if_inst_addr := dec_inst_addr + dec_imm_b_sext
  } .elsewhen (if_inst_en & dec_mret_en) {
    if_inst_addr := u_csrfile.io.mepc
  } .elsewhen(if_inst_en & io.inst_bus.ack) {
    if_inst_addr := if_inst_addr + 4.U
  }

上記のwhen文の羅列は、MuxCaseで書くこともできるらしい。以下と等価だ。

  if_inst_addr := MuxCase (0.U, Array (
    (if_inst_en & dec_jalr_en) -> dec_reg_op0.asUInt,
    (if_inst_en & dec_jal_en)  -> (dec_inst_addr + dec_imm_j),
    (if_inst_en & dec_br_en)   -> (dec_inst_addr + dec_imm_b_sext),
    (if_inst_en & dec_mret_en) -> u_csrfile.io.mepc,
    (if_inst_en & io.inst_bus.ack) -> (if_inst_addr + 4.U)
  ))

Chiselを使ってCPUを作ろう(8. CSR命令の実装方法調査)

Chiselを使って、非常にシンプルなCPUを作ってみるプロジェクト、算術演算命令、ロードストア命令とくれば次はCSR命令だ。

f:id:msyksphinz:20181123005953p:plain

RISC-VにはCSR(Control and Status Register)が多くの数定義されている。 これらをChiselで記述するためにはどうしたらよいのだろうか。 まずは比較的読みやすいRISC-VのChisel実装、Sodorを読んでみる。

  • https://github.com/ucb-bar/riscv-sodor/blob/master/src/common/csr.scala
  val read_mapping = collection.mutable.LinkedHashMap[Int,Bits](
    /* CsrAddr.mcycle    -> reg_time, */
    /* CsrAddr.minstret  -> reg_instret, */
    CsrAddr.mimpid    -> 0.U,
    CsrAddr.marchid   -> 0.U,
    CsrAddr.mvendorid -> 0.U,
    CsrAddr.misa      -> misa.U,
    CsrAddr.mimpid    -> impid.U,
...

ScalaにはLinkedHashMapという関数があるらしい。 名前から容易に想像できるが、Hashみたいなものだ。Intの値に対してBitsの値をMapするための関数。

上記のCsrAddr.misa (= 0x301)に対してmisa.Uをマップしている。これをCSRレジスタ分くっつける訳だ。

CSRをReadする場合には、Mappingで取り出す。 以下の記述は良く分からないけれども、decoded_addrCSRのビット数分だけの長さの1-hot信号になっているのだと思う。そこから読み込みデータを選択する。

  val decoded_addr = read_mapping map { case (k, v) => k -> (io.rw.addr === k.U(12.W)) }
  io.rw.rdata := Mux1H(for ((k, v) <- read_mapping) yield decoded_addr(k) -> v)

データのアップデート(Set / Clear)の場合は、以下の論理を通すらしい。難しいなあ。

  def readModifyWriteCSR(cmd: UInt, rdata: UInt, wdata: UInt) =
    (Mux((cmd === CSR.Set |  cmd === CSR.Clear), rdata, 0.U) | wdata) & ~Mux(cmd === CSR.Clear, wdata, 0.U)

砕いていくと、 cmd === CSR.Setの場合以下のようになる。これで確かにwdataのうち1が立っている部分は必ずセットされる。

def readModifyWriteCSR(CSR.Set, rdata: UInt, wdata: UInt) = (rdata | wdata) & 0xffffffff

cmd == CSR.Clear の場合以下のようになる。これで確かにrddataのうち1が立っている部分は必ずクリアされる。

def readModifyWriteCSR(CSR.Clear, rdata: UInt, wdata: UInt) = (rdata | wdata) & ~wdata

Chiselを使ってCPUを作ろう(7. Load / Store命令の実装)

Chiselを使って、非常にシンプルなCPUを作ってみるプロジェクト、ある程度演算命令は動き始めたので、次はLoad/Store命令を実装する。

RV64の命令系列では、64-bit, 32-bit, 16-bit, 8-bitのメモリアクセスが行える必要がある。 メモリの部分はVerilogで記述してもよいのだが、そうするとScalaでシミュレーションできなくなるのでこれもChiselで実装したい。

f:id:msyksphinz:20181122002550p:plain

8‐bit単位でのアクセスを行うために、メモリモジュールを改造して、8ビットのメモリが8本(バンク)存在する構成に変更した。

ChiselだとVecを使って以下のようにできる。

  for (bank <- 0 until 8) {
    val memory = Mem(math.pow(2, bus_width).toInt , UInt(8.W))
...

データの書き込み

データ幅に合わせて書き込みを行うバンクを制御するために、switch文を使用して書き込みバンクを制御した。

    val data_msb = bank * 8 + 7
    val data_lsb = bank * 8 + 0
    when(io.data_bus.req & (io.data_bus.cmd === CMD_WR)) {
      switch (io.data_bus.size) {
        is (MT_D) {
          memory(io.data_bus.addr(bus_width-1, 3)) := io.data_bus.wrdata(data_msb, data_lsb)
        }
        is (MT_W) {
          if (io.data_bus.addr(2) == bank_idx(2)) {
            memory(io.data_bus.addr(bus_width-1, 3)) := io.data_bus.wrdata(data_msb, data_lsb)
          }
        }
        is (MT_H) {
          if (io.data_bus.addr(2,1) == bank_idx(2,1)) {
            memory(io.data_bus.addr(bus_width-1, 3)) := io.data_bus.wrdata(data_msb, data_lsb)
          }
        }
        is (MT_B) {
          if (io.data_bus.addr(2,0) == bank_idx(2,0)) {
            memory(io.data_bus.addr(bus_width-1, 3)) := io.data_bus.wrdata(data_msb, data_lsb)
          }
        }
      }

データの読み込み

こちらもデータ幅とアドレスに合わせて、switch文を使用してバンクを制御した。

  switch (io.data_bus.size) {
    is(MT_D) {
      io.data_bus.rddata := Cat(data_rd_data(7), data_rd_data(6), data_rd_data(5), data_rd_data(4),
        data_rd_data(3), data_rd_data(2), data_rd_data(1), data_rd_data(0)).asSInt
    }
    is(MT_W) {
      io.data_bus.rddata := Cat(Fill(32, 0.U(1.W)), data_rd_data(io.data_bus.addr(2) * 4.U + 3.U),
        data_rd_data(io.data_bus.addr(2) * 4.U + 2.U),
        data_rd_data(io.data_bus.addr(2) * 4.U + 1.U),
        data_rd_data(io.data_bus.addr(2) * 4.U + 0.U)).asSInt
    }
    is(MT_HU) {
      val target_data = Cat(data_rd_data(io.data_bus.addr(2,1) * 2.U + 1.U), data_rd_data(io.data_bus.addr(2,1) * 2.U + 0.U))
      io.data_bus.rddata := Cat(0.U(48.W), target_data).asSInt
    }
    is(MT_H ) {
      val target_data = Cat(data_rd_data(io.data_bus.addr(2,1) * 2.U + 1.U), data_rd_data(io.data_bus.addr(2,1) * 2.U + 0.U))
      io.data_bus.rddata := Cat(Fill(48, target_data(15)), target_data).asSInt
    }
    is(MT_BU) { io.data_bus.rddata := Cat(Fill(56, 0.U), data_rd_data(io.data_bus.addr(2, 0))).asSInt }
    is(MT_B ) {
      val target_data = data_rd_data(io.data_bus.addr(2,0))
      io.data_bus.rddata := Cat(Fill(56, target_data(7)), target_data).asSInt
    }
  }

テスト

とりあえずロード命令で所望のバンクからデータを読めるかテストしてみる。

    sd  x21,  0(x31)
    sd  x22,  8(x31)
    sd  x23, 16(x31)
    sd  x24, 24(x31)

    ld  x12,  0(x31)
    ld  x13,  8(x31)
    ld  x14, 16(x31)
    ld  x15, 24(x31)

    lw  x1,  0(x31)
    lw  x2,  4(x31)
    lw  x3,  8(x31)
    lw  x4, 12(x31)

    lh  x5,  0(x31)
    lh  x6,  2(x31)
    lh  x7,  4(x31)
    lh  x8,  6(x31)

    lhu x1,  0(x31)
    lhu x2,  2(x31)
    lhu x3,  4(x31)
    lhu x4,  6(x31)

    lb  x5,  0(x31)
    lb  x6,  1(x31)
    lb  x7,  2(x31)
    lb  x8,  3(x31)

    lbu x9,  0(x31)
    lbu x10, 1(x31)
    lbu x11, 2(x31)
    lbu x12, 3(x31)

正しく動作しているようだ。

        95 : x12<=0xb3fefd77eb062f44 [00002000]=>0xb3fefd77eb062f44 : 0x00000174 : INST(0x000fb603) : ld      a2, 0(t6)
        96 : x13<=0x9b0c605e63aadd2b [00002008]=>0x9b0c605e63aadd2b : 0x00000178 : INST(0x008fb683) : ld      a3, 8(t6)
        97 : x14<=0x8cd5b53f23cedcc1 [00002010]=>0x8cd5b53f23cedcc1 : 0x0000017c : INST(0x010fb703) : ld      a4, 16(t6)
        98 : x15<=0x48a2b40b10a2099a [00002018]=>0x48a2b40b10a2099a : 0x00000180 : INST(0x018fb783) : ld      a5, 24(t6)
        99 : x01<=0x00000000eb062f44 [00002000]=>0x00000000eb062f44 : 0x00000184 : INST(0x000fa083) : lw      ra, 0(t6)
       100 : x02<=0x00000000b3fefd77 [00002004]=>0x00000000b3fefd77 : 0x00000188 : INST(0x004fa103) : lw      sp, 4(t6)
       101 : x03<=0x0000000063aadd2b [00002008]=>0x0000000063aadd2b : 0x0000018c : INST(0x008fa183) : lw      gp, 8(t6)
       102 : x04<=0x000000009b0c605e [0000200c]=>0x000000009b0c605e : 0x00000190 : INST(0x00cfa203) : lw      tp, 12(t6)
       103 : x05<=0x0000000000002f44 [00002000]=>0x0000000000002f44 : 0x00000194 : INST(0x000f9283) : lh      t0, 0(t6)
       104 : x06<=0xffffffffffffeb06 [00002002]=>0xffffffffffffeb06 : 0x00000198 : INST(0x002f9303) : lh      t1, 2(t6)
       105 : x07<=0xfffffffffffffd77 [00002004]=>0xfffffffffffffd77 : 0x0000019c : INST(0x004f9383) : lh      t2, 4(t6)
       106 : x08<=0xffffffffffffb3fe [00002006]=>0xffffffffffffb3fe : 0x000001a0 : INST(0x006f9403) : lh      s0, 6(t6)
       107 : x01<=0x0000000000002f44 [00002000]=>0x0000000000002f44 : 0x000001a4 : INST(0x000fd083) : lhu     ra, 0(t6)
       108 : x02<=0x000000000000eb06 [00002002]=>0x000000000000eb06 : 0x000001a8 : INST(0x002fd103) : lhu     sp, 2(t6)
       109 : x03<=0x000000000000fd77 [00002004]=>0x000000000000fd77 : 0x000001ac : INST(0x004fd183) : lhu     gp, 4(t6)
       110 : x04<=0x000000000000b3fe [00002006]=>0x000000000000b3fe : 0x000001b0 : INST(0x006fd203) : lhu     tp, 6(t6)
       111 : x05<=0x0000000000000044 [00002000]=>0x0000000000000044 : 0x000001b4 : INST(0x000f8283) : lb      t0, 0(t6)
       112 : x06<=0x000000000000002f [00002001]=>0x000000000000002f : 0x000001b8 : INST(0x001f8303) : lb      t1, 1(t6)
       113 : x07<=0x0000000000000006 [00002002]=>0x0000000000000006 : 0x000001bc : INST(0x002f8383) : lb      t2, 2(t6)
       114 : x08<=0xffffffffffffffeb [00002003]=>0xffffffffffffffeb : 0x000001c0 : INST(0x003f8403) : lb      s0, 3(t6)
       115 : x09<=0x0000000000000044 [00002000]=>0x0000000000000044 : 0x000001c4 : INST(0x000fc483) : lbu     s1, 0(t6)
       116 : x10<=0x000000000000002f [00002001]=>0x000000000000002f : 0x000001c8 : INST(0x001fc503) : lbu     a0, 1(t6)
       117 : x11<=0x0000000000000006 [00002002]=>0x0000000000000006 : 0x000001cc : INST(0x002fc583) : lbu     a1, 2(t6)
       118 : x12<=0x00000000000000eb [00002003]=>0x00000000000000eb : 0x000001d0 : INST(0x003fc603) : lbu     a2, 3(t6)

Chiselを使ってCPUを作ろう(6. パイプラインと分岐命令の実装)

Chiselを使って、非常にシンプルなCPUを作ってみるプロジェクト、ある程度演算命令は動き始めたが、ちゃんと動作しているのかどうかを確かめたい。

分岐命令はALUで比較を実行して、その結果に応じてPCを切り替える。 RISC-Vには3種類のPCアップデートの方法があって、

  • Branch Conditional式 : 比較の結果、PC+Immで分岐する。
  • J式 : 比較をしない。PC+Immで分岐する。
  • JALR : 比較をしない。レジスタ値で分岐する。

の大きく分けで3つで考えていればよい。Branchの場合は比較処理を実行して、そうでない場合は無条件でPCをアップデートする。 ChiselでもVerilogでも、書くことは一緒だ。

  val dec_jalr_en = u_cpath.io.ctl.jalr
  val dec_jal_en  = u_cpath.io.ctl.jal
  val dec_br_en   = u_cpath.io.ctl.br & (u_alu.io.res === 1.S)
  val dec_jump_en = if_inst_en & (dec_jalr_en | dec_jal_en | dec_br_en)

  when(if_inst_en & dec_jalr_en) {
    if_inst_addr := dec_reg_op0.asUInt
  } .elsewhen (if_inst_en & dec_jal_en) {
    if_inst_addr := dec_inst_addr + dec_imm_j
  } .elsewhen (if_inst_en & dec_br_en) {
    if_inst_addr := dec_inst_addr + dec_imm_b_sext
  } .elsewhen(if_inst_en & io.inst_ack) {
    if_inst_addr := if_inst_addr + 4.U
  }

今のところはパイプラインとしてはたったの2段。データメモリもくっついていないけど、とりあえず演算命令は動き始めている。

f:id:msyksphinz:20181121005036p:plain

ちなみにPCから命令を流しっぱなしだと、PCをアップデート後にすでに次のPCアドレスに移ってしまっているので、それを無効化する処理を入れている(1サイクルのバブル)。以下のコードで、`dec_inst_valid信号は、直前の命令がジャンプ命令ならば次の命令は無効化する。

  when  (if_inst_en & io.inst_ack) {
    dec_inst_data    := io.inst_data
    dec_inst_addr := if_inst_addr
    dec_inst_valid := Mux(dec_jump_en, false.B, true.B)
  } .otherwise {
    dec_inst_valid := false.B
  }

とりあえずこれで演算命令、分岐命令は動作するようになった。次はメモリアクセス命令、CSRの実装、あとはパイプラインをより深くしていく。