Game Boy JOYP Input: Active-Low Matrix Scanning and Interrupt Edges
Read Game Boy buttons correctly through FF00 by scanning its active-low matrix, handling selection-induced edges, debouncing input, and testing joypad interrupts.
Game Boy input is not exposed as eight independent, active-high button bits. The FF00 P1/JOYP register selects one or both rows of a two-by-four button matrix, then reports the selected lines in its low nibble. A pressed button pulls its corresponding line low. This small hardware interface creates several easy-to-miss behaviors: software must invert the bits, sample after changing the row selector, avoid expecting direction and action keys to remain distinguishable when both rows are selected, and treat the joypad interrupt as an edge notification rather than a reliable button identity.
The matrix groups are directions and action buttons. Bit 4 selects the direction row when written low; bit 5 selects the action-button row when written low. The low nibble is read-only and maps paired buttons onto the same four lines: bit 3 is Down or Start, bit 2 is Up or Select, bit 1 is Left or B, and bit 0 is Right or A. Within whichever row is selected, a pressed key reads as zero and an unpressed key reads as one.
Scan one row at a time
To read the directional pad, write bit 4 low and bit 5 high, then read FF00 and invert only the low nibble. The following RGBDS fragment demonstrates the sequence. It reads the register several times after changing the selector and uses the final value, a pattern Pan Docs notes is common because early reads provide a short settling delay.
DEF rP1 EQU $FF00
; Select directions: bit 4 = 0, bit 5 = 1.
ld a, $20
ldh [rP1], a
ldh a, [rP1]
ldh a, [rP1]
ldh a, [rP1] ; use the final sample after the short settling delay
cpl
and $0F ; active-high bits: Down, Up, Left, Right
ld [wDirectionKeys], a
; Select action keys: bit 5 = 0, bit 4 = 1.
ld a, $10
ldh [rP1], a
ldh a, [rP1]
ldh a, [rP1]
ldh a, [rP1]
cpl
and $0F ; active-high bits: Start, Select, B, A
ld [wActionKeys], a
; Leave both rows unselected when a neutral register state is desired.
ld a, $30
ldh [rP1], a
After inversion, use the row-specific meaning of each bit. Bit 0 in the first sample means Right; bit 0 in the second means A. A shared helper can reduce code, but the caller must still keep track of which row was selected. Preserve only the low nibble when deriving button state because the upper bits are not part of the eight-key result and may have model-dependent read behavior.
Writing $30 leaves both rows unselected; Pan Docs specifies that the lower nibble then reads $F, the all-released pattern. If a program needs a complete button snapshot, scan both rows and combine the two active-high results in software. Do not expect one FF00 read to return eight distinct buttons.
Understand what selecting both rows loses
Clearing both selector bits enables both rows. The four low lines then reflect either key connected to each line, using active-low behavior. As a result, an action key and a direction key on the same bit position can collapse into one low input. For example, the interface cannot tell whether bit 0 became low because Right was held, A was held, or both were held. Scanning rows separately preserves that distinction.
This is not an application-level ghost-key filter. It is a consequence of reading a shared matrix without identifying which row supplied a low column. If simultaneous controls matter, read the rows separately and document any input combinations that still cannot be distinguished. Do not “correct” the hardware value with a generic keyboard library’s rollover rules.
There is also a mode boundary: Super Game Boy software can use the joypad register for its own command signaling and can access multiple controller states through that protocol. A program or emulator that supports SGB behavior must not assume every sequence of FF00 writes is an ordinary handheld button scan. Keep the SGB command protocol separate from standard DMG/CGB input handling.
Joypad interrupts report falling edges
The joypad interrupt is requested when any of P1 bits 0-3 changes from high to low. A selected button press can cause this transition, but the interrupt does not encode which key changed. Software that needs identity should sample the matrix and compare the resulting state with its previous snapshot. The interrupt is best treated as a prompt to inspect input, not as a decoded key event.
Because the condition is edge-based, the currently selected rows matter. A button that is held while its row is unselected is not necessarily visible on the low nibble. Selecting that row can expose a low line and cause the high-to-low transition the interrupt detects. Similarly, pressing a second key tied to a line that is already low may produce no new edge. Polling state after the interrupt is therefore still necessary when the game needs to know what is held.
Physical switches can bounce. Pan Docs notes that one press may produce one or more high-to-low transitions, so software should debounce the interpreted button state rather than assuming one interrupt equals one clean press. A common approach is to collect successive snapshots over a small, measured interval and accept a change only after it remains stable. The interval is an application choice; it is not a universal hardware timing constant. Avoid long delays in the interrupt handler itself. Record a pending scan or timestamp, return promptly, and debounce in the main update loop.
The interrupt can also be useful as a wake signal when the CPU is in STOP, provided software leaves the relevant input group selected. If the code has deselected all rows, the register does not show a selected key line going low. When both rows are selected, the interrupt may wake the system but cannot distinguish which of the paired buttons caused it. Use the interrupt enable and IME state independently from the joypad register: a pending request, an enabled source, and CPU interrupt acceptance are separate parts of the interrupt path.
Model input and interrupt behavior without inventing precision
For an emulator, maintain the host button state and the guest’s FF00 selection bits as separate inputs. Derive the four visible lines by starting with all lines high and pulling a line low when a pressed key belongs to a selected row. Compare the old and new visible low nibbles whenever either the host state or selector changes. If any bit changes from one to zero, request the joypad interrupt. Do not request repeatedly merely because a key remains held and the line stays low.
/* Illustrative register-level model; row and interrupt bit names are local. */
uint8_t joyp_low_nibble(uint8_t select, uint8_t directions,
uint8_t actions) {
uint8_t lines = 0x0F; /* unpressed inputs read high */
if ((select & 0x10) == 0)
lines &= directions; /* active-low direction row */
if ((select & 0x20) == 0)
lines &= actions; /* active-low action row */
return lines;
}
void update_joyp(uint8_t new_select, uint8_t new_directions,
uint8_t new_actions) {
uint8_t before = joyp_low_nibble(select, directions, actions);
select = new_select;
directions = new_directions;
actions = new_actions;
uint8_t after = joyp_low_nibble(select, directions, actions);
if ((before & (uint8_t)~after & 0x0F) != 0)
interrupt_flags |= 0x10; /* joypad request in IF */
}
This is a behavioral outline, not cycle-perfect PPU/CPU code. The important invariant is the falling-edge test (before & ~after): a high line that becomes low requests the interrupt. A hardware-accurate implementation should verify selector-write timing, STOP wake behavior, and model-specific electrical details against current primary documentation and test ROMs. Do not add an arbitrary fixed delay or synthetic switch bounce to every emulated press unless the target’s observable behavior requires it. Host keyboard debouncing, controller polling cadence, and guest hardware semantics are different layers.
Build a regression matrix around transitions
Test each row independently with all four buttons released and pressed one at a time. Verify that the low nibble is active-low, that inversion affects only bits 0-3, and that $30 produces the documented all-high low nibble. Add simultaneous keys, both selector bits low, and a held key while switching from an unselected row to a selected one. Specifically test same-column pairs such as Right plus A so the expected information loss is explicit rather than misdiagnosed as a game bug.
For interrupts, test a selected press, release, repeated press, a key held before its row is selected, a second same-column key while the line is already low, and a pending interrupt while CPU interrupt acceptance is disabled. Include a STOP wake scenario with relevant groups selected and a negative case with them unselected. On real hardware, repeated transitions caused by switch bounce may differ between physical devices; do not make emulator conformance depend on one keyboard’s mechanical noise.
The Mooneye Test Suite documents model and SoC-revision coverage and cautions that not all tests pass on every Game Boy-family device. Use its model-aware evidence alongside Pan Docs, and record the exact target when a timing claim depends on hardware. For an emulator, compare P1 reads, IF transitions, and STOP behavior, not just the button state displayed by the frontend.
Reliable Game Boy controls come from respecting the register’s shape: select rows, wait long enough to sample, invert the active-low nibble, and interpret each sample in its row context. Treat interrupts as falling-edge hints, debounce at the game layer, and keep SGB’s additional controller protocol out of the normal handheld matrix. That gives both game code and emulators predictable behavior without claiming precision the public hardware evidence does not establish.
Related:
- Game Boy STAT and LYC Interrupts: Building Reliable Scanline Effects
- Game Boy Link Cable Serial Transfer: Registers, Clocking, and Timeouts
Sources: