🦀
RustでTD4のエミュレーターを書いた話
初投稿です、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
今回作ったエミュレーターはこちらにアップロードしてあります。
Discussion