feat: implement EEPROM

This commit is contained in:
Rekai Nyangadzayi Musuka 2022-04-25 16:08:56 -05:00
parent f4a48d536c
commit 05a432f1c1
3 changed files with 329 additions and 11 deletions

View File

@ -173,7 +173,7 @@ pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
0x07 => self.ppu.oam.write(T, align_addr, value),
// External Memory (Game Pak)
0x08...0x0D => {}, // EEPROM
0x08...0x0D => self.pak.write(T, self.dma._3.word_count, align_addr, value),
0x0E...0x0F => {
const rotate_by = switch (T) {
u32 => address & 3,

View File

@ -56,6 +56,10 @@ fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
};
}
inline fn isLarge(self: *const Self) bool {
return self.buf.len > 0x100_0000;
}
pub fn deinit(self: Self) void {
self.alloc.free(self.buf);
self.backup.deinit();
@ -64,6 +68,22 @@ pub fn deinit(self: Self) void {
pub fn read(self: *const Self, comptime T: type, address: u32) T {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
if (addr > 0x1FF_FEFF)
return self.backup.eeprom.read();
} else {
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM
// * Small ROM (less than 16MB)
if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.read();
}
}
return switch (T) {
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
@ -72,6 +92,30 @@ pub fn read(self: *const Self, comptime T: type, address: u32) T {
};
}
pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value: T) void {
const addr = address & 0x1FF_FFFF;
if (self.backup.kind == .Eeprom) {
const bit = @truncate(u1, value);
if (self.isLarge()) {
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
// * Backup type is EEPROM
// * Large ROM (Size is greater than 16MB)
if (addr > 0x1FF_FEFF)
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
} else {
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
// * Backup type is EEPROM
// * Small ROM (less than 16MB)
if (@truncate(u8, address >> 24) == 0x0D)
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
}
}
log.err("Wrote {} {X:} to 0x{X:0>8}", .{ T, value, address });
}
fn get(self: *const Self, i: u32) u8 {
@setRuntimeSafety(false);

View File

@ -4,6 +4,7 @@ const log = std.log.scoped(.Backup);
const correctTitle = @import("../util.zig").correctTitle;
const safeTitle = @import("../util.zig").safeTitle;
const intToBytes = @import("../util.zig").intToBytes;
const backup_kinds = [5]Needle{
.{ .str = "EEPROM_V", .kind = .Eeprom },
@ -23,8 +24,8 @@ pub const Backup = struct {
title: [12]u8,
save_path: ?[]const u8,
// TODO: Implement EEPROM
flash: Flash,
eeprom: Eeprom,
pub fn init(alloc: Allocator, kind: BackupKind, title: [12]u8, path: ?[]const u8) !Self {
log.info("Kind: {}", .{kind});
@ -33,8 +34,7 @@ pub const Backup = struct {
.Sram => 0x8000, // 32K
.Flash => 0x10000, // 64K
.Flash1M => 0x20000, // 128K
.Eeprom => 0x2000, // FIXME: We assume 8K here
.None => 0,
.None, .Eeprom => 0, // EEPROM is handled upon first Read Request to it
};
const buf = try alloc.alloc(u8, buf_size);
@ -47,6 +47,7 @@ pub const Backup = struct {
.title = title,
.save_path = path,
.flash = Flash.init(),
.eeprom = Eeprom.init(alloc),
};
if (backup.save_path) |p| backup.loadSaveFromDisk(p) catch |e| log.err("Failed to load save: {}", .{e});
@ -86,9 +87,20 @@ pub const Backup = struct {
return log.info("Loaded Save from {s}", .{file_path});
}
log.debug("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
log.err("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
},
else => return SaveError.UnsupportedBackupKind,
.Eeprom => {
if (file_buf.len == 0x200 or file_buf.len == 0x2000) {
self.eeprom.kind = if (file_buf.len == 0x200) .Small else .Large;
self.buf = try self.alloc.alloc(u8, file_buf.len);
std.mem.copy(u8, self.buf, file_buf);
return log.info("Loaded Save from {s}", .{file_path});
}
log.err("EEPROM can either be 0x200 bytes or 0x2000 byes, but {s} was {X:} bytes", .{ file_path, file_buf.len, });
},
.None => return SaveError.UnsupportedBackupKind,
}
}
@ -109,7 +121,7 @@ pub const Backup = struct {
defer self.alloc.free(file_path);
switch (self.kind) {
.Sram, .Flash, .Flash1M => {
.Sram, .Flash, .Flash1M, .Eeprom => {
const file = try std.fs.createFileAbsolute(file_path, .{});
defer file.close();
@ -142,9 +154,8 @@ pub const Backup = struct {
return self.flash.read(self.buf, addr);
},
.Eeprom => return self.buf[addr],
.Sram => return self.buf[addr & 0x7FFF], // 32K SRAM chip is mirrored
.None => return 0xFF,
.None, .Eeprom => return 0xFF,
}
}
@ -175,9 +186,8 @@ pub const Backup = struct {
else => {},
}
},
.Eeprom => self.buf[addr] = byte,
.Sram => self.buf[addr & 0x7FFF] = byte,
.None => {},
.None, .Eeprom => {},
}
}
};
@ -275,3 +285,267 @@ const FlashState = enum {
Set,
Command,
};
const Eeprom = struct {
const Self = @This();
addr: u14,
kind: Kind,
state: State,
writer: Writer,
reader: Reader,
alloc: Allocator,
const Kind = enum {
Unknown,
Small, // 512B
Large, // 8KB
};
const State = enum {
Ready,
Read,
Write,
WriteTransfer,
RequestEnd,
};
fn init(alloc: Allocator) Self {
return .{
.kind = .Unknown,
.state = .Ready,
.writer = Writer.init(),
.reader = Reader.init(),
.addr = 0,
.alloc = alloc,
};
}
pub fn read(self: *const Self) u1 {
// Here I throw away the const qualifier which is bad and dumb but here's why.
// This is one of the few (as of when I write this, **only**) places that mutate
// some value upon access. Before this I've been able to have all read-related functions
// present a *const Self parameter with no issues.
//
// I don't think it's worth throwing away all the good that *const Self brings across the entire
// memory bus because reading from the EEPROM increments an internal counter which isn't even
// visible to neither the cartridge nor any other component of the emulator.
//
// By throwing away const, we can increment self.read_proc.i which has a range of 0 -> 67. This is
// a small enough scope (and a well defined one at that) so that this transgression isn't the worst, I think.
const self_mut = @intToPtr(*Self, @ptrToInt(self));
return self_mut.reader.read();
}
pub fn write(self: *Self, word_count: u16, buf: *[]u8, bit: u1) void {
if (self.guessKind(word_count)) |found| {
log.info("EEPROM Kind: {}", .{found});
self.kind = found;
// buf.len will not equal zero when a save file was found and loaded.
// Right now, we assume that the save file is of the correct size which
// isn't necessarily true, since we can't trust anything a user can influence
// TODO: use ?[]u8 instead of a 0-sized slice?
if (buf.len == 0) {
const len: usize = switch (found) {
.Small => 0x200,
.Large => 0x2000,
else => unreachable,
};
buf.* = self.alloc.alloc(u8, len) catch |e| {
log.err("Failed to resize EEPROM buf to {} bytes", .{len});
std.debug.panic("EEPROM entered irrecoverable state {}", .{e});
};
std.mem.set(u8, buf.*, 0xFF);
}
}
if (self.state == .RequestEnd) {
// std.debug.assert(bit == 0); FIXME: This invariant is violated
self.state = .Ready;
return;
}
switch (self.state) {
.Ready => self.writer.requestWrite(bit),
.Read, .Write => self.writer.addressWrite(self.kind, bit),
.WriteTransfer => self.writer.dataWrite(bit),
.RequestEnd => unreachable, // We return early just above this block
}
self.tick(buf.*);
}
fn guessKind(self: *const Self, word_count: u16) ?Kind {
if (self.kind != .Unknown or self.state != .Read) return null;
return switch (word_count) {
17 => .Large,
9 => .Small,
else => blk: {
log.err("Unexpected length of DMA3 Transfer upon initial EEPROM read: {}", .{word_count});
break :blk null;
},
};
}
fn tick(self: *Self, buf: []u8) void {
switch (self.state) {
.Ready => {
if (self.writer.len() == 2) {
const req = @intCast(u2, self.writer.finish());
switch (req) {
0b11 => self.state = .Read,
0b10 => self.state = .Write,
else => log.err("Unknown EEPROM Request 0b{b:0>2}", .{req}),
}
}
},
.Read => {
switch (self.kind) {
.Large => {
if (self.writer.len() == 14) {
const addr = @intCast(u10, self.writer.finish());
// TODO: Bit Verbose eh?
const value_buf = buf[addr..][0..8];
const value = @as(u64, value_buf[7]) << 56 | @as(u64, value_buf[6]) << 48 | @as(u64, value_buf[5]) << 40 | @as(u64, value_buf[4]) << 32 | @as(u64, value_buf[3]) << 24 | @as(u64, value_buf[2]) << 16 | @as(u64, value_buf[1]) << 8 | @as(u64, value_buf[0]) << 0;
self.reader.configure(value);
self.state = .RequestEnd;
}
},
.Small => {
if (self.writer.len() == 6) {
const addr = @intCast(u6, self.writer.finish());
// TODO: Bit Verbose eh?, also duplicate code
const value_buf = buf[addr..][0..8];
const value = @as(u64, value_buf[7]) << 56 | @as(u64, value_buf[6]) << 48 | @as(u64, value_buf[5]) << 40 | @as(u64, value_buf[4]) << 32 | @as(u64, value_buf[3]) << 24 | @as(u64, value_buf[2]) << 16 | @as(u64, value_buf[1]) << 8 | @as(u64, value_buf[0]) << 0;
self.reader.configure(value);
self.state = .RequestEnd;
}
},
else => log.err("Unable to calculate EEPROM read address. EEPROM size UNKNOWN", .{}),
}
},
.Write => {
switch (self.kind) {
.Large => {
if (self.writer.len() == 14) {
self.addr = @intCast(u10, self.writer.finish());
self.state = .WriteTransfer;
}
},
.Small => {
if (self.writer.len() == 6) {
self.addr = @intCast(u6, self.writer.finish());
self.state = .WriteTransfer;
}
},
else => log.err("Unable to calculate EEPROM write address. EEPROM size UNKNOWN", .{}),
}
},
.WriteTransfer => {
if (self.writer.len() == 64) {
std.mem.copy(u8, buf[self.addr..][0..8], &intToBytes(u64, self.writer.finish()));
self.state = .RequestEnd;
}
},
.RequestEnd => unreachable, // We return early in write() if state is .RequestEnd
}
}
const Reader = struct {
const This = @This();
data: u64,
i: u8,
enabled: bool,
fn init() This {
return .{
.data = 0,
.i = 0,
.enabled = false,
};
}
fn configure(self: *This, value: u64) void {
self.data = value;
self.i = 0;
self.enabled = true;
}
fn read(self: *This) u1 {
if (!self.enabled) return 1;
const bit = if (self.i < 4) blk: {
break :blk 0;
} else blk: {
const idx = @intCast(u6, 63 - (self.i - 4));
break :blk @truncate(u1, self.data >> idx);
};
self.i = (self.i + 1) % (64 + 4);
if (self.i == 0) self.enabled = false;
return bit;
}
};
const Writer = struct {
const This = @This();
data: u64,
i: u8,
fn init() This {
return .{ .data = 0, .i = 0 };
}
fn requestWrite(self: *This, bit: u1) void {
const idx = @intCast(u1, 1 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn addressWrite(self: *This, kind: Eeprom.Kind, bit: u1) void {
if (kind == .Unknown) return;
const size: u4 = switch (kind) {
.Large => 13,
.Small => 5,
.Unknown => unreachable,
};
const idx = @intCast(u4, size - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn dataWrite(self: *This, bit: u1) void {
const idx = @intCast(u6, 63 - self.i);
self.data = (self.data & ~(@as(u64, 1) << idx)) | (@as(u64, bit) << idx);
self.i += 1;
}
fn len(self: *const This) u8 {
return self.i;
}
fn finish(self: *This) u64 {
defer self.reset();
return self.data;
}
fn reset(self: *This) void {
self.i = 0;
self.data = 0;
}
};
};