This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Default Joypad and Paddle
Summary
It was implementedjoypad(button and direction control) of the Game Boy with Active Low logic, and theBGP palette initializationto 0xE4 by default. Diagnostic logs revealed that the game was polling the Joypad (P1) and that the paddle was at 0x00 (all white), making graphics invisible even if rendered correctly. With these corrections, The emulator can respond to button presses and display correctly colored graphics.
Hardware Concept
Hejoypadof the Game Boy uses a system ofActive Lowwhere:
- 0 = Button pressed
- 1 = Button released
Register P1 (0xFF00) is read/write:
- WRITE (bits 4-5):The game selects what to read:
- Bit 4 = 0: Want to read Addresses (Right, Left, Up, Down)
- Bit 5 = 0: Want to read Buttons (A, B, Select, Start)
- READ (bits 0-3):The game reads the state according to the selector:
- Bit 0: Right/A
- Bit 1: Left/B
- Bit 2: Up/Select
- Bit 3: Down/Start
When a button goes from Released (1) to Pressed (0), the Joypad interrupt is activated (Bit 4 in IF, 0xFF0F).
TheBGP palette(Background Palette, 0xFF47) controls the background colors. On a real Game Boy, The Boot ROM leaves this registry set to0xE4(11100100 in binary), which maps:
- Index 0 → White (0)
- Index 1 → Light gray (1)
- Index 2 → Dark gray (2)
- Index 3 → Black (3)
If BGP remains at 0x00 (all white), even if the game renders the tiles correctly, all pixels whites will appear and the screen will appear completely white.
Fountain:Pan Docs - Joypad Input, Background Palette Register (BGP)
Implementation
A class was createdjoypadwhich implements the Active Low logic and manages the P1 register.
Integrated into the MMU to intercept P1 reads/writes, and into Viboy to capture events
of pygame keyboard and update the Joypad status.
Components created/modified
src/io/joypad.py: New classjoypadwith methodspress(),release(),read(),write(). Implements Active Low logic and requests interrupts when a button is pressed.src/io/__init__.py: New I/O module.src/memory/mmu.py:- BGP initialization to 0xE4 on
__init__(). - P1 read/write intercept (0xFF00) delegating to Joypad.
- Method
set_joypad()to connect the Joypad to the MMU.
- BGP initialization to 0xE4 on
src/viboy.py:- Joypad instantiation and connection to the MMU.
- Method
_handle_pygame_events()which captures keyboard events and updates the Joypad. - Key Mapping: K_UP/DOWN/LEFT/RIGHT (directions), K_z (A), K_x (B), K_RETURN (Start), K_RSHIFT (Select).
tests/test_io_joypad.py: Complete test suite (14 tests) validating initialization, Active Low logic, read selector, interrupts and integration with MMU.
Design decisions
Active Low Logic:Correctly implemented where True = pressed (bit 0 in hardware), False = released (bit 1 in hardware). When reading P1, if a button is pressed, the corresponding bit is cleared.
Transition detection:Interrupt is only requested when a button passes release
to pressed. If calledpress()several times withoutrelease()intermediate, only the first
Pressing activates the interruption.
Reading selector:The selector (bits 4-5) is saved when the game writes to P1. When reading, the state of the corresponding buttons is returned according to the selector.
Event management:Centralized pygame event handling in_handle_pygame_events()within Viboy, rather than in the Renderer, to maintain separation of responsibilities.
Affected Files
src/io/joypad.py- New Joypad classsrc/io/__init__.py- New I/O modulesrc/memory/mmu.py- BGP initialization=0xE4, P1 interception, set_joypad() methodsrc/viboy.py- Joypad instantiation, _handle_pygame_events() methodtests/test_io_joypad.py- Complete test suite (14 tests)
Tests and Verification
Unit tests were executed with pytest validating:
Tests executed
- Command:
python3 -m pytest tests/test_io_joypad.py -v - Around:macOS, Python 3.9.6
- Result:14 tests PASSED in 0.33s
Tests implemented
test_default_palette_init: Verifies that BGP is initialized to 0xE4.test_joypad_initial_state: Verify that all buttons are released at startup.test_joypad_read_default: Verify reading with default selector.test_joypad_read_directions: Verifies reading of addresses when they are released.test_joypad_read_directions_pressed: Verifies reading when Right is pressed (bit 0 = 0).test_joypad_read_buttons: Verifies reading of buttons when they are released.test_joypad_read_buttons_pressed: Verifies reading when A is pressed (bit 0 = 0).test_joypad_press_interrupt: Verifies that pressing a button activates IF bit 4.test_joypad_release_no_interrupt: Verify that releasing a button does NOT activate interrupt.test_joypad_press_twice_no_double_interrupt: Verify that pressing twice and holding only activates interruption the first time.test_joypad_press_release_press_interrupt: Verify that press-release-press triggers interrupt both times.test_joypad_mmu_integration: Verify integration with MMU (read/write P1 works correctly).test_joypad_all_directions: Verifies reading of all addresses when they are pressed.test_joypad_all_buttons: Verifies reading of all buttons when they are pressed.
Test code (example: interrupt detection)
def test_joypad_press_interrupt(self) -> None:
"""Test: Pressing a button should activate the Joypad interrupt (bit 4 in IF)"""
mmu = MMU(None)
joypad = Joypad(mmu)
# Clear IF
mmu.write_byte(IO_IF, 0x00)
assert(mmu.read_byte(IO_IF) & 0x10) == 0
# Press Start
joypad.press("start")
# Verify that IF bit 4 is active
if_val = mmu.read_byte(IO_IF)
assert (if_val & 0x10) != 0
What is valid:This test shows that the Joypad hardware requests an interrupt when a button is pressed, activating bit 4 of the IF register (Interrupt Flag). This allows the CPU respond to user input events.
Sources consulted
- Bread Docs:Joypad Input
- Bread Docs:Background Palette Register (BGP)
- Bread Docs:CPU Interrupts (Joypad interrupt, bit 4)
Educational Integrity
What I Understand Now
- Active Low is essential:The reverse logic (0 = pressed, 1 = released) is a feature of the actual Game Boy hardware, not an arbitrary choice. This allows the hardware to use pull-up resistors and detect pulses by connecting to ground.
- Reading selector:The fact that the game has to write to P1 to select what to read (addresses vs buttons) is a hardware limitation that reduces the number of pins needed. The game must alternate between reading directions and buttons.
- Transition interruptions:The interrupt is only activated on the transition from released to pressed, not while the button is held down. This prevents interruption spam and allows the game to detect discrete "clicks."
- Importance of default values:Although we haven't implemented Boot ROM yet, the values left by Boot ROM (such as BGP=0xE4) are critical. Without these values, many games do not work correctly because they assume the Boot ROM set them up.
What remains to be confirmed
- Interrupt Timing:It's not entirely clear if there is any delay between pressing a button and the IF bit setting, or if it is instantaneous. For now, we assume it's instantaneous.
- Behavior of bits 4-5 when reading:On real hardware, when P1 is read, bits 4-5 reflect the selector (what the game wrote), but there could be behavioral details that are not fully documented. For now, we keep bits 4-5 as they are in the selector.
Hypotheses and Assumptions
The interrupt request is assumed to be instantaneous when a button is pressed (no delay). It is assumed that if the game has not written a valid selector (both bits 4-5 = 1), the read returns all bits set to 1 (all buttons "released" from a hardware perspective).
Next Steps
- [ ] Test the emulator with a real game to verify that the Joypad works correctly
- [ ] Implement Scroll (SCX/SCY) so that the game can scroll the background
- [ ] Implement Window (WX/WY) so that the game can display an overlay window
- [ ] Implement Sprites (OAM) so that the game can display moving objects
- [ ] Implement full Timer (TIMA, TMA, TAC) for precise synchronization