Input¶
Joypad reading, state tracking, and input patterns for NGPC homebrew on the TLCS-900H.
1. Hardware Register¶
1.1 Address¶
| Name | Address | Size | Description |
|---|---|---|---|
HW_JOYPAD |
0x6F82 |
u8 |
BIOS RAM variable — joypad state, updated each VBlank |
Important:
0x6F82is a BIOS internal RAM variable (bank 3), refreshed by the BIOS VBlank handler. This is the address to use. Value is active-high: bit = 1 means button pressed.Note: Some code references address
0xB000with inverted bits (keys = ~REG_KEYPAD). That is the raw hardware keypad register (before BIOS processing). Prefer0x6F82(BIOS variable): it is already synchronized to VBlank and does not require bit inversion.
1.2 Button Bitmasks¶
#define PAD_UP 0x01
#define PAD_DOWN 0x02
#define PAD_LEFT 0x04
#define PAD_RIGHT 0x08
#define PAD_A 0x10
#define PAD_B 0x20
#define PAD_OPTION 0x40
#define PAD_POWER 0x80
Alternative naming convention used in several homebrews (equivalent):
#define J_UP 0x01
#define J_DOWN 0x02
#define J_LEFT 0x04
#define J_RIGHT 0x08
#define J_A 0x10
#define J_B 0x20
#define J_OPTION 0x40
#define J_POWER 0x80
Both conventions are interchangeable. Pick one and stick to it.
2. Basic Polling¶
2.1 Simple Read (each frame)¶
static u8 g_pad; /* current state */
void poll_input(void)
{
g_pad = HW_JOYPAD;
}
/* usage */
if (g_pad & PAD_A) { /* A is held */ }
Call once per frame, typically at the start of the game loop (after the VBlank halt sync).
Never read HW_JOYPAD multiple times per frame.
2.2 Position in the Main Loop¶
void main(void)
{
init();
while (1) {
halt; /* sync to VBlank */
watchdog_clear();
poll_input(); /* single read per frame */
update_game();
render();
}
}
CPU cost for input read: negligible (~10 cycles).
3. Advanced State Detection¶
3.1 Edge Detection — Button "just pressed"¶
Essential for one-shot actions (fire, jump, menu confirm) — prevents unwanted key repeat:
static u8 g_pad; /* current state */
static u8 g_pad_prev; /* previous frame state */
void poll_input(void)
{
g_pad_prev = g_pad;
g_pad = HW_JOYPAD;
}
/* Utility macros */
#define PAD_PRESSED(btn) ( (g_pad & (btn)) && !(g_pad_prev & (btn)) )
#define PAD_RELEASED(btn) ( !(g_pad & (btn)) && (g_pad_prev & (btn)) )
#define PAD_HELD(btn) ( g_pad & (btn) )
Usage example:
if (PAD_PRESSED(PAD_A)) { fire_bullet(); } /* one bullet per press */
if (PAD_HELD(PAD_RIGHT)) { move_right(); } /* continuous while held */
3.2 Directional Mask Read¶
Efficient approach — single register access, then a switch on the combined mask:
u8 dirs = g_pad & (PAD_UP | PAD_DOWN | PAD_LEFT | PAD_RIGHT);
switch (dirs) {
case PAD_UP: /* up */ break;
case PAD_DOWN: /* down */ break;
case PAD_LEFT: /* left */ break;
case PAD_RIGHT: /* right */ break;
case PAD_UP | PAD_RIGHT: /* diagonal up-right */ break;
case PAD_UP | PAD_LEFT: /* diagonal up-left */ break;
case PAD_DOWN | PAD_RIGHT: /* diagonal down-right */ break;
case PAD_DOWN | PAD_LEFT: /* diagonal down-left */ break;
case PAD_LEFT | PAD_RIGHT: /* cancel (both held) */ break;
}
One & mask, then a switch — cc900 compiles this to a jump table.
4. Special Patterns¶
4.1 Blocking Wait (menu / title screen)¶
Waiting for a button press outside the normal game loop:
/* Wait for release (debounce), then press, then release */
while (HW_JOYPAD & PAD_A); /* wait for release if already held */
while (!(HW_JOYPAD & PAD_A)); /* wait for press */
while (HW_JOYPAD & PAD_A); /* wait for release */
/* continue */
Do not forget the watchdog in blocking loops that can span multiple frames (risk of hardware reset).
4.2 Sleep for N VBlanks¶
Standard homebrew pattern for transitions and animations:
/* g_vblank_count is incremented in the VBlank handler */
extern volatile u16 g_vblank_count;
void sleep_frames(u16 n)
{
u16 start = g_vblank_count;
while ((g_vblank_count - start) < n)
; /* busy-wait */
}
Use unsigned subtraction (
u16 - u16) to handle counter wrap correctly.
4.3 OPTION / POWER Buttons¶
PAD_OPTION(0x40): Option button (left side). Useful for pause, settings menu.PAD_POWER(0x80): Power button. Handled by the BIOS (shutdown). Do not intercept without a specific reason. CheckHW_USR_SHUTDOWN(0x6F85) if power management is needed.
5. Integration in the VBlank Handler¶
The watchdog must be cleared in the VBlank ISR. The joypad can also be updated there, but only if the ISR remains lightweight — do not put game logic inside the ISR:
void vblank_isr(void)
{
WATCHDOG = WATCHDOG_CLEAR; /* 0x4E -> 0x006F */
g_vblank_count++;
audio_tick();
/* Do NOT read HW_JOYPAD here — read it in the game loop instead */
}
Rule: Read HW_JOYPAD in the game loop (post-halt), not in the ISR.
The ISR must stay under ~2,000 cycles (watchdog + counter + audio).
6. Example Button Mappings¶
6.1 Shmup¶
| Action | Button | Mode |
|---|---|---|
| Move | D-Pad | Continuous (PAD_HELD) |
| Fire | A | Continuous or edge-triggered depending on fire rate |
| Bomb / Option | B | Edge-triggered (PAD_PRESSED) |
| Pause | OPTION | Edge-triggered (PAD_PRESSED) |
6.2 Sports / Menu Game¶
| Context | Button | Action |
|---|---|---|
| Match | D-Pad | Movement |
| Match | A | Throw / Catch |
| Match | B | Dash |
| Menu | D-Pad up/down | Change entry |
| Menu | A | Confirm |
| Settings | B | Back |
| Intro | A | Skip to main menu |
7. Pitfalls and Notes¶
7.1 Read the Register Once per Frame¶
Never call HW_JOYPAD directly from multiple places in the code.
Always read it in poll_input() — snapshot into a global variable.
This prevents inconsistent values if the player releases a button mid-frame.
7.2 volatile Is Mandatory¶
/* CORRECT */
#define HW_JOYPAD (*(volatile u8*)0x6F82)
/* INCORRECT — cc900 may cache the value in a register */
#define HW_JOYPAD (*(u8*)0x6F82)
Without volatile, the compiler is allowed to read the register only once and reuse the cached
value — buttons will appear stuck.
7.3 Initialize g_pad_prev at Boot¶
void input_init(void)
{
g_pad = HW_JOYPAD;
g_pad_prev = g_pad; /* prevents a false "just pressed" on the first frame */
}
Without this, g_pad_prev starts at 0 and g_pad starts with the actual button state —
any held button on boot triggers a spurious PAD_PRESSED event on frame 1.
7.4 PAD_POWER and PAD_OPTION Are Not D-Pad Buttons¶
Both are discrete hardware buttons located on the side of the unit.
PAD_OPTION is often the only "menu/pause" button available — use it sparingly and
ensure the player can always find their way back into the game.
8. Input Module¶
A reusable input module commonly manages:
- Per-frame pad snapshot
held/pressed/releasedstate tracking- Optional
repeatfor menu navigation
8.1 API¶
void ngpc_input_update(void); /* Call once per frame */
extern u8 ngpc_pad_held; /* Buttons currently down */
extern u8 ngpc_pad_pressed; /* Buttons just pressed (this frame) */
extern u8 ngpc_pad_released; /* Buttons just released (this frame) */
Usage:
ngpc_input_update();
if (ngpc_pad_pressed & PAD_A) { /* one-shot action */ }
if (ngpc_pad_held & PAD_RIGHT) { /* continuous move */ }
8.2 Auto-Repeat (Menus)¶
Per-button auto-repeat is useful for list navigation:
void ngpc_input_set_repeat(u8 delay, u8 rate); /* delay/rate in frames */
extern u8 ngpc_pad_repeat; /* repeated presses this frame */
Pattern:
ngpc_input_set_repeat(15, 4); /* 15-frame initial delay, repeat every 4 frames */
if ((ngpc_pad_pressed | ngpc_pad_repeat) & PAD_DOWN) { cursor_next(); }
if ((ngpc_pad_pressed | ngpc_pad_repeat) & PAD_UP) { cursor_prev(); }
The repeat is typically implemented in ~35 lines as a per-button counter.
A representative joypad RAM layout uses one byte per signal:
prev_raw current raw joypad (previous frame, used for delta)
cur_pad current joypad state (this frame)
released released buttons (went 1→0 this frame)
pressed pressed buttons (went 0→1 this frame) / A+B state
repeat_flags auto-repeat fired flags (one bit per button)
repeat_ctr auto-repeat down-counter (resets to 12 on any new press)
ex (addr), R idiom for current/previous swap:
ld H, (HW_JOYPAD) ; H = current hardware reading
ex (prev_raw), H ; H = prev (old prev_raw); (prev_raw) = current
ld (cur_pad), H ; save previous
xor H, (prev_raw) ; H = prev XOR current = changed bits
and H, (cur_pad) ; H = changed AND prev = RELEASED (1→0)
ld (released), H ; save released
For pressed (0→1 rising edge) — separate path using xor W,A; and W,A:
; W = prev A-buttons, A = cur A-buttons
xor W, A ; W = changed
and W, A ; W = changed AND cur = PRESSED (0→1)
ld (pressed), WA
Auto-repeat counter logic:
cp W, 0 ; any new press?
jr NZ, reset_timer ; yes: reset timer
dec (repeat_ctr) ; no: count down
jr NZ, done ; timer not zero: nothing
or (repeat_flags), A ; timer hit 0: fire auto-repeat for this button
reset_timer:
ld (repeat_ctr), 0xC ; reset counter to 12 frames
done: ret
The 12-frame initial repeat delay matches typical menu feel at 60fps (~200 ms before repeat fires).
Quick Reference¶
| Item | Value / Pattern | Notes |
|---|---|---|
| BIOS joypad address | 0x6F82 |
u8, active-high, updated each VBlank |
| Raw hardware address | 0xB000 |
Inverted bits — avoid, use BIOS var instead |
| PAD_UP | 0x01 |
|
| PAD_DOWN | 0x02 |
|
| PAD_LEFT | 0x04 |
|
| PAD_RIGHT | 0x08 |
|
| PAD_A | 0x10 |
|
| PAD_B | 0x20 |
|
| PAD_OPTION | 0x40 |
Side button, use for pause/menu |
| PAD_POWER | 0x80 |
BIOS-managed, use with caution |
| Edge detect — pressed | (g_pad & btn) && !(g_pad_prev & btn) |
One-shot actions |
| Edge detect — released | !(g_pad & btn) && (g_pad_prev & btn) |
|
| Held | g_pad & btn |
Continuous while pressed |
| Blocking wait (debounce) | release → press → release loop on HW_JOYPAD |
Title screen / menus |
| Sleep N frames | while (g_vblank_count - start < n) ; |
u16 subtraction (wrap-safe) |
| volatile | mandatory | Without it, cc900 caches the value |
| Init at boot | Set g_pad = g_pad_prev = HW_JOYPAD |
Prevent frame-1 false press |
| Auto-repeat | ngpc_input_set_repeat(delay, rate) |
Typical: delay=15, rate=4 |
See Also¶
- Hardware Registers — BIOS RAM map (
0x6F82context, watchdog address) - Game Loop — VBlank ISR structure, main loop skeleton, watchdog rules
- BIOS —
HW_USR_SHUTDOWN,HW_LANGUAGE, BIOS variable layout - Gameplay Patterns — State machines, pause, menu flow