From 5b3b81e4dc77dfd338a8579ce2e750178e4a60a6 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Wed, 29 Dec 2021 15:09:00 -0600 Subject: [PATCH] Initial Commit --- .gitignore | 4 + build.zig | 34 ++++++ src/bus.zig | 44 ++++++++ src/cpu.zig | 152 ++++++++++++++++++++++++++ src/cpu/data_processing.zig | 56 ++++++++++ src/cpu/half_signed_data_transfer.zig | 67 ++++++++++++ src/cpu/single_data_transfer.zig | 64 +++++++++++ src/emu.zig | 19 ++++ src/main.zig | 25 +++++ src/pak.zig | 35 ++++++ src/scheduler.zig | 57 ++++++++++ src/util.zig | 7 ++ 12 files changed, 564 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 src/bus.zig create mode 100644 src/cpu.zig create mode 100644 src/cpu/data_processing.zig create mode 100644 src/cpu/half_signed_data_transfer.zig create mode 100644 src/cpu/single_data_transfer.zig create mode 100644 src/emu.zig create mode 100644 src/main.zig create mode 100644 src/pak.zig create mode 100644 src/scheduler.zig create mode 100644 src/util.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b1d77a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.vscode +/bin +/zig-cache +/zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..9e83445 --- /dev/null +++ b/build.zig @@ -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); +} diff --git a/src/bus.zig b/src/bus.zig new file mode 100644 index 0000000..cc66138 --- /dev/null +++ b/src/bus.zig @@ -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", .{}); + } +}; diff --git a/src/cpu.zig b/src/cpu.zig new file mode 100644 index 0000000..41df8f0 --- /dev/null +++ b/src/cpu.zig @@ -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; +} diff --git a/src/cpu/data_processing.zig b/src/cpu/data_processing.zig new file mode 100644 index 0000000..0052edb --- /dev/null +++ b/src/cpu/data_processing.zig @@ -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, + }; +} diff --git a/src/cpu/half_signed_data_transfer.zig b/src/cpu/half_signed_data_transfer.zig new file mode 100644 index 0000000..20735d2 --- /dev/null +++ b/src/cpu/half_signed_data_transfer.zig @@ -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; +} \ No newline at end of file diff --git a/src/cpu/single_data_transfer.zig b/src/cpu/single_data_transfer.zig new file mode 100644 index 0000000..468c15c --- /dev/null +++ b/src/cpu/single_data_transfer.zig @@ -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, + }; +} diff --git a/src/emu.zig b/src/emu.zig new file mode 100644 index 0000000..4c6c662 --- /dev/null +++ b/src/emu.zig @@ -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); + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..55e0684 --- /dev/null +++ b/src/main.zig @@ -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); +} diff --git a/src/pak.zig b/src/pak.zig new file mode 100644 index 0000000..1e77b85 --- /dev/null +++ b/src/pak.zig @@ -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]; + } +}; diff --git a/src/scheduler.zig b/src/scheduler.zig new file mode 100644 index 0000000..a14c013 --- /dev/null +++ b/src/scheduler.zig @@ -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, +}; diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..e621652 --- /dev/null +++ b/src/util.zig @@ -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); +}