mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
gb: added RTC
This commit is contained in:
1 parent
937f6dbf84
commit
602e56db58
4 files changed
+381
-4
No files matched your search
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Digital clock for Game Boy, reading the MBC3 real-time clock.
|
||||
|
||||
The cartridge header declares an MBC3 + timer + battery cart, so
|
||||
the emulator runs a wall-clock RTC that starts at the current time.
|
||||
Reading the clock is the same on real hardware:
|
||||
|
||||
- write 0x08-0x0C to 0x4000 to select a register
|
||||
(0x08 seconds, 0x09 minutes, 0x0A hours, 0x0B/0x0C days+flags)
|
||||
- write 0x00 then 0x01 to 0x6000 to latch the live time
|
||||
- read the latched value from 0xA000
|
||||
|
||||
The screen redraws only when the seconds change.
|
||||
*/
|
||||
|
||||
//#link "gb/sfr.sgb"
|
||||
//#link "gb/crt0.sgb"
|
||||
//#resource "gb/global.sgb"
|
||||
//#symbol ld GB_MAPPER=0x10
|
||||
|
||||
#include <stdint.h>
|
||||
#include "gb/types.h"
|
||||
#include "gb/hardware.h"
|
||||
#include "gb/gb.h"
|
||||
#include "gbtext.h"
|
||||
|
||||
// MBC3 registers
|
||||
#define MBC_SELECT (*(volatile uint8_t*)0x4000)
|
||||
#define MBC_LATCH (*(volatile uint8_t*)0x6000)
|
||||
#define RTC_DATA (*(volatile uint8_t*)0xA000)
|
||||
|
||||
// latch the live clock into the readable registers
|
||||
static void rtc_latch(void) {
|
||||
MBC_LATCH = 0x00;
|
||||
MBC_LATCH = 0x01;
|
||||
}
|
||||
|
||||
// read a latched register (0x08-0x0C)
|
||||
static uint8_t rtc_read(uint8_t reg) {
|
||||
MBC_SELECT = reg;
|
||||
return RTC_DATA;
|
||||
}
|
||||
|
||||
// write two decimal digits, zero-padded
|
||||
static void put2(char* p, uint8_t v) {
|
||||
p[0] = '0' + (v / 10) % 10;
|
||||
p[1] = '0' + (v % 10);
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
char buf[9];
|
||||
uint8_t last_sec = 0xFF;
|
||||
uint8_t sec, min, hour;
|
||||
|
||||
buf[8] = '\0';
|
||||
|
||||
// enable cartridge RAM/RTC access
|
||||
MBC_SELECT = 0x00;
|
||||
*(volatile uint8_t*)0x0000 = 0x0A;
|
||||
|
||||
DISPLAY_OFF;
|
||||
BGP_REG = 0xE4;
|
||||
OBP0_REG = OBP1_REG = 0xE4;
|
||||
|
||||
font_init();
|
||||
put_str(6, 7, "MBC3 RTC");
|
||||
put_str(6, 9, "--:--:--");
|
||||
|
||||
SHOW_BKG;
|
||||
DISPLAY_ON;
|
||||
|
||||
while (1) {
|
||||
wait_vbl_done();
|
||||
|
||||
rtc_latch();
|
||||
sec = rtc_read(0x08);
|
||||
min = rtc_read(0x09);
|
||||
hour = rtc_read(0x0A);
|
||||
|
||||
if (sec != last_sec) {
|
||||
last_sec = sec;
|
||||
put2(buf + 0, hour);
|
||||
buf[2] = ':';
|
||||
put2(buf + 3, min);
|
||||
buf[5] = ':';
|
||||
put2(buf + 6, sec);
|
||||
put_str(6, 9, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
-4
@@ -754,8 +754,66 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
mbcMode: number = 0; // 0=ROM banking, 1=RAM banking
|
||||
romBankMask: number = 0x1F;
|
||||
|
||||
// MBC3 real-time clock (cart types 0x0F, 0x10)
|
||||
hasRTC: boolean = false;
|
||||
rtcRegisterSelect: number = -1; // -1 = none, else 0x08..0x0C
|
||||
rtcLatchLast: number = 0; // last value written to 0x6000-0x7FFF
|
||||
rtcSec: number = 0;
|
||||
rtcMin: number = 0;
|
||||
rtcHour: number = 0;
|
||||
rtcDay: number = 0; // 9-bit day counter
|
||||
rtcHalt: boolean = false;
|
||||
rtcCarry: boolean = false;
|
||||
rtcLatched = new Uint8Array(5); // snapshot returned to the CPU
|
||||
rtcLastSync: number = 0; // host time (ms) at last sync
|
||||
|
||||
getKeyboardMap() { return GB_KEYCODE_MAP; }
|
||||
|
||||
// Host wall clock in ms; override in tests for deterministic time.
|
||||
now(): number { return Date.now(); }
|
||||
|
||||
// Advance the live RTC by elapsed host time (unless halted).
|
||||
syncRTC(): void {
|
||||
if (!this.hasRTC || this.rtcHalt) { this.rtcLastSync = this.now(); return; }
|
||||
var elapsed = Math.floor((this.now() - this.rtcLastSync) / 1000);
|
||||
if (elapsed <= 0) return;
|
||||
this.rtcLastSync += elapsed * 1000;
|
||||
var total = this.rtcSec + elapsed;
|
||||
this.rtcSec = total % 60;
|
||||
total = this.rtcMin + Math.floor(total / 60);
|
||||
this.rtcMin = total % 60;
|
||||
total = this.rtcHour + Math.floor(total / 60);
|
||||
this.rtcHour = total % 24;
|
||||
var days = this.rtcDay + Math.floor(total / 24);
|
||||
if (days > 511) { this.rtcCarry = true; days &= 0x1FF; }
|
||||
this.rtcDay = days;
|
||||
}
|
||||
|
||||
// Copy the live clock into the latched registers (0x6000-0x7FFF: 0 then 1).
|
||||
latchRTC(): void {
|
||||
this.syncRTC();
|
||||
this.rtcLatched[0] = this.rtcSec;
|
||||
this.rtcLatched[1] = this.rtcMin;
|
||||
this.rtcLatched[2] = this.rtcHour;
|
||||
this.rtcLatched[3] = this.rtcDay & 0xFF;
|
||||
this.rtcLatched[4] = ((this.rtcDay >> 8) & 0x01) | (this.rtcHalt ? 0x40 : 0) | (this.rtcCarry ? 0x80 : 0);
|
||||
}
|
||||
|
||||
// Power on the RTC at the current wall-clock time.
|
||||
resetRTC(): void {
|
||||
var d = new Date(this.now());
|
||||
this.rtcSec = d.getSeconds();
|
||||
this.rtcMin = d.getMinutes();
|
||||
this.rtcHour = d.getHours();
|
||||
this.rtcDay = 0;
|
||||
this.rtcHalt = false;
|
||||
this.rtcCarry = false;
|
||||
this.rtcRegisterSelect = -1;
|
||||
this.rtcLatchLast = 0;
|
||||
this.rtcLastSync = this.now();
|
||||
this.latchRTC();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.handler = newKeyboardHandler(this.inputs, this.getKeyboardMap());
|
||||
@@ -852,8 +910,13 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
}
|
||||
} else if (fullAddr < 0x6000) {
|
||||
if (this.mbcType === 3) {
|
||||
// MBC3: RAM bank 0-3 (or RTC register select)
|
||||
this.ramBank = v & 0x0F;
|
||||
// MBC3: RTC register select (0x08-0x0C) or RAM bank (0x00-0x07)
|
||||
if (this.hasRTC && v >= 0x08 && v <= 0x0C) {
|
||||
this.rtcRegisterSelect = v;
|
||||
} else {
|
||||
this.rtcRegisterSelect = -1;
|
||||
this.ramBank = v & 0x03;
|
||||
}
|
||||
} else {
|
||||
// MBC1: RAM bank / upper ROM bank bits
|
||||
if (this.mbcMode === 0) {
|
||||
@@ -864,8 +927,13 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// MBC1: banking mode select; MBC3: latch clock data (ignored)
|
||||
if (this.mbcType === 1) this.mbcMode = v & 0x01;
|
||||
// MBC1: banking mode select; MBC3: latch clock data on 0x00 then 0x01
|
||||
if (this.mbcType === 1) {
|
||||
this.mbcMode = v & 0x01;
|
||||
} else if (this.mbcType === 3 && this.rtcLatchLast === 0x00 && v === 0x01) {
|
||||
this.latchRTC();
|
||||
}
|
||||
this.rtcLatchLast = v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,12 +954,17 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
}
|
||||
|
||||
readExtRAM(a: number): number {
|
||||
if (this.hasRTC && this.rtcRegisterSelect >= 0) {
|
||||
this.syncRTC();
|
||||
return this.rtcLatched[this.rtcRegisterSelect - 0x08];
|
||||
}
|
||||
if (!this.ramEnabled && this.mbcType > 0) return 0xFF;
|
||||
var offset = a + (this.ramBank * 0x2000);
|
||||
return offset < this.extram.length ? this.extram[offset] : 0xFF;
|
||||
}
|
||||
|
||||
writeExtRAM(a: number, v: number): void {
|
||||
if (this.hasRTC && this.rtcRegisterSelect >= 0) return; // RTC registers are read-only
|
||||
if (!this.ramEnabled && this.mbcType > 0) return;
|
||||
var offset = a + (this.ramBank * 0x2000);
|
||||
if (offset < this.extram.length) this.extram[offset] = v;
|
||||
@@ -1518,6 +1591,7 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E: this.mbcType = 5; break; // MBC5
|
||||
default: console.log(`Unhandled cartridge type 0x${cartType.toString(16)} @ 0x147, treating as MBC5`); this.mbcType = 5; break;
|
||||
}
|
||||
this.hasRTC = cartType === 0x0F || cartType === 0x10;
|
||||
} else throw new EmuHalt("ROM not long enough for header");
|
||||
|
||||
// Detect CGB mode from header byte 0x143
|
||||
@@ -1604,6 +1678,13 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
if (this.cgbMode) {
|
||||
this.cpu.A = 0x11;
|
||||
}
|
||||
// Power on the RTC at the current wall-clock time (RTC carts only)
|
||||
if (this.hasRTC) {
|
||||
this.resetRTC();
|
||||
} else {
|
||||
this.rtcRegisterSelect = -1;
|
||||
this.rtcLatchLast = 0;
|
||||
}
|
||||
}
|
||||
|
||||
preFrame(): void {
|
||||
@@ -1638,6 +1719,13 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
};
|
||||
state['apu'] = this.apu.saveState();
|
||||
state['apuCycleAccum'] = this.apuCycleAccum;
|
||||
state['rtc'] = {
|
||||
hasRTC: this.hasRTC,
|
||||
sec: this.rtcSec, min: this.rtcMin, hour: this.rtcHour, day: this.rtcDay,
|
||||
halt: this.rtcHalt, carry: this.rtcCarry,
|
||||
registerSelect: this.rtcRegisterSelect, latchLast: this.rtcLatchLast,
|
||||
latched: this.rtcLatched.slice(0), lastSync: this.rtcLastSync,
|
||||
};
|
||||
// GBC state
|
||||
if (this.cgbMode) {
|
||||
state['cgb'] = {
|
||||
@@ -1677,6 +1765,16 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
this.timerSubCycles = io.timerSubCycles;
|
||||
if (state.apu) this.apu.loadState(state.apu);
|
||||
this.apuCycleAccum = state.apuCycleAccum || 0;
|
||||
if (state.rtc) {
|
||||
var rtc = state.rtc;
|
||||
this.hasRTC = rtc.hasRTC;
|
||||
this.rtcSec = rtc.sec; this.rtcMin = rtc.min; this.rtcHour = rtc.hour; this.rtcDay = rtc.day;
|
||||
this.rtcHalt = rtc.halt; this.rtcCarry = rtc.carry;
|
||||
this.rtcRegisterSelect = rtc.registerSelect; this.rtcLatchLast = rtc.latchLast;
|
||||
if (rtc.latched) this.rtcLatched.set(rtc.latched);
|
||||
// keep wall-clock semantics: elapsed time since the save still counts
|
||||
this.rtcLastSync = rtc.lastSync;
|
||||
}
|
||||
// GBC state
|
||||
if (state.cgb) {
|
||||
var cgb = state.cgb;
|
||||
@@ -1716,6 +1814,12 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
+ "VBK " + this.vramBankSelect + " SVBK " + this.wramBankSelect + "\n"
|
||||
+ "SPD " + (this.doubleSpeed ? "2x" : "1x") + "\n";
|
||||
}
|
||||
if (this.hasRTC) {
|
||||
this.syncRTC();
|
||||
s += "--- RTC ---\n"
|
||||
+ hex(this.rtcHour, 2) + ":" + hex(this.rtcMin, 2) + ":" + hex(this.rtcSec, 2)
|
||||
+ " day " + this.rtcDay + (this.rtcHalt ? " HALT" : "") + (this.rtcCarry ? " CARRY" : "") + "\n";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const GB_PRESETS: Preset[] = [
|
||||
{ id: 'galaxy.c', name: 'Window + Sprites' },
|
||||
{ id: 'gbdecompress.c', name: 'Decompression' },
|
||||
{ id: 'music.c', name: 'Music Player' },
|
||||
{ id: 'clock.c', name: 'MBC3 Real-Time Clock' },
|
||||
{ id: 'banking.c', name: 'ROM Bank Switching' },
|
||||
{ id: 'siegegame.c', name: 'Siege Game' },
|
||||
{ id: 'climber.c', name: 'Climber Game' },
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import { GameBoyMachine } from "../../src/machine/gb";
|
||||
|
||||
// Build a minimal ROM with the given cartridge type and RAM size.
|
||||
function makeROM(cartType: number, ramSize = 0x02): Uint8Array {
|
||||
const rom = new Uint8Array(0x8000);
|
||||
rom[0x143] = 0x00; // DMG mode
|
||||
rom[0x147] = cartType;
|
||||
rom[0x149] = ramSize;
|
||||
return rom;
|
||||
}
|
||||
|
||||
function newMBC3RTC(): GameBoyMachine {
|
||||
const m = new GameBoyMachine();
|
||||
let fakeNow = 1700000000000; // fixed epoch
|
||||
m.now = () => fakeNow;
|
||||
m.loadROM(makeROM(0x10)); // MBC3 + TIMER + RAM + BATTERY
|
||||
return m;
|
||||
}
|
||||
|
||||
// Select an RTC register (0x08-0x0C) then latch (write 0x00, then 0x01).
|
||||
function latch(m: GameBoyMachine, reg: number): number {
|
||||
m.write(0x4000, reg);
|
||||
m.write(0x6000, 0x00);
|
||||
m.write(0x6000, 0x01);
|
||||
return m.read(0xA000);
|
||||
}
|
||||
|
||||
describe('Game Boy MBC3 RTC', function () {
|
||||
|
||||
it('detects RTC cartridges', function () {
|
||||
const rtc = new GameBoyMachine();
|
||||
rtc.loadROM(makeROM(0x10));
|
||||
assert.strictEqual(rtc.hasRTC, true);
|
||||
|
||||
const noTimer = new GameBoyMachine();
|
||||
noTimer.loadROM(makeROM(0x11)); // MBC3 without RTC
|
||||
assert.strictEqual(noTimer.hasRTC, false);
|
||||
|
||||
const mbc1 = new GameBoyMachine();
|
||||
mbc1.loadROM(makeROM(0x03));
|
||||
assert.strictEqual(mbc1.hasRTC, false);
|
||||
});
|
||||
|
||||
it('powers on at the wall-clock time', function () {
|
||||
const m = new GameBoyMachine();
|
||||
const fake = new Date(2024, 0, 15, 9, 30, 45).getTime();
|
||||
m.now = () => fake;
|
||||
m.loadROM(makeROM(0x10));
|
||||
assert.strictEqual(m.rtcHour, 9);
|
||||
assert.strictEqual(m.rtcMin, 30);
|
||||
assert.strictEqual(m.rtcSec, 45);
|
||||
// latched snapshot matches the live clock at boot
|
||||
assert.strictEqual(m.rtcLatched[0], 45);
|
||||
assert.strictEqual(m.rtcLatched[1], 30);
|
||||
assert.strictEqual(m.rtcLatched[2], 9);
|
||||
});
|
||||
|
||||
it('advances by elapsed wall-clock seconds', function () {
|
||||
const m = newMBC3RTC();
|
||||
let fake = m.now();
|
||||
// pin a known time (set through the machine, then reload the base)
|
||||
m.rtcSec = 57; m.rtcMin = 58; m.rtcHour = 0; m.rtcDay = 0;
|
||||
m.rtcHalt = false; m.rtcCarry = false;
|
||||
m.rtcLastSync = fake;
|
||||
|
||||
fake += 5 * 1000;
|
||||
m.now = () => fake;
|
||||
m.syncRTC();
|
||||
assert.strictEqual(m.rtcSec, 2);
|
||||
assert.strictEqual(m.rtcMin, 59);
|
||||
assert.strictEqual(m.rtcHour, 0);
|
||||
assert.strictEqual(m.rtcDay, 0);
|
||||
});
|
||||
|
||||
it('rolls over hours into days', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fake = m.now();
|
||||
m.rtcSec = 0; m.rtcMin = 0; m.rtcHour = 23; m.rtcDay = 0;
|
||||
m.rtcLastSync = fake;
|
||||
m.now = () => fake + 3600 * 1000;
|
||||
m.syncRTC();
|
||||
assert.strictEqual(m.rtcHour, 0);
|
||||
assert.strictEqual(m.rtcDay, 1);
|
||||
});
|
||||
|
||||
it('wraps the 9-bit day counter and sets carry', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fake = m.now();
|
||||
m.rtcSec = 0; m.rtcMin = 0; m.rtcHour = 0; m.rtcDay = 511;
|
||||
m.rtcCarry = false;
|
||||
m.rtcLastSync = fake;
|
||||
m.now = () => fake + 24 * 3600 * 1000;
|
||||
m.syncRTC();
|
||||
assert.strictEqual(m.rtcDay, 0);
|
||||
assert.strictEqual(m.rtcCarry, true);
|
||||
});
|
||||
|
||||
it('freezes while the halt bit is set', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fake = m.now();
|
||||
m.rtcSec = 10; m.rtcMin = 0; m.rtcHour = 0; m.rtcDay = 0;
|
||||
m.rtcHalt = true;
|
||||
m.rtcLastSync = fake;
|
||||
m.now = () => fake + 100 * 1000;
|
||||
m.syncRTC();
|
||||
assert.strictEqual(m.rtcSec, 10);
|
||||
assert.strictEqual(m.rtcMin, 0);
|
||||
});
|
||||
|
||||
it('returns latched registers at A000-BFFF', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fixed = new Date(2024, 0, 15, 1, 2, 3).getTime();
|
||||
m.now = () => fixed;
|
||||
m.loadROM(makeROM(0x10)); // re-init to pick up the new clock
|
||||
assert.strictEqual(latch(m, 0x08), 3); // seconds
|
||||
assert.strictEqual(latch(m, 0x09), 2); // minutes
|
||||
assert.strictEqual(latch(m, 0x0A), 1); // hours
|
||||
assert.strictEqual(latch(m, 0x0B), 0); // day low
|
||||
assert.strictEqual(latch(m, 0x0C), 0); // day high / flags
|
||||
});
|
||||
|
||||
it('reflects elapsed time across a re-latch', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fake = m.now();
|
||||
m.rtcSec = 50; m.rtcMin = 0; m.rtcHour = 0; m.rtcDay = 0;
|
||||
m.rtcLastSync = fake;
|
||||
assert.strictEqual(latch(m, 0x08), 50);
|
||||
assert.strictEqual(latch(m, 0x09), 0);
|
||||
|
||||
m.now = () => fake + 15 * 1000;
|
||||
assert.strictEqual(latch(m, 0x08), 5);
|
||||
assert.strictEqual(latch(m, 0x09), 1);
|
||||
});
|
||||
|
||||
it('ignores writes to RTC registers', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fixed = new Date(2024, 0, 15, 1, 2, 3).getTime();
|
||||
m.now = () => fixed;
|
||||
m.loadROM(makeROM(0x10));
|
||||
m.write(0x4000, 0x08); // select seconds
|
||||
m.write(0x6000, 0x00); // latch
|
||||
m.write(0x6000, 0x01);
|
||||
assert.strictEqual(m.read(0xA000), 3);
|
||||
m.write(0xA000, 0xAA); // read-only
|
||||
assert.strictEqual(m.read(0xA000), 3);
|
||||
});
|
||||
|
||||
it('does not treat RTC select on non-RTC carts', function () {
|
||||
const m = new GameBoyMachine();
|
||||
m.loadROM(makeROM(0x11));
|
||||
m.write(0x4000, 0x08);
|
||||
assert.strictEqual(m.rtcRegisterSelect, -1);
|
||||
// normal external RAM path still works
|
||||
m.write(0x0000, 0x0A); // enable RAM
|
||||
m.write(0xA000, 0x42);
|
||||
assert.strictEqual(m.read(0xA000), 0x42);
|
||||
});
|
||||
|
||||
it('round-trips RTC state through save/load', function () {
|
||||
const m = newMBC3RTC();
|
||||
const fake = m.now();
|
||||
m.rtcSec = 11; m.rtcMin = 22; m.rtcHour = 3; m.rtcDay = 4;
|
||||
m.rtcHalt = true; m.rtcCarry = true;
|
||||
m.rtcLastSync = fake;
|
||||
m.latchRTC();
|
||||
const state = m.saveState();
|
||||
|
||||
const m2 = new GameBoyMachine();
|
||||
m2.loadState(state);
|
||||
assert.strictEqual(m2.hasRTC, true);
|
||||
assert.strictEqual(m2.rtcSec, 11);
|
||||
assert.strictEqual(m2.rtcMin, 22);
|
||||
assert.strictEqual(m2.rtcHour, 3);
|
||||
assert.strictEqual(m2.rtcDay, 4);
|
||||
assert.strictEqual(m2.rtcHalt, true);
|
||||
assert.strictEqual(m2.rtcCarry, true);
|
||||
assert.deepStrictEqual(Array.from(m2.rtcLatched), Array.from(m.rtcLatched));
|
||||
assert.strictEqual(m2.rtcLastSync, fake);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user