chore: change directory structure
This commit is contained in:
58
src/core/bus/Bios.zig
Normal file
58
src/core/bus/Bios.zig
Normal file
@@ -0,0 +1,58 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.Bios);
|
||||
|
||||
/// Size of the BIOS in bytes
|
||||
pub const size = 0x4000;
|
||||
const Self = @This();
|
||||
|
||||
buf: ?[]u8,
|
||||
alloc: Allocator,
|
||||
|
||||
addr_latch: u32,
|
||||
|
||||
pub fn init(alloc: Allocator, maybe_path: ?[]const u8) !Self {
|
||||
var buf: ?[]u8 = null;
|
||||
if (maybe_path) |path| {
|
||||
const file = try std.fs.cwd().openFile(path, .{});
|
||||
defer file.close();
|
||||
buf = try file.readToEndAlloc(alloc, try file.getEndPos());
|
||||
}
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
.alloc = alloc,
|
||||
.addr_latch = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
if (self.buf) |buf| self.alloc.free(buf);
|
||||
}
|
||||
|
||||
pub fn checkedRead(self: *Self, comptime T: type, r15: u32, addr: u32) T {
|
||||
if (r15 < Self.size) {
|
||||
self.addr_latch = addr;
|
||||
return self.read(T, addr);
|
||||
}
|
||||
|
||||
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
|
||||
return @truncate(T, self.read(T, self.addr_latch + 8));
|
||||
}
|
||||
|
||||
fn read(self: *const Self, comptime T: type, addr: u32) T {
|
||||
if (self.buf) |buf| {
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, buf[addr..][0..@sizeOf(T)]),
|
||||
else => @compileError("BIOS: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
std.debug.panic("[BIOS] ZBA tried to read {} from 0x{X:0>8} but not BIOS was present", .{ T, addr });
|
||||
}
|
||||
|
||||
pub fn write(_: *Self, comptime T: type, addr: u32, value: T) void {
|
||||
@setCold(true);
|
||||
log.debug("Tried to write {} 0x{X:} to 0x{X:0>8} ", .{ T, value, addr });
|
||||
}
|
40
src/core/bus/Ewram.zig
Normal file
40
src/core/bus/Ewram.zig
Normal file
@@ -0,0 +1,40 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ewram_size = 0x40000;
|
||||
const Self = @This();
|
||||
|
||||
buf: []u8,
|
||||
alloc: Allocator,
|
||||
|
||||
pub fn init(alloc: Allocator) !Self {
|
||||
const buf = try alloc.alloc(u8, ewram_size);
|
||||
std.mem.set(u8, buf, 0);
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
.alloc = alloc,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
self.alloc.free(self.buf);
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||
const addr = address & 0x3FFFF;
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
else => @compileError("EWRAM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void {
|
||||
const addr = address & 0x3FFFF;
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||
else => @compileError("EWRAM: Unsupported write width"),
|
||||
};
|
||||
}
|
157
src/core/bus/GamePak.zig
Normal file
157
src/core/bus/GamePak.zig
Normal file
@@ -0,0 +1,157 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Backup = @import("backup.zig").Backup;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.GamePak);
|
||||
|
||||
const Self = @This();
|
||||
|
||||
title: [12]u8,
|
||||
buf: []u8,
|
||||
alloc: Allocator,
|
||||
backup: Backup,
|
||||
|
||||
pub fn init(alloc: Allocator, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
||||
const file = try std.fs.cwd().openFile(rom_path, .{});
|
||||
defer file.close();
|
||||
|
||||
const file_buf = try file.readToEndAlloc(alloc, try file.getEndPos());
|
||||
const title = parseTitle(file_buf);
|
||||
const kind = Backup.guessKind(file_buf) orelse .None;
|
||||
|
||||
const pak = Self{
|
||||
.buf = file_buf,
|
||||
.alloc = alloc,
|
||||
.title = title,
|
||||
.backup = try Backup.init(alloc, kind, title, save_path),
|
||||
};
|
||||
pak.parseHeader();
|
||||
|
||||
return pak;
|
||||
}
|
||||
|
||||
fn parseHeader(self: *const Self) void {
|
||||
const title = parseTitle(self.buf);
|
||||
const code = self.buf[0xAC..0xB0];
|
||||
const maker = self.buf[0xB0..0xB2];
|
||||
const version = self.buf[0xBC];
|
||||
|
||||
log.info("Title: {s}", .{title});
|
||||
if (version != 0) log.info("Version: {}", .{version});
|
||||
log.info("Game Code: {s}", .{code});
|
||||
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
|
||||
}
|
||||
|
||||
fn parseTitle(buf: []u8) [12]u8 {
|
||||
return buf[0xA0..0xAC].*;
|
||||
}
|
||||
|
||||
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
|
||||
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
|
||||
return switch (id) {
|
||||
0x3130 => "Nintendo",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn read(self: *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)),
|
||||
u8 => self.get(addr),
|
||||
else => @compileError("GamePak: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 => {},
|
||||
},
|
||||
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 => {},
|
||||
},
|
||||
u8 => log.debug("Wrote {} 0x{X:} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
|
||||
else => @compileError("GamePak: Unsupported write width"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(self: *const Self, i: u32) u8 {
|
||||
@setRuntimeSafety(false);
|
||||
if (i < self.buf.len) return self.buf[i];
|
||||
|
||||
const lhs = i >> 1 & 0xFFFF;
|
||||
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
|
||||
}
|
||||
|
||||
test "OOB Access" {
|
||||
const title = .{ 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '!' };
|
||||
const alloc = std.testing.allocator;
|
||||
const pak = Self{
|
||||
.buf = &.{},
|
||||
.alloc = alloc,
|
||||
.title = title,
|
||||
.backup = try Backup.init(alloc, .None, title, null),
|
||||
};
|
||||
|
||||
std.debug.assert(pak.get(0) == 0x00); // 0x0000
|
||||
std.debug.assert(pak.get(1) == 0x00);
|
||||
|
||||
std.debug.assert(pak.get(2) == 0x01); // 0x0001
|
||||
std.debug.assert(pak.get(3) == 0x00);
|
||||
|
||||
std.debug.assert(pak.get(4) == 0x02); // 0x0002
|
||||
std.debug.assert(pak.get(5) == 0x00);
|
||||
}
|
40
src/core/bus/Iwram.zig
Normal file
40
src/core/bus/Iwram.zig
Normal file
@@ -0,0 +1,40 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const iwram_size = 0x8000;
|
||||
const Self = @This();
|
||||
|
||||
buf: []u8,
|
||||
alloc: Allocator,
|
||||
|
||||
pub fn init(alloc: Allocator) !Self {
|
||||
const buf = try alloc.alloc(u8, iwram_size);
|
||||
std.mem.set(u8, buf, 0);
|
||||
|
||||
return Self{
|
||||
.buf = buf,
|
||||
.alloc = alloc,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
self.alloc.free(self.buf);
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||
const addr = address & 0x7FFF;
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||
else => @compileError("IWRAM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *const Self, comptime T: type, address: usize, value: T) void {
|
||||
const addr = address & 0x7FFF;
|
||||
|
||||
return switch (T) {
|
||||
u32, u16, u8 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||
else => @compileError("IWRAM: Unsupported write width"),
|
||||
};
|
||||
}
|
547
src/core/bus/backup.zig
Normal file
547
src/core/bus/backup.zig
Normal file
@@ -0,0 +1,547 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.Backup);
|
||||
|
||||
const escape = @import("../util.zig").escape;
|
||||
const asString = @import("../util.zig").asString;
|
||||
|
||||
const backup_kinds = [5]Needle{
|
||||
.{ .str = "EEPROM_V", .kind = .Eeprom },
|
||||
.{ .str = "SRAM_V", .kind = .Sram },
|
||||
.{ .str = "FLASH_V", .kind = .Flash },
|
||||
.{ .str = "FLASH512_V", .kind = .Flash },
|
||||
.{ .str = "FLASH1M_V", .kind = .Flash1M },
|
||||
};
|
||||
|
||||
pub const Backup = struct {
|
||||
const Self = @This();
|
||||
|
||||
buf: []u8,
|
||||
alloc: Allocator,
|
||||
kind: BackupKind,
|
||||
|
||||
title: [12]u8,
|
||||
save_path: ?[]const u8,
|
||||
|
||||
flash: Flash,
|
||||
eeprom: Eeprom,
|
||||
|
||||
pub fn init(alloc: Allocator, kind: BackupKind, title: [12]u8, path: ?[]const u8) !Self {
|
||||
log.info("Kind: {}", .{kind});
|
||||
|
||||
const buf_size: usize = switch (kind) {
|
||||
.Sram => 0x8000, // 32K
|
||||
.Flash => 0x10000, // 64K
|
||||
.Flash1M => 0x20000, // 128K
|
||||
.None, .Eeprom => 0, // EEPROM is handled upon first Read Request to it
|
||||
};
|
||||
|
||||
const buf = try alloc.alloc(u8, buf_size);
|
||||
std.mem.set(u8, buf, 0xFF);
|
||||
|
||||
var backup = Self{
|
||||
.buf = buf,
|
||||
.alloc = alloc,
|
||||
.kind = kind,
|
||||
.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});
|
||||
return backup;
|
||||
}
|
||||
|
||||
pub fn guessKind(rom: []const u8) ?BackupKind {
|
||||
for (backup_kinds) |needle| {
|
||||
const needle_len = needle.str.len;
|
||||
|
||||
var i: usize = 0;
|
||||
while ((i + needle_len) < rom.len) : (i += 1) {
|
||||
if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn loadSaveFromDisk(self: *Self, path: []const u8) !void {
|
||||
const file_path = try self.getSaveFilePath(path);
|
||||
defer self.alloc.free(file_path);
|
||||
|
||||
// FIXME: Don't rely on this lol
|
||||
if (std.mem.eql(u8, file_path[file_path.len - 12 .. file_path.len], "untitled.sav")) {
|
||||
return log.err("ROM header lacks title, no save loaded", .{});
|
||||
}
|
||||
|
||||
const file: std.fs.File = try std.fs.openFileAbsolute(file_path, .{});
|
||||
const file_buf = try file.readToEndAlloc(self.alloc, try file.getEndPos());
|
||||
defer self.alloc.free(file_buf);
|
||||
|
||||
switch (self.kind) {
|
||||
.Sram, .Flash, .Flash1M => {
|
||||
if (self.buf.len == file_buf.len) {
|
||||
std.mem.copy(u8, self.buf, file_buf);
|
||||
return log.info("Loaded Save from {s}", .{file_path});
|
||||
}
|
||||
|
||||
log.err("{s} is {} bytes, but we expected {} bytes", .{ file_path, file_buf.len, self.buf.len });
|
||||
},
|
||||
.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,
|
||||
}
|
||||
}
|
||||
|
||||
fn getSaveFilePath(self: *const Self, path: []const u8) ![]const u8 {
|
||||
const filename = try self.getSaveFilename();
|
||||
defer self.alloc.free(filename);
|
||||
|
||||
return try std.fs.path.join(self.alloc, &[_][]const u8{ path, filename });
|
||||
}
|
||||
|
||||
fn getSaveFilename(self: *const Self) ![]const u8 {
|
||||
const title = asString(escape(self.title));
|
||||
const name = if (title.len != 0) title else "untitled";
|
||||
|
||||
return try std.mem.concat(self.alloc, u8, &[_][]const u8{ name, ".sav" });
|
||||
}
|
||||
|
||||
fn writeSaveToDisk(self: Self, path: []const u8) !void {
|
||||
const file_path = try self.getSaveFilePath(path);
|
||||
defer self.alloc.free(file_path);
|
||||
|
||||
switch (self.kind) {
|
||||
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||
defer file.close();
|
||||
|
||||
try file.writeAll(self.buf);
|
||||
log.info("Wrote Save to {s}", .{file_path});
|
||||
},
|
||||
else => return SaveError.UnsupportedBackupKind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, address: usize) u8 {
|
||||
const addr = address & 0xFFFF;
|
||||
|
||||
switch (self.kind) {
|
||||
.Flash => {
|
||||
switch (addr) {
|
||||
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, addr);
|
||||
},
|
||||
.Flash1M => {
|
||||
switch (addr) {
|
||||
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, addr);
|
||||
},
|
||||
.Sram => return self.buf[addr & 0x7FFF], // 32K SRAM chip is mirrored
|
||||
.None, .Eeprom => return 0xFF,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(self: *Self, address: usize, byte: u8) void {
|
||||
const addr = address & 0xFFFF;
|
||||
|
||||
switch (self.kind) {
|
||||
.Flash, .Flash1M => {
|
||||
if (self.flash.prep_write) return self.flash.write(self.buf, addr, byte);
|
||||
if (self.flash.shouldEraseSector(addr, byte)) return self.flash.eraseSector(self.buf, addr);
|
||||
|
||||
switch (addr) {
|
||||
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 => {},
|
||||
}
|
||||
},
|
||||
.Sram => self.buf[addr & 0x7FFF] = byte,
|
||||
.None, .Eeprom => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const BackupKind = enum {
|
||||
Eeprom,
|
||||
Sram,
|
||||
Flash,
|
||||
Flash1M,
|
||||
None,
|
||||
};
|
||||
|
||||
const Needle = struct {
|
||||
const Self = @This();
|
||||
|
||||
str: []const u8,
|
||||
kind: BackupKind,
|
||||
|
||||
fn init(str: []const u8, kind: BackupKind) Self {
|
||||
return .{
|
||||
.str = str,
|
||||
.kind = kind,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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, addr: usize, byte: u8) bool {
|
||||
return self.state == .Command and self.prep_erase and byte == 0x30 and addr & 0xFFF == 0x000;
|
||||
}
|
||||
|
||||
fn write(self: *Self, buf: []u8, idx: usize, byte: u8) void {
|
||||
buf[self.baseAddress() + idx] = byte;
|
||||
self.prep_write = false;
|
||||
}
|
||||
|
||||
fn read(self: *const Self, buf: []u8, idx: usize) u8 {
|
||||
return buf[self.baseAddress() + idx];
|
||||
}
|
||||
|
||||
fn eraseSector(self: *Self, buf: []u8, idx: usize) void {
|
||||
const start = self.baseAddress() + (idx & 0xF000);
|
||||
|
||||
std.mem.set(u8, buf[start..][0..0x1000], 0xFF);
|
||||
self.prep_erase = false;
|
||||
self.state = .Ready;
|
||||
}
|
||||
|
||||
inline fn baseAddress(self: *const Self) usize {
|
||||
return if (self.bank == 1) 0x10000 else @as(usize, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const FlashState = enum {
|
||||
Ready,
|
||||
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: *Self) u1 {
|
||||
return self.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) {
|
||||
if (bit != 0) log.debug("EEPROM Request did not end in 0u1. TODO: is this ok?", .{});
|
||||
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());
|
||||
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
||||
|
||||
self.reader.configure(value);
|
||||
self.state = .RequestEnd;
|
||||
}
|
||||
},
|
||||
.Small => {
|
||||
if (self.writer.len() == 6) {
|
||||
// FIXME: Duplicated code from above
|
||||
const addr = @intCast(u6, self.writer.finish());
|
||||
const value = std.mem.readIntSliceLittle(u64, buf[@as(u13, addr) * 8 ..][0..8]);
|
||||
|
||||
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.writeIntSliceLittle(u64, buf[self.addr * 8 ..][0..8], 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;
|
||||
}
|
||||
};
|
||||
};
|
302
src/core/bus/dma.zig
Normal file
302
src/core/bus/dma.zig
Normal file
@@ -0,0 +1,302 @@
|
||||
const std = @import("std");
|
||||
|
||||
const DmaControl = @import("io.zig").DmaControl;
|
||||
const Bus = @import("../Bus.zig");
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
|
||||
const readUndefined = @import("../util.zig").readUndefined;
|
||||
const writeUndefined = @import("../util.zig").writeUndefined;
|
||||
pub const DmaTuple = std.meta.Tuple(&[_]type{ DmaController(0), DmaController(1), DmaController(2), DmaController(3) });
|
||||
const log = std.log.scoped(.DmaTransfer);
|
||||
|
||||
pub fn create() DmaTuple {
|
||||
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
|
||||
}
|
||||
|
||||
pub fn read(comptime T: type, dma: *const DmaTuple, addr: u32) T {
|
||||
const byte = @truncate(u8, addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (byte) {
|
||||
0xB8 => @as(T, dma.*[0].cnt.raw) << 16,
|
||||
0xC4 => @as(T, dma.*[1].cnt.raw) << 16,
|
||||
0xD0 => @as(T, dma.*[2].cnt.raw) << 16,
|
||||
0xDC => @as(T, dma.*[3].cnt.raw) << 16,
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u16 => switch (byte) {
|
||||
0xBA => dma.*[0].cnt.raw,
|
||||
0xC6 => dma.*[1].cnt.raw,
|
||||
0xD2 => dma.*[2].cnt.raw,
|
||||
0xDE => dma.*[3].cnt.raw,
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => @compileError("DMA: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, dma: *DmaTuple, addr: u32, value: T) void {
|
||||
const byte = @truncate(u8, addr);
|
||||
|
||||
switch (T) {
|
||||
u32 => switch (byte) {
|
||||
0xB0 => dma.*[0].setSad(value),
|
||||
0xB4 => dma.*[0].setDad(value),
|
||||
0xB8 => dma.*[0].setCnt(value),
|
||||
0xBC => dma.*[1].setSad(value),
|
||||
0xC0 => dma.*[1].setDad(value),
|
||||
0xC4 => dma.*[1].setCnt(value),
|
||||
0xC8 => dma.*[2].setSad(value),
|
||||
0xCC => dma.*[2].setDad(value),
|
||||
0xD0 => dma.*[2].setCnt(value),
|
||||
0xD4 => dma.*[3].setSad(value),
|
||||
0xD8 => dma.*[3].setDad(value),
|
||||
0xDC => dma.*[3].setCnt(value),
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u16 => switch (byte) {
|
||||
0xB0 => dma.*[0].setSad(setU32L(dma.*[0].sad, value)),
|
||||
0xB2 => dma.*[0].setSad(setU32H(dma.*[0].sad, value)),
|
||||
0xB4 => dma.*[0].setDad(setU32L(dma.*[0].dad, value)),
|
||||
0xB6 => dma.*[0].setDad(setU32H(dma.*[0].dad, value)),
|
||||
0xB8 => dma.*[0].setCntL(value),
|
||||
0xBA => dma.*[0].setCntH(value),
|
||||
|
||||
0xBC => dma.*[1].setSad(setU32L(dma.*[1].sad, value)),
|
||||
0xBE => dma.*[1].setSad(setU32H(dma.*[1].sad, value)),
|
||||
0xC0 => dma.*[1].setDad(setU32L(dma.*[1].dad, value)),
|
||||
0xC2 => dma.*[1].setDad(setU32H(dma.*[1].dad, value)),
|
||||
0xC4 => dma.*[1].setCntL(value),
|
||||
0xC6 => dma.*[1].setCntH(value),
|
||||
|
||||
0xC8 => dma.*[2].setSad(setU32L(dma.*[2].sad, value)),
|
||||
0xCA => dma.*[2].setSad(setU32H(dma.*[2].sad, value)),
|
||||
0xCC => dma.*[2].setDad(setU32L(dma.*[2].dad, value)),
|
||||
0xCE => dma.*[2].setDad(setU32H(dma.*[2].dad, value)),
|
||||
0xD0 => dma.*[2].setCntL(value),
|
||||
0xD2 => dma.*[2].setCntH(value),
|
||||
|
||||
0xD4 => dma.*[3].setSad(setU32L(dma.*[3].sad, value)),
|
||||
0xD6 => dma.*[3].setSad(setU32H(dma.*[3].sad, value)),
|
||||
0xD8 => dma.*[3].setDad(setU32L(dma.*[3].dad, value)),
|
||||
0xDA => dma.*[3].setDad(setU32H(dma.*[3].dad, value)),
|
||||
0xDC => dma.*[3].setCntL(value),
|
||||
0xDE => dma.*[3].setCntH(value),
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u8 => writeUndefined(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
else => @compileError("DMA: Unsupported write width"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Function that creates a DMAController. Determines unique DMA Controller behaiour at compile-time
|
||||
fn DmaController(comptime id: u2) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
const sad_mask: u32 = if (id == 0) 0x07FF_FFFF else 0x0FFF_FFFF;
|
||||
const dad_mask: u32 = if (id != 3) 0x07FF_FFFF else 0x0FFF_FFFF;
|
||||
|
||||
/// Write-only. The first address in a DMA transfer. (DMASAD)
|
||||
/// Note: use writeSrc instead of manipulating src_addr directly
|
||||
sad: u32,
|
||||
/// Write-only. The final address in a DMA transffer. (DMADAD)
|
||||
/// Note: Use writeDst instead of manipulatig dst_addr directly
|
||||
dad: u32,
|
||||
/// Write-only. The Word Count for the DMA Transfer (DMACNT_L)
|
||||
word_count: if (id == 3) u16 else u14,
|
||||
/// Read / Write. DMACNT_H
|
||||
/// Note: Use writeControl instead of manipulating cnt directly.
|
||||
cnt: DmaControl,
|
||||
|
||||
/// Internal. Currrent Source Address
|
||||
_sad: u32,
|
||||
/// Internal. Current Destination Address
|
||||
_dad: u32,
|
||||
/// Internal. Word Count
|
||||
_word_count: if (id == 3) u16 else u14,
|
||||
|
||||
// Internal. FIFO Word Count
|
||||
_fifo_word_count: u8,
|
||||
|
||||
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
|
||||
/// have delays. Thefore bit 15 of DMACNT isn't actually something
|
||||
/// we can use to control when we do or do not execute a step in a DMA Transfer
|
||||
in_progress: bool,
|
||||
|
||||
pub fn init() Self {
|
||||
return .{
|
||||
.sad = 0,
|
||||
.dad = 0,
|
||||
.word_count = 0,
|
||||
.cnt = .{ .raw = 0x000 },
|
||||
|
||||
// Internals
|
||||
._sad = 0,
|
||||
._dad = 0,
|
||||
._word_count = 0,
|
||||
._fifo_word_count = 4,
|
||||
.in_progress = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setSad(self: *Self, addr: u32) void {
|
||||
self.sad = addr & sad_mask;
|
||||
}
|
||||
|
||||
pub fn setDad(self: *Self, addr: u32) void {
|
||||
self.dad = addr & dad_mask;
|
||||
}
|
||||
|
||||
pub fn setCntL(self: *Self, halfword: u16) void {
|
||||
self.word_count = @truncate(@TypeOf(self.word_count), halfword);
|
||||
}
|
||||
|
||||
pub fn setCntH(self: *Self, halfword: u16) void {
|
||||
const new = DmaControl{ .raw = halfword };
|
||||
|
||||
if (!self.cnt.enabled.read() and new.enabled.read()) {
|
||||
// Reload Internals on Rising Edge.
|
||||
self._sad = self.sad;
|
||||
self._dad = self.dad;
|
||||
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||
|
||||
// Only a Start Timing of 00 has a DMA Transfer immediately begin
|
||||
self.in_progress = new.start_timing.read() == 0b00;
|
||||
}
|
||||
|
||||
self.cnt.raw = halfword;
|
||||
}
|
||||
|
||||
pub fn setCnt(self: *Self, word: u32) void {
|
||||
self.setCntL(@truncate(u16, word));
|
||||
self.setCntH(@truncate(u16, word >> 16));
|
||||
}
|
||||
|
||||
pub fn step(self: *Self, cpu: *Arm7tdmi) void {
|
||||
const is_fifo = (id == 1 or id == 2) and self.cnt.start_timing.read() == 0b11;
|
||||
const sad_adj = Self.adjustment(self.cnt.sad_adj.read());
|
||||
const dad_adj = if (is_fifo) .Fixed else Self.adjustment(self.cnt.dad_adj.read());
|
||||
|
||||
const transfer_type = is_fifo or self.cnt.transfer_type.read();
|
||||
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
||||
|
||||
const mask = if (transfer_type) ~@as(u32, 3) else ~@as(u32, 1);
|
||||
|
||||
if (transfer_type) {
|
||||
cpu.bus.write(u32, self._dad & mask, cpu.bus.read(u32, self._sad & mask));
|
||||
} else {
|
||||
cpu.bus.write(u16, self._dad & mask, cpu.bus.read(u16, self._sad & mask));
|
||||
}
|
||||
|
||||
switch (sad_adj) {
|
||||
.Increment => self._sad +%= offset,
|
||||
.Decrement => self._sad -%= offset,
|
||||
// TODO: Is just ignoring this ok?
|
||||
.IncrementReload => log.err("{} is a prohibited adjustment on SAD", .{sad_adj}),
|
||||
.Fixed => {},
|
||||
}
|
||||
|
||||
switch (dad_adj) {
|
||||
.Increment, .IncrementReload => self._dad +%= offset,
|
||||
.Decrement => self._dad -%= offset,
|
||||
.Fixed => {},
|
||||
}
|
||||
|
||||
self._word_count -= 1;
|
||||
|
||||
if (self._word_count == 0) {
|
||||
if (self.cnt.irq.read()) {
|
||||
switch (id) {
|
||||
0 => cpu.bus.io.irq.dma0.set(),
|
||||
1 => cpu.bus.io.irq.dma1.set(),
|
||||
2 => cpu.bus.io.irq.dma2.set(),
|
||||
3 => cpu.bus.io.irq.dma3.set(),
|
||||
}
|
||||
|
||||
cpu.handleInterrupt();
|
||||
}
|
||||
|
||||
// If we're not repeating, Fire the IRQs and disable the DMA
|
||||
if (!self.cnt.repeat.read()) self.cnt.enabled.unset();
|
||||
|
||||
// We want to disable our internal enabled flag regardless of repeat
|
||||
// because we only want to step A DMA that repeats during it's specific
|
||||
// timing window
|
||||
self.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
|
||||
if (self.in_progress) return; // If there's an ongoing DMA Transfer, exit early
|
||||
|
||||
// No ongoing DMA Transfer, We want to check if we should repeat an existing one
|
||||
// Determined by the repeat bit and whether the DMA is in the right start_timing
|
||||
switch (kind) {
|
||||
.VBlank => self.in_progress = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01,
|
||||
.HBlank => self.in_progress = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10,
|
||||
.Immediate, .Special => {},
|
||||
}
|
||||
|
||||
// If we determined that the repeat bit is set (and now the Hblank / Vblank DMA is now in progress)
|
||||
// Reload internal word count latch
|
||||
// Reload internal DAD latch if we are in IncrementRelaod
|
||||
if (self.in_progress) {
|
||||
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||
if (Self.adjustment(self.cnt.dad_adj.read()) == .IncrementReload) self._dad = self.dad;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn requestSoundDma(self: *Self, _: u32) void {
|
||||
comptime std.debug.assert(id == 1 or id == 2);
|
||||
if (self.in_progress) return; // APU must wait their turn
|
||||
|
||||
// DMA May not be configured for handling DMAs
|
||||
if (self.cnt.start_timing.read() != 0b11) return;
|
||||
|
||||
// We Assume the Repeat Bit is Set
|
||||
// We Assume that DAD is set to 0x0400_00A0 or 0x0400_00A4 (fifo_addr)
|
||||
// We Assume DMACNT_L is set to 4
|
||||
|
||||
// FIXME: Safe to just assume whatever DAD is set to is the FIFO Address?
|
||||
// self._dad = fifo_addr;
|
||||
self.cnt.repeat.set();
|
||||
self._word_count = 4;
|
||||
self.in_progress = true;
|
||||
}
|
||||
|
||||
fn adjustment(idx: u2) Adjustment {
|
||||
return std.meta.intToEnum(Adjustment, idx) catch unreachable;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pollBlankingDma(bus: *Bus, comptime kind: DmaKind) void {
|
||||
bus.dma[0].pollBlankingDma(kind);
|
||||
bus.dma[1].pollBlankingDma(kind);
|
||||
bus.dma[2].pollBlankingDma(kind);
|
||||
bus.dma[3].pollBlankingDma(kind);
|
||||
}
|
||||
|
||||
const Adjustment = enum(u2) {
|
||||
Increment = 0,
|
||||
Decrement = 1,
|
||||
Fixed = 2,
|
||||
IncrementReload = 3,
|
||||
};
|
||||
|
||||
const DmaKind = enum(u2) {
|
||||
Immediate = 0,
|
||||
HBlank,
|
||||
VBlank,
|
||||
Special,
|
||||
};
|
||||
|
||||
fn setU32L(left: u32, right: u16) u32 {
|
||||
return (left & 0xFFFF_0000) | right;
|
||||
}
|
||||
|
||||
fn setU32H(left: u32, right: u16) u32 {
|
||||
return (left & 0x0000_FFFF) | (@as(u32, right) << 16);
|
||||
}
|
665
src/core/bus/io.zig
Normal file
665
src/core/bus/io.zig
Normal file
@@ -0,0 +1,665 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const Bus = @import("../Bus.zig");
|
||||
const DmaController = @import("dma.zig").DmaController;
|
||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||
|
||||
const timer = @import("timer.zig");
|
||||
const dma = @import("dma.zig");
|
||||
const apu = @import("../apu.zig");
|
||||
|
||||
const readUndefined = @import("../util.zig").readUndefined;
|
||||
const writeUndefined = @import("../util.zig").writeUndefined;
|
||||
const log = std.log.scoped(.@"I/O");
|
||||
|
||||
pub const Io = struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Read / Write
|
||||
ime: bool,
|
||||
ie: InterruptEnable,
|
||||
irq: InterruptRequest,
|
||||
postflg: PostFlag,
|
||||
haltcnt: HaltControl,
|
||||
keyinput: KeyInput,
|
||||
|
||||
pub fn init() Self {
|
||||
return .{
|
||||
.ime = false,
|
||||
.ie = .{ .raw = 0x0000 },
|
||||
.irq = .{ .raw = 0x0000 },
|
||||
.keyinput = .{ .raw = 0x03FF },
|
||||
.postflg = .FirstBoot,
|
||||
.haltcnt = .Execute,
|
||||
};
|
||||
}
|
||||
|
||||
fn setIrqs(self: *Io, word: u32) void {
|
||||
self.ie.raw = @truncate(u16, word);
|
||||
self.irq.raw &= ~@truncate(u16, word >> 16);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn read(bus: *const Bus, comptime T: type, address: u32) T {
|
||||
return switch (T) {
|
||||
u32 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw,
|
||||
0x0400_0004 => @as(T, bus.ppu.vcount.raw) << 16 | bus.ppu.dispstat.raw,
|
||||
0x0400_0006 => @as(T, bus.ppu.bg[0].cnt.raw) << 16 | bus.ppu.vcount.raw,
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0...0x0400_00DC => dma.read(T, &bus.dma, address),
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010C => timer.read(T, &bus.tim, address),
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0128 => readTodo("Read {} from SIOCNT and SIOMLT_SEND", .{T}),
|
||||
|
||||
// Keypad Input
|
||||
0x0400_0130 => readTodo("Read {} from KEYINPUT", .{T}),
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0150 => readTodo("Read {} from JOY_RECV", .{T}),
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => @as(T, bus.io.irq.raw) << 16 | bus.io.ie.raw,
|
||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
u16 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw,
|
||||
0x0400_0004 => bus.ppu.dispstat.raw,
|
||||
0x0400_0006 => bus.ppu.vcount.raw,
|
||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw,
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw,
|
||||
0x0400_000C => bus.ppu.bg[2].cnt.raw,
|
||||
0x0400_000E => bus.ppu.bg[3].cnt.raw,
|
||||
0x0400_004C => readTodo("Read {} from MOSAIC", .{T}),
|
||||
0x0400_0050 => bus.ppu.bldcnt.raw,
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_009E => apu.read(T, &bus.apu, address),
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0...0x0400_00DE => dma.read(T, &bus.dma, address),
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010E => timer.read(T, &bus.tim, address),
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0128 => readTodo("Read {} from SIOCNT", .{T}),
|
||||
|
||||
// Keypad Input
|
||||
0x0400_0130 => bus.io.keyinput.raw,
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0134 => readTodo("Read {} from RCNT", .{T}),
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => bus.io.ie.raw,
|
||||
0x0400_0202 => bus.io.irq.raw,
|
||||
0x0400_0204 => readTodo("Read {} from WAITCNT", .{T}),
|
||||
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
u8 => return switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => @truncate(T, bus.ppu.dispcnt.raw),
|
||||
0x0400_0004 => @truncate(T, bus.ppu.dispstat.raw),
|
||||
0x0400_0005 => @truncate(T, bus.ppu.dispcnt.raw >> 8),
|
||||
0x0400_0006 => @truncate(T, bus.ppu.vcount.raw),
|
||||
0x0400_0008 => @truncate(T, bus.ppu.bg[0].cnt.raw),
|
||||
0x0400_0009 => @truncate(T, bus.ppu.bg[0].cnt.raw >> 8),
|
||||
0x0400_000A => @truncate(T, bus.ppu.bg[1].cnt.raw),
|
||||
0x0400_000B => @truncate(T, bus.ppu.bg[1].cnt.raw >> 8),
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_00A7 => apu.read(T, &bus.apu, address),
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0128 => readTodo("Read {} from SIOCNT_L", .{T}),
|
||||
|
||||
// Keypad Input
|
||||
0x0400_0130 => readTodo("read {} from KEYINPUT_L", .{T}),
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0135 => readTodo("Read {} from RCNT_H", .{T}),
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => @truncate(T, bus.io.ie.raw),
|
||||
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, address }),
|
||||
},
|
||||
else => @compileError("I/O: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
||||
return switch (T) {
|
||||
u32 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw = @truncate(u16, value),
|
||||
0x0400_0004 => {
|
||||
bus.ppu.dispstat.raw = @truncate(u16, value);
|
||||
bus.ppu.vcount.raw = @truncate(u16, value >> 16);
|
||||
},
|
||||
0x0400_0008 => bus.ppu.setAdjCnts(0, value),
|
||||
0x0400_000C => bus.ppu.setAdjCnts(2, value),
|
||||
0x0400_0010 => bus.ppu.setBgOffsets(0, value),
|
||||
0x0400_0014 => bus.ppu.setBgOffsets(1, value),
|
||||
0x0400_0018 => bus.ppu.setBgOffsets(2, value),
|
||||
0x0400_001C => bus.ppu.setBgOffsets(3, value),
|
||||
0x0400_0020 => bus.ppu.aff_bg[0].writePaPb(value),
|
||||
0x0400_0024 => bus.ppu.aff_bg[0].writePcPd(value),
|
||||
0x0400_0028 => bus.ppu.aff_bg[0].setX(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_002C => bus.ppu.aff_bg[0].setY(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_0030 => bus.ppu.aff_bg[1].writePaPb(value),
|
||||
0x0400_0034 => bus.ppu.aff_bg[1].writePcPd(value),
|
||||
0x0400_0038 => bus.ppu.aff_bg[1].setX(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_003C => bus.ppu.aff_bg[1].setY(bus.ppu.dispstat.vblank.read(), value),
|
||||
0x0400_0040 => bus.ppu.win.setH(value),
|
||||
0x0400_0044 => bus.ppu.win.setV(value),
|
||||
0x0400_0048 => bus.ppu.win.setIo(value),
|
||||
0x0400_004C => log.debug("Wrote 0x{X:0>8} to MOSAIC", .{value}),
|
||||
0x0400_0050 => {
|
||||
bus.ppu.bldcnt.raw = @truncate(u16, value);
|
||||
bus.ppu.bldalpha.raw = @truncate(u16, value >> 16);
|
||||
},
|
||||
0x0400_0054 => bus.ppu.bldy.raw = @truncate(u16, value),
|
||||
0x0400_0058...0x0400_005C => {}, // Unused
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_00A4 => apu.write(T, &bus.apu, address, value),
|
||||
0x0400_00A8, 0x0400_00AC => {}, // Unused
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0...0x0400_00DC => dma.write(T, &bus.dma, address, value),
|
||||
0x0400_00E0...0x0400_00FC => {}, // Unused
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010C => timer.write(T, &bus.tim, address, value),
|
||||
0x0400_0110...0x0400_011C => {}, // Unused
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0120 => log.debug("Wrote 0x{X:0>8} to SIODATA32/(SIOMULTI0 and SIOMULTI1)", .{value}),
|
||||
0x0400_0124 => log.debug("Wrote 0x{X:0>8} to SIOMULTI2 and SIOMULTI3", .{value}),
|
||||
0x0400_0128 => log.debug("Wrote 0x{X:0>8} to SIOCNT and SIOMLT_SEND/SIODATA8", .{value}),
|
||||
0x0400_012C => {}, // Unused
|
||||
|
||||
// Keypad Input
|
||||
0x0400_0130 => log.debug("Wrote 0x{X:0>8} to KEYINPUT and KEYCNT", .{value}),
|
||||
0x0400_0134 => log.debug("Wrote 0x{X:0>8} to RCNT and IR", .{value}),
|
||||
0x0400_0138, 0x0400_013C => {}, // Unused
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0140 => log.debug("Wrote 0x{X:0>8} to JOYCNT", .{value}),
|
||||
0x0400_0150 => log.debug("Wrote 0x{X:0>8} to JOY_RECV", .{value}),
|
||||
0x0400_0154 => log.debug("Wrote 0x{X:0>8} to JOY_TRANS", .{value}),
|
||||
0x0400_0158 => log.debug("Wrote 0x{X:0>8} to JOYSTAT (?)", .{value}),
|
||||
0x0400_0144...0x0400_014C, 0x0400_015C => {}, // Unused
|
||||
0x0400_0160...0x0400_01FC => {},
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => bus.io.setIrqs(value),
|
||||
0x0400_0204 => log.debug("Wrote 0x{X:0>8} to WAITCNT", .{value}),
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_020C...0x0400_021C => {}, // Unused
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
},
|
||||
u16 => switch (address) {
|
||||
// Display
|
||||
0x0400_0000 => bus.ppu.dispcnt.raw = value,
|
||||
0x0400_0004 => bus.ppu.dispstat.raw = value,
|
||||
0x0400_0006 => {}, // vcount is read-only
|
||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw = value,
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw = value,
|
||||
0x0400_000C => bus.ppu.bg[2].cnt.raw = value,
|
||||
0x0400_000E => bus.ppu.bg[3].cnt.raw = value,
|
||||
0x0400_0010 => bus.ppu.bg[0].hofs.raw = value, // TODO: Don't write out every HOFS / VOFS?
|
||||
0x0400_0012 => bus.ppu.bg[0].vofs.raw = value,
|
||||
0x0400_0014 => bus.ppu.bg[1].hofs.raw = value,
|
||||
0x0400_0016 => bus.ppu.bg[1].vofs.raw = value,
|
||||
0x0400_0018 => bus.ppu.bg[2].hofs.raw = value,
|
||||
0x0400_001A => bus.ppu.bg[2].vofs.raw = value,
|
||||
0x0400_001C => bus.ppu.bg[3].hofs.raw = value,
|
||||
0x0400_001E => bus.ppu.bg[3].vofs.raw = value,
|
||||
0x0400_0020 => bus.ppu.aff_bg[0].pa = @bitCast(i16, value),
|
||||
0x0400_0022 => bus.ppu.aff_bg[0].pb = @bitCast(i16, value),
|
||||
0x0400_0024 => bus.ppu.aff_bg[0].pc = @bitCast(i16, value),
|
||||
0x0400_0026 => bus.ppu.aff_bg[0].pd = @bitCast(i16, value),
|
||||
0x0400_0028 => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0xFFFF_0000 | value),
|
||||
0x0400_002A => bus.ppu.aff_bg[0].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||
0x0400_002C => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0xFFFF_0000 | value),
|
||||
0x0400_002E => bus.ppu.aff_bg[0].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[0].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||
0x0400_0030 => bus.ppu.aff_bg[1].pa = @bitCast(i16, value),
|
||||
0x0400_0032 => bus.ppu.aff_bg[1].pb = @bitCast(i16, value),
|
||||
0x0400_0034 => bus.ppu.aff_bg[1].pc = @bitCast(i16, value),
|
||||
0x0400_0036 => bus.ppu.aff_bg[1].pd = @bitCast(i16, value),
|
||||
0x0400_0038 => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0xFFFF_0000 | value),
|
||||
0x0400_003A => bus.ppu.aff_bg[1].x = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].x) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||
0x0400_003C => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0xFFFF_0000 | value),
|
||||
0x0400_003E => bus.ppu.aff_bg[1].y = @bitCast(i32, @bitCast(u32, bus.ppu.aff_bg[1].y) & 0x0000_FFFF | (@as(u32, value) << 16)),
|
||||
0x0400_0040 => bus.ppu.win.h[0].raw = value,
|
||||
0x0400_0042 => bus.ppu.win.h[1].raw = value,
|
||||
0x0400_0044 => bus.ppu.win.v[0].raw = value,
|
||||
0x0400_0046 => bus.ppu.win.v[1].raw = value,
|
||||
0x0400_0048 => bus.ppu.win.in.raw = value,
|
||||
0x0400_004A => bus.ppu.win.out.raw = value,
|
||||
0x0400_004C => log.debug("Wrote 0x{X:0>4} to MOSAIC", .{value}),
|
||||
0x0400_0050 => bus.ppu.bldcnt.raw = value,
|
||||
0x0400_0052 => bus.ppu.bldalpha.raw = value,
|
||||
0x0400_0054 => bus.ppu.bldy.raw = value,
|
||||
0x0400_004E, 0x0400_0056 => {}, // Not used
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_009E => apu.write(T, &bus.apu, address, value),
|
||||
|
||||
// Dma Transfers
|
||||
0x0400_00B0...0x0400_00DE => dma.write(T, &bus.dma, address, value),
|
||||
|
||||
// Timers
|
||||
0x0400_0100...0x0400_010E => timer.write(T, &bus.tim, address, value),
|
||||
0x0400_0114 => {}, // TODO: Gyakuten Saiban writes 0x8000 to 0x0400_0114
|
||||
0x0400_0110 => {}, // Not Used,
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0120 => log.debug("Wrote 0x{X:0>4} to SIOMULTI0", .{value}),
|
||||
0x0400_0122 => log.debug("Wrote 0x{X:0>4} to SIOMULTI1", .{value}),
|
||||
0x0400_0124 => log.debug("Wrote 0x{X:0>4} to SIOMULTI2", .{value}),
|
||||
0x0400_0126 => log.debug("Wrote 0x{X:0>4} to SIOMULTI3", .{value}),
|
||||
0x0400_0128 => log.debug("Wrote 0x{X:0>4} to SIOCNT", .{value}),
|
||||
0x0400_012A => log.debug("Wrote 0x{X:0>4} to SIOMLT_SEND", .{value}),
|
||||
|
||||
// Keypad Input
|
||||
0x0400_0130 => log.debug("Wrote 0x{X:0>4} to KEYINPUT. Ignored", .{value}),
|
||||
0x0400_0132 => log.debug("Wrote 0x{X:0>4} to KEYCNT", .{value}),
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0134 => log.debug("Wrote 0x{X:0>4} to RCNT", .{value}),
|
||||
0x0400_0140 => log.debug("Wrote 0x{X:0>4} to JOYCNT", .{value}),
|
||||
0x0400_0158 => log.debug("Wrote 0x{X:0>4} to JOYSTAT", .{value}),
|
||||
0x0400_0142, 0x0400_015A => {}, // Not Used
|
||||
|
||||
// Interrupts
|
||||
0x0400_0200 => bus.io.ie.raw = value,
|
||||
0x0400_0202 => bus.io.irq.raw &= ~value,
|
||||
0x0400_0204 => log.debug("Wrote 0x{X:0>4} to WAITCNT", .{value}),
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_0206, 0x0400_020A => {}, // Not Used
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
},
|
||||
u8 => switch (address) {
|
||||
// Display
|
||||
0x0400_0004 => bus.ppu.dispstat.raw = (bus.ppu.dispstat.raw & 0xFF00) | value,
|
||||
0x0400_0005 => bus.ppu.dispstat.raw = (@as(u16, value) << 8) | (bus.ppu.dispstat.raw & 0xFF),
|
||||
0x0400_0008 => bus.ppu.bg[0].cnt.raw = (bus.ppu.bg[0].cnt.raw & 0xFF00) | value,
|
||||
0x0400_0009 => bus.ppu.bg[0].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[0].cnt.raw & 0xFF),
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
|
||||
0x0400_000B => bus.ppu.bg[1].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[1].cnt.raw & 0xFF),
|
||||
0x0400_0048 => bus.ppu.win.setInL(value),
|
||||
0x0400_0049 => bus.ppu.win.setInH(value),
|
||||
0x0400_004A => bus.ppu.win.setOutL(value),
|
||||
0x0400_0054 => bus.ppu.bldy.raw = (bus.ppu.bldy.raw & 0xFF00) | value,
|
||||
|
||||
// Sound
|
||||
0x0400_0060...0x0400_00A7 => apu.write(T, &bus.apu, address, value),
|
||||
|
||||
// Serial Communication 1
|
||||
0x0400_0120 => log.debug("Wrote 0x{X:0>2} to SIODATA32_L_L", .{value}),
|
||||
0x0400_0128 => log.debug("Wrote 0x{X:0>2} to SIOCNT_L", .{value}),
|
||||
|
||||
// Serial Communication 2
|
||||
0x0400_0135 => log.debug("Wrote 0x{X:0>2} to RCNT_H", .{value}),
|
||||
0x0400_0140 => log.debug("Wrote 0x{X:0>2} to JOYCNT_L", .{value}),
|
||||
|
||||
// Interrupts
|
||||
0x0400_0202 => bus.io.irq.raw &= ~@as(u16, value),
|
||||
0x0400_0208 => bus.io.ime = value & 1 == 1,
|
||||
0x0400_0300 => bus.io.postflg = std.meta.intToEnum(PostFlag, value & 1) catch unreachable,
|
||||
0x0400_0301 => bus.io.haltcnt = if (value >> 7 & 1 == 0) .Halt else std.debug.panic("TODO: Implement STOP", .{}),
|
||||
|
||||
0x0400_0410 => log.debug("Wrote 0x{X:0>2} to the common yet undocumented 0x{X:0>8}", .{ value, address }),
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||
},
|
||||
else => @compileError("I/O: Unsupported write width"),
|
||||
};
|
||||
}
|
||||
|
||||
fn readTodo(comptime format: []const u8, args: anytype) u8 {
|
||||
log.debug(format, args);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Read / Write
|
||||
pub const PostFlag = enum(u1) {
|
||||
FirstBoot = 0,
|
||||
FurtherBoots = 1,
|
||||
};
|
||||
|
||||
/// Write Only
|
||||
pub const HaltControl = enum {
|
||||
Halt,
|
||||
Stop,
|
||||
Execute,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const DisplayControl = extern union {
|
||||
bg_mode: Bitfield(u16, 0, 3),
|
||||
frame_select: Bit(u16, 4),
|
||||
hblank_interval_free: Bit(u16, 5),
|
||||
obj_mapping: Bit(u16, 6),
|
||||
forced_blank: Bit(u16, 7),
|
||||
bg_enable: Bitfield(u16, 8, 4),
|
||||
obj_enable: Bit(u16, 12),
|
||||
win_enable: Bitfield(u16, 13, 2),
|
||||
obj_win_enable: Bit(u16, 15),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const DisplayStatus = extern union {
|
||||
vblank: Bit(u16, 0),
|
||||
hblank: Bit(u16, 1),
|
||||
coincidence: Bit(u16, 2),
|
||||
vblank_irq: Bit(u16, 3),
|
||||
hblank_irq: Bit(u16, 4),
|
||||
vcount_irq: Bit(u16, 5),
|
||||
vcount_trigger: Bitfield(u16, 8, 8),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read Only
|
||||
pub const VCount = extern union {
|
||||
scanline: Bitfield(u16, 0, 8),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
const InterruptEnable = extern union {
|
||||
vblank: Bit(u16, 0),
|
||||
hblank: Bit(u16, 1),
|
||||
coincidence: Bit(u16, 2),
|
||||
tm0_overflow: Bit(u16, 3),
|
||||
tm1_overflow: Bit(u16, 4),
|
||||
tm2_overflow: Bit(u16, 5),
|
||||
tm3_overflow: Bit(u16, 6),
|
||||
serial: Bit(u16, 7),
|
||||
dma0: Bit(u16, 8),
|
||||
dma1: Bit(u16, 9),
|
||||
dma2: Bit(u16, 10),
|
||||
dma3: Bit(u16, 11),
|
||||
keypad: Bit(u16, 12),
|
||||
game_pak: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read Only
|
||||
/// 0 = Pressed, 1 = Released
|
||||
const KeyInput = extern union {
|
||||
a: Bit(u16, 0),
|
||||
b: Bit(u16, 1),
|
||||
select: Bit(u16, 2),
|
||||
start: Bit(u16, 3),
|
||||
right: Bit(u16, 4),
|
||||
left: Bit(u16, 5),
|
||||
up: Bit(u16, 6),
|
||||
down: Bit(u16, 7),
|
||||
shoulder_r: Bit(u16, 8),
|
||||
shoulder_l: Bit(u16, 9),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
// Read / Write
|
||||
pub const BackgroundControl = extern union {
|
||||
priority: Bitfield(u16, 0, 2),
|
||||
char_base: Bitfield(u16, 2, 2),
|
||||
mosaic_enable: Bit(u16, 6),
|
||||
colour_mode: Bit(u16, 7),
|
||||
screen_base: Bitfield(u16, 8, 5),
|
||||
display_overflow: Bit(u16, 13),
|
||||
size: Bitfield(u16, 14, 2),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Write Only
|
||||
pub const BackgroundOffset = extern union {
|
||||
offset: Bitfield(u16, 0, 9),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const BldCnt = extern union {
|
||||
/// BLDCNT{0} is BG0 A
|
||||
/// BLDCNT{4} is OBJ A
|
||||
/// BLDCNT{5} is BD A
|
||||
layer_a: Bitfield(u16, 0, 6),
|
||||
mode: Bitfield(u16, 6, 2),
|
||||
|
||||
/// BLDCNT{8} is BG0 B
|
||||
/// BLDCNT{12} is OBJ B
|
||||
/// BLDCNT{13} is BD B
|
||||
layer_b: Bitfield(u16, 8, 6),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read-only?
|
||||
/// Alpha Blending Coefficients
|
||||
pub const BldAlpha = extern union {
|
||||
eva: Bitfield(u16, 0, 5),
|
||||
evb: Bitfield(u16, 8, 5),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Write-only?
|
||||
/// Brightness COefficients
|
||||
pub const BldY = extern union {
|
||||
evy: Bitfield(u16, 0, 5),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Write-only
|
||||
pub const WinH = extern union {
|
||||
x2: Bitfield(u16, 0, 8),
|
||||
x1: Bitfield(u16, 8, 8),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Write-only
|
||||
pub const WinV = extern union {
|
||||
y2: Bitfield(u16, 0, 8),
|
||||
y1: Bitfield(u16, 8, 8),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
pub const WinIn = extern union {
|
||||
w0_bg: Bitfield(u16, 0, 4),
|
||||
w0_obj: Bit(u16, 4),
|
||||
w0_colour: Bit(u16, 5),
|
||||
w1_bg: Bitfield(u16, 8, 4),
|
||||
w1_obj: Bit(u16, 12),
|
||||
w1_colour: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
pub const WinOut = extern union {
|
||||
out_bg: Bitfield(u16, 0, 4),
|
||||
out_obj: Bit(u16, 4),
|
||||
out_colour: Bit(u16, 5),
|
||||
obj_bg: Bitfield(u16, 8, 4),
|
||||
obj_obj: Bit(u16, 12),
|
||||
obj_colour: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
const InterruptRequest = extern union {
|
||||
vblank: Bit(u16, 0),
|
||||
hblank: Bit(u16, 1),
|
||||
coincidence: Bit(u16, 2),
|
||||
tim0: Bit(u16, 3),
|
||||
tim1: Bit(u16, 4),
|
||||
tim2: Bit(u16, 5),
|
||||
tim3: Bit(u16, 6),
|
||||
serial: Bit(u16, 7),
|
||||
dma0: Bit(u16, 8),
|
||||
dma1: Bit(u16, 9),
|
||||
dma2: Bit(u16, 10),
|
||||
dma3: Bit(u16, 11),
|
||||
keypad: Bit(u16, 12),
|
||||
game_pak: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const DmaControl = extern union {
|
||||
dad_adj: Bitfield(u16, 5, 2),
|
||||
sad_adj: Bitfield(u16, 7, 2),
|
||||
repeat: Bit(u16, 9),
|
||||
transfer_type: Bit(u16, 10),
|
||||
pak_drq: Bit(u16, 11),
|
||||
start_timing: Bitfield(u16, 12, 2),
|
||||
irq: Bit(u16, 14),
|
||||
enabled: Bit(u16, 15),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const TimerControl = extern union {
|
||||
frequency: Bitfield(u16, 0, 2),
|
||||
cascade: Bit(u16, 2),
|
||||
irq: Bit(u16, 6),
|
||||
enabled: Bit(u16, 7),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NR10
|
||||
pub const Sweep = extern union {
|
||||
shift: Bitfield(u8, 0, 3),
|
||||
direction: Bit(u8, 3),
|
||||
period: Bitfield(u8, 4, 3),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// This represents the Duty / Len
|
||||
/// NRx1
|
||||
pub const Duty = extern union {
|
||||
/// Write-only
|
||||
/// Only used when bit 6 is set
|
||||
length: Bitfield(u16, 0, 6),
|
||||
pattern: Bitfield(u16, 6, 2),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NRx2
|
||||
pub const Envelope = extern union {
|
||||
period: Bitfield(u8, 0, 3),
|
||||
direction: Bit(u8, 3),
|
||||
init_vol: Bitfield(u8, 4, 4),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NRx3, NRx4
|
||||
pub const Frequency = extern union {
|
||||
/// Write-only
|
||||
frequency: Bitfield(u16, 0, 11),
|
||||
length_enable: Bit(u16, 14),
|
||||
/// Write-only
|
||||
trigger: Bit(u16, 15),
|
||||
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NR30
|
||||
pub const WaveSelect = extern union {
|
||||
dimension: Bit(u8, 5),
|
||||
bank: Bit(u8, 6),
|
||||
enabled: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NR32
|
||||
pub const WaveVolume = extern union {
|
||||
kind: Bitfield(u8, 5, 2),
|
||||
force: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NR43
|
||||
pub const PolyCounter = extern union {
|
||||
div_ratio: Bitfield(u8, 0, 3),
|
||||
width: Bit(u8, 3),
|
||||
shift: Bitfield(u8, 4, 4),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
/// NR44
|
||||
pub const NoiseControl = extern union {
|
||||
length_enable: Bit(u8, 6),
|
||||
trigger: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const ChannelVolumeControl = extern union {
|
||||
right_vol: Bitfield(u16, 0, 3),
|
||||
left_vol: Bitfield(u16, 4, 3),
|
||||
ch_right: Bitfield(u16, 8, 4),
|
||||
ch_left: Bitfield(u16, 12, 4),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const DmaSoundControl = extern union {
|
||||
ch_vol: Bitfield(u16, 0, 2),
|
||||
chA_vol: Bit(u16, 2),
|
||||
chB_vol: Bit(u16, 3),
|
||||
|
||||
chA_right: Bit(u16, 8),
|
||||
chA_left: Bit(u16, 9),
|
||||
chA_timer: Bit(u16, 10),
|
||||
/// Write only?
|
||||
chA_reset: Bit(u16, 11),
|
||||
|
||||
chB_right: Bit(u16, 12),
|
||||
chB_left: Bit(u16, 13),
|
||||
chB_timer: Bit(u16, 14),
|
||||
/// Write only?
|
||||
chB_reset: Bit(u16, 15),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const SoundControl = extern union {
|
||||
/// Read-only
|
||||
ch1_enable: Bit(u8, 0),
|
||||
/// Read-only
|
||||
ch2_enable: Bit(u8, 1),
|
||||
/// Read-only
|
||||
ch3_enable: Bit(u8, 2),
|
||||
/// Read-only
|
||||
ch4_enable: Bit(u8, 3),
|
||||
apu_enable: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
/// Read / Write
|
||||
pub const SoundBias = extern union {
|
||||
level: Bitfield(u16, 1, 9),
|
||||
sampling_cycle: Bitfield(u16, 14, 2),
|
||||
raw: u16,
|
||||
};
|
201
src/core/bus/timer.zig
Normal file
201
src/core/bus/timer.zig
Normal file
@@ -0,0 +1,201 @@
|
||||
const std = @import("std");
|
||||
|
||||
const TimerControl = @import("io.zig").TimerControl;
|
||||
const Io = @import("io.zig").Io;
|
||||
const Scheduler = @import("../scheduler.zig").Scheduler;
|
||||
const Event = @import("../scheduler.zig").Event;
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
|
||||
const readUndefined = @import("../util.zig").readUndefined;
|
||||
const writeUndefined = @import("../util.zig").writeUndefined;
|
||||
pub const TimerTuple = std.meta.Tuple(&[_]type{ Timer(0), Timer(1), Timer(2), Timer(3) });
|
||||
const log = std.log.scoped(.Timer);
|
||||
|
||||
pub fn create(sched: *Scheduler) TimerTuple {
|
||||
return .{ Timer(0).init(sched), Timer(1).init(sched), Timer(2).init(sched), Timer(3).init(sched) };
|
||||
}
|
||||
|
||||
pub fn read(comptime T: type, tim: *const TimerTuple, addr: u32) T {
|
||||
const nybble = @truncate(u4, addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (nybble) {
|
||||
0x0 => @as(T, tim.*[0].cnt.raw) << 16 | tim.*[0].getCntL(),
|
||||
0x4 => @as(T, tim.*[1].cnt.raw) << 16 | tim.*[1].getCntL(),
|
||||
0x8 => @as(T, tim.*[2].cnt.raw) << 16 | tim.*[2].getCntL(),
|
||||
0xC => @as(T, tim.*[3].cnt.raw) << 16 | tim.*[3].getCntL(),
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u16 => switch (nybble) {
|
||||
0x0 => tim.*[0].getCntL(),
|
||||
0x2 => tim.*[0].cnt.raw,
|
||||
0x4 => tim.*[1].getCntL(),
|
||||
0x6 => tim.*[1].cnt.raw,
|
||||
0x8 => tim.*[2].getCntL(),
|
||||
0xA => tim.*[2].cnt.raw,
|
||||
0xC => tim.*[3].getCntL(),
|
||||
0xE => tim.*[3].cnt.raw,
|
||||
else => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
},
|
||||
u8 => readUndefined(log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||
else => @compileError("TIM: Unsupported read width"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(comptime T: type, tim: *TimerTuple, addr: u32, value: T) void {
|
||||
const nybble = @truncate(u4, addr);
|
||||
|
||||
return switch (T) {
|
||||
u32 => switch (nybble) {
|
||||
0x0 => tim.*[0].setCnt(value),
|
||||
0x4 => tim.*[1].setCnt(value),
|
||||
0x8 => tim.*[2].setCnt(value),
|
||||
0xC => tim.*[3].setCnt(value),
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>8}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u16 => switch (nybble) {
|
||||
0x0 => tim.*[0].setCntL(value),
|
||||
0x2 => tim.*[0].setCntH(value),
|
||||
0x4 => tim.*[1].setCntL(value),
|
||||
0x6 => tim.*[1].setCntH(value),
|
||||
0x8 => tim.*[2].setCntL(value),
|
||||
0xA => tim.*[2].setCntH(value),
|
||||
0xC => tim.*[3].setCntL(value),
|
||||
0xE => tim.*[3].setCntH(value),
|
||||
else => writeUndefined(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
},
|
||||
u8 => writeUndefined(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||
else => @compileError("TIM: Unsupported write width"),
|
||||
};
|
||||
}
|
||||
|
||||
fn Timer(comptime id: u2) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Read Only, Internal. Please use self.getCntL()
|
||||
_counter: u16,
|
||||
|
||||
/// Write Only, Internal. Please use self.setCntL()
|
||||
_reload: u16,
|
||||
|
||||
/// Write Only, Internal. Please use self.setCntH()
|
||||
cnt: TimerControl,
|
||||
|
||||
/// Internal.
|
||||
sched: *Scheduler,
|
||||
|
||||
/// Internal
|
||||
_start_timestamp: u64,
|
||||
|
||||
pub fn init(sched: *Scheduler) Self {
|
||||
return .{
|
||||
._reload = 0,
|
||||
._counter = 0,
|
||||
.cnt = .{ .raw = 0x0000 },
|
||||
.sched = sched,
|
||||
._start_timestamp = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// TIMCNT_L
|
||||
pub fn getCntL(self: *const Self) u16 {
|
||||
if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter;
|
||||
|
||||
return self._counter +% @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||
}
|
||||
|
||||
/// TIMCNT_L
|
||||
pub fn setCntL(self: *Self, halfword: u16) void {
|
||||
self._reload = halfword;
|
||||
}
|
||||
|
||||
/// TIMCNT_L & TIMCNT_H
|
||||
pub fn setCnt(self: *Self, word: u32) void {
|
||||
self.setCntL(@truncate(u16, word));
|
||||
self.setCntH(@truncate(u16, word >> 16));
|
||||
}
|
||||
|
||||
/// TIMCNT_H
|
||||
pub fn setCntH(self: *Self, halfword: u16) void {
|
||||
const new = TimerControl{ .raw = halfword };
|
||||
|
||||
// If Timer happens to be enabled, It will either be resheduled or disabled
|
||||
self.sched.removeScheduledEvent(.{ .TimerOverflow = id });
|
||||
|
||||
if (self.cnt.enabled.read() and (new.cascade.read() or !new.enabled.read())) {
|
||||
// Either through the cascade bit or the enable bit, the timer has effectively been disabled
|
||||
// The Counter should hold whatever value it should have been at when it was disabled
|
||||
self._counter +%= @truncate(u16, (self.sched.now() - self._start_timestamp) / self.frequency());
|
||||
}
|
||||
|
||||
// The counter is only reloaded on the rising edge of the enable bit
|
||||
if (!self.cnt.enabled.read() and new.enabled.read()) self._counter = self._reload;
|
||||
|
||||
// If Timer is enabled and we're not cascading, we need to schedule an overflow event
|
||||
if (new.enabled.read() and !new.cascade.read()) self.scheduleOverflow(0);
|
||||
|
||||
self.cnt.raw = halfword;
|
||||
}
|
||||
|
||||
pub fn handleOverflow(self: *Self, cpu: *Arm7tdmi, late: u64) void {
|
||||
// Fire IRQ if enabled
|
||||
const io = &cpu.bus.io;
|
||||
|
||||
if (self.cnt.irq.read()) {
|
||||
switch (id) {
|
||||
0 => io.irq.tim0.set(),
|
||||
1 => io.irq.tim1.set(),
|
||||
2 => io.irq.tim2.set(),
|
||||
3 => io.irq.tim3.set(),
|
||||
}
|
||||
|
||||
cpu.handleInterrupt();
|
||||
}
|
||||
|
||||
// DMA Sound Things
|
||||
if (id == 0 or id == 1) {
|
||||
cpu.bus.apu.handleTimerOverflow(cpu, id);
|
||||
}
|
||||
|
||||
// Perform Cascade Behaviour
|
||||
switch (id) {
|
||||
0 => if (cpu.bus.tim[1].cnt.cascade.read()) {
|
||||
cpu.bus.tim[1]._counter +%= 1;
|
||||
if (cpu.bus.tim[1]._counter == 0) cpu.bus.tim[1].handleOverflow(cpu, late);
|
||||
},
|
||||
1 => if (cpu.bus.tim[2].cnt.cascade.read()) {
|
||||
cpu.bus.tim[2]._counter +%= 1;
|
||||
if (cpu.bus.tim[2]._counter == 0) cpu.bus.tim[2].handleOverflow(cpu, late);
|
||||
},
|
||||
2 => if (cpu.bus.tim[3].cnt.cascade.read()) {
|
||||
cpu.bus.tim[3]._counter +%= 1;
|
||||
if (cpu.bus.tim[3]._counter == 0) cpu.bus.tim[3].handleOverflow(cpu, late);
|
||||
},
|
||||
3 => {}, // There is no Timer for TIM3 to "cascade" to,
|
||||
}
|
||||
|
||||
// Reschedule Timer if we're not cascading
|
||||
if (!self.cnt.cascade.read()) {
|
||||
self._counter = self._reload;
|
||||
self.scheduleOverflow(late);
|
||||
}
|
||||
}
|
||||
|
||||
fn scheduleOverflow(self: *Self, late: u64) void {
|
||||
const when = (@as(u64, 0x10000) - self._counter) * self.frequency();
|
||||
|
||||
self._start_timestamp = self.sched.now();
|
||||
self.sched.push(.{ .TimerOverflow = id }, when -| late);
|
||||
}
|
||||
|
||||
fn frequency(self: *const Self) u16 {
|
||||
return switch (self.cnt.frequency.read()) {
|
||||
0 => 1,
|
||||
1 => 64,
|
||||
2 => 256,
|
||||
3 => 1024,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user