Implement RTC #1
| @@ -1,5 +1,7 @@ | ||||
| const std = @import("std"); | ||||
|  | ||||
| const Bit = @import("bitfield").Bit; | ||||
| const Bitfield = @import("bitfield").Bitfield; | ||||
| const Backup = @import("backup.zig").Backup; | ||||
| const Allocator = std.mem.Allocator; | ||||
| const log = std.log.scoped(.GamePak); | ||||
| @@ -10,6 +12,7 @@ title: [12]u8, | ||||
| buf: []u8, | ||||
| allocator: Allocator, | ||||
| backup: Backup, | ||||
| gpio: Gpio, | ||||
|  | ||||
| pub fn init(allocator: Allocator, rom_path: []const u8, save_path: ?[]const u8) !Self { | ||||
|     const file = try std.fs.cwd().openFile(rom_path, .{}); | ||||
| @@ -19,17 +22,36 @@ pub fn init(allocator: Allocator, rom_path: []const u8, save_path: ?[]const u8) | ||||
|     const title = parseTitle(file_buf); | ||||
|     const kind = Backup.guessKind(file_buf) orelse .None; | ||||
|  | ||||
|     const pak = Self{ | ||||
|     var pak = Self{ | ||||
|         .buf = file_buf, | ||||
|         .allocator = allocator, | ||||
|         .title = title, | ||||
|         .backup = try Backup.init(allocator, kind, title, save_path), | ||||
|         .gpio = Gpio.init(allocator, .Rtc), | ||||
|     }; | ||||
|     pak.parseHeader(); | ||||
|  | ||||
|     return pak; | ||||
| } | ||||
|  | ||||
| /// Configures any GPIO Device that may be enabled | ||||
| /// | ||||
| /// Fundamentally, this just passes a pointer to the initialized GPIO struct to whatever heap allocated GPIO Device struct | ||||
| /// we happen to be using | ||||
| /// | ||||
| /// WARNIG: As far as I know, this method must be called in main() or else we'll have a dangling pointer issue | ||||
| /// Despite using the General Purpose Allocator, Zig doesn't prevent me from doing this :sadface: | ||||
| pub fn setupGpio(self: *Self) void { | ||||
|     switch (self.gpio.device.kind) { | ||||
|         .Rtc => { | ||||
|             const ptr = self.gpio.device.ptr orelse @panic("RTC ptr is missing despite GPIO Device Kind"); | ||||
|             const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), ptr)); | ||||
|             Clock.init(clock, &self.gpio); | ||||
|         }, | ||||
|         .None => {}, | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn parseHeader(self: *const Self) void { | ||||
|     const title = parseTitle(self.buf); | ||||
|     const code = self.buf[0xAC..0xB0]; | ||||
| @@ -60,6 +82,7 @@ inline fn isLarge(self: *const Self) bool { | ||||
|  | ||||
| pub fn deinit(self: *Self) void { | ||||
|     self.backup.deinit(); | ||||
|     self.gpio.deinit(self.allocator); | ||||
|     self.allocator.free(self.buf); | ||||
|     self.* = undefined; | ||||
| } | ||||
| @@ -83,6 +106,35 @@ pub fn read(self: *Self, comptime T: type, address: u32) T { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (self.gpio.cnt == 1) { | ||||
|         // GPIO Can be read from | ||||
|         // We assume that this will only be true when a ROM actually does want something from GPIO | ||||
|  | ||||
|         switch (T) { | ||||
|             u32 => switch (address) { | ||||
|                 // TODO: Do I even need to implement these? | ||||
|                 0x0800_00C4 => std.debug.panic("Handle 32-bit GPIO Data/Direction Reads", .{}), | ||||
|                 0x0800_00C6 => std.debug.panic("Handle 32-bit GPIO Direction/Control Reads", .{}), | ||||
|                 0x0800_00C8 => std.debug.panic("Handle 32-bit GPIO Control Reads", .{}), | ||||
|                 else => {}, | ||||
|             }, | ||||
|             u16 => switch (address) { | ||||
|                 // FIXME: What do 16-bit GPIO Reads look like? | ||||
|                 0x0800_00C4 => return self.gpio.read(.Data), | ||||
|                 0x0800_00C6 => return self.gpio.read(.Direction), | ||||
|                 0x0800_00C8 => return self.gpio.read(.Control), | ||||
|                 else => {}, | ||||
|             }, | ||||
|             u8 => switch (address) { | ||||
|                 0x0800_00C4 => return self.gpio.read(.Data), | ||||
|                 0x0800_00C6 => return self.gpio.read(.Direction), | ||||
|                 0x0800_00C8 => return self.gpio.read(.Control), | ||||
|                 else => {}, | ||||
|             }, | ||||
|             else => @compileError("GamePak[GPIO]: Unsupported read width"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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)), | ||||
| @@ -141,17 +193,23 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value | ||||
|  | ||||
|     switch (T) { | ||||
|         u32 => switch (address) { | ||||
|             0x0800_00C4 => log.debug("Wrote {} 0x{X:} to I/O Port Data and Direction", .{ T, value }), | ||||
|             0x0800_00C6 => log.debug("Wrote {} 0x{X:} to I/O Port Direction and Control", .{ T, value }), | ||||
|             else => {}, | ||||
|             0x0800_00C4 => { | ||||
|                 self.gpio.write(.Data, @truncate(u4, value)); | ||||
|                 self.gpio.write(.Direction, @truncate(u4, value >> 16)); | ||||
|             }, | ||||
|             0x0800_00C6 => { | ||||
|                 self.gpio.write(.Direction, @truncate(u4, value)); | ||||
|                 self.gpio.write(.Control, @truncate(u1, value >> 16)); | ||||
|             }, | ||||
|             else => log.err("Wrote {} 0x{X:0>8} to 0x{X:0>8}, Unhandled", .{ T, value, address }), | ||||
|         }, | ||||
|         u16 => switch (address) { | ||||
|             0x0800_00C4 => log.debug("Wrote {} 0x{X:} to I/O Port Data", .{ T, value }), | ||||
|             0x0800_00C6 => log.debug("Wrote {} 0x{X:} to I/O Port Direction", .{ T, value }), | ||||
|             0x0800_00C8 => log.debug("Wrote {} 0x{X:} to I/O Port Control", .{ T, value }), | ||||
|             else => {}, | ||||
|             0x0800_00C4 => self.gpio.write(.Data, @truncate(u4, value)), | ||||
|             0x0800_00C6 => self.gpio.write(.Direction, @truncate(u4, value)), | ||||
|             0x0800_00C8 => self.gpio.write(.Control, @truncate(u1, value)), | ||||
|             else => log.err("Wrote {} 0x{X:0>4} to 0x{X:0>8}, Unhandled", .{ T, value, address }), | ||||
|         }, | ||||
|         u8 => log.debug("Wrote {} 0x{X:} to 0x{X:0>8}, Ignored.", .{ T, value, address }), | ||||
|         u8 => log.debug("Wrote {} 0x{X:0>2} to 0x{X:0>8}, Ignored.", .{ T, value, address }), | ||||
|         else => @compileError("GamePak: Unsupported write width"), | ||||
|     } | ||||
| } | ||||
| @@ -183,3 +241,343 @@ test "OOB Access" { | ||||
|     std.debug.assert(pak.get(4) == 0x02); // 0x0002 | ||||
|     std.debug.assert(pak.get(5) == 0x00); | ||||
| } | ||||
|  | ||||
| /// GPIO Register Implementation | ||||
| const Gpio = struct { | ||||
|     const This = @This(); | ||||
|  | ||||
|     data: u4, | ||||
|     direction: u4, | ||||
|     cnt: u1, | ||||
|  | ||||
|     device: Device, | ||||
|  | ||||
|     const Device = struct { | ||||
|         ptr: ?*anyopaque, | ||||
|         // TODO: Maybe make this comptime known? Removes some if statements | ||||
|         kind: Kind, | ||||
|  | ||||
|         const Kind = enum { | ||||
|             Rtc, | ||||
|             None, | ||||
|         }; | ||||
|  | ||||
|         fn step(self: *Device, value: u4) void { | ||||
|             switch (self.kind) { | ||||
|                 .Rtc => { | ||||
|                     const ptr = self.ptr orelse @panic("Device.ptr should != null when Device.kind == .Rtc"); | ||||
|                     const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), ptr)); | ||||
|  | ||||
|                     clock.step(Clock.GpioData{ .raw = value }); | ||||
|                 }, | ||||
|                 .None => {}, | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         fn init(kind: Kind, ptr: ?*anyopaque) Device { | ||||
|             return .{ .kind = kind, .ptr = ptr }; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const Register = enum { | ||||
|         Data, | ||||
|         Direction, | ||||
|         Control, | ||||
|     }; | ||||
|  | ||||
|     fn init(allocator: Allocator, kind: Device.Kind) This { | ||||
|         return .{ | ||||
|             .data = 0b0000, | ||||
|             .direction = 0b1111, // TODO: What is GPIO Direction set to by default? | ||||
|             .cnt = 0b0, | ||||
|  | ||||
|             .device = switch (kind) { | ||||
|                 .Rtc => blk: { | ||||
|                     const ptr = allocator.create(Clock) catch @panic("Failed to allocate RTC struct on heap"); | ||||
|                     break :blk Device{ .kind = kind, .ptr = ptr }; | ||||
|                 }, | ||||
|                 .None => Device{ .kind = kind, .ptr = null }, | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     fn deinit(self: This, allocator: Allocator) void { | ||||
|         switch (self.device.kind) { | ||||
|             .Rtc => { | ||||
|                 const ptr = self.device.ptr orelse @panic("Device.ptr should != null when Device.kind == .Rtc"); | ||||
|                 allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), ptr))); | ||||
|             }, | ||||
|             .None => {}, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn write(self: *This, comptime reg: Register, value: if (reg == .Control) u1 else u4) void { | ||||
|         log.debug("RTC: Wrote 0b{b:0>4} to {}", .{ value, reg }); | ||||
|  | ||||
|         // if (reg == .Data) | ||||
|         // log.err("original: 0b{b:0>4} masked: 0b{b:0>4} result: 0b{b:0>4}", .{ self.data, value & self.direction, self.data | (value & self.direction) }); | ||||
|  | ||||
|         switch (reg) { | ||||
|             .Data => { | ||||
|                 const masked_value = value & self.direction; | ||||
|  | ||||
|                 self.device.step(masked_value); | ||||
|                 self.data = masked_value; | ||||
|             }, | ||||
|             .Direction => self.direction = value, | ||||
|             .Control => self.cnt = value, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn read(self: *const This, comptime reg: Register) if (reg == .Control) u1 else u4 { | ||||
|         if (self.cnt == 0) return 0; | ||||
|  | ||||
|         return switch (reg) { | ||||
|             .Data => self.data & ~self.direction, | ||||
|             .Direction => self.direction, | ||||
|             .Control => self.cnt, | ||||
|         }; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /// GBA Real Time Clock | ||||
| const Clock = struct { | ||||
|     const This = @This(); | ||||
|  | ||||
|     cmd: Command, | ||||
|     writer: Writer, | ||||
|     state: State, | ||||
|     cnt: Control, | ||||
|  | ||||
|     year: u8, | ||||
|     month: u5, | ||||
|     day: u6, | ||||
|     day_of_week: u3, | ||||
|     hour: u6, | ||||
|     minute: u7, | ||||
|     second: u7, | ||||
|  | ||||
|     gpio: *const Gpio, | ||||
|  | ||||
|     const Register = enum { | ||||
|         Control, | ||||
|         DateTime, | ||||
|         Time, | ||||
|     }; | ||||
|  | ||||
|     const State = union(enum) { | ||||
|         Idle, | ||||
|         CommandInput, | ||||
|         Write: Register, | ||||
|         Read: Register, | ||||
|     }; | ||||
|  | ||||
|     const Writer = struct { | ||||
|         buf: u8, | ||||
|         i: u4, | ||||
|  | ||||
|         /// The Number of bytes written to since last reset | ||||
|         count: u8, | ||||
|  | ||||
|         fn push(self: *Writer, value: u1) void { | ||||
|             const idx = @intCast(u3, self.i); | ||||
|             self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx; | ||||
|             self.i += 1; | ||||
|         } | ||||
|  | ||||
|         fn lap(self: *Writer) void { | ||||
|             self.buf = 0; | ||||
|             self.i = 0; | ||||
|             self.count += 1; | ||||
|         } | ||||
|  | ||||
|         fn reset(self: *Writer) void { | ||||
|             self.buf = 0; | ||||
|             self.i = 0; | ||||
|             self.count = 0; | ||||
|         } | ||||
|  | ||||
|         fn isFinished(self: *const Writer) bool { | ||||
|             return self.i >= 8; | ||||
|         } | ||||
|  | ||||
|         fn getCount(self: *const Writer) u8 { | ||||
|             return self.count; | ||||
|         } | ||||
|  | ||||
|         fn getValue(self: *const Writer) u8 { | ||||
|             return self.buf; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const Command = struct { | ||||
|         buf: u8, | ||||
|         i: u4, | ||||
|  | ||||
|         fn push(self: *Command, value: u1) void { | ||||
|             const idx = @intCast(u3, self.i); | ||||
|             self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx; | ||||
|             self.i += 1; | ||||
|         } | ||||
|  | ||||
|         fn reset(self: *Command) void { | ||||
|             self.buf = 0; | ||||
|             self.i = 0; | ||||
|         } | ||||
|  | ||||
|         fn isFinished(self: *const Command) bool { | ||||
|             return self.i >= 8; | ||||
|         } | ||||
|  | ||||
|         fn getCommand(self: *const Command) u8 { | ||||
|             // If high Nybble does not contain 0x6, reverse the order of the nybbles. | ||||
|             // For some reason RTC commands can be LSB or MSB which is funny | ||||
|             return if (self.buf >> 4 & 0xF == 0x6) self.buf else (self.buf & 0xF) << 4 | (self.buf >> 4 & 0xF); | ||||
|         } | ||||
|  | ||||
|         fn handleCommand(self: *const Command, rtc: *Clock) State { | ||||
|             log.info("RTC: Failed to handle Command 0b{b:0>8} aka 0x{X:0>2}", .{ self.buf, self.buf }); | ||||
|             const command = self.getCommand(); | ||||
|  | ||||
|             const is_write = command & 1 == 0; | ||||
|             const rtc_register = @intCast(u3, command >> 1 & 0x7); // TODO: Make Truncate | ||||
|  | ||||
|             if (is_write) { | ||||
|                 return switch (rtc_register) { | ||||
|                     0 => blk: { | ||||
|                         rtc.reset(); | ||||
|                         break :blk .Idle; | ||||
|                     }, | ||||
|                     1 => .{ .Write = .Control }, | ||||
|                     2 => .{ .Write = .DateTime }, | ||||
|                     3 => .{ .Write = .Time }, | ||||
|                     6 => blk: { | ||||
|                         rtc.irq(); | ||||
|                         break :blk .Idle; | ||||
|                     }, | ||||
|                     4, 5, 7 => .Idle, | ||||
|                 }; | ||||
|             } else { | ||||
|                 return switch (rtc_register) { | ||||
|                     1 => .{ .Read = .Control }, | ||||
|                     2 => .{ .Read = .DateTime }, | ||||
|                     3 => .{ .Read = .Time }, | ||||
|                     0, 4, 5, 6, 7 => .Idle, // Do Nothing | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const GpioData = extern union { | ||||
|         sck: Bit(u4, 0), | ||||
|         sio: Bit(u4, 1), | ||||
|         cs: Bit(u4, 2), | ||||
|         raw: u4, | ||||
|     }; | ||||
|  | ||||
|     const Control = extern union { | ||||
|         /// Unknown, value should be preserved though | ||||
|         unk: Bit(u8, 1), | ||||
|         /// Per-minute IRQ | ||||
|         /// If set, fire a Gamepak IRQ every 30s, | ||||
|         irq: Bit(u8, 3), | ||||
|         /// 12/24 Hour Bit | ||||
|         /// If set, 12h mode | ||||
|         /// If cleared, 24h mode | ||||
|         mode: Bit(u8, 6), | ||||
|         /// Read-Only, bit cleared on read | ||||
|         /// If is set, means that there has been a failure / time has been lost | ||||
|         off: Bit(u8, 7), | ||||
|         raw: u8, | ||||
|     }; | ||||
|  | ||||
|     fn init(ptr: *This, gpio: *const Gpio) void { | ||||
|         ptr.* = .{ | ||||
|             .cmd = .{ .buf = 0, .i = 0 }, | ||||
|             .writer = .{ .buf = 0, .i = 0, .count = 0 }, | ||||
|             .state = .Idle, | ||||
|             .cnt = .{ .raw = 0 }, | ||||
|             .year = 0, | ||||
|             .month = 0, | ||||
|             .day = 0, | ||||
|             .day_of_week = 0, | ||||
|             .hour = 0, | ||||
|             .minute = 0, | ||||
|             .second = 0, | ||||
|             .gpio = gpio, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     fn attachGpio(self: *This, gpio: *const Gpio) void { | ||||
|         self.gpio = gpio; | ||||
|     } | ||||
|  | ||||
|     fn step(self: *This, value: GpioData) void { | ||||
|         const cache: GpioData = .{ .raw = self.gpio.data }; | ||||
|  | ||||
|         switch (self.state) { | ||||
|             .Idle => { | ||||
|                 // If SCK is high and CS rises, then prepare for Command | ||||
|                 // FIXME: Maybe check incoming value to see if SCK is also high? | ||||
|                 if (cache.sck.read()) { | ||||
|                     if (!cache.cs.read() and value.cs.read()) { | ||||
|                         log.err("RTC: Entering Command Mode", .{}); | ||||
|                         self.state = .CommandInput; | ||||
|                         self.cmd.reset(); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             .CommandInput => { | ||||
|                 if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state}); | ||||
|  | ||||
|                 if (!cache.sck.read() and value.sck.read()) { | ||||
|                     // If SCK rises, sample SIO | ||||
|                     log.debug("RTC: Sampled 0b{b:0>1} from SIO", .{@boolToInt(value.sio.read())}); | ||||
|                     self.cmd.push(@boolToInt(value.sio.read())); | ||||
|  | ||||
|                     if (self.cmd.isFinished()) { | ||||
|                         self.state = self.cmd.handleCommand(self); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             State{ .Write = .Control } => { | ||||
|                 if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state}); | ||||
|  | ||||
|                 if (!cache.sck.read() and value.sck.read()) { | ||||
|                     // If SCK rises, sample SIO | ||||
|  | ||||
|                     log.debug("RTC: Sampled 0b{b:0>1} from SIO", .{@boolToInt(value.sio.read())}); | ||||
|                     self.writer.push(@boolToInt(value.sio.read())); | ||||
|  | ||||
|                     if (self.writer.isFinished()) { | ||||
|                         self.writer.lap(); | ||||
|                         self.cnt.raw = self.writer.getValue(); | ||||
|  | ||||
|                         // FIXME: Move this to a constant or something | ||||
|                         if (self.writer.getCount() == 1) { | ||||
|                             self.writer.reset(); | ||||
|                             self.state = .Idle; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             else => { | ||||
|                 // TODO: Implement Read/Writes for Date/Time and Time and Control | ||||
|                 log.err("RTC: Ignored request to handle {} command", .{self.state}); | ||||
|                 self.state = .Idle; | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn reset(self: *This) void { | ||||
|         // mGBA and NBA only zero the control register | ||||
|         // we'll do the same | ||||
|         self.cnt.raw = 0; | ||||
|         log.info("RTC: Reset executed (control register was zeroed)", .{}); | ||||
|     } | ||||
|  | ||||
|     fn irq(_: *const This) void { | ||||
|         // TODO: Force GamePak IRQ | ||||
|         log.err("RTC: TODO: Force GamePak IRQ", .{}); | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -52,6 +52,7 @@ pub fn main() anyerror!void { | ||||
|     if (paths.bios == null) cpu.fastBoot(); | ||||
|  | ||||
|     try bus.init(allocator, &scheduler, &cpu, paths); | ||||
|     bus.pak.setupGpio(); // FIXME: Can I not call this in main()? | ||||
|     defer bus.deinit(); | ||||
|  | ||||
|     var gui = Gui.init(bus.pak.title, width, height); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user