⚙️
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