🦀

RustでTD4のエミュレーターを書いた話

2023/05/21に公開

初投稿です、yank.nvimと申します。普段はPythonやRustなどで遊んでいるNeovimが好きな者です。

はじめに

最近は低レイヤープログラミングに興味があり、その一環でCPUを自作してみたいなと思いました。しかしゼロからCPUを作るのは非常に難しいので、とりあえず既存のシンプルなCPUを移植してCPUとはなんぞやをつかもうと思いました。

目的

渡波郁氏の著書、『CPUの創りかた』に登場するCPU「TD4」のエミュレーターをRustで実装する。

実装

src/opcode.rs
use num_derive::FromPrimitive;

#[derive(FromPrimitive, PartialEq)]
pub enum Opcode {
    AddA  = 0b0000,
    AddB  = 0b0101,
    MovA  = 0b0011,
    MovB  = 0b0111,
    MovAB = 0b0001,
    MovBA = 0b0100,
    Jmp   = 0b1111,
    Jnc   = 0b1110,
    InA   = 0b0010,
    InB   = 0b0110,
    OutB  = 0b1001,
    OutIm = 0b1011,
    Brk   = 0b1101,
}

まずはオペコードから。基本はTD4と一緒です。本来存在しないはずのBrk命令が存在しますが、これはエミュレーターを終了する命令です。

src/register.rs
pub struct Register {
    pub register_a: u8,
    pub register_b: u8,
    pub carry: bool,
    pub pc: u8,
}

impl Register {
    pub fn new() -> Self {
        Register {
            register_a: 0,
            register_b: 0,
            carry: false,
            pc: 0,
        }
    }
}

次にレジスタ。これは特に変わったところなし。

src/port.rs
pub struct Port {
    pub input: u8,
    pub output: u8,
}

impl Port {
    pub fn new() -> Self {
        Port {
            input: 0,
            output: 0,
        }
    }
}

入出力ポート。

src/memory.rs
use std::fs::File;
use std::io::{self, BufRead};

pub struct Memory {
    pub memory: Vec<u8>,
}
impl Memory {
    pub fn new() -> Self {
        Memory {
            memory: vec![0; 16]
        }
    }
    
    pub fn load_new(path: &str) -> Memory {
        let mut memory = Memory::new();
        memory.load(path);
        memory
    }

    pub fn load(&mut self, path: &str) {
        let bin_file = File::open(path).unwrap();
        let lines = io::BufReader::new(bin_file).lines();
        for (addr, line) in lines.enumerate() {
            if let Ok(bin_str) = line {
                let bin = u8::from_str_radix(&bin_str, 2).unwrap();
                self.memory[addr] = bin;
            }
        }
    }
}

プログラムメモリ。プログラムのファイルを1行ごとに読んでメモリに追加。

src/cpu.rs
use std::process::exit;

use crate::memory::*;
use crate::opcode::*;
use crate::port::*;
use crate::register::*;

pub struct CPU {
    pub memory: Memory,
    pub register: Register,
    pub port: Port,
}

impl CPU {
    pub fn new(path: &str) -> Self {
        CPU {
            memory: Memory::load_new(path),
            register: Register::new(),
            port: Port::new(),
        }
    }

    pub fn fetch(&self) -> (Opcode, u8) {
        let pc = self.register.pc;
        let operation = self.memory.memory[pc as usize];

        let opcode = num_traits::FromPrimitive::from_u8(operation >> 4).unwrap();
        let operand = operation & 0b1111;
        (opcode, operand)
    }

    pub fn execute(&mut self, opcode: &Opcode, operand: u8) {
        match opcode {
            Opcode::AddA => {
                let tmp = self.register.register_a + operand;
                self.register.carry = (0b10000 & tmp) != 0;
                self.register.register_a =  0b1111 & tmp;
            }
            Opcode::AddB => {
                let tmp = self.register.register_b + operand;
                self.register.carry = (0b10000 & tmp) != 0;
                self.register.register_b =  0b1111 & tmp;
            }
            Opcode::MovA => {
                self.register.register_a = operand;
                self.register.carry = false;
            }
            Opcode::MovB => {
                self.register.register_b = operand;
                self.register.carry = false;
            }
            Opcode::MovAB => {
                self.register.register_a = self.register.register_b;
                self.register.carry = false;
            }
            Opcode::MovBA => {
                self.register.register_b = self.register.register_a;
                self.register.carry = false;
            }
            Opcode::Jmp => {
                self.register.pc = operand;
                self.register.carry = false;
                return;
            }
            Opcode::Jnc => {
                if self.register.carry {
                    self.register.pc = operand;
                    self.register.carry = false;
                    return;
                } 
            }
            Opcode::InA => {
                self.register.register_a = self.port.input;
                self.register.carry = false;
            }
            Opcode::InB => {
                self.register.register_b = self.port.input;
                self.register.carry = false;
            }
            Opcode::OutB => {
                self.port.output = self.register.register_b;
                self.register.carry = false;
            }
            Opcode::OutIm => {
                self.port.output = operand;
                self.register.carry = false;
            }
            Opcode::Brk => {
                exit(0);
            }
        }
        self.register.pc = (self.register.pc + 1) & 0b1111;
    }

}

CPU本体。命令をfetchしてexecuteします。オペコードごとにmatch文で挙動を変えています。加算命令以外のところではキャリーレジスタをfalseにして、加算命令で桁溢れが発生したときはキャリーレジスタをtrueにしています。また、桁溢れが発生する可能性のある箇所には& 0b1111をつけて4bit整数に収めています。

src/main.rs
mod opcode;
mod memory;
mod register;
mod port;
mod cpu;

use cpu::*;

fn main() {
    let mut cpu = CPU::new("test.bin");
    for i in 0..100 {
        let (opcode, operand) = cpu.fetch();
        cpu.execute(&opcode, operand);
        println!("{:<2} {:>4b}", i, cpu.port.output);
    }
}

あとはそれを良い感じに実行するコードを書いて…

test.bin
00110001 //Mov A, Im 0001
00000101 //Add A, Im 0101
01000000 //Mov B, A
10010000 //Out B
11010000 //Brk

良い感じのテスト用プログラムを書いて…

$ cargo run                                                        
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/td4-emulator-rs`
0     0
1     0
2     0
3   110

実行すると無事に計算された結果が出力された。

まとめ

Rustを使い、TD4のエミュレーターを書くことができました。テストもコメントも一行も書いていなかったりといった雑なコードではありますが、Rustの恩恵をところどころ受けながら書くことができました。今後はこれ用のアセンブラなどの自作や、より複雑なCPUのエミュレーター自作もやってみたいです。

GitHub

https://github.com/yanknvim/td4-emulator-rs
 今回作ったエミュレーターはこちらにアップロードしてあります。

Discussion