gameboy-rust/docs/hardware/timers-irq.md
0xrsydn 597b02881b feat(input): implement KEYCNT keypad interrupt control
Add a dependency-free keypad state machine in gba-core, map KEYCNT, latch IF bit 12, and wake HALT through the existing interrupt gates. Sample complete CPU/DMA control writes once so transient byte values cannot create false requests.

Document the mGBA-style polling policy and its unverified hardware retrigger limits. Cover every button-state/selection pair, register masks, repeated requests, byte/word writes, failed-store isolation, DMA priority, ARM/Thumb IRQ return, BIOS IntrWait, and retained BIOS reset state.

Follow the restructured workspace and modular documentation. Validated on Darwin arm64: workspace and core/demo tests pass in debug and release, with Clippy, rustdoc, ten native window smoke tests, and terminal demos passing.
2026-10-05 18:05:26 +07:00

165 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Timers, interrupts, and HALT
## Timers, interrupt registers, and the machine clock
`Machine::new(cpu, memory)` combines the CPU and memory bus.
`Machine::step()` first 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 and timer 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.
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
Bus writes take effect before the step's device-clock update.
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
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.**
Device clocks update in batches after instructions, exception entries, DMA units, or HALT idle intervals.
Reads observe register state before that batch.
Writes, including timer start/stop and IF acknowledgement, take effect before the entire batch.
This does not reproduce bus-access timing within an instruction or IRQ synchronization delays.
`Cpu::step` remains a CPU-only API. It does not honor HALT, 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.
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 the supplied cycle total as a wrapping 64-bit counter.
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 supported, STOP returns a diagnostic |
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.
Timer startup delays and shared prescaler phase are not modeled.
The provisional timer model resets its private prescaler phase on enable or clock-source changes.
Audio events, STOP, and 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 ends after 21 steps and 153 nominal cycles with one handler call, using default WAITCNT settings.
These counts test the timing model; they are not hardware timing 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
The current 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 still returns a diagnostic; this change does not implement STOP wake-up.
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.
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.
STOP requests from BIOS code or host setup return `MemoryError::UnsupportedStop` before any write commits.
This prevents STOP from silently behaving like HALT while its clock-gating and wake sources are absent.
Multi-byte and block-store diagnostics preserve POSTFLG, HALT state, CPU registers, and the current step's clock.
CPU-only stepping can record a HALT request but does not enforce the pause. Use `Machine::step()` for device integration.
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.