FPGA開発日記

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

RustでRISC-V命令セットシミュレータを作ろう (7. trait, generics, 64-bitモードへの拡張)

f:id:msyksphinz:20190224185310p:plain

Rustで作るRISC-Vシミュレータ。基本的な形が出来上がった。32bitの整数演算命令はある程度PASSできるようになっている。 C++で作ったRISC-Vシミュレータ同様、64bitモードもサポートして動作させるようにしたい。

C++版では、32bitと64bitモードの実装はtemplateを使って実現していた。具体的には、int32_tint64_tの2つの場合で実装し、オプションでどちらを使うか切り替える。

さて、Rustで同じことを表現したい場合はどうしようか。シミュレータのメインクラスにあたる部分はRustではstructを使って表現している。

pub struct Riscv32Env {
    // m_bitmode: RiscvBitMode,
    pub m_pc: AddrT,
    m_previous_pc: AddrT,
    m_regs: [XlenT; 32],
    pub m_memory: [u8; DRAM_SIZE], // memory
    pub m_csr: RiscvCsr<XlenT>,

    pub m_priv: PrivMode,
    m_maxpriv: PrivMode,
...

これに対してジェネリクスを導入して32bitと64bitの切り替えを行うために、以下のようなものを導入したとする。

pub struct<W> RiscvEnv {
    pub m_pc: AddrT,
    m_previous_pc: AddrT,
    m_regs: [W; 32],
...

なんか難易度が一気に上がった気がする。具体的には、ジェネリクスですべてパラメータ化した部分を、整数などの値でキャストしなければならなくなってしまった。 正しいやり方が全く分からない。

   let reg_data : W = self.reg_read(rs_addr);
   let res = reg_data + 5;

とかやろうとするとすごく怒られた。やり方が間違っているのかもしれないけど。

   let reg_data : W = self.reg_read(rs_addr);
   let res = reg_data + NumCast::from(5).unwrap();

えー、こういうこと書くの?すごく面倒なんだけど。そもそも、Bit Extractionの記述が猛烈に煩雑になるんだけど。

  let extracted_bits : W = (NumCast::from(1).unwrap << reg_data & NumCast::from(15).unwrap()) - NumCast::from(1).unwrap();

何だこりゃ。自分で書いておいて、煩雑すぎて意味が分からない。

このあたりをどのようにテンプレートのように記述すればきれいに書けるのか全く分からなかったので、とりあえずジェネリクスは諦めてRV32とRV64で別々のクラスを作ってしまった。 もったいない。

github.com

  • src/riscv32_core.rs
pub struct Riscv32Env {
...
}
impl Riscv32Env {
    pub fn new() -> Riscv32Env {
        Riscv32Env {
...
}
pub trait Riscv32Core {
    fn get_rs1_addr(inst: InstT) -> RegAddrT;
...
}
impl Riscv32Core for Riscv32Env {
    fn get_rs1_addr(inst: InstT) -> RegAddrT {
        return ((inst >> 15) & 0x1f) as RegAddrT;
    }
...
}
  • src/riscv64_cores.rs
pub struct Riscv64Env {
...
}
impl Riscv64Env {
    pub fn new() -> Riscv64Env {
        Riscv64Env {
...
}
pub trait Riscv64Core {
    fn get_rs1_addr(inst: InstT) -> RegAddrT;
    fn get_rs2_addr(inst: InstT) -> RegAddrT;
...
}
impl Riscv64Core for Riscv64Env {
    fn get_rs1_addr(inst: InstT) -> RegAddrT {
        return ((inst >> 15) & 0x1f) as RegAddrT;
...
}

Csrの実装などは、どうにか上手くジェネリクスを導入で来た気がする。

pub struct RiscvCsrBase<W> {
    pub m_csr: W,
}

impl RiscvCsrBase<i32> {
    pub fn new() -> RiscvCsrBase<i32> {
        RiscvCsrBase { m_csr: 0x0 }
    }
...
impl RiscvCsrBase<i32> {
    pub fn new() -> RiscvCsrBase<i32> {
        RiscvCsrBase { m_csr: 0x0 }
    }
...
impl RiscvCsrBase<i64> {
    pub fn new() -> RiscvCsrBase<i64> {
        RiscvCsrBase { m_csr: 0x0 }
    }

しかし、同じRiscvCsrBaseをベースにしているのだから、RiscvCsrBase<i32>RiscvCsrBase<i64>の両方で実装を定義しなくても、templateを使ったときみたいに型のキャストとかも自動的に実行してくれればよいのに。 そういうことを言い始めると、安全でないプログラムになってしまうのかなあ。

とりあえず、コンパイルできることろまではたどり着いた。テストパタンは全くPASSしていないので、デバッグが必要だ。