BIOS Reference¶
BIOS services, calling conventions, vector table, register bank 3, watchdog, and interrupt vector assignments for the Neo Geo Pocket Color.
Note: All content uses ASCII only (avoids encoding issues on Windows/PowerShell).
1. Calling Convention — Vector Table (0xFFFE00)¶
Most BIOS functions are accessible via a function table at 0xFFFE00.
Each entry is a 32-bit pointer. Index = vector number.
Call sequence:
; 1. Load the vector number into RW3
ld rw3, <VECTOR>
; 2. Load parameters into bank 3 registers (XHL3, XDE3, RB3, RC3...)
; 3. Switch to bank 3
ldf 3
; 4. Compute offset in the table (vector * 4)
add w, w ; w *= 2
add w, w ; w *= 2 => w = vector * 4
; 5. Load address from table and call
ld xix, 0xFFFE00
ld xix, (xix+w)
call xix
C example (RTC get):
__asm(" ld rw3, " NGPC_STR(BIOS_RTCGET));
__asm(" ld xde, (xsp+4)"); /* NgpcTime* from stack */
__asm(" ld xhl3, xde"); /* XHL3 = pointer to struct */
__asm(" ldf 3");
__asm(" add w, w");
__asm(" add w, w");
__asm(" ld xix, 0xfffe00");
__asm(" ld xix, (xix+w)");
__asm(" call xix");
2. Calling Convention — SWI 1¶
Some BIOS functions use the swi 1 (software interrupt 1) instruction,
with parameters loaded into bank 3 registers before the swi.
; Load parameters into bank 3 WITHOUT switching (swi reads them directly)
ldb ra3, <function_code>
ldb rw3, <VECTOR>
swi 1
C example (shutdown):
swi 1and the table are the same dispatcher. The BIOS'sswi 1handler readsRW3and jumps through the very same0xFFFE00 + RW3*4table, so any vector can be reached either way — the two conventions above are a style choice, not two different mechanisms. (Confirmed by disassembly and by a clean-room HLE re-implementation that services both paths through one table.)
3. Register Bank 3¶
BIOS calls use registers from "bank 3" (dedicated system call registers).
| Register | Alias | Size | Role |
|---|---|---|---|
RW3 |
W of bank 3 | 16-bit | Vector number |
RA3 |
A of bank 3 | 8-bit | Function code / parameter |
RB3 |
B of bank 3 | 8-bit | Integer parameter (e.g., flash block number) |
RC3 |
C of bank 3 | 8-bit | Integer parameter (e.g., auto-speedup flag) |
RBC3 |
BC of bank 3 | 16-bit | 16-bit parameter (e.g., number of flash pages) |
XHL3 |
HL of bank 3 | 24-bit | Source pointer (e.g., data address) |
XDE3 |
DE of bank 3 | 24-bit | Destination pointer / offset |
4. BIOS Vector Table¶
Defined in ngpc_hw.h as BIOS_* constants.
| Constant | Value | Description | Convention |
|---|---|---|---|
BIOS_SHUTDOWN |
0 | Power off the console | SWI 1, ra3=3 |
BIOS_CLOCKGEARSET |
1 | Change CPU speed | Table 0xFFFE00 |
BIOS_RTCGET |
2 | Read time/date (BCD) | Table 0xFFFE00 |
BIOS_INTLVSET |
4 | Configure interrupt levels | Table 0xFFFE00 |
BIOS_SYSFONTSET |
5 | Load system font into tile RAM | SWI 1, ra3=3 |
BIOS_FLASHWRITE |
6 | Write to flash | SWI 1 |
BIOS_FLASHALLERS |
7 | Erase all flash blocks | SWI 1 |
BIOS_FLASHERS |
8 | Erase one flash block | SWI 1, ra3=0, rb3=block |
BIOS_ALARMSET |
9 | Set an alarm (console on) | Table 0xFFFE00 |
BIOS_ALARMDOWNSET |
11 | Set a wake-up alarm (console off) | Table 0xFFFE00 |
BIOS_FLASHPROTECT |
13 | Protect flash blocks | Table 0xFFFE00 |
BIOS_GEMODESET |
14 | Switch K1GE/K2GE mode | Table 0xFFFE00 |
BIOS_COMINIT |
16 (0x10) | Serial link-cable: init (8N1, CTS/RTS, 19200 bps) | Table 0xFFFE00 |
BIOS_COMSENDSTART |
17 (0x11) | Serial: start / flush TX ring to the wire | Table 0xFFFE00 |
BIOS_COMRECIVESTART |
18 (0x12) | Serial: enable reception (both consoles ON) | Table 0xFFFE00 |
BIOS_COMCREATEDATA |
19 (0x13) | Serial: push 1 byte to TX ring (rb3) | Table 0xFFFE00 |
BIOS_COMGETDATA |
20 (0x14) | Serial: pop 1 byte from RX ring (rb3) | Table 0xFFFE00 |
BIOS_COMONRTS |
21 (0x15) | Serial: RTS low = allow peer (port 0xB2 bit0) | Table 0xFFFE00 |
BIOS_COMOFFRTS |
22 (0x16) | Serial: RTS high = block peer | Table 0xFFFE00 |
BIOS_COMSENDSTATUS |
23 (0x17) | Serial: TX status (flags|count) | Table 0xFFFE00 |
BIOS_COMRECIVESTATUS |
24 (0x18) | Serial: RX status (flags|count), clears errors | Table 0xFFFE00 |
BIOS_COMCREATEBUFDATA |
25 (0x19) | Serial: block TX (xhl3=ptr, rb3=size) | Table 0xFFFE00 |
BIOS_COMGETBUFDATA |
26 (0x1A) | Serial: block RX (xhl3=ptr, rb3=size) | Table 0xFFFE00 |
Serial vectors
0x10-0x1A(16-26) were absent from every publicngpc.h; recovered from the BIOS dump and each routine disassembled to confirm it. See the dedicated Serial / Link Cable section below.
5. BIOS Call Details¶
5.1 BIOS_SHUTDOWN (0) — Power off¶
/* ra3=3 = shutdown function code */
__asm("ldb ra3, 3");
__asm("ldb rw3, " NGPC_STR(BIOS_SHUTDOWN));
__asm("swi 1");
5.2 BIOS_CLOCKGEARSET (1) — CPU speed¶
/* rb3 = divisor (0=6MHz, 1=3MHz, 2=1.5MHz, 3=768KHz, 4=384KHz) */
/* rc3 = 0 (no joypad auto-speedup), or 1 to enable */
__asm("ld rw3, " NGPC_STR(BIOS_CLOCKGEARSET));
/* load divider from stack: */
__asm("ld xde, (xsp+4)");
__asm("ld b, e"); /* rb3 = divider */
__asm("ld c, 0"); /* rc3 = 0 */
__asm("ldf 3");
__asm("add w, w"); __asm("add w, w");
__asm("ld xix, 0xfffe00");
__asm("ld xix, (xix+w)");
__asm("call xix");
5.3 BIOS_RTCGET (2) — Read clock¶
/* xhl3 = pointer to NgpcTime (7 bytes, BCD) */
__asm(" ld rw3, " NGPC_STR(BIOS_RTCGET));
__asm(" ld xde, (xsp+4)"); /* NgpcTime* from stack */
__asm(" ld xhl3, xde");
__asm(" ldf 3");
__asm(" add w, w"); __asm(" add w, w");
__asm(" ld xix, 0xfffe00");
__asm(" ld xix, (xix+w)");
__asm(" call xix");
5.4 BIOS_SYSFONTSET (5) — Load system font¶
/* Loads 96 ASCII glyphs (0x20-0x7F) into tile RAM starting at slot 32 */
__asm("ldb ra3, 3");
__asm("ldb rw3, " NGPC_STR(BIOS_SYSFONTSET));
__asm("swi 1");
Tile slots 0-31 = reserved. Slots 32-127 = system font after this call. Slots 128+ are free for user tiles.
5.5 BIOS_FLASHERS (8) + BIOS_FLASHWRITE (6) — Save to flash¶
/* Step 1: erase the flash block */
/* ra3=0 (ROM base), rb3=block number (0x21 for offset 0x1FA000), rw3=8 */
/* The block number is per-cartridge-size: 0x09 / 0x11 / 0x21 for 4 / 8 / 16 Mbit. */
/* The BIOS turns it into an address using the card-type byte it stored at 0x6C58 */
/* at power-on — read that byte instead of hardcoding a size. */
/* See Storage-and-Saves §5.0. */
__asm("ld ra3,0");
__asm("ld rb3," NGPC_STR(SAVE_BLOCK));
__asm("ld rw3," NGPC_STR(BIOS_FLASHERS));
HW_WATCHDOG = WATCHDOG_CLEAR; /* mandatory before long flash operation */
__asm("swi 1");
/* Step 2: write 256 bytes */
/* ra3=0, rbc3=1 (1*256=256 bytes), xhl3=source, xde3=dest offset, rw3=6 */
__asm("ld ra3,0");
__asm("ld rbc3,1");
__asm("ld xhl,(xsp+4)"); /* data* from stack */
__asm("ld xhl3,xhl");
__asm("ld xde3," NGPC_STR(SAVE_OFFSET_ASM));
__asm("ld rw3," NGPC_STR(BIOS_FLASHWRITE));
HW_WATCHDOG = WATCHDOG_CLEAR;
__asm("swi 1");
HW_WATCHDOG = WATCHDOG_CLEAR;
See Storage and Saves for the complete flash save strategy (append-only slots, block selection, erase timing).
5.6 BIOS_ALARMSET (9) — In-game alarm¶
/* xiy = pointer to NgpcAlarm { u8 day, u8 hour, u8 minute } (BCD) */
__asm(" ld rw3, " NGPC_STR(BIOS_ALARMSET));
__asm(" ld xiy, (xsp+4)");
__asm(" ldf 3");
__asm(" ld h, (xiy +)"); /* day -> H */
__asm(" ld qc, h"); /* QC = day */
__asm(" ld b, (xiy +)"); /* hour -> B */
__asm(" ld c, (xiy +)"); /* minute -> C */
__asm(" add w, w"); __asm(" add w, w");
__asm(" ld xix, 0xfffe00");
__asm(" ld xix, (xix+w)");
__asm(" call xix");
5.7 BIOS_INTLVSET (4) — Interrupt priority levels¶
Sets the priority level of one interrupt source in the CPU's INTxx control
registers. Reachable via the table (0xFFFE00) or swi 1; system.lib wraps it
as INT_LV_set.
Parameters: RC3 = interrupt source (0-9), RB3 = priority level (0-5, capped at 5).
| RC3 source | Meaning | INTxx register |
Nibble |
|---|---|---|---|
| 0 | RTC alarm | 0x70 |
low |
| 1 | Z80 / system | 0x71 |
high |
| 2 | Timer 0 | 0x73 |
low |
| 3 | Timer 1 | 0x73 |
high |
| 4 | Timer 2 | 0x74 |
low |
| 5 | Timer 3 | 0x74 |
high |
| 6 | DMA 0 | 0x79 |
low |
| 7 | DMA 1 | 0x79 |
high |
| 8 | DMA 2 | 0x7A |
low |
| 9 | DMA 3 | 0x7A |
high |
Register-encoding gotcha (measured). The value written into the source's nibble is
0x08 | min(level, 5), not the bare level — the0x08bit is the interrupt enable bit. Even sources write the low nibble, odd sources the high nibble of the shared register (the other nibble is preserved). Examples:INTLVSET(source=2, level=3)->0x73 = 0x0B; a followingINTLVSET(source=3, level=2)->0x73 = 0xAB. Measured on the retail BIOS across five commercial ROMs. A model that writes the bare level (without0x08) leaves the source disabled and its interrupt never fires — the one detail the reference emulator's HLE model had wrong until it was traced against silicon.
6. BCD Values (RTC)¶
The RTC returns BCD (Binary Coded Decimal) values.
0x23 = twenty-three (not 35).
/* Conversion macros (ngpc_rtc.h) */
#define BCD_TO_BIN(bcd) (((bcd) >> 4) * 10 + ((bcd) & 0xF))
#define BIN_TO_BCD(bin) ((((bin) / 10) << 4) | ((bin) % 10))
/* Example */
NgpcTime t;
ngpc_rtc_get(&t);
u8 hour = BCD_TO_BIN(t.hour); /* 0x14 -> 14 */
u8 minutes = BCD_TO_BIN(t.minute); /* 0x30 -> 30 */
7. Watchdog¶
The console resets the CPU if the watchdog is not fed for ~100ms. VBlank (60 Hz) feeds it automatically in the typical system layer.
#define HW_WATCHDOG (*(volatile u8 *)0x006F)
#define WATCHDOG_CLEAR 0x4E
/* Call manually before any long operation (flash erase/write) */
HW_WATCHDOG = WATCHDOG_CLEAR;
See also Hardware Registers — Watchdog address note for the
0x006Bvs0x006Fdiscrepancy observed across titles.
8. User Interrupt Vectors¶
Vectors are 32-bit pointers stored in RAM at 0x6FB8-0x6FFC.
/* Assign a handler (ngpc_hw.h) */
HW_INT_VBL = isr_vblank; /* MANDATORY */
HW_INT_TIM0 = isr_hblank; /* HBlank (for raster / sprmux) */
HW_INT_DMA0 = isr_dma_done; /* DMA channel 0 completion */
See the full interrupt vector table in Hardware Registers — Interrupts.
9. System Library (system.lib)¶
All system.lib functions use bank 3 registers (same convention as BIOS calls).
system.lib is distinct from the raw BIOS table — it wraps BIOS calls and provides
additional services not available via direct BIOS vectors.
Note:
WRITE_FLASH_RAMandCLR_FLASH_RAMare not strictly required. Standalone AMD flash stubs (ngpc_flash_asm.asm) can replicate the internal mechanism of both functions — extracted by disassembly and validated on real hardware.system.libis kept as an optional legacy path. See Storage and Saves for details.
| Function | Parameters | Return | Notes |
|---|---|---|---|
SYSTEM_CALL |
RW3=vector, params in bank 3 | depends | BIOS gateway, non-blocking on IRQ |
sys_PATCH |
— | — | Applies system bug fix — call at startup before anything else |
WRITE_FLASH_RAM |
same as VECT_FLASHWRITE | RA3 | Legacy — replaced by a standalone write stub |
CLR_FLASH_RAM |
RA3=cs(0/1), RB3=block | RA3 | Legacy — only safe erase for blocks 32-34 (VECT_FLASHERS broken); replaced by a standalone erase stub |
INT_LV_set |
RB3=level(0-5 or _INT_CLR_BIT), RC3=irq# | — | Set IRQ level + optionally clear pending |
INT_LV_CLR |
— | — | Reset all IRQ levels to defaults |
WAVE_OUT |
RA3=timer(0-3), XHL=data address | — | PCM playback, 8 kHz, 8-bit mono — requires 6.144 MHz |
LEVER_GET |
— | — | Read joypad (dev/debug only) |
FLASH_M_READ |
RA3=cs | RA3 | Detect flash capacity (debug) |
Critical notes:
- sys_PATCH must be the first call after startup — fixes an internal BIOS bug.
- CLR_FLASH_RAM on blocks 32-34: works correctly on first call per session;
calling it a second time in the same session silently fails (BIOS internal bug).
Avoid this entirely via append-only slots and standalone AMD stubs.
See Storage and Saves.
- WAVE_OUT requires CPU at 6.144 MHz (BIOS_CLOCKGEARSET RB3=0x00) — PCM will
be garbled at lower speeds.
- INT_LV_CLR flag _INT_CLR_BIT in RB3 causes INT_LV_set to also clear the pending
IRQ flag for that channel.
10. Serial / Link Cable (COM BIOS)¶
The link-cable port is serial channel 0 (SC0) of the TLCS-900, driven entirely
through 11 BIOS vectors (0x10-0x1A). Line config (set by BIOS_COMINIT):
UART 8N1, CTS/RTS hardware handshake, 19200 bps. The BIOS owns two 64-byte ring
buffers and the TX/RX interrupts; you only call the vectors.
These 11 vector numbers are absent from every public
ngpc.h— recovered from the BIOS dump (table @0xFFFE00) and each routine disassembled to confirm its identity. A full write-up + ready-to-use C stub (serial.c/.h,com.inc) lives in the NGPC_RAG research tree under04_MY_PROJECTS/serial_link/.
10.1 Vector roles (all verified on the BIOS)¶
| Vector | # | Role | I/O |
|---|---|---|---|
| COMINIT | 0x10 | init SC0 (SC0MOD=0x49, SC0CR=0x00, BR0CR=0x05) + install TX/RX ISR | - |
| COMSENDSTART | 0x11 | kick TX: first ring byte -> SC0BUF (0x50) | - |
| COMRECIVESTART | 0x12 | enable RX (SC0MOD|=0x20) + RTS low | - |
| COMCREATEDATA | 0x13 | push 1 byte to TX ring | in rb3 / out ra3 |
| COMGETDATA | 0x14 | pop 1 byte from RX ring | out rb3 / ra3 |
| COMONRTS | 0x15 | RTS low (allow peer) - port 0xB2 bit0 | - |
| COMOFFRTS | 0x16 | RTS high (block peer) | - |
| COMSENDSTATUS | 0x17 | TX status word | out rwa3 |
| COMRECIVESTATUS | 0x18 | RX status word (clears error flags) | out rwa3 |
| COMCREATEBUFDATA | 0x19 | block TX | in xhl3=ptr, rb3=size / out rb3=left |
| COMGETBUFDATA | 0x1A | block RX | in xhl3=ptr, rb3=size / out rb3=left |
10.2 Return codes & status bits (verified at disasm)¶
COMCREATEDATA-> ra3 =0x00OK /0xFFTX ring full.COMGETDATA-> ra3 =0x00OK /0x01RX ring empty.- Status word (rwa3): bits 0-7 = byte count; bit 8 = buffer-over; bits 9/10/11 =
framing/parity/overrun (RX only) -> masks
0x0100 / 0x0200 / 0x0400 / 0x0800.
10.3 Rules¶
COMINITfirst;COMRECIVESTARTonly when both consoles are powered on.- Wrap long ops (V-blank) with
COMOFFRTS...COMONRTS. - Use the table (
SYSTEM_CALL), neverswi 1, for the COM vectors. - Serial TX/RX interrupts (
0x6FE4/0x6FE8) are owned by the BIOS — do not hook them. - Cable/peer detection = port
0xB1bit2 (input):1= nothing plugged,0= a peer console is connected. Gate a session's first transmission on bit2 == 0 (this is how Card Fighters' Clash decides it may become the link initiator).0xB1bit1 = the CR2032 sub-battery (must read 1). See Link Cable §5.
📖 Full link-cable reference: Link Cable — registers, all 11 vectors with verified addresses, the CTS/RTS handshake, cable-detect, the symmetric programming loop, a reverse-engineered session handshake (initiator/ responder,
0x55 0x77→0xAA 0x22), gotchas, and emulator-modelling notes.
10.4 Minimal use¶
com_init(); com_recv_start(); /* both consoles ON */
for (;;) {
while (com_create_data(b) == COM_BUF_OVER) ;
com_send_start();
if (com_rx_ok(com_recv_status()))
while (com_get_data(&rx) == COM_BUF_OK) { /* use rx */ }
com_rts_off(); WaitVsync(); com_rts_on();
}
ABI note (cc900): C runtime runs in bank 3, so the BIOS return
ra3is already the C return registerWA; args arrive on the stack (xsp+4,xsp+8).
Quick Reference¶
| Item | Value | Notes |
|---|---|---|
| BIOS vector table | 0xFFFE00 |
32-bit pointers, index * 4 |
| Serial COM vectors | 0x10-0x1A |
link cable, 8N1/CTS-RTS/19200 — see section 10 |
| SHUTDOWN | vector 0 | SWI 1, ra3=3 |
| CLOCKGEARSET | vector 1 | Table call, rb3=divisor |
| RTCGET | vector 2 | Table call, xhl3=NgpcTime* |
| SYSFONTSET | vector 5 | SWI 1, ra3=3 |
| FLASHWRITE | vector 6 | SWI 1 |
| FLASHERS | vector 8 | SWI 1, ra3=0, rb3=block |
| System font tile base | slot 32 | After SYSFONTSET call |
| Tile slots 128+ | free | User tiles |
| Watchdog address | 0x006F |
Write 0x4E |
| VBlank vector | 0x6FCC |
32-bit ptr, mandatory |
| Timer0 vector | 0x6FD4 |
32-bit ptr, HBlank |
| RTC values | BCD format | Use BCD_TO_BIN() macro |
See Also¶
- Hardware Registers — Full hardware register map
- Storage and Saves — Flash save complete strategy
- Game Loop — VBlank handler, watchdog integration
- Build Toolchain —
ngpc_hw.h, compiler setup