FPGA開発日記

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

自作Binary Translation型RISC-VエミュレータでDhrystoneを実行する

自作Binary Translation型RISC-Vエミュレータは、ある程度RISC-Vの命令セットをサポートできるようになってきており、最大の課題だったCompressed命令のサポートもほぼ完了しつつある。テストパタンも安定してPassできるようになってきたので、次はベンチマークプログラムを実行して、本家のQEMUとどの程度の速度差があるのか確認していきたいところである。

本家のQEMUと比較するためには、同じテストパタンを流さなければならないだろう。本家のQEMUで動かすためにはSiFive社が提供しているFreedom-E-SDKで提供されているテストパタンを使うのが良かろう。Dhrystoneをまずは実行してみたい。

Dhrystoneにはprintf()などの関数が入っているが、これをFreedom-E-SDKではfreedom-metalというサポートライブラリによってカバーしている。例えば以下のようにしてDhrystoneをビルドすると、U54のメモリマップを使用してDhrystoneのビルドが行われる。

$ make PROGRAM=dhrystone TARGET=qemu-sifive-u54

ちなみにQEMUの4.1.0と5.1.0ではこのあたりのメモリマップが微妙に違う。

$ ./qemu-4.1.0/build/riscv64-softmmu/qemu-system-riscv64 -nographic -machine sifive_u,dumpdtb=riscv64-sifive_u.4.1.0.dtb
$ ./qemu-5.1.0/build/riscv64-softmmu/qemu-system-riscv64 -nographic -machine sifive_u,dumpdtb=riscv64-sifive_u.5.1.0.dtb
$ dtc riscv64-sifive_u.4.1.0.dtb > riscv64-sifive_u.4.1.0.dts
$ dtc riscv64-sifive_u.5.1.0.dtb > riscv64-sifive_u.5.1.0.dts
  • riscv64-sifive_u.4.1.0.dts
                uart@10013000 {
                        interrupts = < 0x03 >;
                        interrupt-parent = < 0x02 >;
                        clock-frequency = < 0x1dcd6500 >;
                        reg = < 0x00 0x10013000 0x00 0x1000 >;
                        compatible = "sifive,uart0";
                };
  • riscv64-sifive_u.5.1.0.dts
                serial@10010000 {
                        interrupts = < 0x04 >;
                        interrupt-parent = < 0x06 >;
                        clocks = < 0x05 0x03 >;
                        reg = < 0x00 0x10010000 0x00 0x1000 >;
                        compatible = "sifive,uart0";
                };

この辺を考慮して、QEMU-5.1.0に対応したメモリマップで自作エミュレータ側を調整する。とりあえずはUARTだけ実装すればそれっぽくなるであろう。UARTはメモリアドレスとして0x10010000なのだが、現在のエミュレータは0x80000000以下のマップは用意していない。どうしようかといろいろ考えたのだが、結局はこれらのCachedされない場所はTLBに登録しないことで常にメモリ参照ミスを発生させ、手動で操作することにした。

かなり乱暴な方法だが、以下のようにして無理やりメモリマップを貼り付けることで0x1000_0xxxにアクセスする場合は特殊な処理を挿入することにする。

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

        match emu.convert_physical_address(guest_pc, addr, MemAccType::Write) {
            Ok(guest_phy_addr) => { 
                if emu.m_arg_config.mmu_debug {
                    println!("store32 : converted address: {:016x} --> {:016x}", addr, guest_phy_addr);
                }
                if emu.m_arg_config.machine == MachineEnum::RiscvSiFiveU && (guest_phy_addr & !0xfff) == 0x1000_0000 {
                    if emu.m_arg_config.debug {
                        println!("UART Access : {:08x}", guest_phy_addr);
                    }
                    // region : 0x1000_0000 - 0x1000_0fff
                    match guest_phy_addr {
                        0x1000_0000 => { eprint!("{}", (rs2_data & 0xff) as u8 as char) },       // txdata
                        0x1000_0004 => { },       // rxdata
                        0x1000_0008 => { },       // txctrl
                        0x1000_000c => { },       // rxctrl
                        _ => {},
                    }                  
                    return MemResult::NoExcept as usize;

メモリマップ0x1000_0000のTXDATAにアクセスがあると、その時の値を問答無用で出力する。一方でロードの場合は以下のようになった。これはとりあえずスタブを持っているだけで何もしない。

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

        match emu.convert_physical_address(guest_pc, addr, MemAccType::Read) {
            Ok(guest_phy_addr) => { 
                if emu.m_arg_config.mmu_debug {
                    println!("load32 : converted address: {:016x} --> {:016x}", addr, guest_phy_addr);
                }
                if emu.m_arg_config.machine == MachineEnum::RiscvSiFiveU && (guest_phy_addr & !0xfff) == 0x1000_0000 {
                    if emu.m_arg_config.debug {
                        println!("UART Access : {:08x}", guest_phy_addr);
                    }
                    // region : 0x1000_0000 - 0x1000_0fff
                    match guest_phy_addr {
                        0x1000_0000 => { emu.m_iregs[rd as usize] = 0; },       // txdata
                        0x1000_0004 => { emu.m_iregs[rd as usize] = 0; },       // rxdata
                        0x1000_0008 => { emu.m_iregs[rd as usize] = 0; },       // txctrl
                        0x1000_000c => { emu.m_iregs[rd as usize] = 0; },       // rxctrl
                        _ => {},
                    }                  
                    return MemResult::NoExcept as usize;
                }
...

ここまででDhrystoneをある程度ブートできるようになった。以下のように実行すると最初のDhrystoneのメッセージまでは表示される。

$ cargo run -- --machine sifive_u --elf-file /home/msyksphinz/work/riscv/freedom-e-sdk/software/dhrystone/release/dhrystone.elf
Dhrystone Benchmark, Version 2.1 (Language: C)

Program compiled without 'register' attribute

Execution starts, 20000000 runs through Dhrystone

しかしここから先がSegmentation Faultで落ちてしまったのを修正すれば、一応パタンが走り始めた。ただし長すぎて果たしてどこまで進んでいるのか良く分からない...

f:id:msyksphinz:20201129155809p:plain