⚙️

RustでCPUを自作して動くまで📝

2024/10/14に公開

はじめに

CPUの名前は CC8R(ちみ's CPU 8bit RISC)
お仕事のお勉強のメモ📝に近いかも🦆

【サマリー】

  • 8bitCPUの設計
  • RustでCPUを実装、CPUをエミュレート(※)
  • 自作CPUで(5+3)x2=16を計算させる

C++版も実装しました🥳

成果物(できたもの)

Rustでの実装

https://github.com/Chimipupu/cc8r_zenn

C++での実装

https://github.com/Chimipupu/cc8r_zenn_cpp

対象者

わたしと同じ考えのひとへの参考になれば幸いです🥳

  • 『Rustでなにかつくりたい!』
  • 『CPUを作ってみたい!』
  • 『低レイヤーを理解したい!』

設計

下記方針で8bitのRISC CPUを設計します🛠️

  • 計算ができる📊!(例: (5+3)x2)

特徴

  • アーキテクチャ ... 8bit
  • メモリ空間 ... 256Byte
  • 汎用レジスタ ... 8本(R0はアキュムレータ)
  • フラグレジスタ ... ゼロキャリーオーバーフローネガティブ
  • 命令セット ... 18(転送、算術論理演算、ジャンプ命令)

命令セット

  • LDI: レジスタに即値をロード
  • MV: レジスタ間のデータ転送
  • ADD, SUB, MUL, DIV: 四則演算
  • AND, OR, XOR: 論理演算
  • SHL, SHR: シフト操作
  • PUSH, POP: スタック操作
  • JMP, JZ, JNZ: ジャンプ命令
  • HALT: 実行停止
  • NOP: なにもしない

アセンブラ

命令セットからCPUに(5+3)x2をさせるアセンブラを組みます🥳

ORG 0x0000    ; プログラムの開始アドレスを0x0000に設定

LDI R1, 5     ; R1に5をロード
LDI R2, 3     ; R2に3をロード
LDI R3, 2     ; R3に2をロード
ADD R1, R2    ; R1とR2を加算
MV R4, R0     ; R0の値をR4に移動
MUL R3, R4    ; R3とR4を掛け算
HALT           ; プログラムを終了

機械語

CPUに(5+3)x2をさせるアセンブラCPUは理解できません🥲
➡ CPUが理解できる機械語にしていきます📊

命令セットの機械語

命令セットに対応した機械語を決めます📊

  • NOP: 0x00
  • HALT: 0x10
  • LDI: 0x14
  • MV: 0x18
  • ADD: 0x20
  • SUB: 0x30
  • MUL: 0x40
  • DIV: 0x50
  • AND: 0x60
  • OR: 0x70
  • XOR: 0x80
  • SHL: 0x90
  • SHR: 0xA0
  • PUSH: 0xB0
  • POP: 0xC0
  • JMP: 0xD0
  • JZ: 0xE0
  • JNZ: 0xF0

CPUに(5+3)x2をさせる機械語

CPUに(5+3)x2をさせるアセンブラを機械語にします🥳

0x14 0x01 0x05 // LDI R1, 5
0x14 0x02 0x03 // LDI R2, 3
0x14 0x03 0x02 // LDI R3, 2
0x20 0x01 0x02 // ADD R1, R2
0x18 0x04 0x00 // MV R4, R0
0x40 0x03 0x04 // MUL R3, R4
0x10           // HALT

Rust

設計したCPUをRustで実装していきます🛠️

    1. 定義 ... CPU、フラグ、命令セットの定義
    1. 実装 ... CPUがメモリから命令をフェッチデコード実行する
    1. テスト ... CPUが(5+3)x2を計算できるかテスト

開発環境

開発環境はRustRoverがおすすめです!

  • PJの新規作成で構成ファイルが自動生成される
  • WASMへのビルドもできる
  • デバッグやカバレッジ付きでテストもできる

[RustRover URL🔗]
https://www.jetbrains.com/rust/

定義

まずはCPU の構造体フラグ命令セットのオペコードを定義します📊

CPU の構造体

struct CC8R {
    registers: [u8; 8],
    pc: u8,
    sp: u8,
    flags: u8,
    memory: [u8; 256],
}

フラグ

フラグレジスタのゼロキャリーオーバーフローネガティブを定義

const FLAG_ZERO: u8 = 0b10000000;
const FLAG_CARRY: u8 = 0b01000000;
const FLAG_OVERFLOW: u8 = 0b00100000;
const FLAG_NEGATIVE: u8 = 0b00010000;

命令セットのオペコード

命令セット(18命令)の定義

const OP_NOP: u8 = 0x00;
const OP_HALT: u8 = 0x10;
const OP_LDI: u8 = 0x14;
const OP_MV: u8 = 0x18;
const OP_ADD: u8 = 0x20;
const OP_SUB: u8 = 0x30;
const OP_MUL: u8 = 0x40;
const OP_DIV: u8 = 0x50;
const OP_AND: u8 = 0x60;
const OP_OR: u8 = 0x70;
const OP_XOR: u8 = 0x80;
const OP_SHL: u8 = 0x90;
const OP_SHR: u8 = 0xA0;
const OP_PUSH: u8 = 0xB0;
const OP_POP: u8 = 0xC0;
const OP_JMP: u8 = 0xD0;
const OP_JZ: u8 = 0xE0;
const OP_JNZ: u8 = 0xF0;

実装

CPUとして動作するように実装していきます🛠️

  1. CPUのimpl
  2. プログラムローダー
  3. フラグの更新
  4. フェッチ
  5. デコード
  6. 実行
  7. CPUのメインループ

CPUのimpl

まずCPU構造体をimplします

impl CC8R {
    fn new() -> Self {
        CC8R {
            registers: [0; 8],
            pc: 0,
            sp: 0xF0,
            flags: 0,
            memory: [0; 256],
        }
    }
    // ここにフェッチ、デコード、実行を追加していく
}

プログラムローダー

プログラムをメモリに転送します

    fn load_program(&mut self, program: &[u8]) {
        for (i, &byte) in program.iter().enumerate() {
            self.memory[i] = byte;
        }
    }

フラグの更新

ゼロネガティブフラグの更新です

    fn update_flags(&mut self, result: u8) {
        if result == 0 {
            self.flags |= FLAG_ZERO;
        } else {
            self.flags &= !FLAG_ZERO;
        }

        if result & 0x80 != 0 {
            self.flags |= FLAG_NEGATIVE;
        } else {
            self.flags &= !FLAG_NEGATIVE;
        }
    }

フェッチ

  1. CPUのPCのメモリから命令を読み出し
  2. PCを+1
  3. 戻り値を命令でリターン
    fn fetch(&mut self) -> u8 {
        let instruction = self.memory[self.pc as usize];
        self.pc = self.pc.wrapping_add(1);
        instruction
    }

デコード

  1. 命令をデコード
  2. デコードした命令のStringに変換
  3. 戻り値をデコードした命令のStringにしてリターン
    fn decode(&self, instruction: u8) -> String {
        match instruction {
            OP_NOP => "NOP".to_string(),
            OP_LDI => "LDI".to_string(),
            OP_MV => "MV".to_string(),
            OP_ADD => "ADD".to_string(),
            OP_SUB => "SUB".to_string(),
            OP_MUL => "MUL".to_string(),
            OP_DIV => "DIV".to_string(),
            OP_AND => "AND".to_string(),
            OP_OR => "OR".to_string(),
            OP_XOR => "XOR".to_string(),
            OP_SHL => "SHL".to_string(),
            OP_SHR => "SHR".to_string(),
            OP_PUSH => "PUSH".to_string(),
            OP_POP => "POP".to_string(),
            OP_JMP => "JMP".to_string(),
            OP_JZ => "JZ".to_string(),
            OP_JNZ => "JNZ".to_string(),
            OP_HALT => "HALT".to_string(),
            _ => "UNKNOWN".to_string(),
        }
    }

実行

NOPHALTLDIMVADDの実装です🥳

    fn execute(&mut self, instruction: u8) -> bool {
        match instruction {
            OP_NOP => {
                // No operation
                println!("NOP");
            }
            OP_HALT => {
                println!("HALT");
                return false;
            }
            OP_LDI => {
                let ra = self.fetch();
                let value = self.fetch();
                self.registers[ra as usize] = value;
                println!("LDI R{}, {}", ra, value);
                self.update_flags(value);
            }
            OP_MV => {
                let ra = self.fetch();
                let rb = self.fetch();
                self.registers[ra as usize] = self.registers[rb as usize];
                println!("MV R{}, R{}", ra, rb);
                self.update_flags(rb);
            }
            OP_ADD => {
                let ra = self.fetch();
                let rb = self.fetch();
                let (result, carry) = self.registers[ra as usize].overflowing_add(self.registers[rb as usize]);
                self.registers[0] = result;
                println!("ADD R{}, R{}", ra, rb);
                self.update_flags(result);
                if carry {
                    self.flags |= FLAG_CARRY;
                } else {
                    self.flags &= !FLAG_CARRY;
                }
            }
            OP_MUL => {
                let ra = self.fetch();
                let rb = self.fetch();
                let result = self.registers[ra as usize] as u16 * self.registers[rb as usize] as u16;
                self.registers[0] = result as u8;
                println!("MUL R{}, R{}", ra, rb);
                self.update_flags(self.registers[ra as usize]);
                if result > 255 {
                    self.flags |= FLAG_OVERFLOW;
                } else {
                    self.flags &= !FLAG_OVERFLOW;
                }
            }

       // ここにさらに命令を実装していく

            _ => {
                println!("Unknown opcode: 0x{:02X}", instruction);
                return false;
            }
        }
        true
    }

CPUのメインループ

  1. メモリから命令をフェッチ
  2. 命令をデコード
  3. 命令を実行
  4. 1)に戻る
    fn run(&mut self) {
        loop {
            let instruction = self.fetch();
            self.decode(instruction);

            if !self.execute(instruction) {
                break;
            }
        }
    }

テスト

CPUに(5+3)x2をさせるRustのテストコードです🛠️

#[cfg(test)]
mod tests {
    use super::*; // 現在のモジュールをインポート

    #[test]
    fn test_cpu_program() {
        let mut cpu = CC8R::new();

        // (5+3)x2 のプログラム
        let program = [
            0x14, 0x01, 0x05, // LDI R1, 5
            0x14, 0x02, 0x03, // LDI R2, 3
            0x14, 0x03, 0x02, // LDI R3, 2
            0x20, 0x01, 0x02, // ADD R1, R2
            0x18, 0x04, 0x00, // MV R4, R0
            0x40, 0x03, 0x04, // MUL R3, R4
            0x10,             // HALT
        ];

        cpu.load_program(&program);
        cpu.run();
        println!("{}", cpu);

        assert_eq!(cpu.registers[0], 16);
    }
}

期待値

テストコードで出力される文字と期待値です🥳
自作CPUで(5+3)x2=16を計算できているとR0が16になる📊

  • 期待値: R0 ... 16
LDI R1, 5
LDI R2, 3
LDI R3, 2
ADD R1, R2
MV R4, R0
MUL R3, R4
HALT
Register: [16, 5, 3, 2, 8, 0, 0, 0]
Flag: 0x00
SP: 0xF0
PC: 0x13

テスト結果

RustRoverで期待値が出力されている画面🥳

おしまい

CPUってわかっちゃえば簡単かも❓🤔
CC8Rはもう少し実装して遊んでみます🥳

追記

C++での実装(追記:2024/10/15)

Rust版をベースにしたC++版です🥳
https://github.com/Chimipupu/cc8r_zenn_cpp

Discussion