Files
6soz/src/system/nes/apu/frame_counter.zig
T

238 lines
5.8 KiB
Zig

const std = @import("std");
const Region = @import("../common.zig").Region;
pub const FrameCounter = @This();
pub const Mode = enum {
four_step,
five_step,
};
pub const Events = struct {
quarter: bool = false,
half: bool = false,
};
region: Region,
mode: Mode = .four_step,
pending_mode: Mode = .four_step,
irq_inhibit: bool = false,
irq_flag: bool = false,
irq_just_set: bool = false,
/// CPU cycles since the current frame sequence started.
cycle: u32 = 0,
/// $4017 reset delay.
///
/// Hardware applies the reset after either 3 or 4 CPU cycles.
reset_delay: u3 = 0,
pub fn init(region: Region) FrameCounter {
return .{
.region = region,
};
}
/// Write $4017:
///
/// MI--.----
///
/// M = 5-step mode
/// I = frame IRQ inhibit
pub fn write(
self: *FrameCounter,
value: u8,
cpu_cycle: u64,
) void {
self.pending_mode = if ((value & 0x80) != 0)
.five_step
else
.four_step;
self.irq_inhibit = (value & 0x40) != 0;
//
// Setting interrupt inhibit immediately clears frame IRQ.
//
if (self.irq_inhibit) {
self.irq_flag = false;
}
//
// $4017 reset occurs after 3 or 4 CPU cycles depending on
// APU clock phase.
//
// This assumes even cpu_cycle values are our APU-cycle phase.
//
// If your CPU core defines cycle parity oppositely, flip this.
//
self.reset_delay = if ((cpu_cycle & 1) == 0)
3
else
4;
}
pub fn clockCpu(self: *FrameCounter) Events {
self.irq_just_set = false;
var events = self.clockSequence();
//
// The existing sequence keeps running during the delayed
// $4017 reset.
//
if (self.reset_delay != 0) {
self.reset_delay -= 1;
if (self.reset_delay == 0) {
self.mode = self.pending_mode;
self.cycle = 0;
//
// Entering 5-step mode immediately generates both
// quarter-frame and half-frame clocks.
//
if (self.mode == .five_step) {
events.quarter = true;
events.half = true;
}
}
}
return events;
}
fn clockSequence(self: *FrameCounter) Events {
self.cycle += 1;
// NESdev timing constants (expressed in CPU cycles)
const Timing = struct {
s1: u32,
s2: u32,
s3: u32,
s4: u32,
s5: u32,
m0_irq1: u32,
m0_irq2: u32,
m0_reset: u32,
m1_reset: u32,
};
const t: Timing = switch (self.region) {
.ntsc => .{
.s1 = 7457,
.s2 = 14913,
.s3 = 22371,
.s4 = 29829,
.s5 = 37281,
.m0_irq1 = 29828,
.m0_irq2 = 29829,
.m0_reset = 29830,
.m1_reset = 37282,
},
.pal => .{
.s1 = 8313,
.s2 = 16627,
.s3 = 24939,
.s4 = 33253,
.s5 = 41565,
.m0_irq1 = 33252,
.m0_irq2 = 33253,
.m0_reset = 33254,
.m1_reset = 41566,
},
};
var events = Events{};
switch (self.mode) {
.four_step => {
if (self.cycle == t.s1) {
events.quarter = true;
} else if (self.cycle == t.s2) {
events.quarter = true;
events.half = true;
} else if (self.cycle == t.s3) {
events.quarter = true;
} else if (self.cycle == t.m0_irq1) {
if (!self.irq_inhibit) {
self.irq_flag = true;
self.irq_just_set = true;
}
} else if (self.cycle == t.m0_irq2) {
events.quarter = true;
events.half = true;
if (!self.irq_inhibit) {
self.irq_flag = true;
self.irq_just_set = true;
}
} else if (self.cycle >= t.m0_reset) {
if (!self.irq_inhibit) {
self.irq_flag = true;
self.irq_just_set = true;
}
self.cycle = 0;
}
},
.five_step => {
if (self.cycle == t.s1) {
events.quarter = true;
} else if (self.cycle == t.s2) {
events.quarter = true;
events.half = true;
} else if (self.cycle == t.s3) {
events.quarter = true;
} else if (self.cycle == t.s5) {
events.quarter = true;
events.half = true;
} else if (self.cycle >= t.m1_reset) {
self.cycle = 0;
}
},
}
return events;
}
pub fn reset(self: *FrameCounter) void {
self.mode = .four_step;
self.pending_mode = .four_step;
self.irq_inhibit = false;
self.irq_flag = false;
self.irq_just_set = false;
self.cycle = 0;
self.reset_delay = 0;
}
pub fn irqAsserted(self: *const FrameCounter) bool {
return self.irq_flag;
}
pub fn clearIrq(self: *FrameCounter) void {
// ponytail: simultaneous read and IRQ set does not clear flag
if (!self.irq_just_set) {
self.irq_flag = false;
}
}
// ponytail: consolidated frame counter unit test suite
test "frame counter 4-step and 5-step mode timings, irq window and write delays" {
var fc = FrameCounter.init(.ntsc);
// 4-step mode timings and IRQ window
for (0..29828) |_| _ = fc.clockCpu();
try std.testing.expect(fc.irqAsserted());
fc.clearIrq(); // simultaneous set/read check
try std.testing.expect(fc.irqAsserted()); // remains set until next cycle
// 5-step mode
fc = FrameCounter.init(.ntsc);
fc.write(0x80, 0); // 5-step write
for (0..4) |_| _ = fc.clockCpu();
try std.testing.expect(!fc.irqAsserted());
try std.testing.expectEqual(Mode.five_step, fc.mode);
}