Compare commits
469 Commits
976a87e888
...
1f4e01bd32
|
@ -2,3 +2,13 @@
|
||||||
/bin
|
/bin
|
||||||
/zig-cache
|
/zig-cache
|
||||||
/zig-out
|
/zig-out
|
||||||
|
/docs
|
||||||
|
**/*.log
|
||||||
|
**/*.bin
|
||||||
|
|
||||||
|
# Build on Windows
|
||||||
|
/.build_config
|
||||||
|
/lib/SDL2
|
||||||
|
|
||||||
|
# Any Custom Scripts for Debugging purposes
|
||||||
|
*.sh
|
|
@ -0,0 +1,12 @@
|
||||||
|
[submodule "lib/SDL.zig"]
|
||||||
|
path = lib/SDL.zig
|
||||||
|
url = https://github.com/MasterQ32/SDL.zig
|
||||||
|
[submodule "lib/zig-clap"]
|
||||||
|
path = lib/zig-clap
|
||||||
|
url = https://github.com/Hejsil/zig-clap
|
||||||
|
[submodule "lib/known-folders"]
|
||||||
|
path = lib/known-folders
|
||||||
|
url = https://github.com/ziglibs/known-folders
|
||||||
|
[submodule "lib/zig-datetime"]
|
||||||
|
path = lib/zig-datetime
|
||||||
|
url = https://github.com/frmdstryr/zig-datetime
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"augusterame.zls-vscode",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"vadimcn.vscode-lldb",
|
||||||
|
"dan-c-underwood.arm"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
# ZBA (working title)
|
||||||
|
An in-progress Game Boy Advance Emulator written in Zig ⚡!
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- [ ] [jsmolka's GBA Test Collection](https://github.com/jsmolka/gba-tests)
|
||||||
|
- [x] `arm.gba` and `thumb.gba`
|
||||||
|
- [x] `flash64.gba`, `flash128.gba`, `none.gba`, and `sram.gba`
|
||||||
|
- [x] `hello.gba`, `shades.gba`, and `stripes.gba`
|
||||||
|
- [x] `memory.gba`
|
||||||
|
- [x] `bios.gba`
|
||||||
|
- [ ] `nes.gba`
|
||||||
|
- [ ] [DenSinH's GBA ROMs](https://github.com/DenSinH/GBARoms)
|
||||||
|
- [x] `eeprom-test` and `flash-test`
|
||||||
|
- [x] `midikey2freq`
|
||||||
|
- [ ] `swi-tests-random`
|
||||||
|
- [ ] [destoer's GBA Tests](https://github.com/destoer/gba_tests)
|
||||||
|
- [x] `cond_invalid.gba`
|
||||||
|
- [x] `dma_priority.gba`
|
||||||
|
- [x] `hello_world.gba`
|
||||||
|
- [x] `if_ack.gba`
|
||||||
|
- [ ] `line_timing.gba`
|
||||||
|
- [ ] `lyc_midline.gba`
|
||||||
|
- [ ] `window_midframe.gba`
|
||||||
|
- [x] [ladystarbreeze's GBA Test Collection](https://github.com/ladystarbreeze/GBA-Test-Collection)
|
||||||
|
- [x] `retAddr.gba`
|
||||||
|
- [x] `helloWorld.gba`
|
||||||
|
- [x] `helloAudio.gba`
|
||||||
|
- [x] [`armwrestler-gba-fixed.gba`](https://github.com/destoer/armwrestler-gba-fixed)
|
||||||
|
- [x] [FuzzARM](https://github.com/DenSinH/FuzzARM)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
* [GBATEK](https://problemkaputt.de/gbatek.htm)
|
||||||
|
* [TONC](https://coranac.com/tonc/text/toc.htm)
|
||||||
|
* [ARM Architecture Reference Manual](https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/third-party/ddi0100e_arm_arm.pdf)
|
||||||
|
* [ARM7TDMI Data Sheet](https://www.dca.fee.unicamp.br/cursos/EA871/references/ARM/ARM7TDMIDataSheet.pdf)
|
||||||
|
|
||||||
|
## Compiling
|
||||||
|
Most recently built on Zig [0.10.0-dev.3900+ab4b26d8a](https://github.com/ziglang/zig/tree/ab4b26d8a)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
* [SDL.zig](https://github.com/MasterQ32/SDL.zig)
|
||||||
|
* [SDL2](https://www.libsdl.org/download-2.0.php)
|
||||||
|
* [zig-clap](https://github.com/Hejsil/zig-clap)
|
||||||
|
* [known-folders](https://github.com/ziglibs/known-folders)
|
||||||
|
* [`bitfields.zig`](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568197ad24780ec9adb421217530d4466/lib/util/bitfields.zig)
|
||||||
|
|
||||||
|
`bitfields.zig` from [FlorenceOS](https://github.com/FlorenceOS) is included under `lib/util/bitfield.zig`.
|
||||||
|
|
||||||
|
Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, and `known-folders`
|
||||||
|
|
||||||
|
Be sure to provide SDL2 using:
|
||||||
|
* Linux: Your distro's package manager
|
||||||
|
* MacOS: ¯\\\_(ツ)_/¯
|
||||||
|
* Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`)
|
||||||
|
|
||||||
|
`SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2.
|
||||||
|
|
||||||
|
Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
Key | Button
|
||||||
|
--- | ---
|
||||||
|
<kbd>X</kbd> | A
|
||||||
|
<kbd>Z</kbd> | B
|
||||||
|
<kbd>A</kbd> | L
|
||||||
|
<kbd>S</kbd> | R
|
||||||
|
<kbd>Return</kbd> | Start
|
||||||
|
<kbd>RShift</kbd> | Select
|
||||||
|
Arrow Keys | D-Pad
|
20
build.zig
20
build.zig
|
@ -1,4 +1,5 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const Sdk = @import("lib/SDL.zig/Sdk.zig");
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) void {
|
pub fn build(b: *std.build.Builder) void {
|
||||||
// Standard target options allows the person running `zig build` to choose
|
// Standard target options allows the person running `zig build` to choose
|
||||||
|
@ -12,6 +13,25 @@ pub fn build(b: *std.build.Builder) void {
|
||||||
const mode = b.standardReleaseOptions();
|
const mode = b.standardReleaseOptions();
|
||||||
|
|
||||||
const exe = b.addExecutable("zba", "src/main.zig");
|
const exe = b.addExecutable("zba", "src/main.zig");
|
||||||
|
// Known Folders (%APPDATA%, XDG, etc.)
|
||||||
|
exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig");
|
||||||
|
|
||||||
|
// DateTime Library
|
||||||
|
exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig");
|
||||||
|
|
||||||
|
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
|
||||||
|
// exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } });
|
||||||
|
exe.addPackagePath("bitfield", "lib/util/bitfield.zig");
|
||||||
|
|
||||||
|
// Argument Parsing Library
|
||||||
|
exe.addPackagePath("clap", "lib/zig-clap/clap.zig");
|
||||||
|
|
||||||
|
// Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig
|
||||||
|
const sdk = Sdk.init(b);
|
||||||
|
sdk.link(exe, .dynamic);
|
||||||
|
|
||||||
|
exe.addPackage(sdk.getNativePackage("sdl2"));
|
||||||
|
|
||||||
exe.setTarget(target);
|
exe.setTarget(target);
|
||||||
exe.setBuildMode(mode);
|
exe.setBuildMode(mode);
|
||||||
exe.install();
|
exe.install();
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6a9e37687a4b9ae3c14c9ea148ec51d14e01c7db
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 24845b0103e611c108d6bc334231c464e699742c
|
|
@ -0,0 +1,146 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
fn PtrCastPreserveCV(comptime T: type, comptime PtrToT: type, comptime NewT: type) type {
|
||||||
|
return switch (PtrToT) {
|
||||||
|
*T => *NewT,
|
||||||
|
*const T => *const NewT,
|
||||||
|
*volatile T => *volatile NewT,
|
||||||
|
*const volatile T => *const volatile NewT,
|
||||||
|
|
||||||
|
else => @compileError("wtf you doing"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn BitType(comptime FieldType: type, comptime ValueType: type, comptime shamt: usize) type {
|
||||||
|
const self_bit: FieldType = (1 << shamt);
|
||||||
|
|
||||||
|
return extern struct {
|
||||||
|
bits: Bitfield(FieldType, shamt, 1),
|
||||||
|
|
||||||
|
pub fn set(self: anytype) void {
|
||||||
|
self.bits.field().* |= self_bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unset(self: anytype) void {
|
||||||
|
self.bits.field().* &= ~self_bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: anytype) ValueType {
|
||||||
|
return @bitCast(ValueType, @truncate(u1, self.bits.field().* >> shamt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since these are mostly used with MMIO, I want to avoid
|
||||||
|
// reading the memory just to write it again, also races
|
||||||
|
pub fn write(self: anytype, val: ValueType) void {
|
||||||
|
if (@bitCast(bool, val)) {
|
||||||
|
self.set();
|
||||||
|
} else {
|
||||||
|
self.unset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original Bit Constructor
|
||||||
|
// pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
|
||||||
|
// return BitType(FieldType, u1, shamt);
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn Bit(comptime FieldType: type, comptime shamt: usize) type {
|
||||||
|
return BitType(FieldType, bool, shamt);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Boolean(comptime FieldType: type, comptime shamt: usize) type {
|
||||||
|
return BitType(FieldType, bool, shamt);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Bitfield(comptime FieldType: type, comptime shamt: usize, comptime num_bits: usize) type {
|
||||||
|
if (shamt + num_bits > @bitSizeOf(FieldType)) {
|
||||||
|
@compileError("bitfield doesn't fit");
|
||||||
|
}
|
||||||
|
|
||||||
|
const self_mask: FieldType = ((1 << num_bits) - 1) << shamt;
|
||||||
|
|
||||||
|
const ValueType = std.meta.Int(.unsigned, num_bits);
|
||||||
|
|
||||||
|
return extern struct {
|
||||||
|
dummy: FieldType,
|
||||||
|
|
||||||
|
fn field(self: anytype) PtrCastPreserveCV(@This(), @TypeOf(self), FieldType) {
|
||||||
|
return @ptrCast(PtrCastPreserveCV(@This(), @TypeOf(self), FieldType), self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: anytype, val: ValueType) void {
|
||||||
|
self.field().* &= ~self_mask;
|
||||||
|
self.field().* |= @intCast(FieldType, val) << shamt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: anytype) ValueType {
|
||||||
|
const val: FieldType = self.field().*;
|
||||||
|
return @intCast(ValueType, (val & self_mask) >> shamt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "bit" {
|
||||||
|
const S = extern union {
|
||||||
|
low: Bit(u32, 0),
|
||||||
|
high: Bit(u32, 1),
|
||||||
|
val: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expect(@sizeOf(S) == 4);
|
||||||
|
std.testing.expect(@bitSizeOf(S) == 32);
|
||||||
|
|
||||||
|
var s: S = .{ .val = 1 };
|
||||||
|
|
||||||
|
std.testing.expect(s.low.read() == 1);
|
||||||
|
std.testing.expect(s.high.read() == 0);
|
||||||
|
|
||||||
|
s.low.write(0);
|
||||||
|
s.high.write(1);
|
||||||
|
|
||||||
|
std.testing.expect(s.val == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "boolean" {
|
||||||
|
const S = extern union {
|
||||||
|
low: Boolean(u32, 0),
|
||||||
|
high: Boolean(u32, 1),
|
||||||
|
val: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expect(@sizeOf(S) == 4);
|
||||||
|
std.testing.expect(@bitSizeOf(S) == 32);
|
||||||
|
|
||||||
|
var s: S = .{ .val = 2 };
|
||||||
|
|
||||||
|
std.testing.expect(s.low.read() == false);
|
||||||
|
std.testing.expect(s.high.read() == true);
|
||||||
|
|
||||||
|
s.low.write(true);
|
||||||
|
s.high.write(false);
|
||||||
|
|
||||||
|
std.testing.expect(s.val == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "bitfield" {
|
||||||
|
const S = extern union {
|
||||||
|
low: Bitfield(u32, 0, 16),
|
||||||
|
high: Bitfield(u32, 16, 16),
|
||||||
|
val: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
std.testing.expect(@sizeOf(S) == 4);
|
||||||
|
std.testing.expect(@bitSizeOf(S) == 32);
|
||||||
|
|
||||||
|
var s: S = .{ .val = 0x13376969 };
|
||||||
|
|
||||||
|
std.testing.expect(s.low.read() == 0x6969);
|
||||||
|
std.testing.expect(s.high.read() == 0x1337);
|
||||||
|
|
||||||
|
s.low.write(0x1337);
|
||||||
|
s.high.write(0x6969);
|
||||||
|
|
||||||
|
std.testing.expect(s.val == 0x69691337);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 4f4196fc3bc95c4bd3b12ce2e4a5f1050742cd3c
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5ec1c36cf3791b3c6c5b330357bdb6feb93979ba
|
44
src/bus.zig
44
src/bus.zig
|
@ -1,44 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const GamePak = @import("pak.zig").GamePak;
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
pub const Bus = struct {
|
|
||||||
pak: GamePak,
|
|
||||||
|
|
||||||
pub fn withPak(alloc: Allocator, path: []const u8) !@This() {
|
|
||||||
return @This(){
|
|
||||||
.pak = try GamePak.fromPath(alloc, path),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readWord(self: *const @This(), addr: u32) u32 {
|
|
||||||
return self.pak.readWord(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeWord(_: *@This(), _: u32, _: u32) void {
|
|
||||||
std.debug.panic("TODO: Implement Bus#writeWord", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readHalfWord(self: *const @This(), addr: u32) u16 {
|
|
||||||
return self.pak.readHalfWord(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeHalfWord(self: *@This(), addr: u32, halfword: u16) void {
|
|
||||||
|
|
||||||
// TODO: Actually implement the memory mmap
|
|
||||||
if (addr >= self.pak.buf.len) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pak.writeHalfWord(addr, halfword);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readByte(self: *const @This(), addr: u32) u8 {
|
|
||||||
return self.pak.readByte(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeByte(_: *@This(), _: u32, _: u8) void {
|
|
||||||
std.debug.panic("TODO: Implement Bus#writeByte", .{});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -0,0 +1,242 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
|
||||||
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
|
const Bios = @import("bus/Bios.zig");
|
||||||
|
const Ewram = @import("bus/Ewram.zig");
|
||||||
|
const GamePak = @import("bus/GamePak.zig");
|
||||||
|
const Io = @import("bus/io.zig").Io;
|
||||||
|
const Iwram = @import("bus/Iwram.zig");
|
||||||
|
const Ppu = @import("ppu.zig").Ppu;
|
||||||
|
const Apu = @import("apu.zig").Apu;
|
||||||
|
const DmaTuple = @import("bus/dma.zig").DmaTuple;
|
||||||
|
const TimerTuple = @import("bus/timer.zig").TimerTuple;
|
||||||
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
|
const FilePaths = @import("../util.zig").FilePaths;
|
||||||
|
|
||||||
|
const io = @import("bus/io.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Bus);
|
||||||
|
|
||||||
|
const createDmaTuple = @import("bus/dma.zig").create;
|
||||||
|
const createTimerTuple = @import("bus/timer.zig").create;
|
||||||
|
const rotr = @import("../util.zig").rotr;
|
||||||
|
|
||||||
|
const timings: [2][0x10]u8 = [_][0x10]u8{
|
||||||
|
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
||||||
|
[_]u8{ 1, 1, 3, 1, 1, 1, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5 }, // 8-bit & 16-bit
|
||||||
|
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 8, 8, 8, 8, 8, 8, 8, 8 }, // 32-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fetch_timings: [2][0x10]u8 = [_][0x10]u8{
|
||||||
|
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
||||||
|
[_]u8{ 1, 1, 3, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 5, 5 }, // 8-bit & 16-bit
|
||||||
|
[_]u8{ 1, 1, 6, 1, 1, 2, 2, 1, 4, 4, 4, 4, 4, 4, 8, 8 }, // 32-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pak: GamePak,
|
||||||
|
bios: Bios,
|
||||||
|
ppu: Ppu,
|
||||||
|
apu: Apu,
|
||||||
|
dma: DmaTuple,
|
||||||
|
tim: TimerTuple,
|
||||||
|
iwram: Iwram,
|
||||||
|
ewram: Ewram,
|
||||||
|
io: Io,
|
||||||
|
|
||||||
|
cpu: ?*Arm7tdmi,
|
||||||
|
sched: *Scheduler,
|
||||||
|
|
||||||
|
pub fn init(self: *Self, allocator: Allocator, sched: *Scheduler, cpu: *Arm7tdmi, paths: FilePaths) !void {
|
||||||
|
self.* = .{
|
||||||
|
.pak = try GamePak.init(allocator, cpu, paths.rom, paths.save),
|
||||||
|
.bios = try Bios.init(allocator, paths.bios),
|
||||||
|
.ppu = try Ppu.init(allocator, sched),
|
||||||
|
.apu = Apu.init(sched),
|
||||||
|
.iwram = try Iwram.init(allocator),
|
||||||
|
.ewram = try Ewram.init(allocator),
|
||||||
|
.dma = createDmaTuple(),
|
||||||
|
.tim = createTimerTuple(sched),
|
||||||
|
.io = Io.init(),
|
||||||
|
.cpu = cpu,
|
||||||
|
.sched = sched,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.iwram.deinit();
|
||||||
|
self.ewram.deinit();
|
||||||
|
self.pak.deinit();
|
||||||
|
self.bios.deinit();
|
||||||
|
self.ppu.deinit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
const page = @truncate(u8, address >> 24);
|
||||||
|
const aligned_addr = forceAlign(T, address);
|
||||||
|
|
||||||
|
return switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => blk: {
|
||||||
|
if (address < Bios.size)
|
||||||
|
break :blk self.bios.dbgRead(T, self.cpu.?.r[15], aligned_addr);
|
||||||
|
|
||||||
|
break :blk self.readOpenBus(T, address);
|
||||||
|
},
|
||||||
|
0x02 => self.ewram.read(T, aligned_addr),
|
||||||
|
0x03 => self.iwram.read(T, aligned_addr),
|
||||||
|
0x04 => self.readIo(T, address),
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => self.ppu.palette.read(T, aligned_addr),
|
||||||
|
0x06 => self.ppu.vram.read(T, aligned_addr),
|
||||||
|
0x07 => self.ppu.oam.read(T, aligned_addr),
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => self.pak.dbgRead(T, aligned_addr),
|
||||||
|
0x0E...0x0F => blk: {
|
||||||
|
const value = self.pak.backup.read(address);
|
||||||
|
|
||||||
|
const multiplier = switch (T) {
|
||||||
|
u32 => 0x01010101,
|
||||||
|
u16 => 0x0101,
|
||||||
|
u8 => 1,
|
||||||
|
else => @compileError("Backup: Unsupported read width"),
|
||||||
|
};
|
||||||
|
|
||||||
|
break :blk @as(T, value) * multiplier;
|
||||||
|
},
|
||||||
|
else => self.readOpenBus(T, address),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readIo(self: *const Self, comptime T: type, unaligned_address: u32) T {
|
||||||
|
const maybe_value = io.read(self, T, forceAlign(T, unaligned_address));
|
||||||
|
return if (maybe_value) |value| value else self.readOpenBus(T, unaligned_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readOpenBus(self: *const Self, comptime T: type, address: u32) T {
|
||||||
|
const r15 = self.cpu.?.r[15];
|
||||||
|
|
||||||
|
const word = blk: {
|
||||||
|
// If u32 Open Bus, read recently fetched opcode (PC + 8)
|
||||||
|
if (!self.cpu.?.cpsr.t.read()) break :blk self.dbgRead(u32, r15 + 4);
|
||||||
|
const page = @truncate(u8, r15 >> 24);
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
// EWRAM, PALRAM, VRAM, and Game ROM (16-bit)
|
||||||
|
0x02, 0x05, 0x06, 0x08...0x0D => {
|
||||||
|
// (PC + 4)
|
||||||
|
const halfword = self.dbgRead(u16, r15 + 2);
|
||||||
|
|
||||||
|
break :blk @as(u32, halfword) << 16 | halfword;
|
||||||
|
},
|
||||||
|
// BIOS or OAM (32-bit)
|
||||||
|
0x00, 0x07 => {
|
||||||
|
// Aligned: (PC + 6) | (PC + 4)
|
||||||
|
// Unaligned: (PC + 4) | (PC + 2)
|
||||||
|
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
||||||
|
|
||||||
|
break :blk @as(u32, self.dbgRead(u16, r15 + 2 + offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
||||||
|
},
|
||||||
|
// IWRAM (16-bit but special)
|
||||||
|
0x03 => {
|
||||||
|
// Aligned: (PC + 2) | (PC + 4)
|
||||||
|
// Unaligned: (PC + 4) | (PC + 2)
|
||||||
|
const offset: u32 = if (address & 3 == 0b00) 2 else 0;
|
||||||
|
|
||||||
|
break :blk @as(u32, self.dbgRead(u16, r15 + 2 - offset)) << 16 | self.dbgRead(u16, r15 + offset);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return @truncate(T, rotr(u32, word, 8 * (address & 3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *Self, comptime T: type, address: u32) T {
|
||||||
|
const page = @truncate(u8, address >> 24);
|
||||||
|
const aligned_addr = forceAlign(T, address);
|
||||||
|
|
||||||
|
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
||||||
|
|
||||||
|
return switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => blk: {
|
||||||
|
if (address < Bios.size)
|
||||||
|
break :blk self.bios.read(T, self.cpu.?.r[15], aligned_addr);
|
||||||
|
|
||||||
|
break :blk self.readOpenBus(T, address);
|
||||||
|
},
|
||||||
|
0x02 => self.ewram.read(T, aligned_addr),
|
||||||
|
0x03 => self.iwram.read(T, aligned_addr),
|
||||||
|
0x04 => self.readIo(T, address),
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => self.ppu.palette.read(T, aligned_addr),
|
||||||
|
0x06 => self.ppu.vram.read(T, aligned_addr),
|
||||||
|
0x07 => self.ppu.oam.read(T, aligned_addr),
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => self.pak.read(T, aligned_addr),
|
||||||
|
0x0E...0x0F => blk: {
|
||||||
|
const value = self.pak.backup.read(address);
|
||||||
|
|
||||||
|
const multiplier = switch (T) {
|
||||||
|
u32 => 0x01010101,
|
||||||
|
u16 => 0x0101,
|
||||||
|
u8 => 1,
|
||||||
|
else => @compileError("Backup: Unsupported read width"),
|
||||||
|
};
|
||||||
|
|
||||||
|
break :blk @as(T, value) * multiplier;
|
||||||
|
},
|
||||||
|
else => self.readOpenBus(T, address),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, address: u32, value: T) void {
|
||||||
|
const page = @truncate(u8, address >> 24);
|
||||||
|
const aligned_addr = forceAlign(T, address);
|
||||||
|
|
||||||
|
self.sched.tick += timings[@boolToInt(T == u32)][@truncate(u4, page)];
|
||||||
|
|
||||||
|
switch (page) {
|
||||||
|
// General Internal Memory
|
||||||
|
0x00 => self.bios.write(T, aligned_addr, value),
|
||||||
|
0x02 => self.ewram.write(T, aligned_addr, value),
|
||||||
|
0x03 => self.iwram.write(T, aligned_addr, value),
|
||||||
|
0x04 => io.write(self, T, aligned_addr, value),
|
||||||
|
|
||||||
|
// Internal Display Memory
|
||||||
|
0x05 => self.ppu.palette.write(T, aligned_addr, value),
|
||||||
|
0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, aligned_addr, value),
|
||||||
|
0x07 => self.ppu.oam.write(T, aligned_addr, value),
|
||||||
|
|
||||||
|
// External Memory (Game Pak)
|
||||||
|
0x08...0x0D => self.pak.write(T, self.dma[3].word_count, aligned_addr, value),
|
||||||
|
0x0E...0x0F => {
|
||||||
|
const rotate_by = switch (T) {
|
||||||
|
u32 => address & 3,
|
||||||
|
u16 => address & 1,
|
||||||
|
u8 => 0,
|
||||||
|
else => @compileError("Backup: Unsupported write width"),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pak.backup.write(address, @truncate(u8, rotr(T, value, 8 * rotate_by)));
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forceAlign(comptime T: type, address: u32) u32 {
|
||||||
|
return switch (T) {
|
||||||
|
u32 => address & 0xFFFF_FFFC,
|
||||||
|
u16 => address & 0xFFFF_FFFE,
|
||||||
|
u8 => address,
|
||||||
|
else => @compileError("Bus: Invalid read/write type"),
|
||||||
|
};
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,64 @@
|
||||||
|
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,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
addr_latch: u32,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self {
|
||||||
|
const buf: ?[]u8 = if (maybe_path) |path| blk: {
|
||||||
|
const file = try std.fs.cwd().openFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
break :blk try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
.addr_latch = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
if (self.buf) |buf| self.allocator.free(buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *Self, comptime T: type, r15: u32, addr: u32) T {
|
||||||
|
if (r15 < Self.size) {
|
||||||
|
self.addr_latch = addr;
|
||||||
|
return self.uncheckedRead(T, addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Rejected read since r15=0x{X:0>8}", .{r15});
|
||||||
|
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self, comptime T: type, r15: u32, addr: u32) T {
|
||||||
|
if (r15 < Self.size) return self.uncheckedRead(T, addr);
|
||||||
|
return @truncate(T, self.uncheckedRead(T, self.addr_latch + 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uncheckedRead(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 });
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const ewram_size = 0x40000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, ewram_size);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,242 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Bit = @import("bitfield").Bit;
|
||||||
|
const Bitfield = @import("bitfield").Bitfield;
|
||||||
|
const DateTime = @import("datetime").datetime.Datetime;
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
const Backup = @import("backup.zig").Backup;
|
||||||
|
const Gpio = @import("gpio.zig").Gpio;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const force_rtc = @import("../emu.zig").force_rtc;
|
||||||
|
const log = std.log.scoped(.GamePak);
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
title: [12]u8,
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
backup: Backup,
|
||||||
|
gpio: *Gpio,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, 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(allocator, try file.getEndPos());
|
||||||
|
const title = file_buf[0xA0..0xAC].*;
|
||||||
|
const kind = Backup.guessKind(file_buf);
|
||||||
|
const device = if (force_rtc) .Rtc else guessDevice(file_buf);
|
||||||
|
|
||||||
|
logHeader(file_buf, &title);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.buf = file_buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
.title = title,
|
||||||
|
.backup = try Backup.init(allocator, kind, title, save_path),
|
||||||
|
.gpio = try Gpio.init(allocator, cpu, device),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches the ROM to see if it can determine whether the ROM it's searching uses
|
||||||
|
/// any GPIO device, like a RTC for example.
|
||||||
|
fn guessDevice(buf: []const u8) Gpio.Device.Kind {
|
||||||
|
// Try to Guess if ROM uses RTC
|
||||||
|
const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while ((i + needle.len) < buf.len) : (i += 1) {
|
||||||
|
if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Detect other GPIO devices
|
||||||
|
|
||||||
|
return .None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logHeader(buf: []const u8, title: *const [12]u8) void {
|
||||||
|
const code = buf[0xAC..0xB0];
|
||||||
|
const maker = buf[0xB0..0xB2];
|
||||||
|
const version = 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 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.backup.deinit();
|
||||||
|
self.gpio.deinit(self.allocator);
|
||||||
|
self.allocator.destroy(self.gpio);
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)),
|
||||||
|
u8 => self.get(addr),
|
||||||
|
else => @compileError("GamePak: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(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.dbgRead();
|
||||||
|
} 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.dbgRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
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 => 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:0>2} 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);
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const iwram_size = 0x8000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, iwram_size);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,566 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Backup);
|
||||||
|
|
||||||
|
const escape = @import("../../util.zig").escape;
|
||||||
|
const span = @import("../../util.zig").span;
|
||||||
|
|
||||||
|
const backup_kinds = [6]Needle{
|
||||||
|
.{ .str = "EEPROM_V", .kind = .Eeprom },
|
||||||
|
.{ .str = "SRAM_V", .kind = .Sram },
|
||||||
|
.{ .str = "SRAM_F_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,
|
||||||
|
allocator: Allocator,
|
||||||
|
kind: Kind,
|
||||||
|
|
||||||
|
title: [12]u8,
|
||||||
|
save_path: ?[]const u8,
|
||||||
|
|
||||||
|
flash: Flash,
|
||||||
|
eeprom: Eeprom,
|
||||||
|
|
||||||
|
const Kind = enum {
|
||||||
|
Eeprom,
|
||||||
|
Sram,
|
||||||
|
Flash,
|
||||||
|
Flash1M,
|
||||||
|
None,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, kind: Kind, 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 allocator.alloc(u8, buf_size);
|
||||||
|
std.mem.set(u8, buf, 0xFF);
|
||||||
|
|
||||||
|
var backup = Self{
|
||||||
|
.buf = buf,
|
||||||
|
.allocator = allocator,
|
||||||
|
.kind = kind,
|
||||||
|
.title = title,
|
||||||
|
.save_path = path,
|
||||||
|
.flash = Flash.init(),
|
||||||
|
.eeprom = Eeprom.init(allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backup.save_path) |p| backup.loadSaveFromDisk(allocator, p) catch |e| log.err("Failed to load save: {}", .{e});
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guessKind(rom: []const u8) Kind {
|
||||||
|
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 .None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
if (self.save_path) |path| self.writeSaveToDisk(self.allocator, path) catch |e| log.err("Failed to write save: {}", .{e});
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loadSaveFromDisk(self: *Self, allocator: Allocator, path: []const u8) !void {
|
||||||
|
const file_path = try self.getSaveFilePath(allocator, path);
|
||||||
|
defer allocator.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(allocator, try file.getEndPos());
|
||||||
|
defer allocator.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 allocator.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, allocator: Allocator, path: []const u8) ![]const u8 {
|
||||||
|
const filename = try self.getSaveFilename(allocator);
|
||||||
|
defer allocator.free(filename);
|
||||||
|
|
||||||
|
return try std.fs.path.join(allocator, &[_][]const u8{ path, filename });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getSaveFilename(self: *const Self, allocator: Allocator) ![]const u8 {
|
||||||
|
const title_str = span(&escape(self.title));
|
||||||
|
const name = if (title_str.len != 0) title_str else "untitled";
|
||||||
|
|
||||||
|
return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeSaveToDisk(self: Self, allocator: Allocator, path: []const u8) !void {
|
||||||
|
const file_path = try self.getSaveFilePath(allocator, path);
|
||||||
|
defer allocator.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 Needle = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
str: []const u8,
|
||||||
|
kind: Backup.Kind,
|
||||||
|
|
||||||
|
fn init(str: []const u8, kind: Backup.Kind) Self {
|
||||||
|
return .{
|
||||||
|
.str = str,
|
||||||
|
.kind = kind,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SaveError = error{
|
||||||
|
UnsupportedBackupKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Flash = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
state: State,
|
||||||
|
|
||||||
|
id_mode: bool,
|
||||||
|
set_bank: bool,
|
||||||
|
prep_erase: bool,
|
||||||
|
prep_write: bool,
|
||||||
|
|
||||||
|
bank: u1,
|
||||||
|
|
||||||
|
const State = enum {
|
||||||
|
Ready,
|
||||||
|
Set,
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 Eeprom = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
addr: u14,
|
||||||
|
|
||||||
|
kind: Kind,
|
||||||
|
state: State,
|
||||||
|
writer: Writer,
|
||||||
|
reader: Reader,
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
const Kind = enum {
|
||||||
|
Unknown,
|
||||||
|
Small, // 512B
|
||||||
|
Large, // 8KB
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = enum {
|
||||||
|
Ready,
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
WriteTransfer,
|
||||||
|
RequestEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn init(allocator: Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.kind = .Unknown,
|
||||||
|
.state = .Ready,
|
||||||
|
.writer = Writer.init(),
|
||||||
|
.reader = Reader.init(),
|
||||||
|
.addr = 0,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *Self) u1 {
|
||||||
|
return self.reader.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dbgRead(self: *const Self) u1 {
|
||||||
|
return self.reader.dbgRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.allocator.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dbgRead(self: *const 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,301 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
const DmaControl = @import("io.zig").DmaControl;
|
||||||
|
const Bus = @import("../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
|
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 => util.io.read.undef(T, 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 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => util.io.read.undef(T, 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 => util.io.write.undef(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 => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u8 => util.io.write.undef(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 pollBlanking(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 onBlanking(bus: *Bus, comptime kind: DmaKind) void {
|
||||||
|
bus.dma[0].pollBlanking(kind);
|
||||||
|
bus.dma[1].pollBlanking(kind);
|
||||||
|
bus.dma[2].pollBlanking(kind);
|
||||||
|
bus.dma[3].pollBlanking(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);
|
||||||
|
}
|
|
@ -0,0 +1,463 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Bit = @import("bitfield").Bit;
|
||||||
|
const Bitfield = @import("bitfield").Bitfield;
|
||||||
|
const DateTime = @import("datetime").datetime.Datetime;
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// GPIO Register Implementation
|
||||||
|
pub const Gpio = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const log = std.log.scoped(.Gpio);
|
||||||
|
|
||||||
|
data: u4,
|
||||||
|
direction: u4,
|
||||||
|
cnt: u1,
|
||||||
|
|
||||||
|
device: Device,
|
||||||
|
|
||||||
|
const Register = enum { Data, Direction, Control };
|
||||||
|
|
||||||
|
pub const Device = struct {
|
||||||
|
ptr: ?*anyopaque,
|
||||||
|
kind: Kind, // TODO: Make comptime known?
|
||||||
|
|
||||||
|
pub const Kind = enum { Rtc, None };
|
||||||
|
|
||||||
|
fn step(self: *Device, value: u4) u4 {
|
||||||
|
return switch (self.kind) {
|
||||||
|
.Rtc => blk: {
|
||||||
|
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
|
||||||
|
break :blk clock.step(Clock.Data{ .raw = value });
|
||||||
|
},
|
||||||
|
.None => value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(kind: Kind, ptr: ?*anyopaque) Device {
|
||||||
|
return .{ .kind = kind, .ptr = ptr };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime reg: Register, value: if (reg == .Control) u1 else u4) void {
|
||||||
|
switch (reg) {
|
||||||
|
.Data => {
|
||||||
|
const masked_value = value & self.direction;
|
||||||
|
|
||||||
|
// The value which is actually stored in the GPIO register
|
||||||
|
// might be modified by the device implementing the GPIO interface e.g. RTC reads
|
||||||
|
self.data = self.device.step(masked_value);
|
||||||
|
},
|
||||||
|
.Direction => self.direction = value,
|
||||||
|
.Control => self.cnt = value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, kind: Device.Kind) !*Self {
|
||||||
|
log.info("Device: {}", .{kind});
|
||||||
|
|
||||||
|
const self = try allocator.create(Self);
|
||||||
|
self.* = .{
|
||||||
|
.data = 0b0000,
|
||||||
|
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
||||||
|
.cnt = 0b0,
|
||||||
|
|
||||||
|
.device = switch (kind) {
|
||||||
|
.Rtc => blk: {
|
||||||
|
const clock = try allocator.create(Clock);
|
||||||
|
clock.init(cpu, self);
|
||||||
|
|
||||||
|
break :blk Device{ .kind = kind, .ptr = clock };
|
||||||
|
},
|
||||||
|
.None => Device{ .kind = kind, .ptr = null },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||||
|
switch (self.device.kind) {
|
||||||
|
.Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
|
||||||
|
.None => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// GBA Real Time Clock
|
||||||
|
pub const Clock = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const log = std.log.scoped(.Rtc);
|
||||||
|
|
||||||
|
writer: Writer,
|
||||||
|
reader: Reader,
|
||||||
|
state: State,
|
||||||
|
cnt: Control,
|
||||||
|
|
||||||
|
year: u8,
|
||||||
|
month: u5,
|
||||||
|
day: u6,
|
||||||
|
weekday: u3,
|
||||||
|
hour: u6,
|
||||||
|
minute: u7,
|
||||||
|
second: u7,
|
||||||
|
|
||||||
|
cpu: *Arm7tdmi,
|
||||||
|
gpio: *const Gpio,
|
||||||
|
|
||||||
|
const Register = enum {
|
||||||
|
Control,
|
||||||
|
DateTime,
|
||||||
|
Time,
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = union(enum) {
|
||||||
|
Idle,
|
||||||
|
Command,
|
||||||
|
Write: Register,
|
||||||
|
Read: Register,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Reader = struct {
|
||||||
|
i: u4,
|
||||||
|
count: u8,
|
||||||
|
|
||||||
|
/// Reads a bit from RTC registers. Which bit it reads is dependent on
|
||||||
|
///
|
||||||
|
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
||||||
|
/// 2. A `count`, which keeps track of which byte is currently being read
|
||||||
|
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
|
||||||
|
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
|
||||||
|
const idx = @intCast(u3, self.i);
|
||||||
|
defer self.i += 1;
|
||||||
|
|
||||||
|
// FIXME: What do I do about the unused bits?
|
||||||
|
return switch (register) {
|
||||||
|
.Control => @truncate(u1, switch (self.count) {
|
||||||
|
0 => clock.cnt.raw >> idx,
|
||||||
|
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||||
|
}),
|
||||||
|
.DateTime => @truncate(u1, switch (self.count) {
|
||||||
|
// Date
|
||||||
|
0 => clock.year >> idx,
|
||||||
|
1 => @as(u8, clock.month) >> idx,
|
||||||
|
2 => @as(u8, clock.day) >> idx,
|
||||||
|
3 => @as(u8, clock.weekday) >> idx,
|
||||||
|
|
||||||
|
// Time
|
||||||
|
4 => @as(u8, clock.hour) >> idx,
|
||||||
|
5 => @as(u8, clock.minute) >> idx,
|
||||||
|
6 => @as(u8, clock.second) >> idx,
|
||||||
|
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
|
||||||
|
}),
|
||||||
|
.Time => @truncate(u1, switch (self.count) {
|
||||||
|
0 => @as(u8, clock.hour) >> idx,
|
||||||
|
1 => @as(u8, clock.minute) >> idx,
|
||||||
|
2 => @as(u8, clock.second) >> idx,
|
||||||
|
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 3 bytes)", .{ self.count, register }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is true when a Reader has read a u8's worth of bits
|
||||||
|
fn finished(self: *const Reader) bool {
|
||||||
|
return self.i >= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the index used to shift bits out of RTC registers
|
||||||
|
/// and `count`, which is used to keep track of which byte we're reading
|
||||||
|
/// is incremeneted
|
||||||
|
fn lap(self: *Reader) void {
|
||||||
|
self.i = 0;
|
||||||
|
self.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the state of a `Reader` in preparation for a future
|
||||||
|
/// read command
|
||||||
|
fn reset(self: *Reader) void {
|
||||||
|
self.i = 0;
|
||||||
|
self.count = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Writer = struct {
|
||||||
|
buf: u8,
|
||||||
|
i: u4,
|
||||||
|
|
||||||
|
/// The Number of bytes written since last reset
|
||||||
|
count: u8,
|
||||||
|
|
||||||
|
/// Append a bit to the internal bit buffer (aka an integer)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the contents of the internal buffer and writes it to an RTC register
|
||||||
|
/// Where it writes to is dependent on:
|
||||||
|
///
|
||||||
|
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
||||||
|
/// 2. A `count`, which keeps track of which byte is currently being read
|
||||||
|
fn write(self: *const Writer, clock: *Clock, register: Register) void {
|
||||||
|
// FIXME: What do do about unused bits?
|
||||||
|
switch (register) {
|
||||||
|
.Control => switch (self.count) {
|
||||||
|
0 => clock.cnt.raw = (clock.cnt.raw & 0x80) | (self.buf & 0x7F), // Bit 7 read-only
|
||||||
|
else => std.debug.panic("Tried to write to byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||||
|
},
|
||||||
|
.DateTime, .Time => log.debug("Ignoring {} write", .{register}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is true when 8 bits have been shifted into the internal buffer
|
||||||
|
fn finished(self: *const Writer) bool {
|
||||||
|
return self.i >= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the internal buffer
|
||||||
|
/// resets the index used to shift bits into the internal buffer
|
||||||
|
/// increments `count` (which keeps track of byte offsets) by one
|
||||||
|
fn lap(self: *Writer) void {
|
||||||
|
self.buf = 0;
|
||||||
|
self.i = 0;
|
||||||
|
self.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets `Writer` to a clean state in preparation for a future write command
|
||||||
|
fn reset(self: *Writer) void {
|
||||||
|
self.buf = 0;
|
||||||
|
self.i = 0;
|
||||||
|
self.count = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Data = extern union {
|
||||||
|
sck: Bit(u8, 0),
|
||||||
|
sio: Bit(u8, 1),
|
||||||
|
cs: Bit(u8, 2),
|
||||||
|
raw: u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
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: *Self, cpu: *Arm7tdmi, gpio: *const Gpio) void {
|
||||||
|
ptr.* = .{
|
||||||
|
.writer = .{ .buf = 0, .i = 0, .count = 0 },
|
||||||
|
.reader = .{ .i = 0, .count = 0 },
|
||||||
|
.state = .Idle,
|
||||||
|
.cnt = .{ .raw = 0 },
|
||||||
|
.year = 0x01,
|
||||||
|
.month = 0x6,
|
||||||
|
.day = 0x13,
|
||||||
|
.weekday = 0x3,
|
||||||
|
.hour = 0x23,
|
||||||
|
.minute = 0x59,
|
||||||
|
.second = 0x59,
|
||||||
|
.cpu = cpu,
|
||||||
|
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
|
||||||
|
};
|
||||||
|
|
||||||
|
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updateTime(self: *Self, late: u64) void {
|
||||||
|
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
|
||||||
|
|
||||||
|
const now = DateTime.now();
|
||||||
|
self.year = bcd(u8, @intCast(u8, now.date.year - 2000));
|
||||||
|
self.month = bcd(u5, now.date.month);
|
||||||
|
self.day = bcd(u6, now.date.day);
|
||||||
|
self.weekday = bcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
|
||||||
|
self.hour = bcd(u6, now.time.hour);
|
||||||
|
self.minute = bcd(u7, now.time.minute);
|
||||||
|
self.second = bcd(u7, now.time.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(self: *Self, value: Data) u4 {
|
||||||
|
const cache: Data = .{ .raw = self.gpio.data };
|
||||||
|
|
||||||
|
return switch (self.state) {
|
||||||
|
.Idle => blk: {
|
||||||
|
// 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.debug("Entering Command Mode", .{});
|
||||||
|
self.state = .Command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @truncate(u4, value.raw);
|
||||||
|
},
|
||||||
|
.Command => blk: {
|
||||||
|
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||||
|
|
||||||
|
// If SCK rises, sample SIO
|
||||||
|
if (!cache.sck.read() and value.sck.read()) {
|
||||||
|
self.writer.push(@boolToInt(value.sio.read()));
|
||||||
|
|
||||||
|
if (self.writer.finished()) {
|
||||||
|
self.state = self.processCommand(self.writer.buf);
|
||||||
|
self.writer.reset();
|
||||||
|
|
||||||
|
log.debug("Switching to {}", .{self.state});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @truncate(u4, value.raw);
|
||||||
|
},
|
||||||
|
.Write => |register| blk: {
|
||||||
|
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||||
|
|
||||||
|
// If SCK rises, sample SIO
|
||||||
|
if (!cache.sck.read() and value.sck.read()) {
|
||||||
|
self.writer.push(@boolToInt(value.sio.read()));
|
||||||
|
|
||||||
|
const register_width: u32 = switch (register) {
|
||||||
|
.Control => 1,
|
||||||
|
.DateTime => 7,
|
||||||
|
.Time => 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.writer.finished()) {
|
||||||
|
self.writer.write(self, register); // write inner buffer to RTC register
|
||||||
|
self.writer.lap();
|
||||||
|
|
||||||
|
if (self.writer.count == register_width) {
|
||||||
|
self.writer.reset();
|
||||||
|
self.state = .Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @truncate(u4, value.raw);
|
||||||
|
},
|
||||||
|
.Read => |register| blk: {
|
||||||
|
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||||
|
var ret = value;
|
||||||
|
|
||||||
|
// if SCK rises, sample SIO
|
||||||
|
if (!cache.sck.read() and value.sck.read()) {
|
||||||
|
ret.sio.write(self.reader.read(self, register) == 0b1);
|
||||||
|
|
||||||
|
const register_width: u32 = switch (register) {
|
||||||
|
.Control => 1,
|
||||||
|
.DateTime => 7,
|
||||||
|
.Time => 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.reader.finished()) {
|
||||||
|
self.reader.lap();
|
||||||
|
|
||||||
|
if (self.reader.count == register_width) {
|
||||||
|
self.reader.reset();
|
||||||
|
self.state = .Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk @truncate(u4, ret.raw);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(self: *Self) void {
|
||||||
|
// mGBA and NBA only zero the control register. We will do the same
|
||||||
|
log.debug("Reset (control register was zeroed)", .{});
|
||||||
|
|
||||||
|
self.cnt.raw = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn irq(self: *Self) void {
|
||||||
|
// TODO: Confirm that this is the right behaviour
|
||||||
|
log.debug("Force GamePak IRQ", .{});
|
||||||
|
|
||||||
|
self.cpu.bus.io.irq.game_pak.set();
|
||||||
|
self.cpu.handleInterrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn processCommand(self: *Self, raw_command: u8) State {
|
||||||
|
const command = blk: {
|
||||||
|
// If High Nybble is 0x6, no need to switch the endianness
|
||||||
|
if (raw_command >> 4 & 0xF == 0x6) break :blk raw_command;
|
||||||
|
|
||||||
|
// Turns out reversing the order of bits isn't trivial at all
|
||||||
|
// https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte
|
||||||
|
var ret = raw_command;
|
||||||
|
ret = (ret & 0xF0) >> 4 | (ret & 0x0F) << 4;
|
||||||
|
ret = (ret & 0xCC) >> 2 | (ret & 0x33) << 2;
|
||||||
|
ret = (ret & 0xAA) >> 1 | (ret & 0x55) << 1;
|
||||||
|
|
||||||
|
break :blk ret;
|
||||||
|
};
|
||||||
|
log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
|
||||||
|
|
||||||
|
const is_write = command & 1 == 0;
|
||||||
|
const rtc_register = @truncate(u3, command >> 1 & 0x7);
|
||||||
|
|
||||||
|
if (is_write) {
|
||||||
|
return switch (rtc_register) {
|
||||||
|
0 => blk: {
|
||||||
|
self.reset();
|
||||||
|
break :blk .Idle;
|
||||||
|
},
|
||||||
|
1 => .{ .Write = .Control },
|
||||||
|
2 => .{ .Write = .DateTime },
|
||||||
|
3 => .{ .Write = .Time },
|
||||||
|
6 => blk: {
|
||||||
|
self.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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn bcd(comptime T: type, value: u8) T {
|
||||||
|
var input = value;
|
||||||
|
var ret: u8 = 0;
|
||||||
|
var shift: u3 = 0;
|
||||||
|
|
||||||
|
while (input > 0) {
|
||||||
|
ret |= (input % 10) << (shift << 2);
|
||||||
|
shift += 1;
|
||||||
|
input /= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
return @truncate(T, ret);
|
||||||
|
}
|
|
@ -0,0 +1,688 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const timer = @import("timer.zig");
|
||||||
|
const dma = @import("dma.zig");
|
||||||
|
const apu = @import("../apu.zig");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
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 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 => util.io.read.todo(log, "Read {} from SIOCNT and SIOMLT_SEND", .{T}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => util.io.read.todo(log, "Read {} from KEYINPUT", .{T}),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0150 => util.io.read.todo(log, "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 => util.io.read.undef(T, 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 => util.io.read.todo(log, "Read {} from MOSAIC", .{T}),
|
||||||
|
0x0400_0050 => bus.ppu.bldcnt.raw,
|
||||||
|
0x0400_0052 => bus.ppu.bldalpha.raw,
|
||||||
|
0x0400_0054 => bus.ppu.bldy.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 => util.io.read.todo(log, "Read {} from SIOCNT", .{T}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => bus.io.keyinput.raw,
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0134 => util.io.read.todo(log, "Read {} from RCNT", .{T}),
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200 => bus.io.ie.raw,
|
||||||
|
0x0400_0202 => bus.io.irq.raw,
|
||||||
|
0x0400_0204 => util.io.read.todo(log, "Read {} from WAITCNT", .{T}),
|
||||||
|
0x0400_0208 => @boolToInt(bus.io.ime),
|
||||||
|
else => util.io.read.undef(T, 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 => util.io.read.todo(log, "Read {} from SIOCNT_L", .{T}),
|
||||||
|
|
||||||
|
// Keypad Input
|
||||||
|
0x0400_0130 => util.io.read.todo(log, "read {} from KEYINPUT_L", .{T}),
|
||||||
|
|
||||||
|
// Serial Communication 2
|
||||||
|
0x0400_0135 => util.io.read.todo(log, "Read {} from RCNT_H", .{T}),
|
||||||
|
|
||||||
|
// Interrupts
|
||||||
|
0x0400_0200 => @truncate(T, bus.io.ie.raw),
|
||||||
|
0x0400_0300 => @enumToInt(bus.io.postflg),
|
||||||
|
else => util.io.read.undef(T, 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 => util.io.write.undef(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 => util.io.write.undef(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_0040 => bus.ppu.win.h[0].set(.Lo, value),
|
||||||
|
0x0400_0041 => bus.ppu.win.h[0].set(.Hi, value),
|
||||||
|
0x0400_0042 => bus.ppu.win.h[1].set(.Lo, value),
|
||||||
|
0x0400_0043 => bus.ppu.win.h[1].set(.Hi, value),
|
||||||
|
0x0400_0044 => bus.ppu.win.v[0].set(.Lo, value),
|
||||||
|
0x0400_0045 => bus.ppu.win.v[0].set(.Hi, value),
|
||||||
|
0x0400_0046 => bus.ppu.win.v[1].set(.Lo, value),
|
||||||
|
0x0400_0047 => bus.ppu.win.v[1].set(.Hi, value),
|
||||||
|
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 => util.io.write.undef(log, "Tried to write 0x{X:0>2}{} to 0x{X:0>8}", .{ value, T, address }),
|
||||||
|
},
|
||||||
|
else => @compileError("I/O: Unsupported write width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const u8WriteKind = enum { Hi, Lo };
|
||||||
|
|
||||||
|
/// Write-only
|
||||||
|
pub const WinH = extern union {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
x2: Bitfield(u16, 0, 8),
|
||||||
|
x1: Bitfield(u16, 8, 8),
|
||||||
|
raw: u16,
|
||||||
|
|
||||||
|
pub fn set(self: *Self, comptime K: u8WriteKind, value: u8) void {
|
||||||
|
self.raw = switch (K) {
|
||||||
|
.Hi => (@as(u16, value) << 8) | self.raw & 0xFF,
|
||||||
|
.Lo => (self.raw & 0xFF00) | value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write-only
|
||||||
|
pub const WinV = extern union {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
y2: Bitfield(u16, 0, 8),
|
||||||
|
y1: Bitfield(u16, 8, 8),
|
||||||
|
raw: u16,
|
||||||
|
|
||||||
|
pub fn set(self: *Self, comptime K: u8WriteKind, value: u8) void {
|
||||||
|
self.raw = switch (K) {
|
||||||
|
.Hi => (@as(u16, value) << 8) | self.raw & 0xFF,
|
||||||
|
.Lo => (self.raw & 0xFF00) | value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WinIn = extern union {
|
||||||
|
w0_bg: Bitfield(u16, 0, 4),
|
||||||
|
w0_obj: Bit(u16, 4),
|
||||||
|
w0_bld: Bit(u16, 5),
|
||||||
|
w1_bg: Bitfield(u16, 8, 4),
|
||||||
|
w1_obj: Bit(u16, 12),
|
||||||
|
w1_bld: Bit(u16, 13),
|
||||||
|
raw: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WinOut = extern union {
|
||||||
|
out_bg: Bitfield(u16, 0, 4),
|
||||||
|
out_obj: Bit(u16, 4),
|
||||||
|
out_bld: Bit(u16, 5),
|
||||||
|
obj_bg: Bitfield(u16, 8, 4),
|
||||||
|
obj_obj: Bit(u16, 12),
|
||||||
|
obj_bld: 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,
|
||||||
|
};
|
|
@ -0,0 +1,200 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../util.zig");
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 => util.io.read.undef(T, 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 => util.io.read.undef(T, log, "Tried to perform a {} read to 0x{X:0>8}", .{ T, addr }),
|
||||||
|
},
|
||||||
|
u8 => util.io.read.undef(T, 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 => util.io.write.undef(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 => util.io.write.undef(log, "Tried to write 0x{X:0>4}{} to 0x{X:0>8}", .{ value, T, addr }),
|
||||||
|
},
|
||||||
|
u8 => util.io.write.undef(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,668 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../util.zig");
|
||||||
|
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Bit = @import("bitfield").Bit;
|
||||||
|
const Bitfield = @import("bitfield").Bitfield;
|
||||||
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
|
const FilePaths = @import("../util.zig").FilePaths;
|
||||||
|
const Logger = @import("../util.zig").Logger;
|
||||||
|
|
||||||
|
const File = std.fs.File;
|
||||||
|
|
||||||
|
// ARM Instructions
|
||||||
|
pub const arm = struct {
|
||||||
|
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u32) void;
|
||||||
|
const lut: [0x1000]InstrFn = populate();
|
||||||
|
|
||||||
|
const processing = @import("cpu/arm/data_processing.zig").dataProcessing;
|
||||||
|
const psrTransfer = @import("cpu/arm/psr_transfer.zig").psrTransfer;
|
||||||
|
const transfer = @import("cpu/arm/single_data_transfer.zig").singleDataTransfer;
|
||||||
|
const halfSignedTransfer = @import("cpu/arm/half_signed_data_transfer.zig").halfAndSignedDataTransfer;
|
||||||
|
const blockTransfer = @import("cpu/arm/block_data_transfer.zig").blockDataTransfer;
|
||||||
|
const branch = @import("cpu/arm/branch.zig").branch;
|
||||||
|
const branchExchange = @import("cpu/arm/branch.zig").branchAndExchange;
|
||||||
|
const swi = @import("cpu/arm/software_interrupt.zig").armSoftwareInterrupt;
|
||||||
|
const swap = @import("cpu/arm/single_data_swap.zig").singleDataSwap;
|
||||||
|
|
||||||
|
const multiply = @import("cpu/arm/multiply.zig").multiply;
|
||||||
|
const multiplyLong = @import("cpu/arm/multiply.zig").multiplyLong;
|
||||||
|
|
||||||
|
/// Determine index into ARM InstrFn LUT
|
||||||
|
fn idx(opcode: u32) u12 {
|
||||||
|
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 4 & 0xF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undefined ARM Instruction handler
|
||||||
|
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
const id = idx(opcode);
|
||||||
|
cpu.panic("[CPU/Decode] ID: 0x{X:0>3} 0x{X:0>8} is an illegal opcode", .{ id, opcode });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate() [0x1000]InstrFn {
|
||||||
|
return comptime {
|
||||||
|
@setEvalBranchQuota(0xE000);
|
||||||
|
var ret = [_]InstrFn{und} ** 0x1000;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < ret.len) : (i += 1) {
|
||||||
|
ret[i] = switch (@as(u2, i >> 10)) {
|
||||||
|
0b00 => if (i == 0x121) blk: {
|
||||||
|
break :blk branchExchange;
|
||||||
|
} else if (i & 0xFCF == 0x009) blk: {
|
||||||
|
const A = i >> 5 & 1 == 1;
|
||||||
|
const S = i >> 4 & 1 == 1;
|
||||||
|
break :blk multiply(A, S);
|
||||||
|
} else if (i & 0xFBF == 0x109) blk: {
|
||||||
|
const B = i >> 6 & 1 == 1;
|
||||||
|
break :blk swap(B);
|
||||||
|
} else if (i & 0xF8F == 0x089) blk: {
|
||||||
|
const U = i >> 6 & 1 == 1;
|
||||||
|
const A = i >> 5 & 1 == 1;
|
||||||
|
const S = i >> 4 & 1 == 1;
|
||||||
|
break :blk multiplyLong(U, A, S);
|
||||||
|
} else if (i & 0xE49 == 0x009 or i & 0xE49 == 0x049) blk: {
|
||||||
|
const P = i >> 8 & 1 == 1;
|
||||||
|
const U = i >> 7 & 1 == 1;
|
||||||
|
const I = i >> 6 & 1 == 1;
|
||||||
|
const W = i >> 5 & 1 == 1;
|
||||||
|
const L = i >> 4 & 1 == 1;
|
||||||
|
break :blk halfSignedTransfer(P, U, I, W, L);
|
||||||
|
} else if (i & 0xD90 == 0x100) blk: {
|
||||||
|
const I = i >> 9 & 1 == 1;
|
||||||
|
const R = i >> 6 & 1 == 1;
|
||||||
|
const kind = i >> 4 & 0x3;
|
||||||
|
break :blk psrTransfer(I, R, kind);
|
||||||
|
} else blk: {
|
||||||
|
const I = i >> 9 & 1 == 1;
|
||||||
|
const S = i >> 4 & 1 == 1;
|
||||||
|
const instrKind = i >> 5 & 0xF;
|
||||||
|
break :blk processing(I, S, instrKind);
|
||||||
|
},
|
||||||
|
0b01 => if (i >> 9 & 1 == 1 and i & 1 == 1) und else blk: {
|
||||||
|
const I = i >> 9 & 1 == 1;
|
||||||
|
const P = i >> 8 & 1 == 1;
|
||||||
|
const U = i >> 7 & 1 == 1;
|
||||||
|
const B = i >> 6 & 1 == 1;
|
||||||
|
const W = i >> 5 & 1 == 1;
|
||||||
|
const L = i >> 4 & 1 == 1;
|
||||||
|
break :blk transfer(I, P, U, B, W, L);
|
||||||
|
},
|
||||||
|
else => switch (@as(u2, i >> 9 & 0x3)) {
|
||||||
|
// MSB is guaranteed to be 1
|
||||||
|
0b00 => blk: {
|
||||||
|
const P = i >> 8 & 1 == 1;
|
||||||
|
const U = i >> 7 & 1 == 1;
|
||||||
|
const S = i >> 6 & 1 == 1;
|
||||||
|
const W = i >> 5 & 1 == 1;
|
||||||
|
const L = i >> 4 & 1 == 1;
|
||||||
|
break :blk blockTransfer(P, U, S, W, L);
|
||||||
|
},
|
||||||
|
0b01 => blk: {
|
||||||
|
const L = i >> 8 & 1 == 1;
|
||||||
|
break :blk branch(L);
|
||||||
|
},
|
||||||
|
0b10 => und, // COP Data Transfer
|
||||||
|
0b11 => if (i >> 8 & 1 == 1) swi() else und, // COP Data Operation + Register Transfer
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// THUMB Instructions
|
||||||
|
pub const thumb = struct {
|
||||||
|
pub const InstrFn = *const fn (*Arm7tdmi, *Bus, u16) void;
|
||||||
|
const lut: [0x400]InstrFn = populate();
|
||||||
|
|
||||||
|
const processing = @import("cpu/thumb/data_processing.zig");
|
||||||
|
const alu = @import("cpu/thumb/alu.zig").fmt4;
|
||||||
|
const transfer = @import("cpu/thumb/data_transfer.zig");
|
||||||
|
const block_transfer = @import("cpu/thumb/block_data_transfer.zig");
|
||||||
|
const swi = @import("cpu/thumb/software_interrupt.zig").fmt17;
|
||||||
|
const branch = @import("cpu/thumb/branch.zig");
|
||||||
|
|
||||||
|
/// Determine index into THUMB InstrFn LUT
|
||||||
|
fn idx(opcode: u16) u10 {
|
||||||
|
return @truncate(u10, opcode >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undefined THUMB Instruction Handler
|
||||||
|
fn und(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const id = idx(opcode);
|
||||||
|
cpu.panic("[CPU/Decode] ID: 0b{b:0>10} 0x{X:0>2} is an illegal opcode", .{ id, opcode });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate() [0x400]InstrFn {
|
||||||
|
return comptime {
|
||||||
|
@setEvalBranchQuota(5025); // This is exact
|
||||||
|
var ret = [_]InstrFn{und} ** 0x400;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < ret.len) : (i += 1) {
|
||||||
|
ret[i] = switch (@as(u3, i >> 7 & 0x7)) {
|
||||||
|
0b000 => if (i >> 5 & 0x3 == 0b11) blk: {
|
||||||
|
const I = i >> 4 & 1 == 1;
|
||||||
|
const is_sub = i >> 3 & 1 == 1;
|
||||||
|
const rn = i & 0x7;
|
||||||
|
break :blk processing.fmt2(I, is_sub, rn);
|
||||||
|
} else blk: {
|
||||||
|
const op = i >> 5 & 0x3;
|
||||||
|
const offset = i & 0x1F;
|
||||||
|
break :blk processing.fmt1(op, offset);
|
||||||
|
},
|
||||||
|
0b001 => blk: {
|
||||||
|
const op = i >> 5 & 0x3;
|
||||||
|
const rd = i >> 2 & 0x7;
|
||||||
|
break :blk processing.fmt3(op, rd);
|
||||||
|
},
|
||||||
|
0b010 => switch (@as(u2, i >> 5 & 0x3)) {
|
||||||
|
0b00 => if (i >> 4 & 1 == 1) blk: {
|
||||||
|
const op = i >> 2 & 0x3;
|
||||||
|
const h1 = i >> 1 & 1;
|
||||||
|
const h2 = i & 1;
|
||||||
|
break :blk processing.fmt5(op, h1, h2);
|
||||||
|
} else blk: {
|
||||||
|
const op = i & 0xF;
|
||||||
|
break :blk alu(op);
|
||||||
|
},
|
||||||
|
0b01 => blk: {
|
||||||
|
const rd = i >> 2 & 0x7;
|
||||||
|
break :blk transfer.fmt6(rd);
|
||||||
|
},
|
||||||
|
else => blk: {
|
||||||
|
const op = i >> 4 & 0x3;
|
||||||
|
const T = i >> 3 & 1 == 1;
|
||||||
|
break :blk transfer.fmt78(op, T);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0b011 => blk: {
|
||||||
|
const B = i >> 6 & 1 == 1;
|
||||||
|
const L = i >> 5 & 1 == 1;
|
||||||
|
const offset = i & 0x1F;
|
||||||
|
break :blk transfer.fmt9(B, L, offset);
|
||||||
|
},
|
||||||
|
else => switch (@as(u3, i >> 6 & 0x7)) {
|
||||||
|
// MSB is guaranteed to be 1
|
||||||
|
0b000 => blk: {
|
||||||
|
const L = i >> 5 & 1 == 1;
|
||||||
|
const offset = i & 0x1F;
|
||||||
|
break :blk transfer.fmt10(L, offset);
|
||||||
|
},
|
||||||
|
0b001 => blk: {
|
||||||
|
const L = i >> 5 & 1 == 1;
|
||||||
|
const rd = i >> 2 & 0x7;
|
||||||
|
break :blk transfer.fmt11(L, rd);
|
||||||
|
},
|
||||||
|
0b010 => blk: {
|
||||||
|
const isSP = i >> 5 & 1 == 1;
|
||||||
|
const rd = i >> 2 & 0x7;
|
||||||
|
break :blk processing.fmt12(isSP, rd);
|
||||||
|
},
|
||||||
|
0b011 => if (i >> 4 & 1 == 1) blk: {
|
||||||
|
const L = i >> 5 & 1 == 1;
|
||||||
|
const R = i >> 2 & 1 == 1;
|
||||||
|
break :blk block_transfer.fmt14(L, R);
|
||||||
|
} else blk: {
|
||||||
|
const S = i >> 1 & 1 == 1;
|
||||||
|
break :blk processing.fmt13(S);
|
||||||
|
},
|
||||||
|
0b100 => blk: {
|
||||||
|
const L = i >> 5 & 1 == 1;
|
||||||
|
const rb = i >> 2 & 0x7;
|
||||||
|
|
||||||
|
break :blk block_transfer.fmt15(L, rb);
|
||||||
|
},
|
||||||
|
0b101 => if (i >> 2 & 0xF == 0b1111) blk: {
|
||||||
|
break :blk thumb.swi();
|
||||||
|
} else blk: {
|
||||||
|
const cond = i >> 2 & 0xF;
|
||||||
|
break :blk branch.fmt16(cond);
|
||||||
|
},
|
||||||
|
0b110 => branch.fmt18(),
|
||||||
|
0b111 => blk: {
|
||||||
|
const is_low = i >> 5 & 1 == 1;
|
||||||
|
break :blk branch.fmt19(is_low);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cpu_logging = @import("emu.zig").cpu_logging;
|
||||||
|
const log = std.log.scoped(.Arm7Tdmi);
|
||||||
|
|
||||||
|
pub const Arm7tdmi = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
r: [16]u32,
|
||||||
|
sched: *Scheduler,
|
||||||
|
bus: *Bus,
|
||||||
|
cpsr: PSR,
|
||||||
|
spsr: PSR,
|
||||||
|
|
||||||
|
/// Storage for R8_fiq -> R12_fiq and their normal counterparts
|
||||||
|
/// e.g [r[0 + 8], fiq_r[0 + 8], r[1 + 8], fiq_r[1 + 8]...]
|
||||||
|
banked_fiq: [2 * 5]u32,
|
||||||
|
|
||||||
|
/// Storage for r13_<mode>, r14_<mode>
|
||||||
|
/// e.g. [r13, r14, r13_svc, r14_svc]
|
||||||
|
banked_r: [2 * 6]u32,
|
||||||
|
|
||||||
|
banked_spsr: [5]PSR,
|
||||||
|
|
||||||
|
logger: ?Logger,
|
||||||
|
|
||||||
|
pub fn init(sched: *Scheduler, bus: *Bus, log_file: ?std.fs.File) Self {
|
||||||
|
return Self{
|
||||||
|
.r = [_]u32{0x00} ** 16,
|
||||||
|
.sched = sched,
|
||||||
|
.bus = bus,
|
||||||
|
.cpsr = .{ .raw = 0x0000_001F },
|
||||||
|
.spsr = .{ .raw = 0x0000_0000 },
|
||||||
|
.banked_fiq = [_]u32{0x00} ** 10,
|
||||||
|
.banked_r = [_]u32{0x00} ** 12,
|
||||||
|
.banked_spsr = [_]PSR{.{ .raw = 0x0000_0000 }} ** 5,
|
||||||
|
.logger = if (log_file) |file| Logger.init(file) else null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn bankedIdx(mode: Mode, kind: BankedKind) usize {
|
||||||
|
const idx: usize = switch (mode) {
|
||||||
|
.User, .System => 0,
|
||||||
|
.Supervisor => 1,
|
||||||
|
.Abort => 2,
|
||||||
|
.Undefined => 3,
|
||||||
|
.Irq => 4,
|
||||||
|
.Fiq => 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (idx * 2) + if (kind == .R14) @as(usize, 1) else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn bankedSpsrIndex(mode: Mode) usize {
|
||||||
|
return switch (mode) {
|
||||||
|
.Supervisor => 0,
|
||||||
|
.Abort => 1,
|
||||||
|
.Undefined => 2,
|
||||||
|
.Irq => 3,
|
||||||
|
.Fiq => 4,
|
||||||
|
else => std.debug.panic("[CPU/Mode] {} does not have a SPSR Register", .{mode}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn bankedFiqIdx(i: usize, mode: Mode) usize {
|
||||||
|
return (i * 2) + if (mode == .Fiq) @as(usize, 1) else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn hasSPSR(self: *const Self) bool {
|
||||||
|
const mode = getModeChecked(self, self.cpsr.mode.read());
|
||||||
|
return switch (mode) {
|
||||||
|
.System, .User => false,
|
||||||
|
else => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn isPrivileged(self: *const Self) bool {
|
||||||
|
const mode = getModeChecked(self, self.cpsr.mode.read());
|
||||||
|
return switch (mode) {
|
||||||
|
.User => false,
|
||||||
|
else => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn isHalted(self: *const Self) bool {
|
||||||
|
return self.bus.io.haltcnt == .Halt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setCpsr(self: *Self, value: u32) void {
|
||||||
|
if (value & 0x1F != self.cpsr.raw & 0x1F) self.changeModeFromIdx(@truncate(u5, value & 0x1F));
|
||||||
|
self.cpsr.raw = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changeModeFromIdx(self: *Self, next: u5) void {
|
||||||
|
self.changeMode(getModeChecked(self, next));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setUserModeRegister(self: *Self, idx: usize, value: u32) void {
|
||||||
|
const current = getModeChecked(self, self.cpsr.mode.read());
|
||||||
|
|
||||||
|
switch (idx) {
|
||||||
|
8...12 => {
|
||||||
|
if (current == .Fiq) {
|
||||||
|
self.banked_fiq[bankedFiqIdx(idx - 8, .User)] = value;
|
||||||
|
} else self.r[idx] = value;
|
||||||
|
},
|
||||||
|
13, 14 => switch (current) {
|
||||||
|
.User, .System => self.r[idx] = value,
|
||||||
|
else => {
|
||||||
|
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
|
||||||
|
self.banked_r[bankedIdx(.User, kind)] = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
else => self.r[idx] = value, // R0 -> R7 and R15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getUserModeRegister(self: *Self, idx: usize) u32 {
|
||||||
|
const current = getModeChecked(self, self.cpsr.mode.read());
|
||||||
|
|
||||||
|
return switch (idx) {
|
||||||
|
8...12 => if (current == .Fiq) self.banked_fiq[bankedFiqIdx(idx - 8, .User)] else self.r[idx],
|
||||||
|
13, 14 => switch (current) {
|
||||||
|
.User, .System => self.r[idx],
|
||||||
|
else => blk: {
|
||||||
|
const kind = std.meta.intToEnum(BankedKind, idx - 13) catch unreachable;
|
||||||
|
break :blk self.banked_r[bankedIdx(.User, kind)];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
else => self.r[idx], // R0 -> R7 and R15
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn changeMode(self: *Self, next: Mode) void {
|
||||||
|
const now = getModeChecked(self, self.cpsr.mode.read());
|
||||||
|
|
||||||
|
// Bank R8 -> r12
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 5) : (i += 1) {
|
||||||
|
self.banked_fiq[bankedFiqIdx(i, now)] = self.r[8 + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bank r13, r14, SPSR
|
||||||
|
switch (now) {
|
||||||
|
.User, .System => {
|
||||||
|
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
|
||||||
|
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
self.banked_r[bankedIdx(now, .R13)] = self.r[13];
|
||||||
|
self.banked_r[bankedIdx(now, .R14)] = self.r[14];
|
||||||
|
self.banked_spsr[bankedSpsrIndex(now)] = self.spsr;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab R8 -> R12
|
||||||
|
i = 0;
|
||||||
|
while (i < 5) : (i += 1) {
|
||||||
|
self.r[8 + i] = self.banked_fiq[bankedFiqIdx(i, next)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab r13, r14, SPSR
|
||||||
|
switch (next) {
|
||||||
|
.User, .System => {
|
||||||
|
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
|
||||||
|
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
self.r[13] = self.banked_r[bankedIdx(next, .R13)];
|
||||||
|
self.r[14] = self.banked_r[bankedIdx(next, .R14)];
|
||||||
|
self.spsr = self.banked_spsr[bankedSpsrIndex(next)];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cpsr.mode.write(@enumToInt(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fastBoot(self: *Self) void {
|
||||||
|
self.r = std.mem.zeroes([16]u32);
|
||||||
|
|
||||||
|
self.r[0] = 0x08000000;
|
||||||
|
self.r[1] = 0x000000EA;
|
||||||
|
self.r[13] = 0x0300_7F00;
|
||||||
|
self.r[15] = 0x0800_0000;
|
||||||
|
|
||||||
|
self.banked_r[bankedIdx(.Irq, .R13)] = 0x0300_7FA0;
|
||||||
|
self.banked_r[bankedIdx(.Supervisor, .R13)] = 0x0300_7FE0;
|
||||||
|
|
||||||
|
self.cpsr.raw = 0x6000001F;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(self: *Self) void {
|
||||||
|
if (self.cpsr.t.read()) {
|
||||||
|
const opcode = self.fetch(u16);
|
||||||
|
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
||||||
|
|
||||||
|
thumb.lut[thumb.idx(opcode)](self, self.bus, opcode);
|
||||||
|
} else {
|
||||||
|
const opcode = self.fetch(u32);
|
||||||
|
if (cpu_logging) self.logger.?.mgbaLog(self, opcode);
|
||||||
|
|
||||||
|
if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) {
|
||||||
|
arm.lut[arm.idx(opcode)](self, self.bus, opcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stepDmaTransfer(self: *Self) bool {
|
||||||
|
const dma0 = &self.bus.dma[0];
|
||||||
|
const dma1 = &self.bus.dma[1];
|
||||||
|
const dma2 = &self.bus.dma[2];
|
||||||
|
const dma3 = &self.bus.dma[3];
|
||||||
|
|
||||||
|
if (dma0.in_progress) {
|
||||||
|
dma0.step(self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dma1.in_progress) {
|
||||||
|
dma1.step(self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dma2.in_progress) {
|
||||||
|
dma2.step(self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dma3.in_progress) {
|
||||||
|
dma3.step(self);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleInterrupt(self: *Self) void {
|
||||||
|
const should_handle = self.bus.io.ie.raw & self.bus.io.irq.raw;
|
||||||
|
|
||||||
|
if (should_handle != 0) {
|
||||||
|
self.bus.io.haltcnt = .Execute;
|
||||||
|
// log.debug("An Interrupt was Fired!", .{});
|
||||||
|
|
||||||
|
// Either IME is not true or I in CPSR is true
|
||||||
|
// Don't handle interrupts
|
||||||
|
if (!self.bus.io.ime or self.cpsr.i.read()) return;
|
||||||
|
// log.debug("An interrupt was Handled!", .{});
|
||||||
|
|
||||||
|
// retAddr.gba says r15 on it's own is off by -04h in both ARM and THUMB mode
|
||||||
|
const r15 = self.r[15] + 4;
|
||||||
|
const cpsr = self.cpsr.raw;
|
||||||
|
|
||||||
|
self.changeMode(.Irq);
|
||||||
|
self.cpsr.t.write(false);
|
||||||
|
self.cpsr.i.write(true);
|
||||||
|
|
||||||
|
self.r[14] = r15;
|
||||||
|
self.spsr.raw = cpsr;
|
||||||
|
self.r[15] = 0x000_0018;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn fetch(self: *Self, comptime T: type) T {
|
||||||
|
comptime std.debug.assert(T == u32 or T == u16); // Opcode may be 32-bit (ARM) or 16-bit (THUMB)
|
||||||
|
defer self.r[15] += if (T == u32) 4 else 2;
|
||||||
|
|
||||||
|
// FIXME: You better hope this is optimized out
|
||||||
|
const tick_cache = self.sched.tick;
|
||||||
|
defer self.sched.tick = tick_cache + Bus.fetch_timings[@boolToInt(T == u32)][@truncate(u4, self.r[15] >> 24)];
|
||||||
|
|
||||||
|
return self.bus.read(T, self.r[15]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fakePC(self: *const Self) u32 {
|
||||||
|
return self.r[15] + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn panic(self: *const Self, comptime format: []const u8, args: anytype) noreturn {
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 16) : (i += 4) {
|
||||||
|
const i_1 = i + 1;
|
||||||
|
const i_2 = i + 2;
|
||||||
|
const i_3 = i + 3;
|
||||||
|
std.debug.print("R{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\tR{}: 0x{X:0>8}\n", .{ i, self.r[i], i_1, self.r[i_1], i_2, self.r[i_2], i_3, self.r[i_3] });
|
||||||
|
}
|
||||||
|
std.debug.print("cpsr: 0x{X:0>8} ", .{self.cpsr.raw});
|
||||||
|
prettyPrintPsr(&self.cpsr);
|
||||||
|
|
||||||
|
std.debug.print("spsr: 0x{X:0>8} ", .{self.spsr.raw});
|
||||||
|
prettyPrintPsr(&self.spsr);
|
||||||
|
|
||||||
|
if (self.cpsr.t.read()) {
|
||||||
|
const opcode = self.bus.dbgRead(u16, self.r[15] - 4);
|
||||||
|
const id = thumb.idx(opcode);
|
||||||
|
std.debug.print("opcode: ID: 0x{b:0>10} 0x{X:0>4}\n", .{ id, opcode });
|
||||||
|
} else {
|
||||||
|
const opcode = self.bus.dbgRead(u32, self.r[15] - 4);
|
||||||
|
const id = arm.idx(opcode);
|
||||||
|
std.debug.print("opcode: ID: 0x{X:0>3} 0x{X:0>8}\n", .{ id, opcode });
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.print("tick: {}\n\n", .{self.sched.tick});
|
||||||
|
|
||||||
|
std.debug.panic(format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prettyPrintPsr(psr: *const PSR) void {
|
||||||
|
std.debug.print("[", .{});
|
||||||
|
|
||||||
|
if (psr.n.read()) std.debug.print("N", .{}) else std.debug.print("-", .{});
|
||||||
|
if (psr.z.read()) std.debug.print("Z", .{}) else std.debug.print("-", .{});
|
||||||
|
if (psr.c.read()) std.debug.print("C", .{}) else std.debug.print("-", .{});
|
||||||
|
if (psr.v.read()) std.debug.print("V", .{}) else std.debug.print("-", .{});
|
||||||
|
if (psr.i.read()) std.debug.print("I", .{}) else std.debug.print("-", .{});
|
||||||
|
if (psr.f.read()) std.debug.print("F", .{}) else std.debug.print("-", .{});
|
||||||
|
if (psr.t.read()) std.debug.print("T", .{}) else std.debug.print("-", .{});
|
||||||
|
std.debug.print("|", .{});
|
||||||
|
if (getMode(psr.mode.read())) |mode| std.debug.print("{s}", .{modeString(mode)}) else std.debug.print("---", .{});
|
||||||
|
|
||||||
|
std.debug.print("]\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modeString(mode: Mode) []const u8 {
|
||||||
|
return switch (mode) {
|
||||||
|
.User => "usr",
|
||||||
|
.Fiq => "fiq",
|
||||||
|
.Irq => "irq",
|
||||||
|
.Supervisor => "svc",
|
||||||
|
.Abort => "abt",
|
||||||
|
.Undefined => "und",
|
||||||
|
.System => "sys",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mgbaLog(self: *const Self, file: *const File, opcode: u32) !void {
|
||||||
|
const thumb_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>4}:\n";
|
||||||
|
const arm_fmt = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | {X:0>8}:\n";
|
||||||
|
var buf: [0x100]u8 = [_]u8{0x00} ** 0x100; // this is larger than it needs to be
|
||||||
|
|
||||||
|
const r0 = self.r[0];
|
||||||
|
const r1 = self.r[1];
|
||||||
|
const r2 = self.r[2];
|
||||||
|
const r3 = self.r[3];
|
||||||
|
const r4 = self.r[4];
|
||||||
|
const r5 = self.r[5];
|
||||||
|
const r6 = self.r[6];
|
||||||
|
const r7 = self.r[7];
|
||||||
|
const r8 = self.r[8];
|
||||||
|
const r9 = self.r[9];
|
||||||
|
const r10 = self.r[10];
|
||||||
|
const r11 = self.r[11];
|
||||||
|
const r12 = self.r[12];
|
||||||
|
const r13 = self.r[13];
|
||||||
|
const r14 = self.r[14];
|
||||||
|
const r15 = self.r[15];
|
||||||
|
|
||||||
|
const c_psr = self.cpsr.raw;
|
||||||
|
|
||||||
|
var log_str: []u8 = undefined;
|
||||||
|
if (self.cpsr.t.read()) {
|
||||||
|
if (opcode >> 11 == 0x1E) {
|
||||||
|
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||||
|
const other_half = self.bus.dbgRead(u16, self.r[15]);
|
||||||
|
const bl_opcode = @as(u32, opcode) << 16 | other_half;
|
||||||
|
|
||||||
|
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, bl_opcode });
|
||||||
|
} else {
|
||||||
|
log_str = try std.fmt.bufPrint(&buf, thumb_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_str = try std.fmt.bufPrint(&buf, arm_fmt, .{ r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, c_psr, opcode });
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try file.writeAll(log_str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn checkCond(cpsr: PSR, cond: u4) bool {
|
||||||
|
return switch (cond) {
|
||||||
|
0x0 => cpsr.z.read(), // EQ - Equal
|
||||||
|
0x1 => !cpsr.z.read(), // NE - Not equal
|
||||||
|
0x2 => cpsr.c.read(), // CS - Unsigned higher or same
|
||||||
|
0x3 => !cpsr.c.read(), // CC - Unsigned lower
|
||||||
|
0x4 => cpsr.n.read(), // MI - Negative
|
||||||
|
0x5 => !cpsr.n.read(), // PL - Positive or zero
|
||||||
|
0x6 => cpsr.v.read(), // VS - Overflow
|
||||||
|
0x7 => !cpsr.v.read(), // VC - No overflow
|
||||||
|
0x8 => cpsr.c.read() and !cpsr.z.read(), // HI - unsigned higher
|
||||||
|
0x9 => !cpsr.c.read() or cpsr.z.read(), // LS - unsigned lower or same
|
||||||
|
0xA => cpsr.n.read() == cpsr.v.read(), // GE - Greater or equal
|
||||||
|
0xB => cpsr.n.read() != cpsr.v.read(), // LT - Less than
|
||||||
|
0xC => !cpsr.z.read() and (cpsr.n.read() == cpsr.v.read()), // GT - Greater than
|
||||||
|
0xD => cpsr.z.read() or (cpsr.n.read() != cpsr.v.read()), // LE - Less than or equal
|
||||||
|
0xE => true, // AL - Always
|
||||||
|
0xF => false, // NV - Never (reserved in ARMv3 and up, but seems to have not changed?)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PSR = extern union {
|
||||||
|
mode: Bitfield(u32, 0, 5),
|
||||||
|
t: Bit(u32, 5),
|
||||||
|
f: Bit(u32, 6),
|
||||||
|
i: Bit(u32, 7),
|
||||||
|
v: Bit(u32, 28),
|
||||||
|
c: Bit(u32, 29),
|
||||||
|
z: Bit(u32, 30),
|
||||||
|
n: Bit(u32, 31),
|
||||||
|
raw: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mode = enum(u5) {
|
||||||
|
User = 0b10000,
|
||||||
|
Fiq = 0b10001,
|
||||||
|
Irq = 0b10010,
|
||||||
|
Supervisor = 0b10011,
|
||||||
|
Abort = 0b10111,
|
||||||
|
Undefined = 0b11011,
|
||||||
|
System = 0b11111,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BankedKind = enum(u1) {
|
||||||
|
R13 = 0,
|
||||||
|
R14,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn getMode(bits: u5) ?Mode {
|
||||||
|
return std.meta.intToEnum(Mode, bits) catch null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getModeChecked(cpu: *const Arm7tdmi, bits: u5) Mode {
|
||||||
|
return getMode(bits) orelse cpu.panic("[CPU/CPSR] 0b{b:0>5} is an invalid CPU mode", .{bits});
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
pub fn blockDataTransfer(comptime P: bool, comptime U: bool, comptime S: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
||||||
|
const rn = @truncate(u4, opcode >> 16 & 0xF);
|
||||||
|
const rlist = opcode & 0xFFFF;
|
||||||
|
const r15 = rlist >> 15 & 1 == 1;
|
||||||
|
|
||||||
|
var count: u32 = 0;
|
||||||
|
var i: u5 = 0;
|
||||||
|
var first: u4 = 0;
|
||||||
|
var write_to_base = true;
|
||||||
|
|
||||||
|
while (i < 16) : (i += 1) {
|
||||||
|
const r = @truncate(u4, 15 - i);
|
||||||
|
if (rlist >> r & 1 == 1) {
|
||||||
|
first = r;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = cpu.r[rn];
|
||||||
|
if (U) {
|
||||||
|
start += if (P) 4 else 0;
|
||||||
|
} else {
|
||||||
|
start = start - (4 * count) + if (!P) 4 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = cpu.r[rn];
|
||||||
|
if (U) {
|
||||||
|
end = end + (4 * count) - if (!P) 4 else 0;
|
||||||
|
} else {
|
||||||
|
end -= if (P) 4 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var new_base = cpu.r[rn];
|
||||||
|
if (U) {
|
||||||
|
new_base += 4 * count;
|
||||||
|
} else {
|
||||||
|
new_base -= 4 * count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = start;
|
||||||
|
|
||||||
|
if (rlist == 0) {
|
||||||
|
var und_addr = cpu.r[rn];
|
||||||
|
if (U) {
|
||||||
|
und_addr += if (P) 4 else 0;
|
||||||
|
} else {
|
||||||
|
und_addr -= 0x40 - if (!P) 4 else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (L) {
|
||||||
|
cpu.r[15] = bus.read(u32, und_addr);
|
||||||
|
} else {
|
||||||
|
bus.write(u32, und_addr, cpu.r[15] + 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.r[rn] = if (U) cpu.r[rn] + 0x40 else cpu.r[rn] - 0x40;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = first;
|
||||||
|
while (i < 16) : (i += 1) {
|
||||||
|
if (rlist >> i & 1 == 1) {
|
||||||
|
transfer(cpu, bus, r15, i, address);
|
||||||
|
address += 4;
|
||||||
|
|
||||||
|
if (W and !L and write_to_base) {
|
||||||
|
cpu.r[rn] = new_base;
|
||||||
|
write_to_base = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (W and L and rlist >> rn & 1 == 0) cpu.r[rn] = new_base;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer(cpu: *Arm7tdmi, bus: *Bus, r15_present: bool, i: u5, address: u32) void {
|
||||||
|
if (L) {
|
||||||
|
if (S and !r15_present) {
|
||||||
|
// Always Transfer User mode Registers
|
||||||
|
cpu.setUserModeRegister(i, bus.read(u32, address));
|
||||||
|
} else {
|
||||||
|
const value = bus.read(u32, address);
|
||||||
|
cpu.r[i] = if (i == 0xF) value & 0xFFFF_FFFC else value;
|
||||||
|
if (S and i == 0xF) cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (S) {
|
||||||
|
// Always Transfer User mode Registers
|
||||||
|
// This happens regardless if r15 is in the list
|
||||||
|
const value = cpu.getUserModeRegister(i);
|
||||||
|
bus.write(u32, address, value + if (i == 0xF) 8 else @as(u32, 0)); // PC is already 4 ahead to make 12
|
||||||
|
} else {
|
||||||
|
bus.write(u32, address, cpu.r[i] + if (i == 0xF) 8 else @as(u32, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
const sext = @import("../../../util.zig").sext;
|
||||||
|
|
||||||
|
pub fn branch(comptime L: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
if (L) cpu.r[14] = cpu.r[15];
|
||||||
|
cpu.r[15] = cpu.fakePC() +% (sext(u32, u24, opcode) << 2);
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn branchAndExchange(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
const rn = opcode & 0xF;
|
||||||
|
cpu.cpsr.t.write(cpu.r[rn] & 1 == 1);
|
||||||
|
cpu.r[15] = cpu.r[rn] & 0xFFFF_FFFE;
|
||||||
|
}
|
|
@ -0,0 +1,284 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
||||||
|
const execute = @import("../barrel_shifter.zig").execute;
|
||||||
|
|
||||||
|
pub fn dataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
const rd = @truncate(u4, opcode >> 12 & 0xF);
|
||||||
|
const rn = opcode >> 16 & 0xF;
|
||||||
|
const old_carry = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
|
||||||
|
// If certain conditions are met, PC is 12 ahead instead of 8
|
||||||
|
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] += 4;
|
||||||
|
|
||||||
|
const op1 = if (rn == 0xF) cpu.fakePC() else cpu.r[rn];
|
||||||
|
|
||||||
|
var op2: u32 = undefined;
|
||||||
|
if (I) {
|
||||||
|
const amount = @truncate(u8, (opcode >> 8 & 0xF) << 1);
|
||||||
|
op2 = rotateRight(S, &cpu.cpsr, opcode & 0xFF, amount);
|
||||||
|
} else {
|
||||||
|
op2 = execute(S, cpu, opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo special condition from above
|
||||||
|
if (!I and opcode >> 4 & 1 == 1) cpu.r[15] -= 4;
|
||||||
|
|
||||||
|
switch (instrKind) {
|
||||||
|
0x0 => {
|
||||||
|
// AND
|
||||||
|
const result = op1 & op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
|
},
|
||||||
|
0x1 => {
|
||||||
|
// EOR
|
||||||
|
const result = op1 ^ op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
|
},
|
||||||
|
0x2 => {
|
||||||
|
// SUB
|
||||||
|
cpu.r[rd] = armSub(S, cpu, rd, op1, op2);
|
||||||
|
},
|
||||||
|
0x3 => {
|
||||||
|
// RSB
|
||||||
|
cpu.r[rd] = armSub(S, cpu, rd, op2, op1);
|
||||||
|
},
|
||||||
|
0x4 => {
|
||||||
|
// ADD
|
||||||
|
cpu.r[rd] = armAdd(S, cpu, rd, op1, op2);
|
||||||
|
},
|
||||||
|
0x5 => {
|
||||||
|
// ADC
|
||||||
|
cpu.r[rd] = armAdc(S, cpu, rd, op1, op2, old_carry);
|
||||||
|
},
|
||||||
|
0x6 => {
|
||||||
|
// SBC
|
||||||
|
cpu.r[rd] = armSbc(S, cpu, rd, op1, op2, old_carry);
|
||||||
|
},
|
||||||
|
0x7 => {
|
||||||
|
// RSC
|
||||||
|
cpu.r[rd] = armSbc(S, cpu, rd, op2, op1, old_carry);
|
||||||
|
},
|
||||||
|
0x8 => {
|
||||||
|
// TST
|
||||||
|
if (rd == 0xF) {
|
||||||
|
undefinedTestBehaviour(cpu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = op1 & op2;
|
||||||
|
setTestOpFlags(S, cpu, opcode, result);
|
||||||
|
},
|
||||||
|
0x9 => {
|
||||||
|
// TEQ
|
||||||
|
if (rd == 0xF) {
|
||||||
|
undefinedTestBehaviour(cpu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = op1 ^ op2;
|
||||||
|
setTestOpFlags(S, cpu, opcode, result);
|
||||||
|
},
|
||||||
|
0xA => {
|
||||||
|
// CMP
|
||||||
|
if (rd == 0xF) {
|
||||||
|
undefinedTestBehaviour(cpu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp(cpu, op1, op2);
|
||||||
|
},
|
||||||
|
0xB => {
|
||||||
|
// CMN
|
||||||
|
if (rd == 0xF) {
|
||||||
|
undefinedTestBehaviour(cpu);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmn(cpu, op1, op2);
|
||||||
|
},
|
||||||
|
0xC => {
|
||||||
|
// ORR
|
||||||
|
const result = op1 | op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
|
},
|
||||||
|
0xD => {
|
||||||
|
// MOV
|
||||||
|
cpu.r[rd] = op2;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, op2);
|
||||||
|
},
|
||||||
|
0xE => {
|
||||||
|
// BIC
|
||||||
|
const result = op1 & ~op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
|
},
|
||||||
|
0xF => {
|
||||||
|
// MVN
|
||||||
|
const result = ~op2;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setArmLogicOpFlags(S, cpu, rd, result);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armSbc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = sbc(false, cpu, left, right, old_carry);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = sbc(S, cpu, left, right, old_carry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sbc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
// TODO: Make your own version (thanks peach.bot)
|
||||||
|
const subtrahend = @as(u64, right) -% old_carry +% 1;
|
||||||
|
const result = @truncate(u32, left -% subtrahend);
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(subtrahend <= left);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armSub(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = sub(false, cpu, left, right);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = sub(S, cpu, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sub(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
||||||
|
const result = left -% right;
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(right <= left);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armAdd(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = add(false, cpu, left, right);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = add(S, cpu, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(didOverflow);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armAdc(comptime S: bool, cpu: *Arm7tdmi, rd: u4, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
result = adc(false, cpu, left, right, old_carry);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
result = adc(S, cpu, left, right, old_carry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adc(comptime S: bool, cpu: *Arm7tdmi, left: u32, right: u32, old_carry: u1) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
const did = @addWithOverflow(u32, left, right, &result);
|
||||||
|
const overflow = @addWithOverflow(u32, result, old_carry, &result);
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(did or overflow);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmp(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
||||||
|
const result = left -% right;
|
||||||
|
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(right <= left);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (~right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmn(cpu: *Arm7tdmi, left: u32, right: u32) void {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
const didOverflow = @addWithOverflow(u32, left, right, &result);
|
||||||
|
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
cpu.cpsr.c.write(didOverflow);
|
||||||
|
cpu.cpsr.v.write(((left ^ result) & (right ^ result)) >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setArmLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, rd: u4, result: u32) void {
|
||||||
|
if (S and rd == 0xF) {
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
} else {
|
||||||
|
setLogicOpFlags(S, cpu, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setLogicOpFlags(comptime S: bool, cpu: *Arm7tdmi, result: u32) void {
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// C set by Barrel Shifter, V is unaffected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setTestOpFlags(comptime S: bool, cpu: *Arm7tdmi, opcode: u32, result: u32) void {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// Barrel Shifter should always calc CPSR C in TST
|
||||||
|
if (!S) _ = execute(true, cpu, opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undefinedTestBehaviour(cpu: *Arm7tdmi) void {
|
||||||
|
@setCold(true);
|
||||||
|
cpu.setCpsr(cpu.spsr.raw);
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
const sext = @import("../../../util.zig").sext;
|
||||||
|
const rotr = @import("../../../util.zig").rotr;
|
||||||
|
|
||||||
|
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
||||||
|
const rn = opcode >> 16 & 0xF;
|
||||||
|
const rd = opcode >> 12 & 0xF;
|
||||||
|
const rm = opcode & 0xF;
|
||||||
|
const imm_offset_high = opcode >> 8 & 0xF;
|
||||||
|
|
||||||
|
var base: u32 = undefined;
|
||||||
|
if (rn == 0xF) {
|
||||||
|
base = cpu.fakePC();
|
||||||
|
if (!L) base += 4;
|
||||||
|
} else {
|
||||||
|
base = cpu.r[rn];
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset: u32 = undefined;
|
||||||
|
if (I) {
|
||||||
|
offset = imm_offset_high << 4 | rm;
|
||||||
|
} else {
|
||||||
|
offset = cpu.r[rm];
|
||||||
|
}
|
||||||
|
|
||||||
|
const modified_base = if (U) base +% offset else base -% offset;
|
||||||
|
var address = if (P) modified_base else base;
|
||||||
|
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (L) {
|
||||||
|
switch (@truncate(u2, opcode >> 5)) {
|
||||||
|
0b01 => {
|
||||||
|
// LDRH
|
||||||
|
const value = bus.read(u16, address);
|
||||||
|
result = rotr(u32, value, 8 * (address & 1));
|
||||||
|
},
|
||||||
|
0b10 => {
|
||||||
|
// LDRSB
|
||||||
|
result = sext(u32, u8, bus.read(u8, address));
|
||||||
|
},
|
||||||
|
0b11 => {
|
||||||
|
// LDRSH
|
||||||
|
result = if (address & 1 == 1) blk: {
|
||||||
|
break :blk sext(u32, u8, bus.read(u8, address));
|
||||||
|
} else blk: {
|
||||||
|
break :blk sext(u32, u16, bus.read(u16, address));
|
||||||
|
};
|
||||||
|
},
|
||||||
|
0b00 => unreachable, // SWP
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (opcode >> 5 & 0x01 == 0x01) {
|
||||||
|
// STRH
|
||||||
|
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
|
||||||
|
} else unreachable; // SWP
|
||||||
|
}
|
||||||
|
|
||||||
|
address = modified_base;
|
||||||
|
if (W and P or !P) cpu.r[rn] = address;
|
||||||
|
if (L) cpu.r[rd] = result; // // This emulates the LDR rd == rn behaviour
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
pub fn multiply(comptime A: bool, comptime S: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
const rd = opcode >> 16 & 0xF;
|
||||||
|
const rn = opcode >> 12 & 0xF;
|
||||||
|
const rs = opcode >> 8 & 0xF;
|
||||||
|
const rm = opcode & 0xF;
|
||||||
|
|
||||||
|
const temp: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]) + if (A) cpu.r[rn] else 0;
|
||||||
|
const result = @truncate(u32, temp);
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// V is unaffected, C is *actually* undefined in ARMv4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multiplyLong(comptime U: bool, comptime A: bool, comptime S: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
const rd_hi = opcode >> 16 & 0xF;
|
||||||
|
const rd_lo = opcode >> 12 & 0xF;
|
||||||
|
const rs = opcode >> 8 & 0xF;
|
||||||
|
const rm = opcode & 0xF;
|
||||||
|
|
||||||
|
if (U) {
|
||||||
|
// Signed (WHY IS IT U THEN?)
|
||||||
|
var result: i64 = @as(i64, @bitCast(i32, cpu.r[rm])) * @as(i64, @bitCast(i32, cpu.r[rs]));
|
||||||
|
if (A) result +%= @bitCast(i64, @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]));
|
||||||
|
|
||||||
|
cpu.r[rd_hi] = @bitCast(u32, @truncate(i32, result >> 32));
|
||||||
|
cpu.r[rd_lo] = @bitCast(u32, @truncate(i32, result));
|
||||||
|
} else {
|
||||||
|
// Unsigned
|
||||||
|
var result: u64 = @as(u64, cpu.r[rm]) * @as(u64, cpu.r[rs]);
|
||||||
|
if (A) result +%= @as(u64, cpu.r[rd_hi]) << 32 | @as(u64, cpu.r[rd_lo]);
|
||||||
|
|
||||||
|
cpu.r[rd_hi] = @truncate(u32, result >> 32);
|
||||||
|
cpu.r[rd_lo] = @truncate(u32, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S) {
|
||||||
|
cpu.cpsr.z.write(cpu.r[rd_hi] == 0 and cpu.r[rd_lo] == 0);
|
||||||
|
cpu.cpsr.n.write(cpu.r[rd_hi] >> 31 & 1 == 1);
|
||||||
|
// C and V are set to meaningless values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
const PSR = @import("../../cpu.zig").PSR;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.PsrTransfer);
|
||||||
|
|
||||||
|
const rotr = @import("../../../util.zig").rotr;
|
||||||
|
|
||||||
|
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u32) void {
|
||||||
|
switch (kind) {
|
||||||
|
0b00 => {
|
||||||
|
// MRS
|
||||||
|
const rd = opcode >> 12 & 0xF;
|
||||||
|
|
||||||
|
if (R and !cpu.hasSPSR()) log.err("Tried to read SPSR from User/System Mode", .{});
|
||||||
|
cpu.r[rd] = if (R) cpu.spsr.raw else cpu.cpsr.raw;
|
||||||
|
},
|
||||||
|
0b10 => {
|
||||||
|
// MSR
|
||||||
|
const field_mask = @truncate(u4, opcode >> 16 & 0xF);
|
||||||
|
const rm_idx = opcode & 0xF;
|
||||||
|
const right = if (I) rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) * 2) else cpu.r[rm_idx];
|
||||||
|
|
||||||
|
if (R and !cpu.hasSPSR()) log.err("Tried to write to SPSR in User/System Mode", .{});
|
||||||
|
|
||||||
|
if (R) {
|
||||||
|
// arm.gba seems to expect the SPSR to do somethign in SYS mode,
|
||||||
|
// so we just assume that despite writing to the SPSR in USR or SYS mode
|
||||||
|
// being UNPREDICTABLE, it just magically has a working SPSR somehow
|
||||||
|
cpu.spsr.raw = fieldMask(&cpu.spsr, field_mask, right);
|
||||||
|
} else {
|
||||||
|
if (cpu.isPrivileged()) cpu.setCpsr(fieldMask(&cpu.cpsr, field_mask, right));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => cpu.panic("[CPU/PSR Transfer] Bits 21:220 of {X:0>8} are undefined", .{opcode}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fieldMask(psr: *const PSR, field_mask: u4, right: u32) u32 {
|
||||||
|
// This bitwise ORs bits 3 and 0 of the field mask into a u2
|
||||||
|
// We do this because we only care about bits 7:0 and 31:28 of the CPSR
|
||||||
|
const bits = @truncate(u2, (field_mask >> 2 & 0x2) | (field_mask & 1));
|
||||||
|
|
||||||
|
const mask: u32 = switch (bits) {
|
||||||
|
0b00 => 0x0000_0000,
|
||||||
|
0b01 => 0x0000_00FF,
|
||||||
|
0b10 => 0xF000_0000,
|
||||||
|
0b11 => 0xF000_00FF,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (psr.raw & ~mask) | (right & mask);
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
const rotr = @import("../../../util.zig").rotr;
|
||||||
|
|
||||||
|
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
||||||
|
const rn = opcode >> 16 & 0xF;
|
||||||
|
const rd = opcode >> 12 & 0xF;
|
||||||
|
const rm = opcode & 0xF;
|
||||||
|
|
||||||
|
const address = cpu.r[rn];
|
||||||
|
|
||||||
|
if (B) {
|
||||||
|
// SWPB
|
||||||
|
const value = bus.read(u8, address);
|
||||||
|
bus.write(u8, address, @truncate(u8, cpu.r[rm]));
|
||||||
|
cpu.r[rd] = value;
|
||||||
|
} else {
|
||||||
|
// SWP
|
||||||
|
const value = rotr(u32, bus.read(u32, address), 8 * (address & 0x3));
|
||||||
|
bus.write(u32, address, cpu.r[rm]);
|
||||||
|
cpu.r[rd] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const util = @import("../../../util.zig");
|
||||||
|
|
||||||
|
const shifter = @import("../barrel_shifter.zig");
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
const rotr = @import("../../../util.zig").rotr;
|
||||||
|
|
||||||
|
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u32) void {
|
||||||
|
const rn = opcode >> 16 & 0xF;
|
||||||
|
const rd = opcode >> 12 & 0xF;
|
||||||
|
|
||||||
|
var base: u32 = undefined;
|
||||||
|
if (rn == 0xF) {
|
||||||
|
base = cpu.fakePC();
|
||||||
|
if (!L) base += 4; // Offset of 12
|
||||||
|
} else {
|
||||||
|
base = cpu.r[rn];
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = if (I) shifter.immShift(false, cpu, opcode) else opcode & 0xFFF;
|
||||||
|
|
||||||
|
const modified_base = if (U) base +% offset else base -% offset;
|
||||||
|
var address = if (P) modified_base else base;
|
||||||
|
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (L) {
|
||||||
|
if (B) {
|
||||||
|
// LDRB
|
||||||
|
result = bus.read(u8, address);
|
||||||
|
} else {
|
||||||
|
// LDR
|
||||||
|
const value = bus.read(u32, address);
|
||||||
|
result = rotr(u32, value, 8 * (address & 0x3));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (B) {
|
||||||
|
// STRB
|
||||||
|
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
|
||||||
|
bus.write(u8, address, @truncate(u8, value));
|
||||||
|
} else {
|
||||||
|
// STR
|
||||||
|
const value = if (rd == 0xF) cpu.r[rd] + 8 else cpu.r[rd];
|
||||||
|
bus.write(u32, address, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address = modified_base;
|
||||||
|
if (W and P or !P) cpu.r[rn] = address;
|
||||||
|
if (L) cpu.r[rd] = result; // This emulates the LDR rd == rn behaviour
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
|
pub fn armSoftwareInterrupt() InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u32) void {
|
||||||
|
// Copy Values from Current Mode
|
||||||
|
const r15 = cpu.r[15];
|
||||||
|
const cpsr = cpu.cpsr.raw;
|
||||||
|
|
||||||
|
// Switch Mode
|
||||||
|
cpu.changeMode(.Supervisor);
|
||||||
|
cpu.cpsr.t.write(false); // Force ARM Mode
|
||||||
|
cpu.cpsr.i.write(true); // Disable normal interrupts
|
||||||
|
|
||||||
|
cpu.r[14] = r15; // Resume Execution
|
||||||
|
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||||
|
cpu.r[15] = 0x0000_0008;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
|
const CPSR = @import("../cpu.zig").PSR;
|
||||||
|
|
||||||
|
const rotr = @import("../../util.zig").rotr;
|
||||||
|
|
||||||
|
pub fn execute(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (opcode >> 4 & 1 == 1) {
|
||||||
|
result = registerShift(S, cpu, opcode);
|
||||||
|
} else {
|
||||||
|
result = immShift(S, cpu, opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registerShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
|
const rs_idx = opcode >> 8 & 0xF;
|
||||||
|
const rs = @truncate(u8, cpu.r[rs_idx]);
|
||||||
|
|
||||||
|
const rm_idx = opcode & 0xF;
|
||||||
|
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
||||||
|
|
||||||
|
return switch (@truncate(u2, opcode >> 5)) {
|
||||||
|
0b00 => logicalLeft(S, &cpu.cpsr, rm, rs),
|
||||||
|
0b01 => logicalRight(S, &cpu.cpsr, rm, rs),
|
||||||
|
0b10 => arithmeticRight(S, &cpu.cpsr, rm, rs),
|
||||||
|
0b11 => rotateRight(S, &cpu.cpsr, rm, rs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn immShift(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
|
const amount = @truncate(u8, opcode >> 7 & 0x1F);
|
||||||
|
|
||||||
|
const rm_idx = opcode & 0xF;
|
||||||
|
const rm = if (rm_idx == 0xF) cpu.fakePC() else cpu.r[rm_idx];
|
||||||
|
|
||||||
|
var result: u32 = undefined;
|
||||||
|
if (amount == 0) {
|
||||||
|
switch (@truncate(u2, opcode >> 5)) {
|
||||||
|
0b00 => {
|
||||||
|
// LSL #0
|
||||||
|
result = rm;
|
||||||
|
},
|
||||||
|
0b01 => {
|
||||||
|
// LSR #0 aka LSR #32
|
||||||
|
if (S) cpu.cpsr.c.write(rm >> 31 & 1 == 1);
|
||||||
|
result = 0x0000_0000;
|
||||||
|
},
|
||||||
|
0b10 => {
|
||||||
|
// ASR #0 aka ASR #32
|
||||||
|
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
|
||||||
|
if (S) cpu.cpsr.c.write(result >> 31 & 1 == 1);
|
||||||
|
},
|
||||||
|
0b11 => {
|
||||||
|
// ROR #0 aka RRX
|
||||||
|
const carry: u32 = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
if (S) cpu.cpsr.c.write(rm & 1 == 1);
|
||||||
|
|
||||||
|
result = (carry << 31) | (rm >> 1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (@truncate(u2, opcode >> 5)) {
|
||||||
|
0b00 => result = logicalLeft(S, &cpu.cpsr, rm, amount),
|
||||||
|
0b01 => result = logicalRight(S, &cpu.cpsr, rm, amount),
|
||||||
|
0b10 => result = arithmeticRight(S, &cpu.cpsr, rm, amount),
|
||||||
|
0b11 => result = rotateRight(S, &cpu.cpsr, rm, amount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logicalLeft(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
|
const amount = @truncate(u5, total_amount);
|
||||||
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
|
var result: u32 = 0x0000_0000;
|
||||||
|
if (total_amount < bit_count) {
|
||||||
|
// We can perform a well-defined shift here
|
||||||
|
result = rm << amount;
|
||||||
|
|
||||||
|
if (S and total_amount != 0) {
|
||||||
|
const carry_bit = @truncate(u5, bit_count - amount);
|
||||||
|
cpsr.c.write(rm >> carry_bit & 1 == 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (S) {
|
||||||
|
if (total_amount == bit_count) {
|
||||||
|
// Shifted all bits out, carry bit is bit 0 of rm
|
||||||
|
cpsr.c.write(rm & 1 == 1);
|
||||||
|
} else {
|
||||||
|
cpsr.c.write(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logicalRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u32) u32 {
|
||||||
|
const amount = @truncate(u5, total_amount);
|
||||||
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
|
var result: u32 = 0x0000_0000;
|
||||||
|
if (total_amount < bit_count) {
|
||||||
|
// We can perform a well-defined shift
|
||||||
|
result = rm >> amount;
|
||||||
|
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
|
||||||
|
} else {
|
||||||
|
if (S) {
|
||||||
|
if (total_amount == bit_count) {
|
||||||
|
// LSR #32
|
||||||
|
cpsr.c.write(rm >> 31 & 1 == 1);
|
||||||
|
} else {
|
||||||
|
// All bits have been shifted out, including carry bit
|
||||||
|
cpsr.c.write(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arithmeticRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
|
const amount = @truncate(u5, total_amount);
|
||||||
|
const bit_count: u8 = @typeInfo(u32).Int.bits;
|
||||||
|
|
||||||
|
var result: u32 = 0x0000_0000;
|
||||||
|
if (total_amount < bit_count) {
|
||||||
|
result = @bitCast(u32, @bitCast(i32, rm) >> amount);
|
||||||
|
if (S and total_amount != 0) cpsr.c.write(rm >> (amount - 1) & 1 == 1);
|
||||||
|
} else {
|
||||||
|
// ASR #32 and ASR #>32 have the same result
|
||||||
|
result = @bitCast(u32, @bitCast(i32, rm) >> 31);
|
||||||
|
if (S) cpsr.c.write(result >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotateRight(comptime S: bool, cpsr: *CPSR, rm: u32, total_amount: u8) u32 {
|
||||||
|
const result = rotr(u32, rm, total_amount);
|
||||||
|
|
||||||
|
if (S and total_amount != 0) {
|
||||||
|
cpsr.c.write(result >> 31 & 1 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
|
const adc = @import("../arm/data_processing.zig").adc;
|
||||||
|
const sbc = @import("../arm/data_processing.zig").sbc;
|
||||||
|
const sub = @import("../arm/data_processing.zig").sub;
|
||||||
|
const cmp = @import("../arm/data_processing.zig").cmp;
|
||||||
|
const cmn = @import("../arm/data_processing.zig").cmn;
|
||||||
|
const setTestOpFlags = @import("../arm/data_processing.zig").setTestOpFlags;
|
||||||
|
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
||||||
|
|
||||||
|
const logicalLeft = @import("../barrel_shifter.zig").logicalLeft;
|
||||||
|
const logicalRight = @import("../barrel_shifter.zig").logicalRight;
|
||||||
|
const arithmeticRight = @import("../barrel_shifter.zig").arithmeticRight;
|
||||||
|
const rotateRight = @import("../barrel_shifter.zig").rotateRight;
|
||||||
|
|
||||||
|
pub fn fmt4(comptime op: u4) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const rs = opcode >> 3 & 0x7;
|
||||||
|
const rd = opcode & 0x7;
|
||||||
|
const carry = @boolToInt(cpu.cpsr.c.read());
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
0x0 => {
|
||||||
|
// AND
|
||||||
|
const result = cpu.r[rd] & cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x1 => {
|
||||||
|
// EOR
|
||||||
|
const result = cpu.r[rd] ^ cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x2 => {
|
||||||
|
// LSL
|
||||||
|
const result = logicalLeft(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x3 => {
|
||||||
|
// LSR
|
||||||
|
const result = logicalRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x4 => {
|
||||||
|
// ASR
|
||||||
|
const result = arithmeticRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x5 => {
|
||||||
|
// ADC
|
||||||
|
cpu.r[rd] = adc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
||||||
|
},
|
||||||
|
0x6 => {
|
||||||
|
// SBC
|
||||||
|
cpu.r[rd] = sbc(true, cpu, cpu.r[rd], cpu.r[rs], carry);
|
||||||
|
},
|
||||||
|
0x7 => {
|
||||||
|
// ROR
|
||||||
|
const result = rotateRight(true, &cpu.cpsr, cpu.r[rd], @truncate(u8, cpu.r[rs]));
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x8 => {
|
||||||
|
// TST
|
||||||
|
const result = cpu.r[rd] & cpu.r[rs];
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0x9 => {
|
||||||
|
// NEG
|
||||||
|
cpu.r[rd] = sub(true, cpu, 0, cpu.r[rs]);
|
||||||
|
},
|
||||||
|
0xA => {
|
||||||
|
// CMP
|
||||||
|
cmp(cpu, cpu.r[rd], cpu.r[rs]);
|
||||||
|
},
|
||||||
|
0xB => {
|
||||||
|
// CMN
|
||||||
|
cmn(cpu, cpu.r[rd], cpu.r[rs]);
|
||||||
|
},
|
||||||
|
0xC => {
|
||||||
|
// ORR
|
||||||
|
const result = cpu.r[rd] | cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0xD => {
|
||||||
|
// MUL
|
||||||
|
const temp = @as(u64, cpu.r[rs]) * @as(u64, cpu.r[rd]);
|
||||||
|
const result = @truncate(u32, temp);
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
|
||||||
|
cpu.cpsr.n.write(result >> 31 & 1 == 1);
|
||||||
|
cpu.cpsr.z.write(result == 0);
|
||||||
|
// V is unaffected, assuming similar behaviour to ARMv4 MUL C is undefined
|
||||||
|
},
|
||||||
|
0xE => {
|
||||||
|
// BIC
|
||||||
|
const result = cpu.r[rd] & ~cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
0xF => {
|
||||||
|
// MVN
|
||||||
|
const result = ~cpu.r[rs];
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
|
pub fn fmt14(comptime L: bool, comptime R: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
const count = @boolToInt(R) + countRlist(opcode);
|
||||||
|
const start = cpu.r[13] - if (!L) count * 4 else 0;
|
||||||
|
|
||||||
|
var end = cpu.r[13];
|
||||||
|
if (L) {
|
||||||
|
end += count * 4;
|
||||||
|
} else {
|
||||||
|
end -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = start;
|
||||||
|
|
||||||
|
var i: u4 = 0;
|
||||||
|
while (i < 8) : (i += 1) {
|
||||||
|
if (opcode >> i & 1 == 1) {
|
||||||
|
if (L) {
|
||||||
|
cpu.r[i] = bus.read(u32, address);
|
||||||
|
} else {
|
||||||
|
bus.write(u32, address, cpu.r[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
address += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (R) {
|
||||||
|
if (L) {
|
||||||
|
const value = bus.read(u32, address);
|
||||||
|
cpu.r[15] = value & 0xFFFF_FFFE;
|
||||||
|
} else {
|
||||||
|
bus.write(u32, address, cpu.r[14]);
|
||||||
|
}
|
||||||
|
address += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.r[13] = if (L) end else start;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
var address = cpu.r[rb];
|
||||||
|
const end_address = cpu.r[rb] + 4 * countRlist(opcode);
|
||||||
|
|
||||||
|
if (opcode & 0xFF == 0) {
|
||||||
|
if (L) cpu.r[15] = bus.read(u32, address) else bus.write(u32, address, cpu.r[15] + 4);
|
||||||
|
cpu.r[rb] += 0x40;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i: u4 = 0;
|
||||||
|
var first_write = true;
|
||||||
|
|
||||||
|
while (i < 8) : (i += 1) {
|
||||||
|
if (opcode >> i & 1 == 1) {
|
||||||
|
if (L) {
|
||||||
|
cpu.r[i] = bus.read(u32, address);
|
||||||
|
} else {
|
||||||
|
bus.write(u32, address, cpu.r[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!L and first_write) {
|
||||||
|
cpu.r[rb] = end_address;
|
||||||
|
first_write = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
address += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (L and opcode >> rb & 1 != 1) cpu.r[rb] = address;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn countRlist(opcode: u16) u32 {
|
||||||
|
var count: u32 = 0;
|
||||||
|
|
||||||
|
comptime var i: u4 = 0;
|
||||||
|
inline while (i < 8) : (i += 1) {
|
||||||
|
if (opcode >> (7 - i) & 1 == 1) count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
|
const checkCond = @import("../../cpu.zig").checkCond;
|
||||||
|
const sext = @import("../../../util.zig").sext;
|
||||||
|
|
||||||
|
pub fn fmt16(comptime cond: u4) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
// B
|
||||||
|
const offset = sext(u32, u8, opcode & 0xFF) << 1;
|
||||||
|
|
||||||
|
const should_execute = switch (cond) {
|
||||||
|
0xE, 0xF => cpu.panic("[CPU/THUMB.16] Undefined conditional branch with condition {}", .{cond}),
|
||||||
|
else => checkCond(cpu.cpsr, cond),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (should_execute) {
|
||||||
|
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt18() InstrFn {
|
||||||
|
return struct {
|
||||||
|
// B but conditional
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const offset = sext(u32, u11, opcode & 0x7FF) << 1;
|
||||||
|
cpu.r[15] = (cpu.r[15] + 2) +% offset;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt19(comptime is_low: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
// BL
|
||||||
|
const offset = opcode & 0x7FF;
|
||||||
|
|
||||||
|
if (is_low) {
|
||||||
|
// Instruction 2
|
||||||
|
const old_pc = cpu.r[15];
|
||||||
|
|
||||||
|
cpu.r[15] = cpu.r[14] +% (offset << 1);
|
||||||
|
cpu.r[14] = old_pc | 1;
|
||||||
|
} else {
|
||||||
|
// Instruction 1
|
||||||
|
cpu.r[14] = (cpu.r[15] + 2) +% (sext(u32, u11, offset) << 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
const shifter = @import("../barrel_shifter.zig");
|
||||||
|
|
||||||
|
const add = @import("../arm/data_processing.zig").add;
|
||||||
|
const sub = @import("../arm/data_processing.zig").sub;
|
||||||
|
const cmp = @import("../arm/data_processing.zig").cmp;
|
||||||
|
const setLogicOpFlags = @import("../arm/data_processing.zig").setLogicOpFlags;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.Thumb1);
|
||||||
|
|
||||||
|
pub fn fmt1(comptime op: u2, comptime offset: u5) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const rs = opcode >> 3 & 0x7;
|
||||||
|
const rd = opcode & 0x7;
|
||||||
|
|
||||||
|
const result = switch (op) {
|
||||||
|
0b00 => blk: {
|
||||||
|
// LSL
|
||||||
|
if (offset == 0) {
|
||||||
|
break :blk cpu.r[rs];
|
||||||
|
} else {
|
||||||
|
break :blk shifter.logicalLeft(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0b01 => blk: {
|
||||||
|
// LSR
|
||||||
|
if (offset == 0) {
|
||||||
|
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||||
|
break :blk @as(u32, 0);
|
||||||
|
} else {
|
||||||
|
break :blk shifter.logicalRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0b10 => blk: {
|
||||||
|
// ASR
|
||||||
|
if (offset == 0) {
|
||||||
|
cpu.cpsr.c.write(cpu.r[rs] >> 31 & 1 == 1);
|
||||||
|
break :blk @bitCast(u32, @bitCast(i32, cpu.r[rs]) >> 31);
|
||||||
|
} else {
|
||||||
|
break :blk shifter.arithmeticRight(true, &cpu.cpsr, cpu.r[rs], offset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => cpu.panic("[CPU/THUMB.1] 0b{b:0>2} is not a valid op", .{op}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Equivalent to an ARM MOVS
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
setLogicOpFlags(true, cpu, result);
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt5(comptime op: u2, comptime h1: u1, comptime h2: u1) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const src_idx = @as(u4, h2) << 3 | (opcode >> 3 & 0x7);
|
||||||
|
const dst_idx = @as(u4, h1) << 3 | (opcode & 0x7);
|
||||||
|
|
||||||
|
const src = if (src_idx == 0xF) (cpu.r[src_idx] + 2) & 0xFFFF_FFFE else cpu.r[src_idx];
|
||||||
|
const dst = if (dst_idx == 0xF) (cpu.r[dst_idx] + 2) & 0xFFFF_FFFE else cpu.r[dst_idx];
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
0b00 => {
|
||||||
|
// ADD
|
||||||
|
const sum = add(false, cpu, dst, src);
|
||||||
|
cpu.r[dst_idx] = if (dst_idx == 0xF) sum & 0xFFFF_FFFE else sum;
|
||||||
|
},
|
||||||
|
0b01 => cmp(cpu, dst, src), // CMP
|
||||||
|
0b10 => {
|
||||||
|
// MOV
|
||||||
|
cpu.r[dst_idx] = if (dst_idx == 0xF) src & 0xFFFF_FFFE else src;
|
||||||
|
},
|
||||||
|
0b11 => {
|
||||||
|
// BX
|
||||||
|
cpu.cpsr.t.write(src & 1 == 1);
|
||||||
|
cpu.r[15] = src & 0xFFFF_FFFE;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt2(comptime I: bool, is_sub: bool, rn: u3) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const rs = opcode >> 3 & 0x7;
|
||||||
|
const rd = @truncate(u3, opcode);
|
||||||
|
|
||||||
|
if (is_sub) {
|
||||||
|
// SUB
|
||||||
|
cpu.r[rd] = if (I) blk: {
|
||||||
|
break :blk sub(true, cpu, cpu.r[rs], rn);
|
||||||
|
} else blk: {
|
||||||
|
break :blk sub(true, cpu, cpu.r[rs], cpu.r[rn]);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// ADD
|
||||||
|
cpu.r[rd] = if (I) blk: {
|
||||||
|
break :blk add(true, cpu, cpu.r[rs], rn);
|
||||||
|
} else blk: {
|
||||||
|
break :blk add(true, cpu, cpu.r[rs], cpu.r[rn]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt3(comptime op: u2, comptime rd: u3) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
const offset = @truncate(u8, opcode);
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
0b00 => {
|
||||||
|
// MOV
|
||||||
|
cpu.r[rd] = offset;
|
||||||
|
setLogicOpFlags(true, cpu, offset);
|
||||||
|
},
|
||||||
|
0b01 => cmp(cpu, cpu.r[rd], offset), // CMP
|
||||||
|
0b10 => cpu.r[rd] = add(true, cpu, cpu.r[rd], offset), // ADD
|
||||||
|
0b11 => cpu.r[rd] = sub(true, cpu, cpu.r[rd], offset), // SUB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt12(comptime isSP: bool, comptime rd: u3) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
// ADD
|
||||||
|
const left = if (isSP) cpu.r[13] else (cpu.r[15] + 2) & 0xFFFF_FFFD;
|
||||||
|
const right = (opcode & 0xFF) << 2;
|
||||||
|
const result = left + right;
|
||||||
|
cpu.r[rd] = result;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt13(comptime S: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, opcode: u16) void {
|
||||||
|
// ADD
|
||||||
|
const offset = (opcode & 0x7F) << 2;
|
||||||
|
cpu.r[13] = if (S) cpu.r[13] - offset else cpu.r[13] + offset;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
|
const rotr = @import("../../../util.zig").rotr;
|
||||||
|
const sext = @import("../../../util.zig").sext;
|
||||||
|
|
||||||
|
pub fn fmt6(comptime rd: u3) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
// LDR
|
||||||
|
const offset = (opcode & 0xFF) << 2;
|
||||||
|
cpu.r[rd] = bus.read(u32, (cpu.r[15] + 2 & 0xFFFF_FFFD) + offset);
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt78(comptime op: u2, comptime T: bool) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
const ro = opcode >> 6 & 0x7;
|
||||||
|
const rb = opcode >> 3 & 0x7;
|
||||||
|
const rd = opcode & 0x7;
|
||||||
|
|
||||||
|
const address = cpu.r[rb] +% cpu.r[ro];
|
||||||
|
|
||||||
|
if (T) {
|
||||||
|
// Format 8
|
||||||
|
switch (op) {
|
||||||
|
0b00 => {
|
||||||
|
// STRH
|
||||||
|
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
|
||||||
|
},
|
||||||
|
0b01 => {
|
||||||
|
// LDSB
|
||||||
|
cpu.r[rd] = sext(u32, u8, bus.read(u8, address));
|
||||||
|
},
|
||||||
|
0b10 => {
|
||||||
|
// LDRH
|
||||||
|
const value = bus.read(u16, address);
|
||||||
|
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
|
||||||
|
},
|
||||||
|
0b11 => {
|
||||||
|
// LDRSH
|
||||||
|
cpu.r[rd] = if (address & 1 == 1) blk: {
|
||||||
|
break :blk sext(u32, u8, bus.read(u8, address));
|
||||||
|
} else blk: {
|
||||||
|
break :blk sext(u32, u16, bus.read(u16, address));
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Format 7
|
||||||
|
switch (op) {
|
||||||
|
0b00 => {
|
||||||
|
// STR
|
||||||
|
bus.write(u32, address, cpu.r[rd]);
|
||||||
|
},
|
||||||
|
0b01 => {
|
||||||
|
// STRB
|
||||||
|
bus.write(u8, address, @truncate(u8, cpu.r[rd]));
|
||||||
|
},
|
||||||
|
0b10 => {
|
||||||
|
// LDR
|
||||||
|
const value = bus.read(u32, address);
|
||||||
|
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
|
||||||
|
},
|
||||||
|
0b11 => {
|
||||||
|
// LDRB
|
||||||
|
cpu.r[rd] = bus.read(u8, address);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt9(comptime B: bool, comptime L: bool, comptime offset: u5) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
const rb = opcode >> 3 & 0x7;
|
||||||
|
const rd = opcode & 0x7;
|
||||||
|
|
||||||
|
if (L) {
|
||||||
|
if (B) {
|
||||||
|
// LDRB
|
||||||
|
const address = cpu.r[rb] + offset;
|
||||||
|
cpu.r[rd] = bus.read(u8, address);
|
||||||
|
} else {
|
||||||
|
// LDR
|
||||||
|
const address = cpu.r[rb] + (@as(u32, offset) << 2);
|
||||||
|
const value = bus.read(u32, address);
|
||||||
|
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (B) {
|
||||||
|
// STRB
|
||||||
|
const address = cpu.r[rb] + offset;
|
||||||
|
bus.write(u8, address, @truncate(u8, cpu.r[rd]));
|
||||||
|
} else {
|
||||||
|
// STR
|
||||||
|
const address = cpu.r[rb] + (@as(u32, offset) << 2);
|
||||||
|
bus.write(u32, address, cpu.r[rd]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt10(comptime L: bool, comptime offset: u5) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
const rb = opcode >> 3 & 0x7;
|
||||||
|
const rd = opcode & 0x7;
|
||||||
|
|
||||||
|
const address = cpu.r[rb] + (@as(u6, offset) << 1);
|
||||||
|
|
||||||
|
if (L) {
|
||||||
|
// LDRH
|
||||||
|
const value = bus.read(u16, address);
|
||||||
|
cpu.r[rd] = rotr(u32, value, 8 * (address & 1));
|
||||||
|
} else {
|
||||||
|
// STRH
|
||||||
|
bus.write(u16, address, @truncate(u16, cpu.r[rd]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt11(comptime L: bool, comptime rd: u3) InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, bus: *Bus, opcode: u16) void {
|
||||||
|
const offset = (opcode & 0xFF) << 2;
|
||||||
|
const address = cpu.r[13] + offset;
|
||||||
|
|
||||||
|
if (L) {
|
||||||
|
// LDR
|
||||||
|
const value = bus.read(u32, address);
|
||||||
|
cpu.r[rd] = rotr(u32, value, 8 * (address & 0x3));
|
||||||
|
} else {
|
||||||
|
// STR
|
||||||
|
bus.write(u32, address, cpu.r[rd]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
const Bus = @import("../../Bus.zig");
|
||||||
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
|
pub fn fmt17() InstrFn {
|
||||||
|
return struct {
|
||||||
|
fn inner(cpu: *Arm7tdmi, _: *Bus, _: u16) void {
|
||||||
|
// Copy Values from Current Mode
|
||||||
|
const r15 = cpu.r[15];
|
||||||
|
const cpsr = cpu.cpsr.raw;
|
||||||
|
|
||||||
|
// Switch Mode
|
||||||
|
cpu.changeMode(.Supervisor);
|
||||||
|
cpu.cpsr.t.write(false); // Force ARM Mode
|
||||||
|
cpu.cpsr.i.write(true); // Disable normal interrupts
|
||||||
|
|
||||||
|
cpu.r[14] = r15; // Resume Execution
|
||||||
|
cpu.spsr.raw = cpsr; // Previous mode CPSR
|
||||||
|
cpu.r[15] = 0x0000_0008;
|
||||||
|
}
|
||||||
|
}.inner;
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const SDL = @import("sdl2");
|
||||||
|
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
|
const FpsTracker = @import("../util.zig").FpsTracker;
|
||||||
|
const FilePaths = @import("../util.zig").FilePaths;
|
||||||
|
|
||||||
|
const Timer = std.time.Timer;
|
||||||
|
const Thread = std.Thread;
|
||||||
|
const Atomic = std.atomic.Atomic;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
// TODO: Move these to a TOML File
|
||||||
|
const sync_audio = false; // Enable Audio Sync
|
||||||
|
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
|
||||||
|
pub const win_scale = 4; // 1x, 2x, 3x, etc. Window Scaling
|
||||||
|
pub const cpu_logging = false; // Enable detailed CPU logging
|
||||||
|
pub const allow_unhandled_io = true; // Only relevant in Debug Builds
|
||||||
|
pub const force_rtc = false;
|
||||||
|
|
||||||
|
// 228 Lines which consist of 308 dots (which are 4 cycles long)
|
||||||
|
const cycles_per_frame: u64 = 228 * (308 * 4); //280896
|
||||||
|
const clock_rate: u64 = 1 << 24; // 16.78MHz
|
||||||
|
|
||||||
|
// TODO: Don't truncate this, be more accurate w/ timing
|
||||||
|
// 59.6046447754ns (truncated to just 59ns)
|
||||||
|
const clock_period: u64 = std.time.ns_per_s / clock_rate;
|
||||||
|
const frame_period = (clock_period * cycles_per_frame);
|
||||||
|
|
||||||
|
// 59.7275005696Hz
|
||||||
|
pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) /
|
||||||
|
((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame));
|
||||||
|
|
||||||
|
const log = std.log.scoped(.Emulation);
|
||||||
|
|
||||||
|
const RunKind = enum {
|
||||||
|
Unlimited,
|
||||||
|
UnlimitedFPS,
|
||||||
|
Limited,
|
||||||
|
LimitedFPS,
|
||||||
|
LimitedBusy,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
|
if (sync_audio) log.info("Audio sync enabled", .{});
|
||||||
|
|
||||||
|
switch (sync_video) {
|
||||||
|
.Unlimited => runUnsynchronized(quit, sched, cpu, null),
|
||||||
|
.Limited => runSynchronized(quit, sched, cpu, null),
|
||||||
|
.UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps),
|
||||||
|
.LimitedFPS => runSynchronized(quit, sched, cpu, fps),
|
||||||
|
.LimitedBusy => runBusyLoop(quit, sched, cpu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
|
const frame_end = sched.tick + cycles_per_frame;
|
||||||
|
|
||||||
|
while (sched.tick < frame_end) {
|
||||||
|
if (!cpu.stepDmaTransfer()) {
|
||||||
|
if (cpu.isHalted()) {
|
||||||
|
// Fast-forward to next Event
|
||||||
|
sched.tick = sched.queue.peek().?.tick;
|
||||||
|
} else {
|
||||||
|
cpu.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void {
|
||||||
|
const sample_size = 2 * @sizeOf(u16);
|
||||||
|
const max_buf_size: c_int = 0x400;
|
||||||
|
|
||||||
|
// Determine whether the APU is busy right at this moment
|
||||||
|
var still_full: bool = SDL.SDL_AudioStreamAvailable(stream) > sample_size * if (is_buffer_full.*) max_buf_size >> 1 else max_buf_size;
|
||||||
|
defer is_buffer_full.* = still_full; // Update APU Busy status right before exiting scope
|
||||||
|
|
||||||
|
// If Busy is false, there's no need to sync here
|
||||||
|
if (!still_full) return;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1;
|
||||||
|
if (!sync_audio or !still_full) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
||||||
|
log.info("Emulation thread w/out video sync", .{});
|
||||||
|
|
||||||
|
if (fps) |tracker| {
|
||||||
|
log.info("FPS Tracking Enabled", .{});
|
||||||
|
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||||
|
|
||||||
|
tracker.tick();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void {
|
||||||
|
log.info("Emulation thread w/ video sync", .{});
|
||||||
|
var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{});
|
||||||
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
|
if (fps) |tracker| {
|
||||||
|
log.info("FPS Tracking Enabled", .{});
|
||||||
|
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
const new_wake_time = blockOnVideo(&timer, wake_time);
|
||||||
|
|
||||||
|
// Spin to make up the difference of OS scheduler innacuracies
|
||||||
|
// If we happen to also be syncing to audio, we choose to spin on
|
||||||
|
// the amount of time needed for audio to catch up rather than
|
||||||
|
// our expected wake-up time
|
||||||
|
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||||
|
if (!sync_audio) spinLoop(&timer, wake_time);
|
||||||
|
wake_time = new_wake_time;
|
||||||
|
|
||||||
|
tracker.tick();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
const new_wake_time = blockOnVideo(&timer, wake_time);
|
||||||
|
|
||||||
|
// see above comment
|
||||||
|
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||||
|
if (!sync_audio) spinLoop(&timer, wake_time);
|
||||||
|
wake_time = new_wake_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 {
|
||||||
|
// Use the OS scheduler to put the emulation thread to sleep
|
||||||
|
const maybe_recalc_wake_time = sleep(timer, wake_time);
|
||||||
|
|
||||||
|
// If sleep() determined we need to adjust our wake up time, do so
|
||||||
|
// otherwise predict our next wake up time according to the frame period
|
||||||
|
return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void {
|
||||||
|
log.info("Emulation thread with video sync using busy loop", .{});
|
||||||
|
var timer = Timer.start() catch unreachable;
|
||||||
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
|
while (!quit.load(.SeqCst)) {
|
||||||
|
runFrame(sched, cpu);
|
||||||
|
spinLoop(&timer, wake_time);
|
||||||
|
|
||||||
|
syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||||
|
|
||||||
|
// Update to the new wake time
|
||||||
|
wake_time += frame_period;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sleep(timer: *Timer, wake_time: u64) ?u64 {
|
||||||
|
// const step = std.time.ns_per_ms * 10; // 10ms
|
||||||
|
const timestamp = timer.read();
|
||||||
|
|
||||||
|
// ns_late is non zero if we are late.
|
||||||
|
const ns_late = timestamp -| wake_time;
|
||||||
|
|
||||||
|
// If we're more than a frame late, skip the rest of this loop
|
||||||
|
// Recalculate what our new wake time should be so that we can
|
||||||
|
// get "back on track"
|
||||||
|
if (ns_late > frame_period) return timestamp + frame_period;
|
||||||
|
const sleep_for = frame_period - ns_late;
|
||||||
|
|
||||||
|
// // Employ several sleep calls in periods of 10ms
|
||||||
|
// // By doing this the behaviour should average out to be
|
||||||
|
// // more consistent
|
||||||
|
// const loop_count = sleep_for / step; // How many groups of 10ms
|
||||||
|
|
||||||
|
// var i: usize = 0;
|
||||||
|
// while (i < loop_count) : (i += 1) std.time.sleep(step);
|
||||||
|
|
||||||
|
std.time.sleep(sleep_for);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spinLoop(timer: *Timer, wake_time: u64) void {
|
||||||
|
while (true) if (timer.read() > wake_time) break;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const buf_len = 0x400;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||||
|
else => @compileError("OAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||||
|
u8 => return, // 8-bit writes are explicitly ignored
|
||||||
|
else => @compileError("OAM: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, buf_len);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const buf_len = 0x400;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||||
|
else => @compileError("PALRAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, address: usize, value: T) void {
|
||||||
|
const addr = address & 0x3FF;
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)], value),
|
||||||
|
u8 => {
|
||||||
|
const align_addr = addr & ~@as(u32, 1); // Aligned to Halfword boundary
|
||||||
|
std.mem.writeIntSliceLittle(u16, self.buf[align_addr..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
|
||||||
|
},
|
||||||
|
else => @compileError("PALRAM: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, buf_len);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backdrop(self: *const Self) u16 {
|
||||||
|
return self.read(u16, 0);
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const io = @import("../bus/io.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const buf_len = 0x18000;
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
buf: []u8,
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
pub fn read(self: *const Self, comptime T: type, address: usize) T {
|
||||||
|
const addr = Self.mirror(address);
|
||||||
|
|
||||||
|
return switch (T) {
|
||||||
|
u32, u16, u8 => std.mem.readIntSliceLittle(T, self.buf[addr..][0..@sizeOf(T)]),
|
||||||
|
else => @compileError("VRAM: Unsupported read width"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: *Self, comptime T: type, dispcnt: io.DisplayControl, address: usize, value: T) void {
|
||||||
|
const mode: u3 = dispcnt.bg_mode.read();
|
||||||
|
const idx = Self.mirror(address);
|
||||||
|
|
||||||
|
switch (T) {
|
||||||
|
u32, u16 => std.mem.writeIntSliceLittle(T, self.buf[idx..][0..@sizeOf(T)], value),
|
||||||
|
u8 => {
|
||||||
|
// Ignore write if it falls within the boundaries of OBJ VRAM
|
||||||
|
switch (mode) {
|
||||||
|
0, 1, 2 => if (0x0001_0000 <= idx) return,
|
||||||
|
else => if (0x0001_4000 <= idx) return,
|
||||||
|
}
|
||||||
|
|
||||||
|
const align_idx = idx & ~@as(u32, 1); // Aligned to a halfword boundary
|
||||||
|
std.mem.writeIntSliceLittle(u16, self.buf[align_idx..][0..@sizeOf(u16)], @as(u16, value) * 0x101);
|
||||||
|
},
|
||||||
|
else => @compileError("VRAM: Unsupported write width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, buf_len);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirror(address: usize) usize {
|
||||||
|
// Mirrored in steps of 128K (64K + 32K + 32K) (abcc)
|
||||||
|
const addr = address & 0x1FFFF;
|
||||||
|
|
||||||
|
// If the address is within 96K we don't do anything,
|
||||||
|
// otherwise we want to mirror the last 32K (addresses between 64K and 96K)
|
||||||
|
return if (addr < buf_len) addr else 0x10000 + (addr & 0x7FFF);
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Bus = @import("Bus.zig");
|
||||||
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
|
const Clock = @import("bus/gpio.zig").Clock;
|
||||||
|
|
||||||
|
const Order = std.math.Order;
|
||||||
|
const PriorityQueue = std.PriorityQueue;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Scheduler);
|
||||||
|
|
||||||
|
pub const Scheduler = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
tick: u64,
|
||||||
|
queue: PriorityQueue(Event, void, lessThan),
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) Self {
|
||||||
|
var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) };
|
||||||
|
sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable;
|
||||||
|
|
||||||
|
return sched;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.queue.deinit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn now(self: *const Self) u64 {
|
||||||
|
return self.tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleEvent(self: *Self, cpu: *Arm7tdmi) void {
|
||||||
|
if (self.queue.removeOrNull()) |event| {
|
||||||
|
const late = self.tick - event.tick;
|
||||||
|
|
||||||
|
switch (event.kind) {
|
||||||
|
.HeatDeath => {
|
||||||
|
log.err("u64 overflow. This *actually* should never happen.", .{});
|
||||||
|
unreachable;
|
||||||
|
},
|
||||||
|
.Draw => {
|
||||||
|
// The end of a VDraw
|
||||||
|
cpu.bus.ppu.drawScanline();
|
||||||
|
cpu.bus.ppu.handleHDrawEnd(cpu, late);
|
||||||
|
},
|
||||||
|
.TimerOverflow => |id| {
|
||||||
|
switch (id) {
|
||||||
|
0 => cpu.bus.tim[0].handleOverflow(cpu, late),
|
||||||
|
1 => cpu.bus.tim[1].handleOverflow(cpu, late),
|
||||||
|
2 => cpu.bus.tim[2].handleOverflow(cpu, late),
|
||||||
|
3 => cpu.bus.tim[3].handleOverflow(cpu, late),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ApuChannel => |id| {
|
||||||
|
switch (id) {
|
||||||
|
0 => cpu.bus.apu.ch1.channelTimerOverflow(late),
|
||||||
|
1 => cpu.bus.apu.ch2.channelTimerOverflow(late),
|
||||||
|
2 => cpu.bus.apu.ch3.channelTimerOverflow(late),
|
||||||
|
3 => cpu.bus.apu.ch4.channelTimerOverflow(late),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.RealTimeClock => {
|
||||||
|
const device = &cpu.bus.pak.gpio.device;
|
||||||
|
if (device.kind != .Rtc or device.ptr == null) return;
|
||||||
|
|
||||||
|
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), device.ptr.?));
|
||||||
|
clock.updateTime(late);
|
||||||
|
},
|
||||||
|
.FrameSequencer => cpu.bus.apu.tickFrameSequencer(late),
|
||||||
|
.SampleAudio => cpu.bus.apu.sampleAudio(late),
|
||||||
|
.HBlank => cpu.bus.ppu.handleHBlankEnd(cpu, late), // The end of a HBlank
|
||||||
|
.VBlank => cpu.bus.ppu.handleHDrawEnd(cpu, late), // The end of a VBlank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the **first** scheduled event of type `needle`
|
||||||
|
pub fn removeScheduledEvent(self: *Self, needle: EventKind) void {
|
||||||
|
var it = self.queue.iterator();
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (it.next()) |event| : (i += 1) {
|
||||||
|
if (std.meta.eql(event.kind, needle)) {
|
||||||
|
|
||||||
|
// This invalidates the iterator
|
||||||
|
_ = self.queue.removeIndex(i);
|
||||||
|
|
||||||
|
// Since removing something from the PQ invalidates the iterator,
|
||||||
|
// this implementation can safely only remove the first instance of
|
||||||
|
// a Scheduled Event. Exit Early
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *Self, kind: EventKind, end: u64) void {
|
||||||
|
self.queue.add(.{ .kind = kind, .tick = self.now() + end }) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn nextTimestamp(self: *const Self) u64 {
|
||||||
|
@setRuntimeSafety(false);
|
||||||
|
|
||||||
|
// Typically you'd use PriorityQueue.peek here, but there's always at least a HeatDeath
|
||||||
|
// event in the PQ so we can just do this instead. Should be faster in ReleaseSafe
|
||||||
|
return self.queue.items[0].tick;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Event = struct {
|
||||||
|
kind: EventKind,
|
||||||
|
tick: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn lessThan(_: void, a: Event, b: Event) Order {
|
||||||
|
return std.math.order(a.tick, b.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const EventKind = union(enum) {
|
||||||
|
HeatDeath,
|
||||||
|
HBlank,
|
||||||
|
VBlank,
|
||||||
|
Draw,
|
||||||
|
TimerOverflow: u2,
|
||||||
|
SampleAudio,
|
||||||
|
FrameSequencer,
|
||||||
|
ApuChannel: u2,
|
||||||
|
RealTimeClock,
|
||||||
|
};
|
152
src/cpu.zig
152
src/cpu.zig
|
@ -1,152 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const Bus = @import("bus.zig").Bus;
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
|
||||||
|
|
||||||
const comptimeDataProcessing = @import("cpu/data_processing.zig").comptimeDataProcessing;
|
|
||||||
const comptimeSingleDataTransfer = @import("cpu/single_data_transfer.zig").comptimeSingleDataTransfer;
|
|
||||||
const comptimeHalfSignedDataTransfer = @import("cpu/half_signed_data_transfer.zig").comptimeHalfSignedDataTransfer;
|
|
||||||
|
|
||||||
pub const InstrFn = fn (*ARM7TDMI, *Bus, u32) void;
|
|
||||||
const ARM_LUT: [0x1000]InstrFn = populate();
|
|
||||||
|
|
||||||
pub const ARM7TDMI = struct {
|
|
||||||
r: [16]u32,
|
|
||||||
sch: *Scheduler,
|
|
||||||
bus: *Bus,
|
|
||||||
cpsr: CPSR,
|
|
||||||
|
|
||||||
pub fn new(scheduler: *Scheduler, bus: *Bus) @This() {
|
|
||||||
const cpsr: u32 = 0x0000_00DF;
|
|
||||||
return .{
|
|
||||||
.r = [_]u32{0x00} ** 16,
|
|
||||||
.sch = scheduler,
|
|
||||||
.bus = bus,
|
|
||||||
.cpsr = @bitCast(CPSR, cpsr),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn step(self: *@This()) u64 {
|
|
||||||
const opcode = self.fetch();
|
|
||||||
// Debug
|
|
||||||
std.debug.print("R15: 0x{X:}\n", .{ opcode });
|
|
||||||
|
|
||||||
ARM_LUT[armIdx(opcode)](self, self.bus, opcode);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch(self: *@This()) u32 {
|
|
||||||
const word = self.bus.readWord(self.r[15]);
|
|
||||||
self.r[15] += 4;
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fakePC(self: *const @This()) u32 {
|
|
||||||
return self.r[15] + 4;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn armIdx(opcode: u32) u12 {
|
|
||||||
return @truncate(u12, opcode >> 20 & 0xFF) << 4 | @truncate(u12, opcode >> 8 & 0xF);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn populate() [0x1000]InstrFn {
|
|
||||||
return comptime {
|
|
||||||
@setEvalBranchQuota(0x5000);
|
|
||||||
var lut = [_]InstrFn{undefined_instr} ** 0x1000;
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < lut.len) : (i += 1) {
|
|
||||||
if (i >> 10 & 0x3 == 0b00) {
|
|
||||||
const I = i >> 9 & 0x01 == 0x01;
|
|
||||||
const S = i >> 4 & 0x01 == 0x01;
|
|
||||||
const instrKind = i >> 5 & 0x0F;
|
|
||||||
|
|
||||||
lut[i] = comptimeDataProcessing(I, S, instrKind);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >> 9 & 0x7 == 0b000 and i >> 6 & 0x01 == 0x00 and i & 0xF == 0x0) {
|
|
||||||
// Halfword and Signed Data Transfer with register offset
|
|
||||||
const P = i >> 8 & 0x01 == 0x01;
|
|
||||||
const U = i >> 7 & 0x01 == 0x01;
|
|
||||||
const I = true;
|
|
||||||
const W = i >> 5 & 0x01 == 0x01;
|
|
||||||
const L = i >> 4 & 0x01 == 0x01;
|
|
||||||
|
|
||||||
lut[i] = comptimeHalfSignedDataTransfer(P, U, I, W, L);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >> 9 & 0x7 == 0b000 and i >> 6 & 0x01 == 0x01) {
|
|
||||||
// Halfword and Signed Data Tranfer with immediate offset
|
|
||||||
const P = i >> 8 & 0x01 == 0x01;
|
|
||||||
const U = i >> 7 & 0x01 == 0x01;
|
|
||||||
const I = false;
|
|
||||||
const W = i >> 5 & 0x01 == 0x01;
|
|
||||||
const L = i >> 4 & 0x01 == 0x01;
|
|
||||||
|
|
||||||
lut[i] = comptimeHalfSignedDataTransfer(P, U, I, W, L);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >> 10 & 0x3 == 0b01 and i & 0x01 == 0x00) {
|
|
||||||
const I = i >> 9 & 0x01 == 0x01;
|
|
||||||
const P = i >> 8 & 0x01 == 0x01;
|
|
||||||
const U = i >> 7 & 0x01 == 0x01;
|
|
||||||
const B = i >> 6 & 0x01 == 0x01;
|
|
||||||
const W = i >> 5 & 0x01 == 0x01;
|
|
||||||
const L = i >> 4 & 0x01 == 0x01;
|
|
||||||
|
|
||||||
lut[i] = comptimeSingleDataTransfer(I, P, U, B, W, L);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >> 9 & 0x7 == 0b101) {
|
|
||||||
const L = i >> 8 & 0x01 == 0x01;
|
|
||||||
lut[i] = comptimeBranch(L);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lut;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const CPSR = packed struct {
|
|
||||||
n: bool, // Negative / Less Than
|
|
||||||
z: bool, // Zero
|
|
||||||
c: bool, // Carry / Borrow / Extend
|
|
||||||
v: bool, // Overflow
|
|
||||||
_: u20,
|
|
||||||
i: bool, // IRQ Disable
|
|
||||||
f: bool, // FIQ Diable
|
|
||||||
t: bool, // State
|
|
||||||
m: Mode, // Mode
|
|
||||||
};
|
|
||||||
|
|
||||||
const Mode = enum(u5) {
|
|
||||||
User = 0b10000,
|
|
||||||
Fiq = 0b10001,
|
|
||||||
Irq = 0b10010,
|
|
||||||
Supervisor = 0b10011,
|
|
||||||
Abort = 0b10111,
|
|
||||||
Undefined = 0b11011,
|
|
||||||
System = 0b11111,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn undefined_instr(_: *ARM7TDMI, _: *Bus, opcode: u32) void {
|
|
||||||
const id = armIdx(opcode);
|
|
||||||
std.debug.panic("[0x{X:}] 0x{X:} is an illegal opcode", .{ id, opcode });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn comptimeBranch(comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn branch(cpu: *ARM7TDMI, _: *Bus, opcode: u32) void {
|
|
||||||
if (L) {
|
|
||||||
cpu.r[14] = cpu.r[15] - 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = @bitCast(i32, (opcode << 2) << 8) >> 8;
|
|
||||||
cpu.r[15] = cpu.fakePC() + @bitCast(u32, offset);
|
|
||||||
}
|
|
||||||
}.branch;
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const cpu_mod = @import("../cpu.zig");
|
|
||||||
|
|
||||||
const Bus = @import("../bus.zig").Bus;
|
|
||||||
const ARM7TDMI = cpu_mod.ARM7TDMI;
|
|
||||||
const InstrFn = cpu_mod.InstrFn;
|
|
||||||
|
|
||||||
pub fn comptimeDataProcessing(comptime I: bool, comptime S: bool, comptime instrKind: u4) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn dataProcessing(cpu: *ARM7TDMI, _: *Bus, opcode: u32) void {
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
const op1 = opcode >> 16 & 0xF;
|
|
||||||
|
|
||||||
var op2: u32 = undefined;
|
|
||||||
if (I) {
|
|
||||||
op2 = std.math.rotr(u32, opcode & 0xFF, (opcode >> 8 & 0xF) << 1);
|
|
||||||
} else {
|
|
||||||
op2 = reg_op2(cpu, opcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (instrKind) {
|
|
||||||
0x4 => {
|
|
||||||
cpu.r[rd] = cpu.r[op1] + op2;
|
|
||||||
|
|
||||||
if (S) std.debug.panic("TODO: implement ADD condition codes", .{});
|
|
||||||
},
|
|
||||||
0xD => {
|
|
||||||
cpu.r[rd] = op2;
|
|
||||||
|
|
||||||
if (S) std.debug.panic("TODO: implement MOV condition codes", .{});
|
|
||||||
},
|
|
||||||
else => std.debug.panic("TODO: implement data processing type {}", .{instrKind}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.dataProcessing;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reg_op2(cpu: *const ARM7TDMI, opcode: u32) u32 {
|
|
||||||
var amount: u32 = undefined;
|
|
||||||
if (opcode >> 4 & 0x01 == 0x01) {
|
|
||||||
amount = cpu.r[opcode >> 8 & 0xF] & 0xFF;
|
|
||||||
} else {
|
|
||||||
amount = opcode >> 7 & 0x1F;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
const r_val = cpu.r[rm];
|
|
||||||
|
|
||||||
return switch (opcode >> 5 & 0x03) {
|
|
||||||
0b00 => r_val << @truncate(u5, amount),
|
|
||||||
0b01 => r_val >> @truncate(u5, amount),
|
|
||||||
0b10 => @bitCast(u32, @bitCast(i32, r_val) >> @truncate(u5, amount)),
|
|
||||||
0b11 => std.math.rotr(u32, r_val, amount),
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const cpu_mod = @import("../cpu.zig");
|
|
||||||
const util = @import("../util.zig");
|
|
||||||
|
|
||||||
const Bus = @import("../bus.zig").Bus;
|
|
||||||
const ARM7TDMI = cpu_mod.ARM7TDMI;
|
|
||||||
const InstrFn = cpu_mod.InstrFn;
|
|
||||||
|
|
||||||
pub fn comptimeHalfSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn halfSignedDataTransfer(cpu: *ARM7TDMI, bus: *Bus, opcode: u32) void {
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
const imm_offset_high = opcode >> 8 & 0xF;
|
|
||||||
|
|
||||||
const base = cpu.r[rn];
|
|
||||||
|
|
||||||
var offset: u32 = undefined;
|
|
||||||
if (I) {
|
|
||||||
offset = imm_offset_high << 4 | rm;
|
|
||||||
} else {
|
|
||||||
offset = cpu.r[rm];
|
|
||||||
}
|
|
||||||
|
|
||||||
const modified_base = if (U) base + offset else base - offset;
|
|
||||||
var address = if (P) modified_base else base;
|
|
||||||
|
|
||||||
if (L) {
|
|
||||||
switch(@truncate(u2, opcode >> 5)) {
|
|
||||||
0b00 => {
|
|
||||||
// SWP
|
|
||||||
std.debug.panic("TODO: Implement SWP", .{});
|
|
||||||
},
|
|
||||||
0b01 => {
|
|
||||||
// LDRH
|
|
||||||
const halfword = bus.readHalfWord(address);
|
|
||||||
cpu.r[rd] = @as(u32, halfword);
|
|
||||||
},
|
|
||||||
0b10 => {
|
|
||||||
// LDRSB
|
|
||||||
const byte = bus.readByte(address);
|
|
||||||
cpu.r[rd] = util.u32_sign_extend(@as(u32, byte), 8);
|
|
||||||
},
|
|
||||||
0b11 => {
|
|
||||||
// LDRSH
|
|
||||||
const halfword = bus.readHalfWord(address);
|
|
||||||
cpu.r[rd] = util.u32_sign_extend(@as(u32, halfword), 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (opcode >> 5 & 0x01 == 0x01) {
|
|
||||||
// STRH
|
|
||||||
const src = @truncate(u16, cpu.r[rd]);
|
|
||||||
|
|
||||||
bus.writeHalfWord(address + 2, src);
|
|
||||||
bus.writeHalfWord(address, src);
|
|
||||||
} else {
|
|
||||||
std.debug.panic("TODO Figure out if this is also SWP", .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
address = modified_base;
|
|
||||||
if (W and P) cpu.r[rn] = address;
|
|
||||||
}
|
|
||||||
}.halfSignedDataTransfer;
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const util = @import("../util.zig");
|
|
||||||
const mod_cpu = @import("../cpu.zig");
|
|
||||||
|
|
||||||
const ARM7TDMI = mod_cpu.ARM7TDMI;
|
|
||||||
const InstrFn = mod_cpu.InstrFn;
|
|
||||||
const Bus = @import("../bus.zig").Bus;
|
|
||||||
|
|
||||||
pub fn comptimeSingleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
|
||||||
return struct {
|
|
||||||
fn singleDataTransfer(cpu: *ARM7TDMI, bus: *Bus, opcode: u32) void {
|
|
||||||
const rn = opcode >> 16 & 0xF;
|
|
||||||
const rd = opcode >> 12 & 0xF;
|
|
||||||
|
|
||||||
const base = cpu.r[rn];
|
|
||||||
const offset = if (I) opcode & 0xFFF else registerOffset(cpu, opcode);
|
|
||||||
|
|
||||||
const modified_base = if (U) base + offset else base - offset;
|
|
||||||
var address = if (P) modified_base else base;
|
|
||||||
|
|
||||||
if (L) {
|
|
||||||
if (B) {
|
|
||||||
// LDRB
|
|
||||||
cpu.r[rd] = bus.readByte(address);
|
|
||||||
} else {
|
|
||||||
// LDR
|
|
||||||
std.debug.panic("Implement LDR", .{});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (B) {
|
|
||||||
// STRB
|
|
||||||
const src = @truncate(u8, cpu.r[rd]);
|
|
||||||
|
|
||||||
bus.writeByte(address + 3, src);
|
|
||||||
bus.writeByte(address + 2, src);
|
|
||||||
bus.writeByte(address + 1, src);
|
|
||||||
bus.writeByte(address, src);
|
|
||||||
} else {
|
|
||||||
// STR
|
|
||||||
std.debug.panic("Implement STR", .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
address = modified_base;
|
|
||||||
if (W and P) cpu.r[rn] = address;
|
|
||||||
|
|
||||||
// TODO: W-bit forces non-privledged mode for the transfer
|
|
||||||
}
|
|
||||||
}.singleDataTransfer;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn registerOffset(cpu: *ARM7TDMI, opcode: u32) u32 {
|
|
||||||
const amount = opcode >> 7 & 0x1F;
|
|
||||||
const rm = opcode & 0xF;
|
|
||||||
const r_val = cpu.r[rm];
|
|
||||||
|
|
||||||
return switch (opcode >> 5 & 0x03) {
|
|
||||||
0b00 => r_val << @truncate(u5, amount),
|
|
||||||
0b01 => r_val >> @truncate(u5, amount),
|
|
||||||
0b10 => @bitCast(u32, @bitCast(i32, r_val) >> @truncate(u5, amount)),
|
|
||||||
0b11 => std.math.rotr(u32, r_val, amount),
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
19
src/emu.zig
19
src/emu.zig
|
@ -1,19 +0,0 @@
|
||||||
const _ = @import("std");
|
|
||||||
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
|
||||||
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
|
|
||||||
const Bus = @import("bus.zig").Bus;
|
|
||||||
|
|
||||||
const CYCLES_PER_FRAME: u64 = 10_000; // TODO: What is this?
|
|
||||||
|
|
||||||
pub fn runFrame(sch: *Scheduler, cpu: *ARM7TDMI, bus: *Bus) void {
|
|
||||||
const frame_end = sch.tick + CYCLES_PER_FRAME;
|
|
||||||
|
|
||||||
while (sch.tick < frame_end) {
|
|
||||||
while (sch.tick < sch.nextTimestamp()) {
|
|
||||||
sch.tick += cpu.step();
|
|
||||||
}
|
|
||||||
|
|
||||||
sch.handleEvent(cpu, bus);
|
|
||||||
}
|
|
||||||
}
|
|
105
src/main.zig
105
src/main.zig
|
@ -1,25 +1,102 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const known_folders = @import("known_folders");
|
||||||
const Bus = @import("bus.zig").Bus;
|
const clap = @import("clap");
|
||||||
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
|
|
||||||
|
|
||||||
const emu = @import("emu.zig");
|
const Gui = @import("platform.zig").Gui;
|
||||||
|
const Bus = @import("core/Bus.zig");
|
||||||
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
|
const FilePaths = @import("util.zig").FilePaths;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log.scoped(.Cli);
|
||||||
|
const width = @import("core/ppu.zig").width;
|
||||||
|
const height = @import("core/ppu.zig").height;
|
||||||
|
const cpu_logging = @import("core/emu.zig").cpu_logging;
|
||||||
|
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
||||||
|
|
||||||
|
// TODO: Reimpl Logging
|
||||||
|
|
||||||
|
// CLI Arguments + Help Text
|
||||||
|
const params = clap.parseParamsComptime(
|
||||||
|
\\-h, --help Display this help and exit.
|
||||||
|
\\-b, --bios <str> Optional path to a GBA BIOS ROM.
|
||||||
|
\\<str> Path to the GBA GamePak ROM
|
||||||
|
\\
|
||||||
|
);
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
pub fn main() anyerror!void {
|
||||||
|
// Main Allocator for ZBA
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
const alloc = gpa.allocator();
|
defer std.debug.assert(!gpa.deinit());
|
||||||
// defer gpa.deinit();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var bus = try Bus.withPak(alloc, "./bin/demo/beeg/beeg.gba");
|
// Handle CLI Input
|
||||||
var scheduler = Scheduler.new(alloc);
|
const result = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{});
|
||||||
var cpu = ARM7TDMI.new(&scheduler, &bus);
|
defer result.deinit();
|
||||||
|
|
||||||
while (true) {
|
const paths = try handleArguments(allocator, &result);
|
||||||
emu.runFrame(&scheduler, &cpu, &bus);
|
defer if (paths.save) |path| allocator.free(path);
|
||||||
}
|
|
||||||
|
const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null;
|
||||||
|
defer if (log_file) |file| file.close();
|
||||||
|
|
||||||
|
// TODO: Take Emulator Init Code out of main.zig
|
||||||
|
var scheduler = Scheduler.init(allocator);
|
||||||
|
defer scheduler.deinit();
|
||||||
|
|
||||||
|
var bus: Bus = undefined;
|
||||||
|
var cpu = Arm7tdmi.init(&scheduler, &bus, log_file);
|
||||||
|
if (paths.bios == null) cpu.fastBoot();
|
||||||
|
|
||||||
|
try bus.init(allocator, &scheduler, &cpu, paths);
|
||||||
|
defer bus.deinit();
|
||||||
|
|
||||||
|
var gui = Gui.init(&bus.pak.title, &bus.apu, width, height);
|
||||||
|
defer gui.deinit();
|
||||||
|
|
||||||
|
try gui.run(&cpu, &scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "basic test" {
|
fn getSavePath(allocator: Allocator) !?[]const u8 {
|
||||||
try std.testing.expectEqual(10, 3 + 7);
|
const save_subpath = "zba" ++ [_]u8{std.fs.path.sep} ++ "save";
|
||||||
|
|
||||||
|
const maybe_data_path = try known_folders.getPath(allocator, .data);
|
||||||
|
defer if (maybe_data_path) |path| allocator.free(path);
|
||||||
|
|
||||||
|
const save_path = if (maybe_data_path) |base| try std.fs.path.join(allocator, &[_][]const u8{ base, "zba", "save" }) else null;
|
||||||
|
|
||||||
|
if (save_path) |_| {
|
||||||
|
// If we've determined what our save path should be, ensure the prereq directories
|
||||||
|
// are present so that we can successfully write to the path when necessary
|
||||||
|
const maybe_data_dir = try known_folders.open(allocator, .data, .{});
|
||||||
|
if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return save_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getRomPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) ![]const u8 {
|
||||||
|
return switch (result.positionals.len) {
|
||||||
|
1 => result.positionals[0],
|
||||||
|
0 => std.debug.panic("ZBA requires a positional path to a GamePak ROM.\n", .{}),
|
||||||
|
else => std.debug.panic("ZBA received too many arguments.\n", .{}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleArguments(allocator: Allocator, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||||
|
const rom_path = try getRomPath(result);
|
||||||
|
log.info("ROM path: {s}", .{rom_path});
|
||||||
|
const bios_path = result.args.bios;
|
||||||
|
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.info("No BIOS provided", .{});
|
||||||
|
const save_path = try getSavePath(allocator);
|
||||||
|
if (save_path) |path| log.info("Save path: {s}", .{path});
|
||||||
|
|
||||||
|
return FilePaths{
|
||||||
|
.rom = rom_path,
|
||||||
|
.bios = bios_path,
|
||||||
|
.save = save_path,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
35
src/pak.zig
35
src/pak.zig
|
@ -1,35 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
pub const GamePak = struct {
|
|
||||||
buf: []u8,
|
|
||||||
|
|
||||||
pub fn fromPath(alloc: Allocator, path: []const u8) !@This() {
|
|
||||||
const file = try std.fs.cwd().openFile(path, .{ .read = true });
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
const len = try file.getEndPos();
|
|
||||||
|
|
||||||
return @This(){
|
|
||||||
.buf = try file.readToEndAlloc(alloc, len),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readWord(self: *const @This(), addr: u32) u32 {
|
|
||||||
return (@as(u32, self.buf[addr + 3]) << 24) | (@as(u32, self.buf[addr + 2]) << 16) | (@as(u32, self.buf[addr + 1]) << 8) | (@as(u32, self.buf[addr]));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readHalfWord(self: *const @This(), addr: u32) u16 {
|
|
||||||
return (@as(u16, self.buf[addr + 1]) << 8) | @as(u16, self.buf[addr]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn writeHalfWord(self: *@This(), addr: u32, halfword: u16) void {
|
|
||||||
self.buf[addr + 1] = @truncate(u8, halfword >> 8);
|
|
||||||
self.buf[addr] = @truncate(u8, halfword);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readByte(self: *const @This(), addr: u32) u8 {
|
|
||||||
return self.buf[addr];
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const SDL = @import("sdl2");
|
||||||
|
const emu = @import("core/emu.zig");
|
||||||
|
|
||||||
|
const Apu = @import("core/apu.zig").Apu;
|
||||||
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
|
const FpsTracker = @import("util.zig").FpsTracker;
|
||||||
|
|
||||||
|
const span = @import("util.zig").span;
|
||||||
|
|
||||||
|
const pitch = @import("core/ppu.zig").framebuf_pitch;
|
||||||
|
const scale = @import("core/emu.zig").win_scale;
|
||||||
|
|
||||||
|
const default_title: []const u8 = "ZBA";
|
||||||
|
|
||||||
|
pub const Gui = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const log = std.log.scoped(.Gui);
|
||||||
|
|
||||||
|
window: *SDL.SDL_Window,
|
||||||
|
title: []const u8,
|
||||||
|
renderer: *SDL.SDL_Renderer,
|
||||||
|
texture: *SDL.SDL_Texture,
|
||||||
|
audio: Audio,
|
||||||
|
|
||||||
|
pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self {
|
||||||
|
const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
||||||
|
if (ret < 0) panic();
|
||||||
|
|
||||||
|
const window = SDL.SDL_CreateWindow(
|
||||||
|
default_title.ptr,
|
||||||
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
|
SDL.SDL_WINDOWPOS_CENTERED,
|
||||||
|
@as(c_int, width * scale),
|
||||||
|
@as(c_int, height * scale),
|
||||||
|
SDL.SDL_WINDOW_SHOWN,
|
||||||
|
) orelse panic();
|
||||||
|
|
||||||
|
const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic();
|
||||||
|
|
||||||
|
const texture = SDL.SDL_CreateTexture(
|
||||||
|
renderer,
|
||||||
|
SDL.SDL_PIXELFORMAT_RGBA8888,
|
||||||
|
SDL.SDL_TEXTUREACCESS_STREAMING,
|
||||||
|
@as(c_int, width),
|
||||||
|
@as(c_int, height),
|
||||||
|
) orelse panic();
|
||||||
|
|
||||||
|
return Self{
|
||||||
|
.window = window,
|
||||||
|
.title = span(title),
|
||||||
|
.renderer = renderer,
|
||||||
|
.texture = texture,
|
||||||
|
.audio = Audio.init(apu),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void {
|
||||||
|
var quit = std.atomic.Atomic(bool).init(false);
|
||||||
|
var frame_rate = FpsTracker.init();
|
||||||
|
|
||||||
|
const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu });
|
||||||
|
defer thread.join();
|
||||||
|
|
||||||
|
var title_buf: [0x100]u8 = [_]u8{0} ** 0x100;
|
||||||
|
|
||||||
|
emu_loop: while (true) {
|
||||||
|
var event: SDL.SDL_Event = undefined;
|
||||||
|
while (SDL.SDL_PollEvent(&event) != 0) {
|
||||||
|
switch (event.type) {
|
||||||
|
SDL.SDL_QUIT => break :emu_loop,
|
||||||
|
SDL.SDL_KEYDOWN => {
|
||||||
|
const io = &cpu.bus.io;
|
||||||
|
const key_code = event.key.keysym.sym;
|
||||||
|
|
||||||
|
switch (key_code) {
|
||||||
|
SDL.SDLK_UP => io.keyinput.up.unset(),
|
||||||
|
SDL.SDLK_DOWN => io.keyinput.down.unset(),
|
||||||
|
SDL.SDLK_LEFT => io.keyinput.left.unset(),
|
||||||
|
SDL.SDLK_RIGHT => io.keyinput.right.unset(),
|
||||||
|
SDL.SDLK_x => io.keyinput.a.unset(),
|
||||||
|
SDL.SDLK_z => io.keyinput.b.unset(),
|
||||||
|
SDL.SDLK_a => io.keyinput.shoulder_l.unset(),
|
||||||
|
SDL.SDLK_s => io.keyinput.shoulder_r.unset(),
|
||||||
|
SDL.SDLK_RETURN => io.keyinput.start.unset(),
|
||||||
|
SDL.SDLK_RSHIFT => io.keyinput.select.unset(),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SDL.SDL_KEYUP => {
|
||||||
|
const io = &cpu.bus.io;
|
||||||
|
const key_code = event.key.keysym.sym;
|
||||||
|
|
||||||
|
switch (key_code) {
|
||||||
|
SDL.SDLK_UP => io.keyinput.up.set(),
|
||||||
|
SDL.SDLK_DOWN => io.keyinput.down.set(),
|
||||||
|
SDL.SDLK_LEFT => io.keyinput.left.set(),
|
||||||
|
SDL.SDLK_RIGHT => io.keyinput.right.set(),
|
||||||
|
SDL.SDLK_x => io.keyinput.a.set(),
|
||||||
|
SDL.SDLK_z => io.keyinput.b.set(),
|
||||||
|
SDL.SDLK_a => io.keyinput.shoulder_l.set(),
|
||||||
|
SDL.SDLK_s => io.keyinput.shoulder_r.set(),
|
||||||
|
SDL.SDLK_RETURN => io.keyinput.start.set(),
|
||||||
|
SDL.SDLK_RSHIFT => io.keyinput.select.set(),
|
||||||
|
SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}),
|
||||||
|
SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }),
|
||||||
|
SDL.SDLK_k => {
|
||||||
|
// Dump IWRAM to file
|
||||||
|
log.info("PC: 0x{X:0>8}", .{cpu.r[15]});
|
||||||
|
log.info("LR: 0x{X:0>8}", .{cpu.r[14]});
|
||||||
|
// const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{});
|
||||||
|
// defer iwram_file.close();
|
||||||
|
|
||||||
|
// try iwram_file.writeAll(cpu.bus.iwram.buf);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulator has an internal Double Buffer
|
||||||
|
const framebuf = cpu.bus.ppu.framebuf.get(.Renderer);
|
||||||
|
_ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch);
|
||||||
|
_ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null);
|
||||||
|
SDL.SDL_RenderPresent(self.renderer);
|
||||||
|
|
||||||
|
const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, frame_rate.value() }) catch unreachable;
|
||||||
|
SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
quit.store(true, .SeqCst); // Terminate Emulator Thread
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.audio.deinit();
|
||||||
|
SDL.SDL_DestroyTexture(self.texture);
|
||||||
|
SDL.SDL_DestroyRenderer(self.renderer);
|
||||||
|
SDL.SDL_DestroyWindow(self.window);
|
||||||
|
SDL.SDL_Quit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Audio = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const log = std.log.scoped(.PlatformAudio);
|
||||||
|
const sample_rate = @import("core/apu.zig").host_sample_rate;
|
||||||
|
|
||||||
|
device: SDL.SDL_AudioDeviceID,
|
||||||
|
|
||||||
|
fn init(apu: *Apu) Self {
|
||||||
|
var have: SDL.SDL_AudioSpec = undefined;
|
||||||
|
var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec);
|
||||||
|
want.freq = sample_rate;
|
||||||
|
want.format = SDL.AUDIO_U16;
|
||||||
|
want.channels = 2;
|
||||||
|
want.samples = 0x100;
|
||||||
|
want.callback = Self.callback;
|
||||||
|
want.userdata = apu;
|
||||||
|
|
||||||
|
const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
|
||||||
|
if (device == 0) panic();
|
||||||
|
|
||||||
|
SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio
|
||||||
|
|
||||||
|
return .{ .device = device };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Self) void {
|
||||||
|
SDL.SDL_CloseAudioDevice(self.device);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void {
|
||||||
|
const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata));
|
||||||
|
_ = SDL.SDL_AudioStreamGet(apu.stream, stream, len);
|
||||||
|
|
||||||
|
// If we don't write anything, play silence otherwise garbage will be played
|
||||||
|
// FIXME: I don't think this hack to remove DC Offset is acceptable :thinking:
|
||||||
|
// if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn panic() noreturn {
|
||||||
|
const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error";
|
||||||
|
@panic(std.mem.sliceTo(str, 0));
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const ARM7TDMI = @import("cpu.zig").ARM7TDMI;
|
|
||||||
const Bus = @import("bus.zig").Bus;
|
|
||||||
|
|
||||||
const Order = std.math.Order;
|
|
||||||
const PriorityQueue = std.PriorityQueue;
|
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
pub const Scheduler = struct {
|
|
||||||
tick: u64,
|
|
||||||
queue: PriorityQueue(Event, void, lessThan),
|
|
||||||
|
|
||||||
pub fn new(alloc: Allocator) @This() {
|
|
||||||
var scheduler = Scheduler{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(alloc, {}) };
|
|
||||||
|
|
||||||
scheduler.queue.add(.{
|
|
||||||
.kind = EventKind.HeatDeath,
|
|
||||||
.tick = std.math.maxInt(u64),
|
|
||||||
}) catch unreachable;
|
|
||||||
|
|
||||||
return scheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handleEvent(self: *@This(), _: *ARM7TDMI, _: *Bus) void {
|
|
||||||
const should_handle = if (self.queue.peek()) |e| self.tick >= e.tick else false;
|
|
||||||
|
|
||||||
if (should_handle) {
|
|
||||||
const event = self.queue.remove();
|
|
||||||
|
|
||||||
switch (event.kind) {
|
|
||||||
.HeatDeath => {
|
|
||||||
std.debug.panic("Somehow, a u64 overflowed", .{});
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn nextTimestamp(self: *@This()) u64 {
|
|
||||||
if (self.queue.peek()) |e| {
|
|
||||||
return e.tick;
|
|
||||||
} else unreachable;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Event = struct {
|
|
||||||
kind: EventKind,
|
|
||||||
tick: u64,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn lessThan(context: void, a: Event, b: Event) Order {
|
|
||||||
_ = context;
|
|
||||||
return std.math.order(a.tick, b.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const EventKind = enum {
|
|
||||||
HeatDeath,
|
|
||||||
};
|
|
273
src/util.zig
273
src/util.zig
|
@ -1,7 +1,274 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const Log2Int = std.math.Log2Int;
|
||||||
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
pub fn u32_sign_extend(value: u32, bitSize: anytype) u32 {
|
const allow_unhandled_io = @import("core/emu.zig").allow_unhandled_io;
|
||||||
const amount: u5 = 32 - bitSize;
|
|
||||||
return @bitCast(u32, @bitCast(i32, value << amount) >> amount);
|
// Sign-Extend value of type `T` to type `U`
|
||||||
|
pub fn sext(comptime T: type, comptime U: type, value: T) T {
|
||||||
|
// U must have less bits than T
|
||||||
|
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
|
||||||
|
|
||||||
|
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
|
||||||
|
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
|
||||||
|
const shift = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
||||||
|
|
||||||
|
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift) >> shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See https://godbolt.org/z/W3en9Eche
|
||||||
|
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
|
||||||
|
if (@typeInfo(T).Int.signedness == .signed)
|
||||||
|
@compileError("cannot rotate signed integer");
|
||||||
|
|
||||||
|
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
|
||||||
|
return x >> ar | x << (1 +% ~ar);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FpsTracker = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
fps: u32,
|
||||||
|
count: std.atomic.Atomic(u32),
|
||||||
|
timer: std.time.Timer,
|
||||||
|
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{
|
||||||
|
.fps = 0,
|
||||||
|
.count = std.atomic.Atomic(u32).init(0),
|
||||||
|
.timer = std.time.Timer.start() catch unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *Self) void {
|
||||||
|
_ = self.count.fetchAdd(1, .Monotonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(self: *Self) u32 {
|
||||||
|
if (self.timer.read() >= std.time.ns_per_s) {
|
||||||
|
self.fps = self.count.swap(0, .SeqCst);
|
||||||
|
self.timer.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.fps;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
|
||||||
|
comptime std.debug.assert(@typeInfo(T) == .Int);
|
||||||
|
|
||||||
|
var result: [@sizeOf(T)]u8 = undefined;
|
||||||
|
|
||||||
|
var i: Log2Int(T) = 0;
|
||||||
|
while (i < result.len) : (i += 1) result[i] = @truncate(u8, value >> i * @bitSizeOf(u8));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Title from the GBA Cartridge is an Uppercase ASCII string which is
|
||||||
|
/// null-padded to 12 bytes
|
||||||
|
///
|
||||||
|
/// This function returns a slice of the ASCII string without the null terminator(s)
|
||||||
|
/// (essentially, a proper Zig/Rust/Any modern language String)
|
||||||
|
pub fn span(title: *const [12]u8) []const u8 {
|
||||||
|
const end = std.mem.indexOfScalar(u8, title, '\x00');
|
||||||
|
return title[0 .. end orelse title.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
test "span" {
|
||||||
|
var example: *const [12]u8 = "POKEMON_EMER";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMON_EMER", span(example));
|
||||||
|
|
||||||
|
example = "POKEMON_EME\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMON_EME", span(example));
|
||||||
|
|
||||||
|
example = "POKEMON_EM\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMON_EM", span(example));
|
||||||
|
|
||||||
|
example = "POKEMON_E\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMON_E", span(example));
|
||||||
|
|
||||||
|
example = "POKEMON_\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMON_", span(example));
|
||||||
|
|
||||||
|
example = "POKEMON\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMON", span(example));
|
||||||
|
|
||||||
|
example = "POKEMO\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEMO", span(example));
|
||||||
|
|
||||||
|
example = "POKEM\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKEM", span(example));
|
||||||
|
|
||||||
|
example = "POKE\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POKE", span(example));
|
||||||
|
|
||||||
|
example = "POK\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "POK", span(example));
|
||||||
|
|
||||||
|
example = "PO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "PO", span(example));
|
||||||
|
|
||||||
|
example = "P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "P", span(example));
|
||||||
|
|
||||||
|
example = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
try std.testing.expectEqualSlices(u8, "", span(example));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a copy of a title with all Filesystem-invalid characters replaced
|
||||||
|
///
|
||||||
|
/// e.g. POKEPIN R/S to POKEPIN R_S
|
||||||
|
pub fn escape(title: [12]u8) [12]u8 {
|
||||||
|
var ret: [12]u8 = title;
|
||||||
|
|
||||||
|
//TODO: Add more replacements
|
||||||
|
std.mem.replaceScalar(u8, &ret, '/', '_');
|
||||||
|
std.mem.replaceScalar(u8, &ret, '\\', '_');
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FilePaths = struct {
|
||||||
|
rom: []const u8,
|
||||||
|
bios: ?[]const u8,
|
||||||
|
save: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const io = struct {
|
||||||
|
pub const read = struct {
|
||||||
|
pub fn todo(comptime log: anytype, comptime format: []const u8, args: anytype) u8 {
|
||||||
|
log.debug(format, args);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T {
|
||||||
|
log.warn(format, args);
|
||||||
|
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const write = struct {
|
||||||
|
pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void {
|
||||||
|
log.warn(format, args);
|
||||||
|
if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
pub fn readUndefined(log: anytype, comptime format: []const u8, args: anytype) u8 {
|
||||||
|
log.warn(format, args);
|
||||||
|
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeUndefined(log: anytype, comptime format: []const u8, args: anytype) void {
|
||||||
|
log.warn(format, args);
|
||||||
|
if (builtin.mode == .Debug) std.debug.panic("TODO: Implement I/O Register", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Logger = struct {
|
||||||
|
const Self = @This();
|
||||||
|
const FmtArgTuple = std.meta.Tuple(&.{ u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32 });
|
||||||
|
|
||||||
|
buf: std.io.BufferedWriter(4096 << 2, std.fs.File.Writer),
|
||||||
|
|
||||||
|
pub fn init(file: std.fs.File) Self {
|
||||||
|
return .{
|
||||||
|
.buf = .{ .unbuffered_writer = file.writer() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(self: *Self, comptime format: []const u8, args: anytype) !void {
|
||||||
|
try self.buf.writer().print(format, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mgbaLog(self: *Self, cpu: *const Arm7tdmi, opcode: u32) void {
|
||||||
|
const fmt_base = "{X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} {X:0>8} cpsr: {X:0>8} | ";
|
||||||
|
const thumb_fmt = fmt_base ++ "{X:0>4}:\n";
|
||||||
|
const arm_fmt = fmt_base ++ "{X:0>8}:\n";
|
||||||
|
|
||||||
|
if (cpu.cpsr.t.read()) {
|
||||||
|
if (opcode >> 11 == 0x1E) {
|
||||||
|
// Instruction 1 of a BL Opcode, print in ARM mode
|
||||||
|
const low = cpu.bus.dbgRead(u16, cpu.r[15]);
|
||||||
|
const bl_opcode = @as(u32, opcode) << 16 | low;
|
||||||
|
|
||||||
|
self.print(arm_fmt, Self.fmtArgs(cpu, bl_opcode)) catch @panic("failed to write to log file");
|
||||||
|
} else {
|
||||||
|
self.print(thumb_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.print(arm_fmt, Self.fmtArgs(cpu, opcode)) catch @panic("failed to write to log file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmtArgs(cpu: *const Arm7tdmi, opcode: u32) FmtArgTuple {
|
||||||
|
return .{
|
||||||
|
cpu.r[0],
|
||||||
|
cpu.r[1],
|
||||||
|
cpu.r[2],
|
||||||
|
cpu.r[3],
|
||||||
|
cpu.r[4],
|
||||||
|
cpu.r[5],
|
||||||
|
cpu.r[6],
|
||||||
|
cpu.r[7],
|
||||||
|
cpu.r[8],
|
||||||
|
cpu.r[9],
|
||||||
|
cpu.r[10],
|
||||||
|
cpu.r[11],
|
||||||
|
cpu.r[12],
|
||||||
|
cpu.r[13],
|
||||||
|
cpu.r[14],
|
||||||
|
cpu.r[15],
|
||||||
|
cpu.cpsr.raw,
|
||||||
|
opcode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Double Buffering Implementation
|
||||||
|
pub const FrameBuffer = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
layers: [2][]u8,
|
||||||
|
buf: []u8,
|
||||||
|
current: u1,
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
// TODO: Rename
|
||||||
|
const Device = enum { Emulator, Renderer };
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, comptime len: comptime_int) !Self {
|
||||||
|
const buf = try allocator.alloc(u8, len * 2);
|
||||||
|
std.mem.set(u8, buf, 0);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
// Front and Back Framebuffers
|
||||||
|
.layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] },
|
||||||
|
.buf = buf,
|
||||||
|
.current = 0,
|
||||||
|
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.allocator.free(self.buf);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap(self: *Self) void {
|
||||||
|
self.current = ~self.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *Self, comptime dev: Device) []u8 {
|
||||||
|
return self.layers[if (dev == .Emulator) self.current else ~self.current];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue