diff --git a/README.md b/README.md index 9242e2c..d3f8d4b 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,27 @@ An in-progress Gameboy Advance Emulator written in Zig ⚡! * [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf) ## Compiling -Most recently built on Zig [v0.10.0-dev.1037+331cc810d](https://github.com/ziglang/zig/tree/331cc810d) +Most recently built on Zig [v0.10.0-dev.1659+4dd65316b](https://github.com/ziglang/zig/tree/4dd65316b) ### Dependencies * [SDL.zig](https://github.com/MasterQ32/SDL.zig) * [SDL2](https://www.libsdl.org/download-2.0.php) * [zig-clap](https://github.com/Hejsil/zig-clap) +* [known-folders](https://github.com/ziglibs/known-folders) * [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/f6044db788d35d43d66c1d7e58ef1e3c79f10d6f/lib/util/bitfields.zig) `bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`. -`SDL.zig` and `zig-clap` are git submodules you can init using `git submodule update --init` from your terminal. +Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders` -On Linux, be sure to have SDL2 installed using whatever package manager your distro uses. +Be sure to provide SDL2 using: +* Linux: Your distro's package manager +* MacOS: ¯\\\_(ツ)_/¯ +* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`) -On Windows, it's easiest if you use [`vcpkg`](https://github.com/Microsoft/vcpkg) to install `sdl2:x64-windows`. If not, -`SDL2.zig` will provide a helpful compile error which should help you get what you need. +`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2. -On macOS? ¯\\\_(ツ)_/¯ I hope it isn't too hard to compile though. - -Once you've got all the dependencies, run `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`. +Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`. ## Controls Key | Button diff --git a/src/Bus.zig b/src/Bus.zig index 72add6d..a1eec70 100644 --- a/src/Bus.zig +++ b/src/Bus.zig @@ -125,6 +125,7 @@ pub fn write16(self: *Self, addr: u32, halfword: u16) void { 0x0500_0000...0x05FF_FFFF => self.ppu.palette.set16(addr & 0x3FF, halfword), 0x0600_0000...0x0601_7FFF => self.ppu.vram.set16(addr - 0x0600_0000, halfword), 0x0700_0000...0x07FF_FFFF => self.ppu.oam.set16(addr & 0x3FF, halfword), + 0x0800_00C4, 0x0800_00C6, 0x0800_00C8 => log.warn("Tried to write 0x{X:0>4} to GPIO", .{halfword}), else => undWrite("Tried to write 0x{X:0>4} to 0x{X:0>8}", .{ halfword, addr }), } diff --git a/src/apu.zig b/src/apu.zig index 840a248..854db35 100644 --- a/src/apu.zig +++ b/src/apu.zig @@ -43,6 +43,10 @@ pub const Apu = struct { self.ch_vol_cnt.raw = (self.ch_vol_cnt.raw & 0xFF00) | byte; } + pub fn setSoundCntLHigh(self: *Self, byte: u8) void { + self.ch_vol_cnt.raw = @as(u16, byte) << 8 | (self.ch_vol_cnt.raw & 0xFF); + } + pub fn setBiasHigh(self: *Self, byte: u8) void { self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF); } @@ -51,9 +55,13 @@ pub const Apu = struct { const ToneSweep = struct { const Self = @This(); + /// NR10 sweep: io.Sweep, + /// NR11 duty: io.Duty, + /// NR12 envelope: io.Envelope, + /// NR13, NR14 freq: io.Frequency, fn init() Self { @@ -65,6 +73,10 @@ const ToneSweep = struct { }; } + pub fn setFreqLow(self: *Self, byte: u8) void { + self.freq.raw = (self.freq.raw & 0xFF00) | byte; + } + pub fn setFreqHigh(self: *Self, byte: u8) void { self.freq.raw = (@as(u16, byte) << 8) | (self.freq.raw & 0xFF); } @@ -73,8 +85,11 @@ const ToneSweep = struct { const Tone = struct { const Self = @This(); + /// NR21 duty: io.Duty, + /// NR22 envelope: io.Envelope, + /// NR23, NR24 freq: io.Frequency, fn init() Self { @@ -85,8 +100,12 @@ const Tone = struct { }; } + pub fn setFreqLow(self: *Self, byte: u8) void { + self.freq.raw = (self.freq.raw & 0xFF00) | byte; + } + pub fn setFreqHigh(self: *Self, byte: u8) void { - self.freq.raw = (self.freq.raw & 0x00FF) | (@as(u16, byte) << 8); + self.freq.raw = @as(u16, byte) << 8 | (self.freq.raw & 0xFF); } }; @@ -94,10 +113,13 @@ const Wave = struct { const Self = @This(); /// Write-only + /// NR30 select: io.WaveSelect, /// NR31 length: u8, + /// NR32 vol: io.WaveVolume, + /// NR33, NR34 freq: io.Frequency, fn init() Self { @@ -108,6 +130,14 @@ const Wave = struct { .length = 0, }; } + + pub fn setFreqLow(self: *Self, byte: u8) void { + self.freq.raw = (self.freq.raw & 0xFF00) | byte; + } + + pub fn setFreqHigh(self: *Self, byte: u8) void { + self.freq.raw = @as(u16, byte) << 8 | (self.freq.raw & 0xFF); + } }; const Noise = struct { @@ -116,8 +146,11 @@ const Noise = struct { /// Write-only /// NR41 len: u6, + /// NR42 envelope: io.Envelope, + /// NR43 poly: io.PolyCounter, + /// NR44 cnt: io.NoiseControl, fn init() Self { diff --git a/src/bus/backup.zig b/src/bus/backup.zig index ce639b3..6199718 100644 --- a/src/bus/backup.zig +++ b/src/bus/backup.zig @@ -23,6 +23,9 @@ pub const Backup = struct { title: [12]u8, save_path: ?[]const u8, + // TODO: Implement EEPROM + flash: Flash, + pub fn init(alloc: Allocator, kind: BackupKind, title: [12]u8, path: ?[]const u8) !Self { const buf_len: usize = switch (kind) { .Sram => 0x8000, // 32K @@ -40,15 +43,14 @@ pub const Backup = struct { .kind = kind, .title = title, .save_path = path, + .flash = Flash.init(), }; - if (backup.save_path) |p| backup.loadSaveFromDisk(p) catch |e| log.err("Failed to load save: {}", .{e}); + if (backup.save_path) |p| backup.loadSaveFromDisk(p) catch |e| log.err("Failed to load save: {}", .{e}); return backup; } pub fn guessKind(rom: []const u8) ?BackupKind { - @setRuntimeSafety(false); - for (backup_kinds) |needle| { const needle_len = needle.str.len; @@ -63,7 +65,6 @@ pub const Backup = struct { pub fn deinit(self: Self) void { if (self.save_path) |path| self.writeSaveToDisk(path) catch |e| log.err("Failed to write save: {}", .{e}); - self.alloc.free(self.buf); } @@ -78,7 +79,7 @@ pub const Backup = struct { defer self.alloc.free(file_buf); switch (self.kind) { - .Sram => { + .Sram, .Flash, .Flash1M => { std.mem.copy(u8, self.buf, file_buf); log.info("Loaded Save from {s}", .{file_path}); }, @@ -103,37 +104,70 @@ pub const Backup = struct { defer self.alloc.free(file_path); switch (self.kind) { - .Sram => { + .Sram, .Flash, .Flash1M => { const file = try std.fs.createFileAbsolute(file_path, .{}); defer file.close(); try file.writeAll(self.buf); - log.info("Dumped SRAM to {s}", .{file_path}); + log.info("Wrote Save to {s}", .{file_path}); }, else => return SaveError.UnsupportedBackupKind, } } pub fn get8(self: *const Self, idx: usize) u8 { - // TODO: Implement Flash and EEPROM switch (self.kind) { - .Flash => return switch (idx) { - 0x0000 => 0x32, // Panasonic manufacturer ID - 0x0001 => 0x1B, // Panasonic device ID - else => self.buf[idx], + .Flash => { + switch (idx) { + 0x0000 => if (self.flash.id_mode) return 0x32, // Panasonic manufacturer ID + 0x0001 => if (self.flash.id_mode) return 0x1B, // Panasonic device ID + else => {}, + } + + return self.flash.read(self.buf, idx); }, - .Flash1M => return switch (idx) { - 0x0000 => 0x62, // Sanyo manufacturer ID - 0x0001 => 0x13, // Sanyo device ID - else => self.buf[idx], + .Flash1M => { + switch (idx) { + 0x0000 => if (self.flash.id_mode) return 0x62, // Sanyo manufacturer ID + 0x0001 => if (self.flash.id_mode) return 0x13, // Sanyo device ID + else => {}, + } + + return self.flash.read(self.buf, idx); }, .Eeprom => return self.buf[idx], - .Sram => return self.buf[idx & 0x7FFF], // 32K SRAM chips are repeated + .Sram => return self.buf[idx & 0x7FFF], // 32K SRAM chip is mirrored } } pub fn set8(self: *Self, idx: usize, byte: u8) void { - self.buf[idx] = byte; + switch (self.kind) { + .Flash, .Flash1M => { + if (self.flash.prep_write) return self.flash.write(self.buf, idx, byte); + if (self.flash.shouldEraseSector(idx, byte)) return self.flash.eraseSector(self.buf, idx); + + switch (idx) { + 0x0000 => if (self.kind == .Flash1M and self.flash.set_bank) { + self.flash.bank = @truncate(u1, byte); + }, + 0x5555 => { + if (self.flash.state == .Command) { + self.flash.handleCommand(self.buf, byte); + } else if (byte == 0xAA and self.flash.state == .Ready) { + self.flash.state = .Set; + } else if (byte == 0xF0) { + self.flash.state = .Ready; + } + }, + 0x2AAA => if (byte == 0x55 and self.flash.state == .Set) { + self.flash.state = .Command; + }, + else => {}, + } + }, + .Eeprom => self.buf[idx] = byte, + .Sram => self.buf[idx & 0x7FFF] = byte, + } } }; @@ -145,10 +179,12 @@ const BackupKind = enum { }; const Needle = struct { + const Self = @This(); + str: []const u8, kind: BackupKind, - fn init(str: []const u8, kind: BackupKind) @This() { + fn init(str: []const u8, kind: BackupKind) Self { return .{ .str = str, .kind = kind, @@ -159,3 +195,71 @@ const Needle = struct { const SaveError = error{ UnsupportedBackupKind, }; + +const Flash = struct { + const Self = @This(); + + state: FlashState, + + id_mode: bool, + set_bank: bool, + prep_erase: bool, + prep_write: bool, + + bank: u1, + + fn init() Self { + return .{ + .state = .Ready, + .id_mode = false, + .set_bank = false, + .prep_erase = false, + .prep_write = false, + .bank = 0, + }; + } + + fn handleCommand(self: *Self, buf: []u8, byte: u8) void { + switch (byte) { + 0x90 => self.id_mode = true, + 0xF0 => self.id_mode = false, + 0xB0 => self.set_bank = true, + 0x80 => self.prep_erase = true, + 0x10 => { + std.mem.set(u8, buf, 0xFF); + self.prep_erase = false; + }, + 0xA0 => self.prep_write = true, + else => std.debug.panic("Unhandled Flash Command: 0x{X:0>2}", .{byte}), + } + + self.state = .Ready; + } + + fn shouldEraseSector(self: *const Self, idx: usize, byte: u8) bool { + return self.prep_erase and idx & 0xFFF == 0x000 and byte == 0x30; + } + + fn write(self: *Self, buf: []u8, idx: usize, byte: u8) void { + buf[idx + if (self.bank == 1) 0x1000 else @as(usize, 0)] = byte; + self.prep_write = false; + } + + fn read(self: *const Self, buf: []u8, idx: usize) u8 { + return buf[idx + if (self.bank == 1) 0x1000 else @as(usize, 0)]; + } + + fn eraseSector(self: *Self, buf: []u8, idx: usize) void { + const start = (idx & 0xF000) + if (self.bank == 1) 0x1000 else @as(usize, 0); + + std.mem.set(u8, buf[start..][0..0x1000], 0xFF); + self.prep_erase = false; + self.state = .Ready; + } +}; + +const FlashState = enum { + Ready, + Set, + Command, +}; diff --git a/src/bus/io.zig b/src/bus/io.zig index b3ff351..b365004 100644 --- a/src/bus/io.zig +++ b/src/bus/io.zig @@ -124,6 +124,9 @@ pub fn read16(bus: *const Bus, addr: u32) u16 { // Sound 0x0400_0088 => bus.apu.bias.raw, + // DMA Transfers + 0x0400_00BA => bus.dma._0.cnt.raw, + // Timers 0x0400_0100 => bus.tim._0.counter(), 0x0400_0102 => bus.tim._0.cnt.raw, @@ -221,6 +224,8 @@ pub fn write16(bus: *Bus, addr: u32, halfword: u16) void { // Serial Communication 2 0x0400_0134 => log.warn("Wrote 0x{X:0>4} to RCNT", .{halfword}), + 0x0400_0140 => log.warn("Wrote 0x{X:0>4} to JOYCNT", .{halfword}), + 0x0400_0158 => log.warn("Wrote 0x{X:0>4} to JOYSTAT", .{halfword}), // Interrupts 0x0400_0200 => bus.io.ie.raw = halfword, @@ -239,6 +244,13 @@ pub fn read8(bus: *const Bus, addr: u32) u8 { 0x0400_0006 => @truncate(u8, bus.ppu.vcount.raw), // Sound + 0x0400_0060 => bus.apu.ch1.sweep.raw, + 0x0400_0063 => bus.apu.ch1.envelope.raw, + 0x0400_0069 => bus.apu.ch2.envelope.raw, + 0x0400_0073 => bus.apu.ch3.vol.raw, + 0x0400_0079 => bus.apu.ch4.envelope.raw, + 0x0400_007C => bus.apu.ch4.poly.raw, + 0x0400_0081 => @truncate(u8, bus.apu.ch_vol_cnt.raw >> 8), 0x0400_0089 => @truncate(u8, bus.apu.bias.raw >> 8), // Serial Communication 1 @@ -258,14 +270,26 @@ pub fn write8(bus: *Bus, addr: u32, byte: u8) void { 0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, byte) << 8) | (bus.ppu.dispstat.raw & 0xFF), // Sound + 0x0400_0060 => bus.apu.ch1.sweep.raw = byte, + 0x0400_0062 => bus.apu.ch1.duty.raw = byte, 0x0400_0063 => bus.apu.ch1.envelope.raw = byte, + 0x0400_0064 => bus.apu.ch1.setFreqLow(byte), 0x0400_0065 => bus.apu.ch1.setFreqHigh(byte), + 0x0400_0068 => bus.apu.ch2.duty.raw = byte, 0x0400_0069 => bus.apu.ch2.envelope.raw = byte, + 0x0400_006C => bus.apu.ch2.setFreqLow(byte), 0x0400_006D => bus.apu.ch2.setFreqHigh(byte), 0x0400_0070 => bus.apu.ch3.select.raw = byte, + 0x0400_0072 => bus.apu.ch3.length = byte, + 0x0400_0073 => bus.apu.ch3.vol.raw = byte, + 0x0400_0074 => bus.apu.ch3.setFreqLow(byte), + 0x0400_0075 => bus.apu.ch3.setFreqHigh(byte), + 0x0400_0078 => bus.apu.ch4.len = @truncate(u6, byte), 0x0400_0079 => bus.apu.ch4.envelope.raw = byte, + 0x0400_007C => bus.apu.ch4.poly.raw = byte, 0x0400_007D => bus.apu.ch4.cnt.raw = byte, 0x0400_0080 => bus.apu.setSoundCntLLow(byte), + 0x0400_0081 => bus.apu.setSoundCntLHigh(byte), 0x0400_0084 => bus.apu.setSoundCntX(byte >> 7 & 1 == 1), 0x0400_0089 => bus.apu.setBiasHigh(byte), diff --git a/src/emu.zig b/src/emu.zig index 2ff3990..74db05d 100644 --- a/src/emu.zig +++ b/src/emu.zig @@ -43,8 +43,9 @@ pub fn run(kind: RunKind, quit: *Atomic(bool), fps: *FpsAverage, sched: *Schedul } pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void { - var cycles: u64 = 0; - while (cycles < cycles_per_frame) : (cycles += 1) { + const frame_end = sched.tick + cycles_per_frame; + + while (sched.tick < frame_end) { sched.tick += 1; _ = cpu.step();