⚙️
RustでCPUを自作して動くまで📝
はじめに
CPUの名前は CC8R(ちみ's CPU 8bit RISC) !
お仕事のお勉強のメモ📝に近いかも🦆
【サマリー】
- 8bitCPUの設計
- RustでCPUを実装、CPUをエミュレート(※)
- 自作CPUで
(5+3)x2=16を計算させる
※C++版も実装しました🥳
成果物(できたもの)
Rustでの実装
C++での実装
対象者
わたしと同じ考えのひとへの参考になれば幸いです🥳
- 『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で実装していきます🛠️
-
- 定義 ... CPU、フラグ、命令セットの定義
-
- 実装 ... CPUがメモリから命令を
フェッチ、デコード、実行する
- 実装 ... CPUがメモリから命令を
-
- テスト ... CPUが
(5+3)x2を計算できるかテスト
- テスト ... CPUが
開発環境
開発環境はRustRoverがおすすめです!
- PJの新規作成で構成ファイルが自動生成される
- WASMへのビルドもできる
- デバッグやカバレッジ付きでテストもできる
[RustRover URL🔗]
定義
まずは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として動作するように実装していきます🛠️
- CPUのimpl
- プログラムローダー
- フラグの更新
- フェッチ
- デコード
- 実行
- 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;
}
}
フェッチ
- CPUのPCのメモリから命令を読み出し
- PCを+1
- 戻り値を命令でリターン
fn fetch(&mut self) -> u8 {
let instruction = self.memory[self.pc as usize];
self.pc = self.pc.wrapping_add(1);
instruction
}
デコード
- 命令をデコード
- デコードした命令のStringに変換
- 戻り値をデコードした命令の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(),
}
}
実行
NOP、HALT、LDI、MV、ADDの実装です🥳
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)に戻る
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++版です🥳
Discussion