FPGA開発日記

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

RustでRISC-V命令セットシミュレータを作ろう (1. 基本的なテストパタンを通す)

f:id:msyksphinz:20190224185310p:plain

今年の目標の一つに、新しいプログラミング言語を覚える、というものを追加していた。ターゲットの言語としてはRustにした。 Rustは低レベルのプログラミングにも使えそうだし、C/C++系の普通のプログラミング言語しか触ったことのない私としては何かと面白そうだったからだ。

Rustの資料は本を読んで写経しただけでは何も理解した気にならなかったので、なにかコードを書いてみたい。 とりあえず、RISC-VシミュレータをRustを使って書くことにした。 RISC-Vシミュレータなら、いままでC++やVivado-HLS向けのC言語など、たくさん作ってきたので移植がやりやすいだろう。

github.com

とりあえずRust特有の制限があるため、非常に実装に苦労した。

メモリ確保

まずは命令とデータを格納するためのメモリを確保する方法が分からない。C++ならば必要に応じてmallocすればよいけど、Rustでそんなことできるのか? というわけで、とりあえず決め打ちでメモリ領域を確保した。

pub struct EnvBase
{
    pub m_pc: AddrType,
    pub m_regs:  [XlenType; 32],
    pub m_memory: [u8; DRAM_SIZE], // memory
    pub m_csr: RiscvCsr,
...

フェッチする方法も一筋縄ではいかない。本当は引数でポインタを渡してデータをフェッチして、MisAlignならばエラーコードを返す、などを実装しなければならないのだが、ポインタを渡してデータをフェッチするとデータのLivenessエラーなどになり全く回避方法が分からなかったので、とりあえずエラー処理などは無視した。

    fn fetch_memory(&mut self) -> XlenType {
        let base_addr: AddrType = self.m_pc - DRAM_BASE;
        let fetch_data = ((self.m_memory[base_addr as usize + 3] as XlenType) << 24) |
                         ((self.m_memory[base_addr as usize + 2] as XlenType) << 16) |
                         ((self.m_memory[base_addr as usize + 1] as XlenType) <<  8) |
                         ((self.m_memory[base_addr as usize + 0] as XlenType) <<  0);
        return fetch_data;
    }

それ以外では、matchでデコードできるのは楽しい。swtchではなくmatchで命令デコーダを作っていった。switchに対してどのような利点があるのかはわからないが、とりあえず楽しい。

    fn decode_inst(&mut self, inst:XlenType) -> RiscvInst {
        let opcode = inst & 0x7f;
        let funct3 = (inst >> 12) & 0x07;
        let funct5 = (inst >> 25) & 0x7f;
        let imm12  = (inst >> 20) & 0xfff;

        let dec_inst: RiscvInst;

        match opcode {
            0x0f => {
                match funct3 {
                    0b000 => dec_inst = RiscvInst::FENCE,
                    0b001 => dec_inst = RiscvInst::FENCEI,
                    _     => dec_inst = RiscvInst::NOP,
                }
            }
            0x33 => {
                match funct3 {
                    0b000 => {
...

Mutable / Borrowなどの知識が乏しい

次に実装をしていると、C++で書いていたようなことが余裕でエラーになってしまい厳しい。 例えば、以下はエラーとなる。

            RiscvInst::CSRRW  => {
                let reg_data:XlenType = self.m_csr.csrrw (csr_addr, self.read_reg(rs1));
                self.write_reg(rd, reg_data);
            }
error[E0499]: cannot borrow `*self` as mutable more than once at a time
   --> src/riscv_core.rs:390:69
    |
390 |                 let reg_data:XlenType = self.m_csr.csrrw (csr_addr, self.read_reg(rs1));
    |                                         ---------- -----            ^^^^ second mutable borrow occurs here
    |                                         |          |
    |                                         |          first borrow later used by call
    |                                         first mutable borrow occurs here

ただし以下は通る。何が違うんだ!?

            RiscvInst::CSRRW  => {
                let rs1_data = self.read_reg(rs1);
                let reg_data:XlenType = self.m_csr.csrrw (csr_addr, rs1_data);
                self.write_reg(rd, reg_data);
            }

命令セットシミュレータだからオーバフローとか簡単に発生する

テストパタンを通していると、余裕でオーバフローが発生する。 まあ、RISC-Vの初期PCが0x8000_0000だからしょうがないのだけれども。

とりあえず、RISC-Vではオーバフロー例外を取り扱わないので、余裕でWrappingで囲んでいった。 こんな適当な実装でいいんかいな。

            RiscvInst::ADDI => {
                let rs1_data = self.read_reg(rs1);
                let imm_data = Self::extract_ifield (inst);
                let reg_data:XlenType = (Wrapping(rs1_data) + Wrapping(imm_data)).0;
                self.write_reg(rd, reg_data);
            }

実行

とりあえず、以下のようにして最も基本的なパタンを実行した。

$ cargo run riscv-tests/isa/rv32ui-p-simple.bin | spike-dasm
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/swimmer_rust_origin riscv-tests/isa/rv32ui-p-simple.bin`
80000000 : 04c0006f // j       pc + 0x4c
8000004c : f1402573 // csrr    a0, mhartid
     x10 <= 00000000
80000050 : 00051063 // bnez    a0, pc + 0
80000054 : 00000297 // auipc   t0, 0x0
     x05 <= 80000054
80000058 : 01028293 // addi    t0, t0, 16
     x05 <= 80000064
8000005c : 30529073 // csrw    mtvec, t0
80000060 : 18005073 // csrwi   satp, 0
80000064 : 00000297 // auipc   t0, 0x0
     x05 <= 80000064
80000068 : 02028293 // addi    t0, t0, 32
     x05 <= 80000084
8000006c : 30529073 // csrw    mtvec, t0
80000070 : 800002b7 // lui     t0, 0x80000
     x05 <= 80000000
80000074 : fff28293 // addi    t0, t0, -1
     x05 <= 7fffffff
...

とりあえず何となく動き始めたぞ。Finishの条件などは、何も実装していないから固定回数命令をフェッチして終了する。 まあいいか。