Initial Commit

This commit is contained in:
Rekai Nyangadzayi Musuka 2021-12-29 15:09:00 -06:00
commit 5b3b81e4dc
12 changed files with 564 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/.vscode
/bin
/zig-cache
/zig-out

34
build.zig Normal file
View File

@ -0,0 +1,34 @@
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("zba", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
}

44
src/bus.zig Normal file
View File

@ -0,0 +1,44 @@
const std = @import("std");
const GamePak = @import("pak.zig").GamePak;
const Allocator = std.mem.Allocator;
pub const Bus = struct {
pak: GamePak,
pub fn withPak(alloc: Allocator, path: []const u8) !@This() {
return @This(){
.pak = try GamePak.fromPath(alloc, path),
};
}
pub fn readWord(self: *const @This(), addr: u32) u32 {
return self.pak.readWord(addr);
}
pub fn writeWord(_: *@This(), _: u32, _: u32) void {
std.debug.panic("TODO: Implement Bus#writeWord", .{});
}
pub fn readHalfWord(self: *const @This(), addr: u32) u16 {
return self.pak.readHalfWord(addr);
}
pub fn writeHalfWord(self: *@This(), addr: u32, halfword: u16) void {
// TODO: Actually implement the memory mmap
if (addr >= self.pak.buf.len) {
return;
}
self.pak.writeHalfWord(addr, halfword);
}
pub fn readByte(self: *const @This(), addr: u32) u8 {
return self.pak.readByte(addr);
}
pub fn writeByte(_: *@This(), _: u32, _: u8) void {
std.debug.panic("TODO: Implement Bus#writeByte", .{});
}
};

152
src/cpu.zig Normal file
View File

@ -0,0 +1,152 @@
const std = @import("std");
const Bus = @import("bus.zig").Bus;
const Scheduler = @import("scheduler.zig").Scheduler;
const comptimeDataProcessing = @import("cpu/data_processing.zig").comptimeDataProcessing;
const comptimeSingleDataTransfer = @import("cpu/single_data_transfer.zig").comptimeSingleDataTransfer;
const comptimeHalfSignedDataTransfer = @import("cpu/half_signed_data_transfer.zig").comptimeHalfSignedDataTransfer;
pub const InstrFn = fn (*ARM7TDMI, *Bus, u32) void;
const ARM_LUT: [0x1000]InstrFn = populate();
pub const ARM7TDMI = struct {
r: [16]u32,
sch: *Scheduler,
bus: *Bus,
cpsr: CPSR,
pub fn new(scheduler: *Scheduler, bus: *Bus) @This() {
const cpsr: u32 = 0x0000_00DF;
return .{
.r = [_]u32{0x00} ** 16,
.sch = scheduler,
.bus = bus,
.cpsr = @bitCast(CPSR, cpsr),
};
}
pub inline fn step(self: *@This()) u64 {
const opcode = self.fetch();
// Debug
std.debug.print("R15: 0x{X:}\n", .{ opcode });
ARM_LUT[armIdx(opcode)](self, self.bus, opcode);
return 1;
}
fn fetch(self: *@This()) u32 {
const word = self.bus.readWord(self.r[15]);
self.r[15] += 4;
return word;
}
fn fakePC(self: *const @This()) u32 {
return self.r[15] + 4;
}
};
fn armIdx(opcode: u32) u12 {
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 8 & 0xF);
}
fn populate() [0x1000]InstrFn {
return comptime {
@setEvalBranchQuota(0x5000);
var lut = [_]InstrFn{undefined_instr} ** 0x1000;
var i: usize = 0;
while (i < lut.len) : (i += 1) {
if (i >> 10 & 0x3 == 0b00) {
const I = i >> 9 & 0x01 == 0x01;
const S = i >> 4 & 0x01 == 0x01;
const instrKind = i >> 5 & 0x0F;
lut[i] = comptimeDataProcessing(I, S, instrKind);
}
if (i >> 9 & 0x7 == 0b000 and i >> 6 & 0x01 == 0x00 and i & 0xF == 0x0) {
// Halfword and Signed Data Transfer with register offset
const P = i >> 8 & 0x01 == 0x01;
const U = i >> 7 & 0x01 == 0x01;
const I = true;
const W = i >> 5 & 0x01 == 0x01;
const L = i >> 4 & 0x01 == 0x01;
lut[i] = comptimeHalfSignedDataTransfer(P, U, I, W, L);
}
if (i >> 9 & 0x7 == 0b000 and i >> 6 & 0x01 == 0x01) {
// Halfword and Signed Data Tranfer with immediate offset
const P = i >> 8 & 0x01 == 0x01;
const U = i >> 7 & 0x01 == 0x01;
const I = false;
const W = i >> 5 & 0x01 == 0x01;
const L = i >> 4 & 0x01 == 0x01;
lut[i] = comptimeHalfSignedDataTransfer(P, U, I, W, L);
}
if (i >> 10 & 0x3 == 0b01 and i & 0x01 == 0x00) {
const I = i >> 9 & 0x01 == 0x01;
const P = i >> 8 & 0x01 == 0x01;
const U = i >> 7 & 0x01 == 0x01;
const B = i >> 6 & 0x01 == 0x01;
const W = i >> 5 & 0x01 == 0x01;
const L = i >> 4 & 0x01 == 0x01;
lut[i] = comptimeSingleDataTransfer(I, P, U, B, W, L);
}
if (i >> 9 & 0x7 == 0b101) {
const L = i >> 8 & 0x01 == 0x01;
lut[i] = comptimeBranch(L);
}
}
return lut;
};
}
const CPSR = packed struct {
n: bool, // Negative / Less Than
z: bool, // Zero
c: bool, // Carry / Borrow / Extend
v: bool, // Overflow
_: u20,
i: bool, // IRQ Disable
f: bool, // FIQ Diable
t: bool, // State
m: Mode, // Mode
};
const Mode = enum(u5) {
User = 0b10000,
Fiq = 0b10001,
Irq = 0b10010,
Supervisor = 0b10011,
Abort = 0b10111,
Undefined = 0b11011,
System = 0b11111,
};
fn undefined_instr(_: *ARM7TDMI, _: *Bus, opcode: u32) void {
const id = armIdx(opcode);
std.debug.panic("[0x{X:}] 0x{X:} is an illegal opcode", .{ id, opcode });
}
fn comptimeBranch(comptime L: bool) InstrFn {
return struct {
fn branch(cpu: *ARM7TDMI, _: *Bus, opcode: u32) void {
if (L) {
cpu.r[14] = cpu.r[15] - 4;
}
const offset = @bitCast(i32, (opcode << 2) << 8) >> 8;
cpu.r[15] = cpu.fakePC() + @bitCast(u32, offset);
}
}.branch;
}

View File

@ -0,0 +1,56 @@
const std = @import("std");
const cpu_mod = @import("../cpu.zig");
const Bus = @import("../bus.zig").Bus;
const ARM7TDMI = cpu_mod.ARM7TDMI;
const InstrFn = cpu_mod.InstrFn;
pub fn comptimeDataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
return struct {
fn dataProcessing(cpu: *ARM7TDMI, _: *Bus, opcode: u32) void {
const rd = opcode >> 12 & 0xF;
const op1 = opcode >> 16 & 0xF;
var op2: u32 = undefined;
if (I) {
op2 = std.math.rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) << 1);
} else {
op2 = reg_op2(cpu, opcode);
}
switch (instrKind) {
0x4 => {
cpu.r[rd] = cpu.r[op1] + op2;
if (S) std.debug.panic("TODO: implement ADD condition codes", .{});
},
0xD => {
cpu.r[rd] = op2;
if (S) std.debug.panic("TODO: implement MOV condition codes", .{});
},
else => std.debug.panic("TODO: implement data processing type {}", .{instrKind}),
}
}
}.dataProcessing;
}
fn reg_op2(cpu: *const ARM7TDMI, opcode: u32) u32 {
var amount: u32 = undefined;
if (opcode >> 4 & 0x01 == 0x01) {
amount = cpu.r[opcode >> 8 & 0xF] & 0xFF;
} else {
amount = opcode >> 7 & 0x1F;
}
const rm = opcode & 0xF;
const r_val = cpu.r[rm];
return switch (opcode >> 5 & 0x03) {
0b00 => r_val << @truncate(u5, amount),
0b01 => r_val >> @truncate(u5, amount),
0b10 => @bitCast(u32, @bitCast(i32, r_val) >> @truncate(u5, amount)),
0b11 => std.math.rotr(u32, r_val, amount),
else => unreachable,
};
}

View File

@ -0,0 +1,67 @@
const std = @import("std");
const cpu_mod = @import("../cpu.zig");
const util = @import("../util.zig");
const Bus = @import("../bus.zig").Bus;
const ARM7TDMI = cpu_mod.ARM7TDMI;
const InstrFn = cpu_mod.InstrFn;
pub fn comptimeHalfSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn halfSignedDataTransfer(cpu: *ARM7TDMI, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const rm = opcode & 0xF;
const imm_offset_high = opcode >> 8 & 0xF;
const base = cpu.r[rn];
var offset: u32 = undefined;
if (I) {
offset = imm_offset_high << 4 | rm;
} else {
offset = cpu.r[rm];
}
const modified_base = if (U) base + offset else base - offset;
var address = if (P) modified_base else base;
if (L) {
switch(@truncate(u2, opcode >> 5)) {
0b00 => {
// SWP
std.debug.panic("TODO: Implement SWP", .{});
},
0b01 => {
// LDRH
const halfword = bus.readHalfWord(address);
cpu.r[rd] = @as(u32, halfword);
},
0b10 => {
// LDRSB
const byte = bus.readByte(address);
cpu.r[rd] = util.u32_sign_extend(@as(u32, byte), 8);
},
0b11 => {
// LDRSH
const halfword = bus.readHalfWord(address);
cpu.r[rd] = util.u32_sign_extend(@as(u32, halfword), 16);
}
}
} else {
if (opcode >> 5 & 0x01 == 0x01) {
// STRH
const src = @truncate(u16, cpu.r[rd]);
bus.writeHalfWord(address + 2, src);
bus.writeHalfWord(address, src);
} else {
std.debug.panic("TODO Figure out if this is also SWP", .{});
}
}
address = modified_base;
if (W and P) cpu.r[rn] = address;
}
}.halfSignedDataTransfer;
}

View File

@ -0,0 +1,64 @@
const std = @import("std");
const util = @import("../util.zig");
const mod_cpu = @import("../cpu.zig");
const ARM7TDMI = mod_cpu.ARM7TDMI;
const InstrFn = mod_cpu.InstrFn;
const Bus = @import("../bus.zig").Bus;
pub fn comptimeSingleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
return struct {
fn singleDataTransfer(cpu: *ARM7TDMI, bus: *Bus, opcode: u32) void {
const rn = opcode >> 16 & 0xF;
const rd = opcode >> 12 & 0xF;
const base = cpu.r[rn];
const offset = if (I) opcode & 0xFFF else registerOffset(cpu, opcode);
const modified_base = if (U) base + offset else base - offset;
var address = if (P) modified_base else base;
if (L) {
if (B) {
// LDRB
cpu.r[rd] = bus.readByte(address);
} else {
// LDR
std.debug.panic("Implement LDR", .{});
}
} else {
if (B) {
// STRB
const src = @truncate(u8, cpu.r[rd]);
bus.writeByte(address + 3, src);
bus.writeByte(address + 2, src);
bus.writeByte(address + 1, src);
bus.writeByte(address, src);
} else {
// STR
std.debug.panic("Implement STR", .{});
}
}
address = modified_base;
if (W and P) cpu.r[rn] = address;
// TODO: W-bit forces non-privledged mode for the transfer
}
}.singleDataTransfer;
}
fn registerOffset(cpu: *ARM7TDMI, opcode: u32) u32 {
const amount = opcode >> 7 & 0x1F;
const rm = opcode & 0xF;
const r_val = cpu.r[rm];
return switch (opcode >> 5 & 0x03) {
0b00 => r_val << @truncate(u5, amount),
0b01 => r_val >> @truncate(u5, amount),
0b10 => @bitCast(u32, @bitCast(i32, r_val) >> @truncate(u5, amount)),
0b11 => std.math.rotr(u32, r_val, amount),
else => unreachable,
};
}

19
src/emu.zig Normal file
View File

@ -0,0 +1,19 @@
const _ = @import("std");
const Scheduler = @import("scheduler.zig").Scheduler;
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
const Bus = @import("bus.zig").Bus;
const CYCLES_PER_FRAME: u64 = 10_000; // TODO: What is this?
pub fn runFrame(sch: *Scheduler, cpu: *ARM7TDMI, bus: *Bus) void {
const frame_end = sch.tick + CYCLES_PER_FRAME;
while (sch.tick < frame_end) {
while (sch.tick < sch.nextTimestamp()) {
sch.tick += cpu.step();
}
sch.handleEvent(cpu, bus);
}
}

25
src/main.zig Normal file
View File

@ -0,0 +1,25 @@
const std = @import("std");
const Scheduler = @import("scheduler.zig").Scheduler;
const Bus = @import("bus.zig").Bus;
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
const emu = @import("emu.zig");
pub fn main() anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
// defer gpa.deinit();
var bus = try Bus.withPak(alloc, "./bin/demo/beeg/beeg.gba");
var scheduler = Scheduler.new(alloc);
var cpu = ARM7TDMI.new(&scheduler, &bus);
while (true) {
emu.runFrame(&scheduler, &cpu, &bus);
}
}
test "basic test" {
try std.testing.expectEqual(10, 3 + 7);
}

35
src/pak.zig Normal file
View File

@ -0,0 +1,35 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const GamePak = struct {
buf: []u8,
pub fn fromPath(alloc: Allocator, path: []const u8) !@This() {
const file = try std.fs.cwd().openFile(path, .{ .read = true });
defer file.close();
const len = try file.getEndPos();
return @This(){
.buf = try file.readToEndAlloc(alloc, len),
};
}
pub fn readWord(self: *const @This(), addr: u32) u32 {
return (@as(u32, self.buf[addr + 3]) << 24) | (@as(u32, self.buf[addr + 2]) << 16) | (@as(u32, self.buf[addr + 1]) << 8) | (@as(u32, self.buf[addr]));
}
pub fn readHalfWord(self: *const @This(), addr: u32) u16 {
return (@as(u16, self.buf[addr + 1]) << 8) | @as(u16, self.buf[addr]);
}
pub fn writeHalfWord(self: *@This(), addr: u32, halfword: u16) void {
self.buf[addr + 1] = @truncate(u8, halfword >> 8);
self.buf[addr] = @truncate(u8, halfword);
}
pub fn readByte(self: *const @This(), addr: u32) u8 {
return self.buf[addr];
}
};

57
src/scheduler.zig Normal file
View File

@ -0,0 +1,57 @@
const std = @import("std");
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
const Bus = @import("bus.zig").Bus;
const Order = std.math.Order;
const PriorityQueue = std.PriorityQueue;
const Allocator = std.mem.Allocator;
pub const Scheduler = struct {
tick: u64,
queue: PriorityQueue(Event, void, lessThan),
pub fn new(alloc: Allocator) @This() {
var scheduler = Scheduler{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(alloc, {}) };
scheduler.queue.add(.{
.kind = EventKind.HeatDeath,
.tick = std.math.maxInt(u64),
}) catch unreachable;
return scheduler;
}
pub fn handleEvent(self: *@This(), _: *ARM7TDMI, _: *Bus) void {
const should_handle = if (self.queue.peek()) |e| self.tick >= e.tick else false;
if (should_handle) {
const event = self.queue.remove();
switch (event.kind) {
.HeatDeath => {
std.debug.panic("Somehow, a u64 overflowed", .{});
},
}
}
}
pub inline fn nextTimestamp(self: *@This()) u64 {
if (self.queue.peek()) |e| {
return e.tick;
} else unreachable;
}
};
pub const Event = struct {
kind: EventKind,
tick: u64,
};
fn lessThan(context: void, a: Event, b: Event) Order {
_ = context;
return std.math.order(a.tick, b.tick);
}
pub const EventKind = enum {
HeatDeath,
};

7
src/util.zig Normal file
View File

@ -0,0 +1,7 @@
const std = @import("std");
pub fn u32_sign_extend(value: u32, bitSize: anytype) u32 {
const amount: u5 = 32 - bitSize;
return @bitCast(u32, @bitCast(i32, value << amount) >> amount);
}