238 lines
5.8 KiB
Zig
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);
|
|
}
|