gameboy-rust/docs/hardware/timers-irq.md
2026-10-07 02:54:57 +07:00

17 KiB
Raw Blame History

Timers, interrupts, HALT, and STOP

Timers, interrupt registers, and the machine clock

Machine::new(cpu, memory) combines the CPU and memory bus. Machine::step() first checks STOP and returns StopIdle with zero timing if the system is still stopped. Otherwise it services one ready DMA data unit, keeping the CPU paused. If HALT is waiting and no DMA is ready, it advances device clocks to the next event without executing CPU code. Otherwise, it samples the IRQ line, including the CPU's interrupt mask, then enters IRQ mode or executes one instruction. Each successful step advances display, timer, and audio clocks by its nominal cost. Machine::last_timing() reports separate code, data, internal, and idle cycle totals for the last successful step. StepTiming::idle_cycles counts device-clock cycles during HALT; it does not count CPU work. IRQ entry is a separate step; the vector instruction runs on the following step. Entry charges the discarded incoming-state PC+8/PC+4 fetch and the ARM vector pair at 0x18 and 0x1c. DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry. See refill timing for history effects and remaining limits. A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery. Timer and sound accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update. StepKind::Dma { channel } identifies a DMA unit. StepKind::HaltIdle identifies one bounded HALT clock advance. StepKind::StopIdle identifies a stopped system with no clock progress. The other step kinds remain Instruction and IrqEntry. MachineError::Cpu and MachineError::Dma identify the source of a diagnostic. Failed steps do not advance the clock or partially change CPU or device state. Earlier successful steps remain committed.

The clock now uses instruction-specific costs, but it is not cycle-accurate. Timers now use a staged clock during machine instructions, IRQ entry, and DMA units. Source-fetch cycles advance that clock before data work. Each data access advances it before reading or writing timer registers. All byte lanes of one access share the same completion time. Internal and target-fetch cycles follow in order. A timer load therefore includes its source and data cycles, but not its trailing internal cycle. Timer-start writes do not count preceding cycles. Timer-stop writes count through their own bus completion. Block transfers preserve distinct word phases; combined reload/control writes merge at one phase, with reload bytes first.

The transaction also owns timer IF bits. Reads see overflows through the current access; acknowledgements clear earlier timer requests. A later internal cycle can raise an acknowledged timer request again. Non-timer IF bits retain their existing behavior. IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP.

Successful steps commit timers, the shared divider phase, timer IF bits, and audio state. Failed steps discard staged counters, divider progress, timer/audio writes, FIFO consumption, and requests. Display and other device clocks then advance once, without advancing timers again. Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled. Direct Sound consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries. The same transaction advances both PSG pulse oscillators, the noise counter, and their shared 512 Hz sequencer independently of the general-purpose timers. Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.

The unchanged prefetch cancellation probe now matches both published interval totals and actual timer samples. No offset is added to its timer values. See clock ownership and evidence for the bounded scope.

Cpu::step remains a CPU-only API. It does not honor HALT/STOP, execute DMA, advance device clocks, or sample device IRQs. Cpu::step_timed executes the same operation and returns StepTiming, without advancing devices. Machine::step uses that result to advance device clocks. Failed steps preserve the previous timing result. Memory::advance_cycles(n) advances device clocks, latches DMA requests, and updates HALT wake-up state while not stopped. In STOP, it ignores supplied cycles and leaves device phases and captured frames unchanged. It does not execute CPU instructions or DMA units. Multiple unserviced DMA requests coalesce; bulk clock advances do not replay every missed transfer. Reads and writes alone consume no cycles. Memory::cycles() and Machine::cycles() report elapsed emulated clocks as a wrapping 64-bit counter, excluding stopped time.

Mapped input/output (I/O) registers:

Address Register Behavior
0x04000100 + 4*n Timer n counter/reload Reads the counter; writes the separate reload value
0x04000102 + 4*n Timer n control Prescaler, count-up, local IRQ enable, and start/stop
0x04000130 KEYINPUT Ten active-low button bits; read-only
0x04000132 KEYCNT Button selection, keypad IRQ enable, and OR/AND condition
0x04000200 IE Enables selected interrupt sources; bits 0–13 are writable
0x04000202 IF Latches requests; writing 1 clears the corresponding bit
0x04000204 WAITCNT Configures first/second ROM access wait states; unused bits read as zero
0x04000208 IME Bit zero gates IRQ delivery; other bits read as zero
0x04000300 POSTFLG Bit-zero post-boot flag; CPU writes require BIOS execution
0x04000301 HALTCNT Write-only low-power control; HALT and keypad-wake STOP supported

Timers count at supplied clock rates divided by 1, 64, 256, or 1024. Timers 1–3 can instead count the preceding timer's overflows. Timer 0 ignores count-up selection. Starting a stopped timer copies reload into the counter. Overflow also reloads the counter. Writing reload while running leaves the counter unchanged until overflow or restart. A combined 32-bit reload/control write uses the new reload value when starting the timer. Stopping a timer freezes its counter. Restarting it reloads the counter. Bulk clock advances preserve all overflow pulses through cascaded timers without looping once per cycle.

Overflow latches IF only when the timer's local IRQ-enable bit is set. IE, IME, and CPSR.I gate delivery, not the pending flag. Entering IRQ mode does not clear IF. Software must acknowledge the request. Unused register bits read as zero. Unknown I/O addresses and mirrors remain unmapped. Byte, halfword, and word accesses are supported for this register subset.

Independent timers share a free-running divider with a synthetic zero phase at reset. The divider advances even when all timers are disabled. Enable, restart, and clock-source changes do not reset it. A timer receives selected divider edges after the start of an interval and through its end. Changing out of cascade mode uses the existing shared phase; cascade mode ignores the divider selection. HALT next-event bounds use that phase. STOP freezes it, and wake resumes without resetting it. CPU/DMA timer transactions stage divider progress and commit it only on success, without a second bulk tick. See prescaler evidence and original tests.

Hardware startup and register-write delays are still not modeled. Writes retain the immediate bus-completion policy. The shared-phase tests validate this bounded model, not absolute enable/reload edges on hardware. Timers 0 and 1 can clock Direct Sound playback and request FIFO DMA independently of timer IRQ enable. Interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented.

--timer-demo executes original ARM code that configures Timer 0, IE, and IME. The timer overflows after 16 supplied cycles and enters the original handler through vector 0x18. The handler stops Timer 0, acknowledges IF, increments r10, and returns with SUBS pc, lr, #4. The demo completes with one handler call, using default WAITCNT settings. Its initial ARM ROM fetch uses PC+8 sequential timing; cold pipeline filling has no separate startup charge. The stopped counter is now 0xfff4, rather than the earlier bulk-write result 0xfffb. Timer start excludes its preceding nine cycles; timer stop includes its two bus cycles. These are nominal regression results, not hardware startup-delay measurements. Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention.

Keypad interrupt control

KEYCNT resets to zero and stores only bits selected by 0xc3ff:

Bits Meaning
0–9 Select A, B, Select, Start, Right, Left, Up, Down, R, and L
10–13 Unused; read as zero
14 Enable keypad interrupt requests
15 Zero: OR condition; one: AND condition

OR matches when any selected button is pressed. AND matches when every selected button is pressed; unselected pressed buttons do not prevent a match. An empty selection never matches OR and always matches AND.

A matching enabled sample can latch IF bit 12, independently of IE, IME, and CPSR.I. Changing input or disabling KEYCNT does not clear an existing request. Software acknowledges IF by writing bit 12 as one. IE bit 12 gates HALT wake-up. IME and CPSR.I gate IRQ delivery, not wake-up or request latching. Ready DMA units still take priority over CPU IRQ entry. The original BIOS IntrWait can consume keypad events through the normal IRQ callback contract.

Sampling policy and limits

While the system clock runs, the functional model follows mGBA's polling behavior, not a verified hardware edge detector:

  • Each Memory::set_buttons call samples the input, including repeated identical values.
  • Each successful write touching KEYCNT samples after the complete register update.
  • OR requests on every matching enabled sample, so another identical input sample can reassert an acknowledged request.
  • AND remembers the complete pressed-button snapshot and suppresses an identical matching sample.
  • A different matching snapshot requests again, including changes to unselected buttons.
  • An enabled nonmatching sample clears that history. Disabled samples retain it.
  • Newly selected buttons are removed from the remembered snapshot before testing a control write. A newly selected held button can therefore cause another AND request.
  • Reads, KEYINPUT-only writes, IF acknowledgement, and clock advancement do not sample the keypad.

This policy makes IRQ generation depend on input sampling, not only on elapsed emulated time. Host input calls consume no emulated cycles and do not modify CPU registers. They can wake HALT immediately; Machine::step subsequently samples the IRQ line. The desktop demos currently supply frame-based input snapshots. Physical key-event timing, switch bounce, exact hardware retrigger rules, and synchronization delays remain unverified. STOP uses a separate live-condition wake path described below, without latching IF or updating this polling history.

Byte writes merge with the untouched KEYCNT byte. Halfword writes, and word writes at KEYINPUT, evaluate the final KEYCNT value once. They do not generate requests from temporary byte-by-byte values. The lower half of a word write remains a read-only KEYINPUT write. CPU, DMA, and host/debug writes use the same path. Failed writes and failed block stores leave control, IRQ history, pending flags, and HALT state unchanged.

The current BIOS SoftReset and RegisterRamReset subset preserve KEYCNT and button state. RegisterRamReset bit 7 still acknowledges IF, but a later matching input sample can request keypad IRQ again.

HALT and power-control registers

Writing HALTCNT with bit 7 clear requests HALT. Bits 0–6 are ignored. The current instruction finishes before the machine pauses CPU execution. Timers, display timing, and DMA continue to run. HALT does not pause the native window or host input. Memory::halted() and Machine::halted() report whether HALT is still waiting.

HALT ends when IE & IF becomes nonzero, even when IME or CPSR.I blocks IRQ delivery. An already pending enabled request prevents the CPU from sleeping. Wake-up does not acknowledge IF. Clearing IF after wake-up does not put the CPU back into HALT. On the next machine step, ready DMA still takes priority. The CPU then enters an unmasked IRQ handler or resumes the next instruction.

Each HaltIdle step advances to the earliest display edge or independently clocked timer overflow. Display edges include every HBlank entry and scanline start. Cascaded timers receive pulses at their predecessor's overflow. These bounds prevent idle advances from skipping DMA triggers or wake-up events. They also keep frame execution bounded when no interrupt can wake the CPU. Idle steps neither fetch instructions nor change CPU registers; code, data, and internal cycle counts remain zero. HALT idle cycles advance an already active Game Pak opcode prefetch stream, without starting one. Exact hardware entry/exit delays and pipeline effects are not modeled.

CPU writes to POSTFLG and HALTCNT take effect only when the executing instruction address is within the 16 KiB BIOS region. ARM and Thumb use the same rule. This is an instruction-address check, not a simulated prefetched PC signal. CPU writes from RAM or cartridge ROM are ignored, including STOP requests. Bare Memory::write* calls remain host/debug setup and can write these registers without a CPU access context. DMA writes to the power-control block return DmaError::PowerControlDestination; this hardware edge case is not modeled.

POSTFLG resets to zero and stores only bit 0. Setting it does not perform a BIOS boot or initialize RAM. HALTCNT reads return zero as a placeholder, not hardware open-bus behavior. The reserved bytes at 0x04000302..0x04000303 read zero and ignore writes. Byte, halfword, and word writes are supported; a halfword/word at POSTFLG also writes HALTCNT.

Multi-byte and block-store diagnostics preserve POSTFLG, power state, CPU registers, and the current step's clock. CPU-only stepping can record HALT/STOP but does not enforce either pause. Use Machine::step() for device integration.

STOP with keypad wake-up

Writing HALTCNT with bit 7 set requests STOP. Bits 0–6 are ignored. The instruction that writes STOP finishes and pays its full nominal cycle cost. Later machine steps perform no CPU fetch, DMA transfer, timer tick, or display update. The nominal Game Pak prefetch queue also remains frozen. StepKind::StopIdle reports zero timing; it does not represent elapsed host wall time. Memory::stopped() and Machine::stopped() report this state. The corresponding halted() methods return false in STOP.

A matching live keypad condition wakes STOP when KEYCNT bit 14 and IE bit 12 are enabled. The same selected-button OR/AND logic applies, including the empty-mask behavior. IME and CPSR.I do not gate wake-up in this functional model. A matching condition already present at entry prevents a sustained stop. Stale IF flags alone cannot wake STOP, including an old keypad request. Timer, display, and DMA requests cannot wake STOP.

An input sample supplied while stopped updates KEYINPUT and the wake condition without setting IF. It does not consume the normal running-state keypad polling history. This follows GBATEK's note that IF is not set while the system clock is stopped. Existing IF bits remain unchanged. After wake-up, an old pending enabled IRQ may still be delivered. A later running-state input sample follows the normal keypad IRQ policy. Host/debug writes to IE or KEYCNT can also satisfy the wake condition; stopped CPU code cannot execute such writes.

After wake-up, ready DMA again takes priority, then IRQ delivery or CPU execution resumes. The shared timer divider phase, display position, RAM, and completed captured frames are retained, not reset. STOP does not automatically set forced blank or clear DISPCNT. Software can force blank before STOP if it wants the displayed image disabled.

Machine::run_until_vblank returns FrameRunError::Stopped on a stopped idle step rather than exhausting the step limit. A zero step limit still returns StepLimit(0). The demo frame runner maps this to GraphicsError::Stopped, including during startup, without changing its output image. A host can supply input and retry after wake-up. The current desktop demos do not enter STOP; the frontend does not yet provide a general game-sleep screen.

Only keypad wake-up is implemented. General-purpose serial and Game Pak wake sources require their missing external devices. Oscillator restart delays, exact entry/wake edges, and hardware-specific IF behavior remain unverified. There is no host-thread sleep or wall-clock accounting. STOP freezes the emulated timer-driven audio clock.

The graphics demo now uses the optional original BIOS replacement's VBlankIntrWait service. Its IRQ callback acknowledges VBlank and updates the BIOS RAM flag before the service returns.