最近 ESM と RISC-V のロゴの配色が似ていると感じている @wat-aro です。
はたけやまさん の
blog.agile.esm.co.jp
はおもしろい記事でしたね。
この記事は最後こう締めくくられています。
以上、Rubyを使ったシンプルなRISC-Vシミュレータの作り方のご紹介しました。
皆さんも梅雨の時期のおうち時間にお好きなプログラム言語でCPUシミュレータ自作なんていかがですか?
なのでこの記事を参考に Rust で RISC-V シミュレータを作成しました。
https://github.com/wat-aro/rv32sim.rs
処理の流れはだいたい元記事と同じですのでもっと詳しく知りたい人は元記事からどうぞ。
メモリ
まずメモリの定義です。
write
と read
と、初期データの読み込み用の initialize
メソッドを持っています。
32 bit のデータを 8 bit ずつ分けて data に格納します。
#[derive(Debug)] pub struct Memory { data: Vec<u8>, } const MEMORY_SIZE: u32 = 1024 * 1024; impl Memory { pub fn new() -> Self { Memory { data: vec![0; MEMORY_SIZE as usize], } } pub fn write(&mut self, addr: u32, word: u32) { let index = addr as usize; self.data[index] = (word & 0xff) as u8; self.data[index + 1] = ((word >> 8) & 0xff) as u8; self.data[index + 2] = ((word >> 16) & 0xff) as u8; self.data[index + 3] = ((word >> 24) & 0xff) as u8; } pub fn read(&self, addr: u32) -> u32 { let index = addr as usize; self.data[index] as u32 | (self.data[index + 1] as u32) << 8 | (self.data[index + 2] as u32) << 16 | (self.data[index + 3] as u32) << 24 } pub fn initialize(&mut self, data: Vec<u8>) { self.data.splice(..data.len(), data); } }
命令デコーダ
元記事とは違いデコーダオブジェクトを作らずに、デコードした結果どの命令かわかるようにした Instruction
を作成しました。
#[derive(Debug, PartialEq)] pub enum Instruction { Add { rd: u32, rs1: u32, rs2: u32 }, Sub { rd: u32, rs1: u32, rs2: u32 }, Or { rd: u32, rs1: u32, rs2: u32 }, And { rd: u32, rs1: u32, rs2: u32 }, Addi { rd: u32, rs1: u32, imm: u32 }, Slli { rd: u32, rs1: u32, imm: u32 }, Beq { rs1: u32, rs2: u32, imm: u32 }, Lw { rd: u32, rs1: u32, imm: u32 }, Sw { rs1: u32, rs2: u32, imm: u32 }, }
こういうデータを扱いたい場合に enum
は便利ですね。
Instruction::decode
で Instruction
を返すようにしました。
元記事のほうでは NOP
はデコーダの特別な状態になっていましたが、ここでは命令のひとつとして Instruction::Nop
を実装しています。
impl Instruction { pub fn decode(inst: u32) -> Result<Instruction, Error> { let opcode = inst & 0x0000007f; let rd = (inst & 0x00000f80) >> 7; let funct3 = (inst & 0x00007000) >> 12; let rs1 = (inst & 0x000f8000) >> 15; let rs2 = (inst & 0x01f00000) >> 20; let funct7 = (inst & 0xfe000000) >> 25; match opcode { ..., // I-Type // I-Type 0b0010011 => { let imm = (inst & 0xfff00000) >> 20; match funct3 { 0x0 => Ok(Addi { rd, rs1, imm }), 0x1 => Ok(Slli { rd, rs1, imm }), _ => Err(Error::IllegalInstruction(inst)), } }, ... } } }
https://github.com/jameslzhu/riscv-card/blob/master/riscv-card.pdf にはお世話になりました。
CPU
まずはレジスタの定義から
#[derive(Debug)] pub struct XRegisters { data: [u32; 32], } const REGISTERS_COUNT: u32 = 32; impl XRegisters { pub fn new() -> Self { let data = [0; REGISTERS_COUNT as usize]; XRegisters { data } } pub fn read(&self, addr: u32) -> u32 { assert!(addr < 32); self.data[addr as usize] } pub fn write(&mut self, addr: u32, value: u32) { assert!(addr < 32); if addr != 0 { self.data[addr as usize] = value; } } }
メモリと同じように read
と write
ができますが、32 個しかないため assert!
を使っています。
CPU
の構造体には元記事同様 program counter
x_registers
memory
serial
nop_count
を持たせています。
pub struct Cpu { pub pc: u32, pub x_registers: XRegisters, memory: Memory, serial: Serial, nop_count: u8, }
CPU
は初期データをメモリに投入し、命令列を取得・デコードし、そして実行します。
impl Cpu { pub fn initialize_memory(&mut self, data: Vec<u8>) { self.memory.initialize(data); } pub fn fetch(&self) -> u32 { self.memory.read(self.pc) } pub fn run(&mut self) -> Result<Status> { let raw_inst = self.fetch(); let inst = Instruction::decode(raw_inst)?; // Addi { rd: 0, rs1: 0, imm: 0 } を NOP として扱う if let Instruction::Addi { rd: 0, rs1: 0, imm: 0, } = inst { self.nop_count += 1; } else { self.nop_count = 0; } if self.nop_count >= 5 { return Ok(Status::Finished); } self.execute(inst)?; Ok(Status::Processing) } }
命令を enum
で定義したためパターンマッチで記述しやすくなりました。
加算・減算はオーバーフローを無視して値を返してほしいため、 wrapping_add
wrapping_sub
を使っています。
impl Cpu { pub fn execute(&mut self, inst: Instruction) -> Result<()> { match inst { Instruction::Add { rd, rs1, rs2 } => { let value = self .x_registers .read(rs1) .wrapping_add(self.x_registers.read(rs2)); self.x_registers.write(rd, value); self.pc += 4; Ok(()) } ..., Instruction::Addi { rd, rs1, imm } => { // imm は 12bit の signed int なので 12bit 目が 0 なら正、1なら負 let num = match (imm & 0x80) == 0 { true => imm, false => 0xfffff000 | imm, // 13bit目以降を 1 で埋める }; let value = self.x_registers.read(rs1).wrapping_add(num); self.x_registers.write(rd, value); self.pc += 4; Ok(()) } ..., } } }
シミュレータ
CPU
を実行するシミュレータを定義します。
CPU
が NOP を 5 回処理して終了状態を返すかエラーが起きるまで実行を繰り返します。
pub struct Simulator { cpu: Cpu, } impl Simulator { pub fn new() -> Simulator { Self { cpu: Cpu::new() } } pub fn start(&mut self) -> Result<()> { loop { match self.cpu.run() { Ok(status) => match status { Status::Processing => {} Status::Finished => { break; } }, Err(e) => { return Err(e); } } } Ok(()) } }
後は main
を用意すればシミュレータを動かすことができます。
4649.rom
と hello.rom
を実行した結果は以下。
$ rv32sim 4649.rom -------------------------------------------------------------------------------- x00 = 0x0 (0) x01 = 0x46 (70) x02 = 0x49 (73) x03 = 0x0 (0) x04 = 0x0 (0) x05 = 0x0 (0) x06 = 0x0 (0) x07 = 0x0 (0) x08 = 0x0 (0) x09 = 0x0 (0) x10 = 0x0 (0) x11 = 0x0 (0) x12 = 0x0 (0) x13 = 0x0 (0) x14 = 0x0 (0) x15 = 0x0 (0) x16 = 0x0 (0) x17 = 0x0 (0) x18 = 0x0 (0) x19 = 0x0 (0) x20 = 0x0 (0) x21 = 0x0 (0) x22 = 0x0 (0) x23 = 0x0 (0) x24 = 0x0 (0) x25 = 0x0 (0) x26 = 0x0 (0) x27 = 0x0 (0) x28 = 0x0 (0) x29 = 0x0 (0) x30 = 0x0 (0) x31 = 0x0 (0) -------------------------------------------------------------------------------- pc = 0x18 (24)
$ rv32sim hello.rom HELLO WORLD!! -------------------------------------------------------------------------------- x00 = 0x0 (0) x01 = 0x0 (0) x02 = 0x0 (0) x03 = 0x10000000 (268435456) x04 = 0x0 (0) x05 = 0xa (10) x06 = 0x0 (0) x07 = 0x0 (0) x08 = 0x0 (0) x09 = 0x0 (0) x10 = 0x0 (0) x11 = 0x0 (0) x12 = 0x0 (0) x13 = 0x0 (0) x14 = 0x0 (0) x15 = 0x0 (0) x16 = 0x0 (0) x17 = 0x0 (0) x18 = 0x0 (0) x19 = 0x0 (0) x20 = 0x0 (0) x21 = 0x0 (0) x22 = 0x0 (0) x23 = 0x0 (0) x24 = 0x0 (0) x25 = 0x0 (0) x26 = 0x0 (0) x27 = 0x0 (0) x28 = 0x0 (0) x29 = 0x0 (0) x30 = 0x0 (0) x31 = 0x0 (0) -------------------------------------------------------------------------------- pc = 0x88 (136)
終わりに
シミュレータ作りを通して今まで避けていたビット演算と少し仲良くなれました。
梅雨はもう明けてしまいましたが、避暑がわりに CPU シミュレータを作ってみませんか?
永和システムマネジメント アジャイル事業部では Rust のお仕事もお待ちしています!
また一緒に CPU の勉強をしたいメンバーも絶賛募集中です!