Skip to content

Kernel (BIOS)

BIOS Overview
BIOS Memory Map
BIOS Function Summary
BIOS File Functions
BIOS File Execute and Flush Cache
BIOS CDROM Functions
BIOS Memory Card Functions
BIOS Interrupt/Exception Handling
BIOS Event Functions
BIOS Event Summary
BIOS Thread Functions
BIOS Timer Functions
BIOS Joypad Functions
BIOS GPU Functions
BIOS Memory Allocation
BIOS Memory Fill/Copy/Compare (SLOW)
BIOS String Functions
BIOS Number/String/Character Conversion
BIOS Misc Functions
BIOS Internal Boot Functions
BIOS More Internal Functions
BIOS PC File Server
BIOS TTY Console (std_io)
BIOS Character Sets
BIOS Control Blocks
BIOS Versions
BIOS Patches

BIOS Overview


The main purpose of the BIOS is to boot games from CDROM, unfortunately, before doing that, it displays the Sony intro. It's also doing some copy protection and region checks, and refuses to boot unlicensed games, or illegal copies, or games for other regions.

BIOS Bootmenu

The bootmenu shows up when starting the Playstation without CDROM inserted. The menu allows to play Audio CDs, and to erase or copy game positions on Memory Cards.

BIOS Functions

The BIOS contains a number of more or less useful, and probably more or less inefficient functions that can be used by software.
No idea if it's easy to take full control of the CPU, ie. to do all hardware access and interrupt handling by software, without using the BIOS at all?
Eventually the BIOS functions for accessing the CDROM drive are important, not sure how complicated/compatible it'd be to access the CDROM drive directly via I/O ports... among others, there might be different drives used in different versions of the Playstation, which aren't fully compatible with each other?

BIOS Memory

The BIOS occupies 512Kbyte ROM with 8bit address bus (so the BIOS ROM is rather slow, for faster execution, portions of it are relocated to the first 64K of RAM). For some very strange reason, the original PSX BIOS executes all ROM functions in uncached ROM, which is incredible slow (nocash BIOS uses cached ROM, which does work without problems).
The first 64Kbyte of the 2Mbyte Main RAM are reserved for the BIOS (containing exception handlers, jump tables, other data, and relocated code). That reserved region does unfortunately include the "valuable" first 32Kbytes (valuable because that memory could be accessed directly via [R0+immediate], without needing to use R1..R31 as base register).

BIOS Memory Map

BIOS ROM Map (512Kbytes)

  BFC00000h Kernel Part 1  (code/data executed in uncached ROM)
  BFC10000h Kernel Part 2  (code/data relocated to cached RAM)
  BFC18000h Intro/Bootmenu (code/data decompressed and relocated to RAM)
  BFC64000h Character Sets

BIOS ROM Header/Footer

  BFC00100h Kernel BCD date  (YYYYMMDDh)
  BFC00104h Console Type     (see Port 1F802030h, Secondary IRQ10 Controller)
  BFC00108h Kernel Maker/Version Strings (separated by one or more 00h bytes)
  BFC7FF32h GUI Version/Copyright Strings (if any) (separated by one 00h byte)

BIOS RAM Map (1st 64Kbytes of RAM) (fixed addresses mainly in 1st 500h bytes)

  00000000h 10h    Garbage Area (see notes below)
  00000010h 30h    Unused/reserved
  00000040h 20h    COP0 debug-break vector (not used by Kernel) (in KSEG0)
  00000060h 4      RAM Size (in megabytes) (2 or 8)
  00000064h 4      Unknown (set to 00000000h)
  00000068h 4      Unknown (set to 000000FFh)
  0000006Ch 14h    Unused/reserved
  00000080h 10h    Exception vector (actually in KSEG0, ie. at 80000080h)
  00000090h 10h    Unused/reserved
  000000A0h 10h    A(nnh) Function Vector
  000000B0h 10h    B(nnh) Function Vector
  000000C0h 10h    C(nnh) Function Vector
  000000D0h 30h    Unused/reserved
  00000100h 58h    Table of Tables (BIOS Control Blocks) (see below)
  00000158h 28h    Unused/reserved
  00000180h 80h    Command line argument from SYSTEM.CNF; BOOT = fname argument
  00000200h 300h   A(nnh) Jump Table
  00000500h ...    Kernel Code/Data (relocated from ROM)
  0000Cxxxh ...    Unused/reserved
  0000DF80h 80h    Used for BIOS Patches (ie. used by games, not used by BIOS)
  0000DFFCh 4      Response value from Intro/Bootmenu
  0000E000h 2000h  Kernel Memory; ExCBs, EvCBs, and TCBs allocated via B(00h)

User Memory (not used by Kernel)

  00010000h ...    Begin of User RAM (Exefile, Data, Heap, Stack, etc.)
  001FFF00h ...    Default Stacktop (usually in KSEG0)
  1F800000h 400h   Scratchpad (Data-Cache mis-used as Fast RAM)

Table of Tables (see BIOS Control Blocks for details)

Each table entry consists of two 32bit values; containing the base address, and total size (in bytes) of the corresponding control blocks.

  00000100h  ExCB Exception Chain Entrypoints (addr=var, size=4*08h)
  00000108h  PCB  Process Control Block       (addr=var, size=1*04h)
  00000110h  TCB  Thread Control Blocks       (addr=var, size=N*C0h)
  00000118h  -    Unused/reserved
  00000120h  EvCB Event Control Blocks        (addr=var, size=N*1Ch)
  00000128h  -    Unused/reserved
  00000130h  -    Unused/reserved
  00000138h  -    Unused/reserved
  00000140h  FCB  File Control Blocks         (addr=fixed, size=10h*2Ch)
  00000148h  -    Unused/reserved
  00000150h  DCB  Device Control Blocks       (addr=fixed, size=0Ah*50h)

File handles (fd=00h..0Fh) can be simply converted as fcb=[140h]+fd*2Ch.
Event handles (event=F10000xxh) as evcb=[120h]+(event AND FFFFh)*1Ch.

Garbage Area at Address 00000000h

The first some bytes of memory address 00000000h aren't actually used by the Kernel, except for storing some garbage at that locations. However, this garbage is actually important for bugged games like R-Types and Fade to Black (ie. games that do read from address 00000000h due to using uninitialized pointers).
Initially, the garbage area is containing a copy of the 16-byte exception handler at address 80h, but the first 4-bytes are typically smashed (set to 00000003h from some useless dummy writes in some useless CDROM delays). Ie. the 16-bytes should have these values:

  [00000000h]=3C1A0000h  ;<-- but overwritten by 00000003h after soon
  [00000004h]=275A0C80h  ;<-- or 275A0C50h (in older BIOS)

For R-Types, the halfword at [0] must non-zero (else the game will do a DMA to address 0, and thereby destroy kernel memory). Fade to Black does several garbage reads from [0..9], a wrong byte value at [5] can cause the game to crash with an invalid memory access exception upon memory card access.

BIOS Function Summary

Parameters, Registers, Stack

Argument(s) are passed in R4,R5,R6,R7,[SP+10h],[SP+14h],etc.
Caution: When calling a sub-function with N parameters, the caller MUST always allocate N words on the stack, and, although the first four parameters are passed in registers rather than on stack, the sub-function is allowed to use/destroy these words at [SP+0..N*4-1].
BIOS Functions (and custom callback functions) are allowed to destroy registers R1-R15, R24-R25, R31 (RA), and HI/LO. Registers R16-R23, R29 (SP), and R30 (FP) must be left unchanged (if the function uses that registers, then it must push/pop them). R26 (K0) is reserved for exception handler and should be usually not used by other functions. R27 (K1) and R28 (GP) are left more or less unused by the BIOS, so one can more or less freely use them for whatever purpose.
The return value (if any) is stored in R2 register.

A-Functions (Call 00A0h with function number in R9 Register)

  A(00h) or B(32h) open(filename,accessmode)
  A(01h) or B(33h) lseek(fd,offset,seektype)
  A(02h) or B(34h) read(fd,dst,length)
  A(03h) or B(35h) write(fd,src,length)
  A(04h) or B(36h) close(fd)
  A(05h) or B(37h) ioctl(fd,cmd,arg)
  A(06h) or B(38h) exit(exitcode)
  A(07h) or B(39h) isatty(fd)
  A(08h) or B(3Ah) getc(fd)
  A(09h) or B(3Bh) putc(char,fd)
  A(0Ah) todigit(char)
  A(0Bh) atof(src)     ;Does NOT work - uses (ABSENT) cop1 !!!
  A(0Ch) strtoul(src,src_end,base)
  A(0Dh) strtol(src,src_end,base)
  A(0Eh) abs(val)
  A(0Fh) labs(val)
  A(10h) atoi(src)
  A(11h) atol(src)
  A(12h) atob(src,num_dst)
  A(13h) setjmp(buf)
  A(14h) longjmp(buf,param)
  A(15h) strcat(dst,src)
  A(16h) strncat(dst,src,maxlen)
  A(17h) strcmp(str1,str2)
  A(18h) strncmp(str1,str2,maxlen)
  A(19h) strcpy(dst,src)
  A(1Ah) strncpy(dst,src,maxlen)
  A(1Bh) strlen(src)
  A(1Ch) index(src,char)
  A(1Dh) rindex(src,char)
  A(1Eh) strchr(src,char)  ;exactly the same as "index"
  A(1Fh) strrchr(src,char) ;exactly the same as "rindex"
  A(20h) strpbrk(src,list)
  A(21h) strspn(src,list)
  A(22h) strcspn(src,list)
  A(23h) strtok(src,list)  ;use strtok(0,list) in further calls
  A(24h) strstr(str,substr)       ;Bugged
  A(25h) toupper(char)
  A(26h) tolower(char)
  A(27h) bcopy(src,dst,len)
  A(28h) bzero(dst,len)
  A(29h) bcmp(ptr1,ptr2,len)      ;Bugged
  A(2Ah) memcpy(dst,src,len)
  A(2Bh) memset(dst,fillbyte,len)
  A(2Ch) memmove(dst,src,len)     ;Bugged
  A(2Dh) memcmp(src1,src2,len)    ;Bugged
  A(2Eh) memchr(src,scanbyte,len)
  A(2Fh) rand()
  A(30h) srand(seed)
  A(31h) qsort(base,nel,width,callback)
  A(32h) strtod(src,src_end) ;Does NOT work - uses (ABSENT) cop1 !!!
  A(33h) malloc(size)
  A(34h) free(buf)
  A(35h) lsearch(key,base,nel,width,callback)
  A(36h) bsearch(key,base,nel,width,callback)
  A(37h) calloc(sizx,sizy)            ;SLOW!
  A(38h) realloc(old_buf,new_siz)     ;SLOW!
  A(39h) InitHeap(addr,size)
  A(3Ah) _exit(exitcode)
  A(3Bh) or B(3Ch) getchar()
  A(3Ch) or B(3Dh) putchar(char)
  A(3Dh) or B(3Eh) gets(dst)
  A(3Eh) or B(3Fh) puts(src)
  A(3Fh) printf(txt,param1,param2,etc.)
  A(40h) SystemErrorUnresolvedException()
  A(41h) LoadTest(filename,headerbuf)
  A(42h) Load(filename,headerbuf)
  A(43h) Exec(headerbuf,param1,param2)
  A(44h) FlushCache()
  A(45h) init_a0_b0_c0_vectors
  A(46h) GPU_dw(Xdst,Ydst,Xsiz,Ysiz,src)
  A(47h) gpu_send_dma(Xdst,Ydst,Xsiz,Ysiz,src)
  A(48h) SendGP1Command(gp1cmd)
  A(49h) GPU_cw(gp0cmd)   ;send GP0 command word
  A(4Ah) GPU_cwp(src,num) ;send GP0 command word and parameter words
  A(4Bh) send_gpu_linked_list(src)
  A(4Ch) gpu_abort_dma()
  A(4Dh) GetGPUStatus()
  A(4Eh) gpu_sync()
  A(4Fh) SystemError
  A(50h) SystemError
  A(51h) LoadExec(filename,stackbase,stackoffset)
  A(52h) GetSysSp
  A(53h) SystemError           ;PS2: set_ioabort_handler(src)
  A(54h) or A(71h) _96_init()
  A(55h) or A(70h) _bu_init()
  A(56h) or A(72h) _96_remove()  ;does NOT work due to SysDeqIntRP bug
  A(57h) return 0
  A(58h) return 0
  A(59h) return 0
  A(5Ah) return 0
  A(5Bh) dev_tty_init()                                      ;PS2: SystemError
  A(5Ch) dev_tty_open(fcb,and unused:"path\name",accessmode) ;PS2: SystemError
  A(5Dh) dev_tty_in_out(fcb,cmd)                             ;PS2: SystemError
  A(5Eh) dev_tty_ioctl(fcb,cmd,arg)                          ;PS2: SystemError
  A(5Fh) dev_cd_open(fcb,"path\name",accessmode)
  A(60h) dev_cd_read(fcb,dst,len)
  A(61h) dev_cd_close(fcb)
  A(62h) dev_cd_firstfile(fcb,"path\name",direntry)
  A(63h) dev_cd_nextfile(fcb,direntry)
  A(64h) dev_cd_chdir(fcb,"path")
  A(65h) dev_card_open(fcb,"path\name",accessmode)
  A(66h) dev_card_read(fcb,dst,len)
  A(67h) dev_card_write(fcb,src,len)
  A(68h) dev_card_close(fcb)
  A(69h) dev_card_firstfile(fcb,"path\name",direntry)
  A(6Ah) dev_card_nextfile(fcb,direntry)
  A(6Bh) dev_card_erase(fcb,"path\name")
  A(6Ch) dev_card_undelete(fcb,"path\name")
  A(6Dh) dev_card_format(fcb)
  A(6Eh) dev_card_rename(fcb1,"path\name1",fcb2,"path\name2")
  A(6Fh) ?   ;card ;[r4+18h]=00000000h  ;card_clear_error(fcb) or so
  A(70h) or A(55h) _bu_init()
  A(71h) or A(54h) _96_init()
  A(72h) or A(56h) _96_remove()   ;does NOT work due to SysDeqIntRP bug
  A(73h) return 0
  A(74h) return 0
  A(75h) return 0
  A(76h) return 0
  A(77h) return 0
  A(78h) CdAsyncSeekL(src)
  A(79h) return 0               ;DTL-H: Unknown?
  A(7Ah) return 0               ;DTL-H: Unknown?
  A(7Bh) return 0               ;DTL-H: Unknown?
  A(7Ch) CdAsyncGetStatus(dst)
  A(7Dh) return 0               ;DTL-H: Unknown?
  A(7Eh) CdAsyncReadSector(count,dst,mode)
  A(7Fh) return 0               ;DTL-H: Unknown?
  A(80h) return 0               ;DTL-H: Unknown?
  A(81h) CdAsyncSetMode(mode)
  A(82h) return 0               ;DTL-H: Unknown?
  A(83h) return 0               ;DTL-H: Unknown?
  A(84h) return 0               ;DTL-H: Unknown?
  A(85h) return 0               ;DTL-H: Unknown?, or reportedly, CdStop (?)
  A(86h) return 0               ;DTL-H: Unknown?
  A(87h) return 0               ;DTL-H: Unknown?
  A(88h) return 0               ;DTL-H: Unknown?
  A(89h) return 0               ;DTL-H: Unknown?
  A(8Ah) return 0               ;DTL-H: Unknown?
  A(8Bh) return 0               ;DTL-H: Unknown?
  A(8Ch) return 0               ;DTL-H: Unknown?
  A(8Dh) return 0               ;DTL-H: Unknown?
  A(8Eh) return 0               ;DTL-H: Unknown?
  A(8Fh) return 0               ;DTL-H: Unknown?
  A(90h) CdromIoIrqFunc1()
  A(91h) CdromDmaIrqFunc1()
  A(92h) CdromIoIrqFunc2()
  A(93h) CdromDmaIrqFunc2()
  A(94h) CdromGetInt5errCode(dst1,dst2)
  A(95h) CdInitSubFunc()
  A(96h) AddCDROMDevice()
  A(97h) AddMemCardDevice()     ;DTL-H: SystemError
  A(98h) AddDuartTtyDevice()    ;DTL-H: AddAdconsTtyDevice ;PS2: SystemError
  A(99h) add_nullcon_driver()
  A(9Ah) SystemError            ;DTL-H: AddMessageWindowDevice
  A(9Bh) SystemError            ;DTL-H: AddCdromSimDevice
  A(9Ch) SetConf(num_EvCB,num_TCB,stacktop)
  A(9Dh) GetConf(num_EvCB_dst,num_TCB_dst,stacktop_dst)
  A(9Eh) SetCdromIrqAutoAbort(type,flag)
  A(9Fh) SetMem(megabytes)

Below functions A(A0h..B4h) not supported on pre-retail DTL-H2000 devboard:

  A(A0h) _boot()
  A(A1h) SystemError(type,errorcode)
  A(A2h) EnqueueCdIntr()  ;with prio=0 (fixed)
  A(A3h) DequeueCdIntr()  ;does NOT work due to SysDeqIntRP bug
  A(A4h) CdGetLbn(filename) ;get 1st sector number (or garbage when not found)
  A(A5h) CdReadSector(count,sector,buffer)
  A(A6h) CdGetStatus()
  A(A7h) bufs_cb_0()
  A(A8h) bufs_cb_1()
  A(A9h) bufs_cb_2()
  A(AAh) bufs_cb_3()
  A(ABh) _card_info(port)
  A(ACh) _card_load(port)
  A(ADh) _card_auto(flag)
  A(AEh) bufs_cb_4()
  A(AFh) card_write_test(port)  ;CEX-1000: jump_to_00000000h
  A(B0h) return 0               ;CEX-1000: jump_to_00000000h
  A(B1h) return 0               ;CEX-1000: jump_to_00000000h
  A(B2h) ioabort_raw(param)     ;CEX-1000: jump_to_00000000h
  A(B3h) return 0               ;CEX-1000: jump_to_00000000h
  A(B4h) GetSystemInfo(index)   ;CEX-1000: jump_to_00000000h
  A(B5h..BFh) N/A ;jump_to_00000000h

B-Functions (Call 00B0h with function number in R9 Register)

  B(00h) alloc_kernel_memory(size)
  B(01h) free_kernel_memory(buf)
  B(02h) init_timer(t,reload,flags)
  B(03h) get_timer(t)
  B(04h) enable_timer_irq(t)
  B(05h) disable_timer_irq(t)
  B(06h) restart_timer(t)
  B(07h) DeliverEvent(class, spec)
  B(08h) OpenEvent(class,spec,mode,func)
  B(09h) CloseEvent(event)
  B(0Ah) WaitEvent(event)
  B(0Bh) TestEvent(event)
  B(0Ch) EnableEvent(event)
  B(0Dh) DisableEvent(event)
  B(0Eh) OpenTh(reg_PC,reg_SP_FP,reg_GP)
  B(0Fh) CloseTh(handle)
  B(10h) ChangeTh(handle)
  B(11h) jump_to_00000000h
  B(12h) InitPAD2(buf1,siz1,buf2,siz2)
  B(13h) StartPAD2()
  B(14h) StopPAD2()
  B(15h) PAD_init2(type,button_dest,unused,unused)
  B(16h) PAD_dr()
  B(17h) ReturnFromException()
  B(18h) ResetEntryInt()
  B(19h) HookEntryInt(addr)
  B(1Ah) SystemError  ;PS2: return 0
  B(1Bh) SystemError  ;PS2: return 0
  B(1Ch) SystemError  ;PS2: return 0
  B(1Dh) SystemError  ;PS2: return 0
  B(1Eh) SystemError  ;PS2: return 0
  B(1Fh) SystemError  ;PS2: return 0
  B(20h) UnDeliverEvent(class,spec)
  B(21h) SystemError  ;PS2: return 0
  B(22h) SystemError  ;PS2: return 0
  B(23h) SystemError  ;PS2: return 0
  B(24h) jump_to_00000000h
  B(25h) jump_to_00000000h
  B(26h) jump_to_00000000h
  B(27h) jump_to_00000000h
  B(28h) jump_to_00000000h
  B(29h) jump_to_00000000h
  B(2Ah) SystemError  ;PS2: return 0
  B(2Bh) SystemError  ;PS2: return 0
  B(2Ch) jump_to_00000000h
  B(2Dh) jump_to_00000000h
  B(2Eh) jump_to_00000000h
  B(2Fh) jump_to_00000000h
  B(30h) jump_to_00000000h
  B(31h) jump_to_00000000h
  B(32h) or A(00h) open(filename,accessmode)
  B(33h) or A(01h) lseek(fd,offset,seektype)
  B(34h) or A(02h) read(fd,dst,length)
  B(35h) or A(03h) write(fd,src,length)
  B(36h) or A(04h) close(fd)
  B(37h) or A(05h) ioctl(fd,cmd,arg)
  B(38h) or A(06h) exit(exitcode)
  B(39h) or A(07h) isatty(fd)
  B(3Ah) or A(08h) getc(fd)
  B(3Bh) or A(09h) putc(char,fd)
  B(3Ch) or A(3Bh) getchar()
  B(3Dh) or A(3Ch) putchar(char)
  B(3Eh) or A(3Dh) gets(dst)
  B(3Fh) or A(3Eh) puts(src)
  B(40h) cd(name)
  B(41h) format(devicename)
  B(42h) firstfile2(filename,direntry)
  B(43h) nextfile(direntry)
  B(44h) rename(old_filename,new_filename)
  B(45h) erase(filename)
  B(46h) undelete(filename)
  B(47h) AddDrv(device_info)  ;subfunction for AddXxxDevice functions
  B(48h) DelDrv(device_name_lowercase)
  B(49h) PrintInstalledDevices()

Below functions B(4Ah..5Dh) not supported on pre-retail DTL-H2000 devboard:

  B(4Ah) InitCARD2(pad_enable)  ;uses/destroys k0/k1 !!!
  B(4Bh) StartCARD2()
  B(4Ch) StopCARD2()
  B(4Dh) _card_info_subfunc(port)  ;subfunction for "_card_info"
  B(4Eh) _card_write(port,sector,src)
  B(4Fh) _card_read(port,sector,dst)
  B(50h) _new_card()
  B(51h) Krom2RawAdd(shiftjis_code)
  B(52h) SystemError  ;PS2: return 0
  B(53h) Krom2Offset(shiftjis_code)
  B(54h) _get_errno()
  B(55h) _get_error(fd)
  B(56h) GetC0Table
  B(57h) GetB0Table
  B(58h) _card_chan()
  B(59h) testdevice(devicename)
  B(5Ah) SystemError  ;PS2: return 0
  B(5Bh) ChangeClearPAD(int)
  B(5Ch) _card_status(slot)
  B(5Dh) _card_wait(slot)
  B(5Eh..FFh) N/A ;jump_to_00000000h    ;CEX-1000: B(5Eh..F6h) only
  B(100h....) N/A ;garbage              ;CEX-1000: B(F7h.....) and up

C-Functions (Call 00C0h with function number in R9 Register)

  C(00h) EnqueueTimerAndVblankIrqs(priority) ;used with prio=1
  C(01h) EnqueueSyscallHandler(priority)     ;used with prio=0
  C(02h) SysEnqIntRP(priority,struc)  ;bugged, use with care
  C(03h) SysDeqIntRP(priority,struc)  ;bugged, use with care
  C(04h) get_free_EvCB_slot()
  C(05h) get_free_TCB_slot()
  C(06h) ExceptionHandler()
  C(07h) InstallExceptionHandlers()  ;destroys/uses k0/k1
  C(08h) SysInitMemory(addr,size)
  C(09h) SysInitKernelVariables()
  C(0Ah) ChangeClearRCnt(t,flag)
  C(0Bh) SystemError  ;PS2: return 0
  C(0Ch) InitDefInt(priority) ;used with prio=3
  C(0Dh) SetIrqAutoAck(irq,flag)
  C(0Eh) return 0               ;DTL-H2000: dev_sio_init
  C(0Fh) return 0               ;DTL-H2000: dev_sio_open
  C(10h) return 0               ;DTL-H2000: dev_sio_in_out
  C(11h) return 0               ;DTL-H2000: dev_sio_ioctl
  C(12h) InstallDevices(ttyflag)
  C(13h) FlushStdInOutPut()
  C(14h) return 0               ;DTL-H2000: SystemError
  C(15h) _cdevinput(circ,char)
  C(16h) _cdevscan()
  C(17h) _circgetc(circ)    ;uses r5 as garbage txt for _ioabort
  C(18h) _circputc(char,circ)
  C(19h) _ioabort(txt1,txt2)
  C(1Ah) set_card_find_mode(mode)  ;0=normal, 1=find deleted files
  C(1Bh) KernelRedirect(ttyflag)   ;PS2: ttyflag=1 causes SystemError
  C(1Ch) AdjustA0Table()
  C(1Dh) get_card_find_mode()
  C(1Eh..7Fh) N/A ;jump_to_00000000h
  C(80h.....) N/A ;mirrors to B(00h.....)

SYS-Functions (Syscall opcode with function number in R4 aka A0 Register)

  SYS(00h) NoFunction()
  SYS(01h) EnterCriticalSection()
  SYS(02h) ExitCriticalSection()
  SYS(03h) ChangeThreadSubFunction(addr) ;syscall with r4=03h, r5=addr
  SYS(04h..FFFFFFFFh) calls DeliverEvent(F0000010h,4000h)

The 20bit immediate in the "syscall imm" opcode is unused (should be zero).

BREAK-Functions (Break opcode with function number in opcode's immediate)

BRK opcodes may be used within devkits, however, the standard BIOS simply calls DeliverEvent(F0000010h,1000h) and SystemError_A_40h upon any BRK opcodes (as well as on any other unresolved exceptions).

  BRK(1C00h) Division by zero (commonly checked/invoked by software)
  BRK(1800h) Division overflow (-80000000h/-1, sometimes checked by software)

Below breaks are used in DTL-H2000 BIOS:

  BRK(1h) Whatever lockup or so?
  BRK(101h) PCInit()   Inits the fileserver.
  BRK(102h) PCCreat(filename, fileattributes)
  BRK(103h) PCOpen(filename, accessmode)
  BRK(104h) PCClose(filehandle)
  BRK(105h) PCRead(filehandle, length, memory_destination_address)
  BRK(106h) PCWrite(filehandle, length, memory_source_address)
  BRK(107h) PClSeek(filehandle, file_offset, seekmode)
  BRK(3C400h) User has typed "break" command in debug console

The break functions have argument(s) in A1,A2,A3 (ie. unlike normal BIOS functions not in A0,A1,A2), and TWO return values (in R2, and R3). These functions require a commercial/homebrew devkit... consisting of a Data Cable (for accessing the PC's harddisk)... and an Expansion ROM (for handling the BREAK opcodes)... or so?

BIOS File Functions

A(00h) or B(32h) - open(filename, accessmode) - Opens a file for IO

  out: V0  File handle (00h..0Fh), or -1 if error.

Opens a file on the target device for io. Accessmode is set like this:

  bit0     1=Read  ;\These bits aren't actually used by the BIOS, however, at
  bit1     1=Write ;/least 1 should be set; won't work when all 32bits are zero
  bit2     1=Exit without waiting for incoming data (when TTY buffer empty)
  bit9     0=Open Existing File, 1=Create New file (memory card only)
  bit15    1=Asynchronous mode (memory card only; don't wait for completion)
  bit16-31 Number of memory card blocks for a new file on the memory card

The PSX can have a maximum of 16 files open at any time, of which, 2 handles are always reserved for std_io, so only 14 handles are available for actual files. Some functions (cd, testdevice, erase, undelete, format, firstfile2, rename) are temporarily allocating 1 filehandle (rename tries to use 2 filehandles, but, it does accidently use only 1 handle, too). So, for example, erase would fail if more than 13 file handles are opened by the game.

A(01h) or B(33h) - lseek(fd, offset, seektype) - Move the file pointer

   seektype 0 = from start of file        (with positive offset)
            1 = from current file pointer (with positive/negative offset)
            2 = Bugs. Should be from end of file.

Moves the file pointer the number of bytes in A1, relative to the location specified by A2. Movement from the eof is incorrect. Also, movement beyond the end of the file is not checked.

A(02h) or B(34h) - read(fd, dst, length) - Read data from an open file

  out: V0  Number of bytes actually read, -1 if failed.

Reads the number of bytes from the specified open file. If length is not specified an error is returned. Read per $0080 bytes from memory card (bu:) and per $0800 from cdrom (cdrom:).

A(03h) or B(35h) - write(fd, src, length) - Write data to an open file

  out: V0  Number of bytes written.

Writes the number of bytes to the specified open file. Write to the memory card per $0080 bytes. Writing to the cdrom returns 0.

A(04h) or B(36h) - close(fd) - Close an open file

Returns r2=fd (or r2=-1 if failed).

A(08h) or B(3Ah) - getc(fd) - read one byte from file

  out: R2=character (sign-expanded) or FFFFFFFFh=error

Internally redirects to "read(fd,tempbuf,1)". For some strange reason, the returned character is sign-expanded; so, a return value of FFFFFFFFh could mean either character FFh, or error.

A(09h) or B(3Bh) - putc(char,fd) - write one byte to file

Observe that "fd" is the 2nd paramter (not the 1st paramter as usually).

  out:  R2=Number of bytes actually written, -1 if failed

Internally redirects to "write(fd,tempbuf,1)".

B(40h) - cd(name) - Change the current directory on target device

Changes the current directory on the specified device, which should be "cdrom:" (memory cards don't support directories). The PSX supports only a current directory, but NOT a current device (ie. after cd, the directory name may be ommited from filenames, but the device name must be still included in all filenames).

  in:  A0  Pointer to new directory path (eg. "cdrom:\path")

Returns 1=okay, or 0=failed.
The function doesn't verify if the directory exists. Caution: For cdrom, the function does always load the path table from the disk (even if it was already stored in RAM, so cd is causing useless SLOW read/seek delays).

B(42h) - firstfile2(filename,direntry) - Find first file to match the name

Returns r2=direntry (or r2=0 if no matching files).
Searches for the first file to match the specified filename; the filename may contain "?" and "*" wildcards. "*" means to ignore ALL following characters; accordingly one cannot specify any further characters after the "*" (eg. "DATA*" would work, but "*.DAT" won't work). "?" is meant to ignore a single character cell. Note: The "?" wildcards (but not "*") can be used also in all other file functions; causing the function to use the first matching name (eg. erase "????" would erase the first matching file, not all matching files).
Start the name with the device you want to address. (ie. pcdrv:) Different drives can be accessed as normally by their drive names (a:, c:, huh?) if path is omitted after the device, the current directory will be used.
A direntry structure looks like this:

  00h 14h Filename, terminated with 00h
  14h 4   File attribute (always 0 for cdrom) (50h=norm or A0h=del for card)
  18h 4   File size
  1Ch 4   Pointer to next direntry? (not used?)
  20h 4   First Sector Number
  24h 4   Reserved (not used)

BUG: If "?" matches the ending 00h byte of a name, then any further characters in the search expression are ignored (eg. "FILE?.DAT" would match to "FILE2.DAT", but accidently also to "FILE").
BUG: For CDROM, the BIOS includes some code that is intended to realize disk changes during firstfile2/nextfile operations, however, that code is so bugged that it does rather ensure that the BIOS does NOT realize new disks being inserted during firstfile2/nextfile.
BUG: firstfile2/nextfile is internally using a FCB. On the first call to firstfile2, the BIOS is searching a free FCB, and does apply that as "search fcb", but it doesn't mark that FCB as allocated, so other file functions may accidently use the same FCB. Moreover, the BIOS does memorize that "search fcb", and, even when starting a new search via another call to firstfile2, it keeps using that FCB for search (without checking if the FCB is still free). A possible workaround is not to have any files opened during firstfile2/nextfile operations.

B(43h) - nextfile(direntry) - Searches for the next file to match the name

Returns r2=direntry (or r2=0 if no more matching files).
Uses the settings of a previous firstfile2/nextfile command.

B(44h) - rename(old_filename, new_filename)

Returns 1=okay, or 0=failed.

B(45h) - erase(filename) - Delete a file on target device

Returns 1=okay, or 0=failed.

B(46h) - undelete(filename)

Returns 1=okay, or 0=failed.

B(41h) - format(devicename)

Erases all files on the device (ie. for formatting memory cards).
Returns 1=okay, or 0=failed.

B(54h) - _get_errno()

Indicates the reason of the most recent file function error (open, lseek, read, write, close, _get_error, ioctl, cd, testdevice, erase, undelete, format, rename). Use _get_errno() ONLY if an error has occured (the error code isn't reset to zero by functions that are passing okay). firstfile2/nextfile do NOT affect _get_errno(). See below list of File Error Numbers for more info.

B(55h) - _get_error(fd)

Basically same as B(54h), but allowing to specify a file handle for which error information is to be received; accordingly it doesn't work for functions that do use 'hidden' internal file handles (eg. erase, or unsuccessful open). Returns FCB[18h], or FFFFFFFFh if the handle is invalid/unused.

A(05h) or B(37h) - ioctl(fd,cmd,arg)

Used only for TTY.

A(07h) or B(39h) - isatty(fd)

Returns bit1 of the file's DCB flags. That bit is set only for Duart/TTY, and is cleared for Dummy/TTY, Memory Card, and CDROM.

B(59h) - testdevice(devicename)

Whatever. Checks the devicename, and if it's accepted, calls a device specific function. For the existing devices (cdrom,bu,tty) that specific function simply returns without doing anything. Maybe other devices (like printers or modems) would do something more interesting.

File Error Numbers for B(54h) and B(55h)

  00h okay (though many successful functions leave old error code unchanged)
  02h file not found
  06h bad device port number (tty2 and up)
  09h invalid or unused file handle
  10h general error (physical I/O error, unformatted, disk changed for old fcb)
  11h file already exists error (create/undelete/rename)
  12h tried to rename a file from one device to another device
  13h unknown device name
  16h sector alignment error, or fpos>=filesize, unknown seektype or ioctl cmd
  18h not enough free file handles
  1Ch not enough free memory card blocks
  FFFFFFFFh invalid or unused file handle passed to B(55h) function

BIOS File Execute and Flush Cache

A(41h) - LoadTest(filename, headerbuf)

Loads the 800h-byte exe file header to an internal sector buffer, and does then copy bytes [10h..4Bh] of that header to headerbuf[00h..3Bh].

A(42h) - Load(filename, headerbuf)

Same as LoadTest (see there for details), but additionally loads the body of the executable (using the size and destination address in the file header), and does call FlushCache. The exe can be then started via Exec (this isn't done automatically by LoadTest). Unlike "LoadExec", the "LoadTest/Exec" combination allows to return the new exe file to return to the old exe file (instead of restarting the boot executable).
BUG: Uses the unstable FlushCache function (see there for details).

A(43h) - Exec(headerbuf, param1, param2)

Can be used to start a previously loaded executable. The function saves R16,R28,R30,SP,RA in the reserved region of headerbuf (rather than on stack), more or less slowly zerofills the memfill region specified in headerbuf, reads the stack base and offset values and sets SP and FP to base+offset (or leaves them unchanged if base=0), reads the GP value from headerbuf and sets GP to that value. Then calls the excecutables entrypoint, with param1 and param2 passed in r4,r5.
If the executable (should) return, then R16,R28,R30,SP,RA are restored from headerbuf, and the function returns with r2=1.

A(51h) - LoadExec(filename, stackbase, stackoffset)

This is a rather bizarre function. In short, it does load and execute the specified file, and thereafter, it (tries to) reload and restart to boot executable.
Part1: Takes a copy of the filename, with some adjustments: Everything up to the first ":" or 00h byte is copied as is (ie. the device name, if it does exist, or otherwise the whole path\filename.ext;ver), the remaining characters are copied and converted to uppercase (ie. the path\filename.ext;ver part, or none if the device name didn't exist), finally, checks if a ";" exists (ie. the version suffix), if there's none, then it appends ";1" as default version. CAUTION: The BIOS allocates ONLY 28 bytes on stack for the copy of the filename, that region is followed by 4 unused bytes, so the maximum length would be 32 bytes (31 characters plus EOL) (eg. "device:\pathname\filename.ext;1",00h).
Part2: Enables IRQs via ExitCriticalSection, memorizes the stack base/offset values from the previously loaded executable (which should have been the boot executable, unless LoadExec should have been used in nested fashion), does then use LoadTest to load the desired file, replaces the stack base/offset values in its headerbuf by the LoadExec parameter values, and does then execute it via Exec(headerbuf,1,0).
Part3: If the exefile returns, or if it couldn't be loaded, then the boot file is (unsuccessfully) attempted to be reloaded: Enables IRQs via ExitCriticalSection, loads the boot file via LoadTest, replaces the stack base/offset values in its headerbuf by the values memorized in Part2 (which \<should> be the boot executable's values from SYSTEM.CNF, unless the nesting stuff occurred), and does then execute the boot file via Exec(headerbuf,1,0).
Part4: If the boot file returns, or if it couldn't be loaded, then the function looks up in a "JMP $" endless loop (normally, returning from the boot exe causes SystemError("B",38Ch), however, after using LoadExec, this functionality is replaced by the "JMP $" lockup.
BUG: Uses the unstable FlushCache function (see there for details).
BUG: Part3 accidently treats the first 4 characters of the exename as memory address (causing an invalid memory address exception on address 6F726463h, for "cdrom:filename.exe").

A(9Ch) - SetConf(num_EvCB, num_TCB, stacktop)

Changes the number of EvCBs and TCBs, and the stacktop. These values are usually initialized from the settings in the SYSTEM.CNF file, so using this function usually shouldn't ever be required.
The function deallocates all old ExCBs, EvCBs, TCBs (so all Exception handlers, Events, and Threads (except the current one) are lost, and all other memory that may have been allocated via alloc_kernel_memory(size) is deallocated, too. It does then allocate the new control blocks, and enqueue the default handlers. Despite of the changed stacktop, the current stack pointer is kept intact, and the function returns to the caller.

A(9Dh) - GetConf(num_EvCB_dst, num_TCB_dst, stacktop_dst)

Returns the number of EvCBs, TCBs, and the initial stacktop. There's no return value in the R2 register, instead, the three 32bit return values are stored at the specified "dst" addresses.

A(44h) - FlushCache()

Flushes the Code Cache, so opcodes are ensured to be loaded from RAM. This is required when loading program code via DMA (ie. from CDROM) (the cache controller apparently doesn't realize changes to RAM that are caused by DMA). The LoadTest and LoadExec functions are automatically calling FlushCache (so FlushCache is required only when loading program code via "read" or via "CdReadSector").
FlushCache may be also required when relocating or modifying program code by software (the cache controller doesn't seem to realize modifications to memory mirrors, eg. patching the exception handler at 80000080h seems to be work without FlushCache, but patching the same bytes at 00000080h doesn't).
Note: The PSX doesn't have a Data Cache (or actually, it has, but it's misused as Fast RAM, mapped to a fixed memory region, and which isn't accessable by DMA), so FlushCache isn't required for regions that contain data.
BUG: The FlushCache function contains a handful of opcodes that do use the k0 register without having IRQs disabled at that time, if an IRQ occurs during those opcodes, then the k0 value gets destroyed by the exception handler, causing FlushCache to get trapped in an endless loop.
One workaround would be to disable all IRQs before calling FlushCache, however, the BIOS does internally call the function without IRQs disabled, that applies to:

  load_file  A(42h)
  load_exec  A(51h)
  add_device B(47h) (and all "add_xxx_device" functions)
  init_card  B(4Ah)
  and by intro/boot code

for load_file/load_exec, IRQ2 (cdrom) and IRQ3 (dma) need to be enabled, so the "disable all IRQs" workaround cannot be used for that functions, however, one can/should disable as many IRQs as possible, ie. everything except IRQ2/IRQ3, and all DMA interrupts except DMA3 (cdrom).

Executable Memory Allocation

LoadTest and LoadExec are simply loading the file to the address specified in the exe file header. There's absolutely no verification whether that memory is (or isn't) allocated via malloc, or if it is used by the boot executable, or by the kernel, or if it does contain RAM at all.
When using the "malloc" function combined with loading exe files, it may be recommended not to pass all memory to InitHeap (ie. to keep a memory region being reserved for loading executables).


For more info about EXE files and their headers, see
CDROM File Formats

BIOS CDROM Functions

General File Functions

CDROMs are basically accessed via normal file functions, with device name "cdrom:" (which is an abbreviation for "cdrom0:", anyways, the port number is ignored).
BIOS File Functions
BIOS File Execute and Flush Cache
Before starting the boot executable, the BIOS automatically calls _96_init(), so the game doesn't need to do any initializations before using CDROM file functions.

Absent CD-Audio Support

The Kernel doesn't include any functions for playing Audio tracks. Also, there's no BIOS function for setting the XA-ADPCM file/channel filter values. So CD Audio can be used only by directly programming the CDROM I/O ports.

Asynchronous CDROM Access

The normal File functions are always using synchroneous access for CDROM (ie. the functions do wait until all data is transferred) (unlike as for memory cards, accessmode.bit15 cannot be used to activate asynchronous cdrom access).
However, one can read files in asynchrouneous fashion via CdGetLbn, CdAsyncSeekL, and CdAsyncReadSector. CDROM files are non-fragmented, so they can be read simply from incrementing sector numbers.

A(A4h) - CdGetLbn(filename)

Returns the first sector number used by the file, or -1 in case of error.
BUG: The function accidently returns -1 for the first file in the directory (the first file should be a dummy entry for the current or parent directory or so, so that bug isn't much of a problem), if the file is not found, then the function accidently returns garbage (rather than -1).

A(A5h) - CdReadSector(count,sector,buffer)

Reads \<count> sectors, starting at \<sector>, and writes data to \<buffer>. The read is done in mode=80h (double speed, 800h-bytes per sector). The function waits until all sectors are transferred, and does then return the number of sectors (ie. count), or -1 in case of error.

A(A6h) - CdGetStatus()

Retrieves the cdrom status via CdAsyncGetStatus(dst) (see there for details; especially for cautions on door-open flag). The function waits until the event indicates completion, and does then return the status byte (or -1 in case of error).

A(78h) - CdAsyncSeekL(src)

Issues Setloc and SeekL commands. The parameter (src) is a pointer to a 3-byte sector number (MM,SS,FF) (in BCD format).
The function returns 0=failed, or 1=okay. Completion is indicated by events (class=F0000003h, and spec=20h, or 8000h).

A(7Ch) - CdAsyncGetStatus(dst)

Issues a GetStat command. The parameter (dst) is a pointer to a 1-byte location that receives the status response byte.
The function returns 0=failed, or 1=okay. Completion is indicated by events (class=F0000003h, and spec=20h, or 8000h).
Caution: The command acknowledges the door-open flag, but doesn't automatically reload the path table (which is required if a new disk is inserted); if the door-open flag was set, one should call a function that does forcefully load the path table (like cd).

A(7Eh) - CdAsyncReadSector(count,dst,mode)

Issues SetMode and ReadN (when mode.bit8=0), or ReadS (when mode.bit8=1) commands. count is the number of sectors to be read, dst is the destination address in RAM, mode.bit0-7 are passed as parameter to the SetMode command, mode.bit8 is the ReadN/ReadS flag (as described above). The sector size (for DMA) depends on the mode value: 918h-bytes (bit4=1, bit5=X), 924h-bytes (bit4=0, bit5=1), or 800h-bytes (bit4,5=0).
Before CdAsyncReadSector, the sector number should be set via CdAsyncSeekL(src).
The function returns 0=failed, or 1=okay. Completion is indicated by events (class=F0000003h, and spec=20h, 80h, or 8000h).

A(81h) - CdAsyncSetMode(mode)

Similar to CdAsyncReadSector (see there for details), but issues only the SetMode command, without any following ReadN/ReadS command.

A(94h) - CdromGetInt5errCode(dst1,dst2)

Returns the first two response bytes from the most recent INT5 error: [dst1]=status, [dst2]=errorcode. The BIOS doesn't reset these values in case of successful completion, so the values are quite useless.

A(54h) or A(71h) - _96_init()

A(56h) or A(72h) - _96_remove() ;does NOT work due to SysDeqIntRP bug

A(90h) - CdromIoIrqFunc1()

A(91h) - CdromDmaIrqFunc1()

A(92h) - CdromIoIrqFunc2()

A(93h) - CdromDmaIrqFunc2()

A(95h) - CdInitSubFunc() ;subfunction for _96_init()

A(9Eh) - SetCdromIrqAutoAbort(type,flag)

A(A2h) - EnqueueCdIntr() ;with prio=0 (fixed)

A(A3h) - DequeueCdIntr() ;does NOT work due to SysDeqIntRP bug

Internally used CDROM functions for initialization and IRQ handling.

BIOS Memory Card Functions

General File Functions

Memory Cards aka Backup Units (bu) are basically accessed via normal file functions, with device names "bu00:" (Slot 1) and "bu10:" (Slot 2),
BIOS File Functions
Before using the file functions for memory cards, first call InitCARD2(pad_enable), then StartCARD2(), and then _bu_init().

File Header, Filesize, and Sector Alignment

The first 100h..200h bytes (2..4 sectors) of the file must contain the title and icon bitmap(s). For details, see:
Memory Card Data Format
The filesize must be a multiple of 2000h bytes (one block), the maximum size would be 1E000h bytes (when using all 15 blocks on the memory card). The filesize must be specified when creating the file (ie. accessmode bit9=1, and bit16-31=number of blocks). Once when the file is created, the BIOS does NOT allow to change the filesize (unless by deleting and re-creating the file).
When reading/writing files, the amount of data must be a multiple of 80h bytes (one sector), and the file position must be aligned to a 80h-byte boundary, too. There's no restriction on fragmented files (ie. one may cross 2000h-byte block boundaries within the file).

Poor Memcard Performance

PSX memory card accesses are typically super-slow. That, not so much because the hardware would be slow, but rather because of improper inefficent code at the BIOS side. The original BIOS tries to synchronize memory card accesses with joypad accesses simply by accessing only one sector per frame (although it could access circa two sectors). To the worst, the BIOS accesses Slot 1 only on each second frame, and Slot 2 only each other frame (although in 99% of all cases only one slot is accessed at once, so the access time drops to 0.5 sectors per frame).
Moreover, the memory card id, directory, and broken sector list do occupy 26 sectors (although the whole information would fit into 4 or 5 sectors) (a workaround would be to read only the first some bytes, and to skip the additional unused bytes - though that'd also mean to skip the checksums which are unfortunately stored at the end of the sector).
And, anytime when opening a file (in synchronous mode), the BIOS does additionally read sector 0 (which is totally useless, and gets especially slow when opening a bunch of files; eg. when extracting the title/icon from all available files on the card).

Asynchronous Access

The BIOS supports synchronous and asynchronous memory card access. Synchronous means that the BIOS function doesn't return until the access has completed (which means, due to the poor performance, that the function spends about 75% of the time on inactivity) (except in nocash PSX bios, which has better performance), whilst asynchronous access means that the BIOS function returns immediately after invoking the access (which does then continue on interrupt level, and does return an event when finished).
The file "read" and "write" functions act asynchronous when accessmode bit15 is set when opening the file. Additionally, the A(ACh) _card_load(port) function can be used to tell the BIOS to load the directory entries and broken sector list to its internal RAM buffers (eg. during the games title screen, so the BIOS doesn't need to load that data once when the game enters its memory card menu). All other functions like erase or format always act synchronous. The open/findfirst/findnext functions do normally complete immediately without accessing the card at all (unless the directory wasn't yet read; in that case the directory is loading in synchronous fashion).
Unfortunately, the asynchronous response doesn't rely on a single callback event, but rather on a bunch of different events which must be all allocated and tested by the game (and of which, one event is delivered on completion) (which one depends on whether function completed okay, or if an error occurred).

Multitap Support (and Multitap Problems)

The BIOS does have some partial support for accessing more than two memory cards (via Multitap adaptors). Device/port names "bu01:", "bu02:", "bu03:" allow to access extra memory carts in slot1 (and "bu11:", "bu12:", "bu13:" in slot2). Namely, those names will send values 82h, 83h, 84h to the memory card slot (instead of the normal 81h value).
However, the BIOS directory_buffer and broken_sector_list do support only two memory cards (one in slot1 and one in slot2). So, trying to access more memory cards may cause great data corruption (though there might be a way to get the BIOS to reload those buffers before accessing a different memory card).
Aside from that problem, the BIOS functions are very-very-very slow even when accessing only two memory cards. Trying to use the BIOS to access up to eight memory cards would be very-extremly-very slow, which would be more annoying than useful.

B(4Ah) - InitCARD2(pad_enable) ;uses/destroys k0/k1 !!!

B(4Bh) - StartCARD2()

B(4Ch) - StopCARD2()

A(55h) or A(70h) - _bu_init()

  --- Below are some lower level memory card functions ---

A(ABh) - _card_info(port)

B(4Dh) - _card_info_subfunc(port) ;subfunction for "_card_info"

Can be used to check if the most recent call to _card_write has completed okay. Issues an incomplete dummy read command (similar to B(4Fh) - _card_read). The read command is aborted once when receiving the status byte from the memory card (the actual data transfer is skipped).

A(AFh) - card_write_test(port) ;not supported by old CEX-1000 version

Resets the card changed flag. For some strange reason, this flag isn't automatically reset after reading the flag, instead, the flag is reset upon sector writes. To do that, this function issues a dummy write to sector 3Fh.

B(50h) - _new_card()

Normally any memory card read/write functions fail if the BIOS senses the card change flag to be set. Calling this function tells the BIOS to ignore the card change flag on the next read/write operation (the function is internally used when loading the "MC" ID from sector 0, and when calling the card_write_test function to acknowledge the card change flag).

B(4Eh) - _card_write(port,sector,src)

B(4Fh) - _card_read(port,sector,dst)

Invokes asynchronous reading/writing of a single sector. The function returns 1=okay, or 0=failed (on invalid sector numbers). The actual I/O is done on IRQ level, completion of the I/O command transmission can be checked, among others, via get/wait_card_status(slot) functions (with slot=port/10h).
In case of the write function, completion of the \<transmission> does NOT mean that the actual \<writing> has completed, instead, write errors are indicated upon completion of the \<next sector> read/write transmission (or, if there are no further sectors to be accessed; one can use _card_info to verify completion of the last written sector).
The sector number should be in range of 0..3FFh, for some strange reason, probably a BUG, the function also accepts sector 400h. The specified sector number is directly accessed (it is NOT parsed through the broken sector replacement list).

B(5Ch) - _card_status(slot)

B(5Dh) - _card_wait(slot)

Returns the status of the most recent I/O command, possible values are:

  11h=failed/timeout (eg. when no cartridge inserted)
  21h=failed/general error

_card_status returns immediately, _card_wait waits until a non-busy state occurs.

A(A7h) - bufs_cb_0()

A(A8h) - bufs_cb_1()

A(A9h) - bufs_cb_2()

A(AAh) - bufs_cb_3()

A(AEh) - bufs_cb_4()

These five callback functions are internally used by the BIOS, notifying other BIOS functions about (un-)successful completion of memory card I/O commands.

B(58h) - _card_chan()

This is a subfunction for the five bufs_cb__xxx functions (indicating whether the callback occured for a slot1 or slot2 access).

A(ACh) - _card_load(port)

Invokes asynchronous reading of the memory card directory. The function isn't too useful because the BIOS tends to read the directory automatically in various places in synchronous mode, so there isn't too much chance to replace the automatic synchronous reading by asynchronous reading.

A(ADh) - _card_auto(flag)

Can be used to enable/disable auto format (0=off, 1=on). The _bu_init function initializes auto format as disabled. If auto format is enabled, then the BIOS does automatically format memory cards if it has failed to read the "MC" ID bytes on sector 0. Although potentially useful, activating this feature is rather destructive (for example, read errors on sector 0 might occur accidently due to improperly inserted cards with dirty contacts, so it'd be better to prompt the user whether or not to format the card, rather than doing that automatically).

C(1Ah) - set_card_find_mode(mode)

C(1Dh) - get_card_find_mode()

Allows to get/set the card find mode (0=normal, 1=find deleted files), the mode setting affects only the firstfile2/nextfile functions. All other file functions are used fixed mode settings (always mode=0 for open, rename, erase, and mode=1 for undelete).

BIOS Interrupt/Exception Handling

The Playstation's Kernel uses an uncredible inefficient and unstable exception handler; which may have been believed to be very powerful and flexible.


For a maximum of slowness, it does always save and restore all CPU registers (including such that aren't used in the exception handler). It does then go through a list of installed interrupt handlers - and executes ALL of them. For example, a Timer0 IRQ is first passed to the Cdrom and Vblank handlers (which must reject it, no thanks), before it does eventually reach the Timer0 handler.

Unstable IRQ Handling

A fundamental mistake in the exception handler is that it doesn't memorize the incoming IRQ flags. So the various interrupt handlers must check Port 1F801070h one after each other. That means, if a high priority handler has rejected IRQ processing (because the desired IRQ flag wasn't set at that time), then a lower priority handler may process it (assuming that the IRQ flag got set in the meantime), and, in worst case it may even acknowledge it (so the high priority handler does never receive it).
To avoid such problems, there should be only ONE handler installed for each IRQ source. However, that isn't always possible because the Kernel automatically installs some predefined handlers. Most noteworthy, the totally bugged DefaultInterruptHandlers is always installed (and cannot be removed), so it does randomly trigger Events. Fortunately, it does not acknowledge the IRQs (unless SetIrqAutoAck was used to enable that fatal behaviour).

B(18h) - ResetEntryInt()

Applies the default "Exit" structure (which consists of a pointer to ReturnFromException, and the Kernel's exception stacktop (minus 4, for whatever reason), and zeroes for the R16..R23,R28,R30 registers. Returns the address of that structure.
See HookEntryInt for details.

B(19h) - HookEntryInt(addr)

addr points to a structure (with same format as for the setjmp function):

  00h 4    r31/ra,pc ;usually ptr to ReturnFromException function
  04h 4    r28/sp    ;usually exception stacktop, minus 4, for whatever reason
  08h 4    r30/fp    ;usually 0
  0Ch 4x8  r16..r23  ;usually 0
  2Ch 4    r28/gp    ;usually 0

The hook function is executed only if the ExceptionHandler has been fully executed (after processing an IRQ, many interrupt handlers are calling ReturnFromException to abort further exception handling, and thus do skip the hook function). Once when the hook function has finished, it should execute ReturnFromException. The hook function is called with r2=1 (that is important if the hook address was recorded with setjmp, where it "returns" to the setjmp caller, with r2 as "return value").

Priority Chains

The Kernel's exception handler has four priority chains, each may contain one or more Interrupt or Exception handlers. The default handlers are:

  Prio Chain Content
  0    CdromDmaIrq, CdromIoIrq, SyscallException
  1    CardSpecificIrq, VblankIrq, Timer2Irq, Timer1Irq, Timer0Irq
  2    PadCardIrq
  3    DefInt

The exception handler calls all handlers, starting with the first element in the priority 0 chain (ie. usually CdromDmaIrq). The separate handlers must check if they want to process the IRQ (eg. CdromDmaIrq would process only CDROM DMA IRQs, but not joypad IRQs or so). If it has processed and acknowledged the IRQ, then the handler may execute ReturnFromException, which causes the handlers of lower priority to be skipped (if there are still other unacknowledge IRQs pending, then the hardware will re-enter the exception handler as soon as the RFE opcode in ReturnFromException does re-enable interrupts).

C(02h) - SysEnqIntRP(priority,struc) ;bugged, use with care

Inserts a new element in the specified priority chain. The new element is inserted at the begin of the chain, so (within that priority chain) the new element has highest priority.

  00h 4  pointer to next element    (0=none)  ;this pointer is inserted by BIOS
  04h 4  pointer to SECOND function (0=none)  ;executed if func1 returns r2<>0
  08h 4  pointer to FIRST  function (0=none)  ;executed first
  0Ch 4  Not used (usually zero)

BUG: SysDeqIntRP can remove only the first element in the chain (see there for details), so adding new chain elements may cause OTHER functions to be unable to remove their chain elements. The BIOS seems to be occassionally adding/removing the "CardSpecificIrq" and "PadCardIrq" (with priority 1 and 2), so using that priorities may cause the BIOS to be unable to remove that IRQ handlers. Using priority 0 and 3 should work (as long as the software takes care to remove only the newest elements) (but there should be no conflicts with the BIOS which does never remove priority 0 and 3 elements) (leaving apart that DequeueCdIntr and _96_remove try to remove priority 0 elements, but that functions won't work anyways; due to the same bug).

C(03h) - SysDeqIntRP(priority,struc) ;bugged, use with care

Removes the specified element from the specified priority chain.
BUG: The function tries to search the whole chain (and to remove the element if it finds it). However, it does only check the first element properly, and, thereafter it reads a garbage value from an uninitialized stack location, and acts more or less unpredictable. So, it can remove only the first element in the chain, ie. it should be called only if you are SURE that there's no newer element in the chain, and only if you are SURE that the element IS in the chain.

SYS(01h) - EnterCriticalSection() ;syscall with r4=01h

Disables interrupts by clearing SR (cop0r12) Bit 2 and 10 (of which, Bit2 gets copied to Bit0 once when returning from the syscall exception). Returns 1 if both bits were set, returns 0 if one or both of the bits were already zero.

SYS(02h) - ExitCriticalSection() ;syscall with r4=02h

Enables interrupts by set SR (cop0r12) Bit 2 and 10 (of which, Bit2 gets copied to Bit0 once when returning from the syscall exception). There's no return value (all registers except SR and K0 are unchanged).

C(0Dh) - SetIrqAutoAck(irq,flag)

Specifies if the DefaultInterruptHandler shall automatically acknowledge IRQs. The "irq" paramter is the number of the interrupt, ie. 00h..0Ah = IRQ0..IRQ10. The "flag" value should be 0 to disable AutoAck, or non-zero to enable AutoAck. By default, AutoAck is disabled for all IRQs.
Mind that the DefaultInterruptHandler is totally bugged. Especially the AutoAck feature doesn't work very well: It may cause higher priority handlers to miss their IRQ, and it may even cause the DefaultInterruptHandler to miss its own IRQs.

C(06h) - ExceptionHandler()

The C(06h) vector points to the exception handler, ie. to the function that is invoked from the 4 opcodes at address 80000080h, that opcodes jump directly to the exception handler, so patching the C(06h) vector has no effect.
Reading the C(06h) entry can be used to let a custom 80000080h handler pass control back to the default handler (that, by a "direct" jump, not by the usual "MOV R9,06h / CALL 0C0h" method, which would destroy main programs R9 register).
Also, reading C(06h) may be useful for patching the exception handler (which contains a bunch of NOP opcodes, which seem to be intended to insert additional opcodes, such like debugger exception handling) (Note: some of that NOPs are reserved for Memory Card IRQ handling).
BUG: Early BIOS versions did try to examine a copy of cop0r13 in r2 register, but did forgot cop0r13 to r2 (so they examined garbage), this was fixed in newer BIOS versions, additionally, most commercial games still include patches for compatibility with the old BIOS.

B(17h) - ReturnFromException()

Restores the CPU registers (R1-R31,HI,LO,SR,PC) (except R26/K0) from the current TCB. This function is usually executed automatically at the end of the ExceptionHandler, however, functions in the exception chain may call ReturnFromException to return immediately, without processing chain elements of lower priority.

C(00h) - EnqueueTimerAndVblankIrqs(priority) ;used with prio=1

C(01h) - EnqueueSyscallHandler(priority) ;used with prio=0

C(0Ch) - InitDefInt(priority) ;used with prio=3

Internally used to add some default IRQ and Exception handlers.

No Nested Exceptions

The Kernel doesn't support nested exceptions, that would require a decreasing exception stack, however, the kernel saves the incoming CPU registers in the current TCB, and an exception stack with fixed start address for internal push/pop during exception handling. So, nesting would overwrite these values. Do not enable IRQs, and don't trap other exceptions (like break or syscall opcodes, or memory or overlow errors) during exception handling.
Note: The execption stack has a fixed size of 1000h bytes (and is located somewhere in the first 64Kbytes of memory).

BIOS Event Functions

B(08h) - OpenEvent(class, spec, mode, func)

Adds an event structure to the event table.

     class,spec - triggers if BOTH values match
     mode - (1000h=execute function/stay busy, 2000h=no func/mark ready)
     func - Address of callback function (0=None) (used only when mode=1000h)
  out: R2 = Event descriptor (F1000000h and up), or FFFFFFFFh if failed

Opens an event, should be called within a critical section. The return value is used to identify the event to the other event functions. A list of event classes, specs and modes is at the end of this section. Initially, the event is disabled.
Note: The desired max number of events can be specified in the SYSTEM.CNF boot file (the default is "EVENT = 10" (which is a HEX number, ie. 16 decimal; of which 5 events are internally used by the BIOS for CDROM functions, so, of the 16 events, only 11 events are available to the game). A bigger amount of events will slowdown the DeliverEvent function (which always scans all EvCBs, even if all events are disabled).

B(09h) - CloseEvent(event) - releases event from the event table

Always returns 1 (even if the event handle is unused or invalid).

B(0Ch) - EnableEvent(event) - Turns on event handling for specified event

Always returns 1 (even if the event handle is unused or invalid).

B(0Dh) - DisableEvent(event) - Turns off event handling for specified event

Always returns 1 (even if the event handle is unused or invalid).

B(0Ah) - WaitEvent(event)

Returns 0 if the event is disabled. Otherwise hangs in a loop until the event becomes ready, and returns 1 once when it is ready (and automatically switches the event back to busy status). Callback events (mode=1000h) do never set the ready flag (and thus WaitEvent would hang forever).
The main program simply hangs during the wait, so when using multiple threads, it may be more recommended to create an own waitloop that checks TestEvent, and to call ChangeTh when the event is busy.
BUG: The return value is unstable (sometimes accidently returns 0=disabled if the event status changes from not-ready to ready shortly after the function call).

B(0Bh) - TestEvent(event)

Returns 0 if the event is busy or disabled. Otherwise, when it is ready, returns 1 (and automatically switches the event back to busy status). Callback events (mode=1000h) do never set the ready flag.

B(07h) - DeliverEvent(class, spec)

This function is usually called by the kernel, it triggers all events that are enabled/busy, and that have the specified class and spec values. Depending on the mode, either the callback function is called (mode=1000h), or the event is marked as enabled/ready (mode=2000h).

B(20h) - UnDeliverEvent(class, spec)

This function is usually called by the kernel, undelivers all events that are enabled/ready, and that have mode=2000h, and that have the specified class and spec values. Undeliver means that the events are marked as enabled/busy.

C(04h) - get_free_EvCB_slot()

A subfunction for OpenEvent.

Event Classes

File Events:

  0000000xh memory card (for file handle fd=x)

Hardware Events:

  F0000001h IRQ0  VBLANK
  F0000002h IRQ1  GPU
  F0000003h IRQ2  CDROM Decoder
  F0000004h IRQ3  DMA controller
  F0000005h IRQ4  RTC0 (timer0)
  F0000006h IRQ5/IRQ6 RTC1 (timer1 or timer2)
  F0000007h N/A   Not used (this should be timer2)
  F0000008h IRQ7  Controller (joypad/memcard)
  F0000009h IRQ9  SPU
  F000000Ah IRQ10 PIO ;uh, does the PIO have an IRQ signal? (IRQ10 is joypad)
  F000000Bh IRQ8  SIO
  F0000010h Exception ;CPU crashed (BRK,BadSyscall,Overflow,MemoryError, etc.)
  F0000011h memory card (lower level BIOS functions)
  F0000012h memory card (not used by BIOS; maybe used by Sony's devkit?)
  F0000013h memory card (not used by BIOS; maybe used by Sony's devkit?)

Event Events:

  F1xxxxxxh event (not used by BIOS; maybe used by Sony's devkit?)

Root Counter Events (Timers and Vblank):

  F2000000h Root counter 0 (Dotclock)                    (hardware timer)
  F2000001h Root counter 1 (horizontal retrace?)         (hardware timer)
  F2000002h Root counter 2 (one-eighth of system clock)  (hardware timer)
  F2000003h Root counter 3 (vertical retrace?) (this is a software timer)

User Events:

  F3xxxxxxh user (not used by BIOS; maybe used by games and/or Sony's devkit?)

BIOS Events (including such that have nothing to do with BIOS):

  F4000001h memory card (higher level BIOS functions)
  F4000002h libmath (not used by BIOS; maybe used by Sony's devkit?)

Thread Events:

  FFxxxxxxh thread (not used by BIOS; maybe used by Sony's devkit?)

Event Specs

  0001h counter becomes zero
  0002h interrupted
  0004h end of i/o
  0008h file was closed
  0010h command acknowledged
  0020h command completed
  0040h data ready
  0080h data end
  0100h time out
  0200h unknown command
  0400h end of read buffer
  0800h end of write buffer
  1000h general interrupt
  2000h new device
  4000h system call instruction ;SYS(04h..FFFFFFFFh)
  8000h error happened
  8001h previous write error happened
  0301h domain error in libmath
  0302h range error in libmath

Event modes

  1000h Execute callback function, and stay busy (do NOT mark event as ready)
  2000h Do NOT execute callback function, and mark event as ready

BIOS Event Summary

Below is a list of all events (class,spec values) that are delivered and/or undelivered by the BIOS in one way or another. The BIOS does internally open five events for cdrom (class=F0000003h with spec=10h,20h,40h,80h,8000h). The various other class/spec's are only delivered by the BIOS (but not received by the BIOS) (ie. a game may open/enable memory card events to receive notifications from the BIOS).

CDROM Events

  F0000003h,10h  cdrom DMA finished (all sectors finished)
  F0000003h,20h  cdrom ?
  F0000003h,40h  cdrom dead feature (delivered only by unused functions)
  F0000003h,80h  cdrom INT4 (reached end of disk)
  F0000003h,100h         n/a ?  ;undelivered, but not opened, nor delivered
  F0000003h,200h                ;undelivered, but not opened

Memory Card - Higher Level File/Device Events

  0000000xh,4     card file handle (x=fd) done okay
  F4000001h,4     card done okay (len=0)
  F4000001h,100h  card err busy  ;A(A9h)
  F4000001h,2000h card err eject ;A(AAh) or unformatted (bad "MC" id)
  F4000001h,8000h card err write ;A(A8h) or A(AEh) or general error

Memory Card - Lower Level Hardware I/O Events

  F0000011h,4      finished okay
  F0000011h,100h   err busy
  F0000011h,200h     n/a ?
  F0000011h,2000h  err
  F0000011h,8000h  err
  F0000011h,8001h  err (this one is NOT undelivered!)

Timer/Vblank Events

  F2000000h,2   Timer0 (IRQ4)
  F2000001h,2   Timer1 (IRQ5)
  F2000002h,2   Timer2 (IRQ6)
  F2000003h,2   Vblank (IRQ0) (unstable since IRQ0 is also used for joypad)

Default IRQ Handler Events (very unstable, don't use)

  F0000001h,1000h ;IRQ0  (VBLANK)
  F0000002h,1000h ;IRQ1  (GPU)
  F0000003h,1000h ;IRQ2  (CDROM)
  F0000004h,1000h ;IRQ3  (DMA)
  F0000005h,1000h ;IRQ4  (TMR0)
  F0000006h,1000h ;IRQ5  (TMR1)
  F0000006h,1000h ;IRQ6  (TMR2) (accidently uses same event as TMR1)
  F0000008h,1000h ;IRQ7  (joypad/memcard)
  F0000009h,1000h ;IRQ9  (SPU)
  F000000Ah,1000h ;IRQ10 (Joypad and PIO)
  F000000Bh,1000h ;IRQ8  (SIO)

Unresolved Exception Events

  F0000010h,1000h unknown exception ;neither IRQ nor SYSCALL
  F0000010h,4000h unknown syscall   ;syscall(04h..FFFFFFFFh)

BIOS Thread Functions

B(0Eh) - OpenTh(reg_PC,reg_SP_FP,reg_GP)

Searches a free TCB, marks it as used, and stores the inital program counter (PC), global pointer (GP aka R28), stack pointer (SP aka R29), and frame pointer (FP aka R30) (using the same value for SP and FP). All other registers are left uninitialized (eg. may contain values from an older closed thread, that includes the SR register, see note).
The return value is the new thread handle (in range FF000000h..FF000003h, assuming that 4 TCBs are allocated) or FFFFFFFFh if there's no free TCB. The function returns to the old current thread, use "ChangeTh" to switch to the new thread.
Note: The desired max number of TCBs can be specified in the SYSTEM.CNF boot file (the default is "TCB = 4", one initially used for the boot executable, plus 3 free threads).

BUG - Unitialized SR Register

OpenTh does NOT initialize the SR register (cop0r12) of the new thread. Upon powerup, the bootcode zerofills the TCB memory (so, the SR of new threads will be initially zero; ie. Kernel Mode, IRQ's disabled, and COP2 disabled). However, when closing/reopening threads, the SR register will have the value of the old closed thread (so it may get started with IRQs enabled, and, in worst case, if the old thread should have switched to User Mode, even without access to KSEG0, KSEG1 memory).
Or, ACTUALLY, the memory is NOT zerofilled on powerup... so SR is total random?

B(0Fh) - CloseTh(handle)

Marks the TCB for the specified thread as unused. The function can be used for any threads, including for the current thread.
Closing the current thread doesn't terminate the current thread, so it may cause problems once when opening a new thread, however, it should be stable to execute the sequence "DisableInterrupts, CloseCurrentThread, ChangeOtherThread".
The return value is always 1 (even if the handle was already closed).

B(10h) - ChangeTh(handle)

Pauses the current thread, and activates the selected new thread (or crashes if the specified handle was unused or invalid).
The return value is always 1 (stored in the R2 entry of the TCB of the old thread, so the return value will be received once when changing back to the old thread).
Note: The BIOS doesn't automatically switch from one thread to another. So, all other threads remain paused until the current thread uses ChangeTh to pass control to another thread.
Each thread is having it's own CPU registers (R1..R31,HI,LO,SR,PC), the registers are stored in the TCB of the old thread, and restored when switching back to that thread. Mind that other registers (I/O Ports or GTE registers aren't stored automatically, so, when needed, they need to be pushed/popped by software before/after ChangeTh).

C(05h) - get_free_TCB_slot()

Subfunction for OpenTh, returns the number of the first free TCB (usually in range 0..3) or FFFFFFFFh if there's no free TCB.

SYS(03h) ChangeThreadSubFunction(addr) ;syscall with r4=03h, r5=addr

Subfunction for ChangeTh, R5 contains the address of the new TCB, just like all exceptions, the syscall exception is saving the CPU registers in the current TCB, but does then apply the new TCB as current TCB, and so, it does then enter the new thread when returning from the exception.

BIOS Timer Functions

Timers (aka Root Counters)

The three hardware timers aren't internally used by any BIOS functions, so they can be freely used by the game, either via below functions, or via direct I/O access.


Some of the below functions are allowing to use Vblank IRQs as a fourth "timer". However, Vblank IRQs are internally used by the BIOS for handling joypad and memory card accesses. One could theoretically use two separate Vblank IRQ handlers, one for joypad, and one as "timer", but the BIOS is much too unstable for such "shared" IRQ handling (it may occassionally miss one of the two handlers).
So, although Vblank IRQs are most important for games, the PSX BIOS doesn't actually allow to use them for purposes other than joypad access. A possible workaround is to examine the status byte in one of the joypad buffers (ie. the InitPAD2(buf1,22h,buf2,22h) buffers). Eg. a wait_for_vblank function could look like so: set buf1[0]=55h, then wait until buf1[0]=00h or buf1[0]=FFh.

B(02h) - init_timer(t,reload,flags)

When t=0..2, resets the old timer mode by setting [1F801104h+t*16]=0000h, applies the reload value by [1F801108h+t*16]=reload, computes the new mode:

  if flags.bit4=0 then mode=0048h else mode=0049h
  if flags.bit0=0 then mode=mode OR 100h
  if flags.bit12=1 then mode=mode OR 10h

and applies it by setting [1F801104h+t*16]=mode, and returns 1. Does nothing and returns zero for t>2.

B(03h) - get_timer(t)

Reads the current timer value: Returns halfword[1F801100h+t*16] for t=0..2. Does nothing and returns zero for t>2.

B(04h) - enable_timer_irq(t)

B(05h) - disable_timer_irq(t)

Enables/disables timer or vblank interrupt enable bits in [1F801074h], bit4,5,6 for t=0,1,2, or bit0 for t=3, or random/garbage bits for t>3. The enable function returns 1 for t=0..2, and 0 for t=3. The disable function returns always 1.

B(06h) - restart_timer(t)

Sets the current timer value to zero: Sets [1F801100h+t*16]=0000h and returns 1 for t=0..2. Does nothing and returns zero for t>2.

C(0Ah) - ChangeClearRCnt(t,flag) ;root counter (aka timer)

Selects what the kernel's timer/vblank IRQ handlers shall do after they have processed an IRQ (t=0..2: timer 0..2, or t=3: vblank) (flag=0: do nothing; or flag=1: automatically acknowledge the IRQ and immediately return from exception). The function returns the old (previous) flag value.

BIOS Joypad Functions

Pad Input

Joypads should be initialized via InitPAD2(buf1,22h,buf2,22h), and StartPAD2(). The main program can read the pad data from the buf1/buf2 addresses (including Status, ID1, button states, and any kind of analogue inputs). For more info on ID1, Buttons and analogue inputs, see
Controllers and Memory Cards
Note: The BIOS doesn't include any functions for sending custom data to the pads (such like for controlling rumble motors).

B(12h) - InitPAD2(buf1, siz1, buf2, siz2)

Memorizes the desired buf1/buf2 addresses, zerofills the buffers by using the siz1/siz2 buffer size values (which should be 22h bytes each). And does some initialization on the PadCardIrq element (but doesn't enqueue it, that must be done by a following call to StartPAD2), and does set the "pad_enable_flag", that flag can be also set/cleared via InitCARD2(pad_enable), where it selects if the Pads are kept handled together with Memory Cards. buf1/buf2 are having the following format:

  00h      Status (00h=okay, FFh=timeout/wrong ID2)
  01h      ID1    (eg. 41h=digital_pad, 73h=analogue_pad, 12h=mouse, etc.)
  02h..21h Data   (max 16 halfwords, depending on lower 4bit of ID1)

Note: InitPAD2 does initially zerofill the buffers, so, until the first IRQ is processed, the initial status is 00h=okay, with buttons=0000h (all buttons pressed), to fix that situation, change the two status bytes to FFh after calling InitPAD2 (or alternately, reject ID1=00h).
Once when the PadCardIrq is enqueued via StartPAD2, and while "pad_enable_flag" is set, the data for (both) Pad1 and Pad2 is read on Vblank interrupts, and stored in the buffers, the IRQ handler stores up to 22h bytes in the buffer (regardless of the siz1/siz2 values) (eg. a Multitap adaptor uses all 22h bytes).

B(13h) - StartPAD2()

Should be used after InitPAD2. Enqueues the PadCardIrq handler, and does additionally initialize some flags.

B(14h) - StopPAD2()

Dequeues the PadCardIrq handler. Note that this handler is also used for memory cards, so it'll "stop" cards, too.

B(15h) - PAD_init2(type, button_dest, unused, unused)

This is an extremely bizarre and restrictive function - don't use! The function fails unless type is 20000000h or 20000001h (the type value has no other function). The function uses "buf1/buf2" addresses that are located somewhere "hidden" within the BIOS variables region, the only way to read from that internal buffers is to use the ugly "PAD_dr()" function. For some strange reason it FFh-fills buf1/buf2, and does then call InitPAD2(buf1,22h,buf2,22) (which does immediately 00h-fill the previously FFh-filled buffers), and does then call StartPAD2().
Finally, it does memorize the "button_dest" address (see PAD_dr() for details on that value). The two unused parameters have no function, however, they are internally written back to the stack locations reserved for parameter 2 and 3, ie. at [SP+08h] and [SP+0Ch] on the caller's stack, so the function MUST be called with all four parameters allocated on stack. Return value is 2 (or 0 if type was disliked).

B(16h) - PAD_dr()

This is a very ugly function, using the internal "buf1/buf2" values from "PAD_init2" and the "button_dest" value that was passed to that function.
If "button_dest" is non-zero, then this function is automatically called by the PadCardIrq handler, and stores it's return value at [button_dest] (where it may be read by the main program). If "button_dest" is zero, then it isn't called automatically, and it \<can> be called manually (with return value in R2), however, it does additionally write the return value to [button_dest], ie. to [00000000h] in this case, destroying that memory location.
The return value itself is useless garbage: The lower 16bit contain the pad1 buttons, the upper 16bit the pad2 buttons, of which, both values have reversed byte-order (ie. the first button byte in upper 8bit). The function works only with controller IDs 41h (digital joypad) and 23h (nonstandard device). For ID=23h, the halfword is ORed with 07C7h, and bit6,7 are then cleared if the analogue inputs are greater than 10h. For ID=41h the data is left intact. Any other ID values, or disconnected joypads, cause the halfword to be set to FFFFh (same as when no buttons are pressed), that includes newer analogue pads (unless they are switched to "digital" mode).

BIOS GPU Functions

A(48h) - SendGP1Command(gp1cmd)

Writes [1F801814h]=gp1cmd. There's no return value (r2 is left unchanged).

A(49h) - GPU_cw(gp0cmd) ;send GP0 command word

Calls gpu_sync(), and does then write [1F801810h]=gp0cmd. Returns the return value from the gpu_sync() call.

A(4Ah) - GPU_cwp(src,num) ;send GP0 command word and parameter words

Calls gpu_sync(), and does then copy "num" words from [src and up] to [1F801810h], src should usually point to a command word, followed by num-1 parameter words. Transfer is done by software (without DMA). Always returns 0.

A(4Bh) - send_gpu_linked_list(src)

Transfer an OT via DMA. Calls gpu_sync(), and does then write [1F801814h]=4000002h, [1F8010F4h]=0, [1F8010F0h]=[1F8010F0h] OR 800h, [1F8010A0h]=src, [1F8010A4h]=0, [1F8010A8h]=1000401h. The function does additionally output a bunch of TTY status messages via printf. The function doesn't wait until the DMA is completed. There's no return value.

A(4Ch) - gpu_abort_dma()

Writes [1F8010A8h]=401h, [1F801814h]=4000000h, [1F801814h]=2000000h, [1F801814h]=1000000h. Ie. stops GPU DMA, and issues GP1(4), GP1(2), GP1(1). Returns 1F801814h, ie. the I/O address.

A(4Dh) - GetGPUStatus()

Reads [1F801814h] and returns that value.

A(46h) - GPU_dw(Xdst,Ydst,Xsiz,Ysiz,src)

Waits until GPUSTAT.Bit26 is set (unlike gpu_sync, which waits for Bit28), and does then [1F801810h]=A0000000h, [1F801810h]=YdstXdst, [1F801810h]=YsizXsiz, and finally transfers "N" words from [src and up] to [1F801810h], where "N" is "Xsiz*Ysiz/2". The data is transferred by software (without DMA) (by code executed in the uncached BIOS region with high waitstates, so the data transfer is very SLOW).
Caution: If "Xsiz*Ysiz" is odd, then the last halfword is NOT transferred, so the GPU stays waiting for the last data value.
Returns [SP+04h]=Ydst, [SP+08h]=Xsiz, [SP+0Ch]=Ysiz, [SP+10h]=src+N*4, and R2=src=N*4.

A(47h) - gpu_send_dma(Xdst,Ydst,Xsiz,Ysiz,src)

Calls gpu_sync(), writes [1F801810h]=A0000000h, [1F801814h]=4000002h, [1F8010F0h]=[1F8010F0h] OR 800h, [1F8010A0h]=src, [1F8010A4h]=N*10000h+10h (where N="Xsiz*Ysiz/32"), [1F8010A8h]=1000201h.
Caution: If "Xsiz*Ysiz" is not a multiple of 32, then the last halfword(s) are NOT transferred, so the GPU stays waiting for that values.
Returns R2=1F801810h, and [SP+04h]=Ydst, [SP+08h]=Xsiz, [SP+0Ch]=Ysiz.

A(4Eh) - gpu_sync()

If DMA is off (when GPUSTAT.Bit29-30 are zero): Waits until GPUSTAT.Bit28=1 (or until timeout).
If DMA is on: Waits until D2_CHCR.Bit24=0 (or until timeout), and does then wait until GPUSTAT.Bit28=1 (without timeout, ie. may hang forever), and does then turn off DMA via GP1(04h).
Returns 0 (or -1 in case of timeout, however, the timeout values are very big, so it may take a LOT of seconds before it returns).

BIOS Memory Allocation

A(33h) - malloc(size)

Allocates size bytes on the heap, and returns the memory handle (aka the address of the allocated memory block). The address of the block is guaranteed to by aligned to 4-byte memory boundaries. Size is rounded up to a multiple of 4 bytes. The address may be in KUSEG, KSEG0, or KSEG1, depending on the address passed to InitHeap.
Caution: The BIOS (tries to) initialize the heap size to 0 bytes (actually it accidently overwrites that initial setting by garbage during relocation), so any call to malloc will fail, unless InitHeap has been used to initialize the address/size of the heap.

A(34h) - free(buf)

Deallocates the memory block. There's no return value, and no error checking. The function simply sets [buf-4]=[buf-4] OR 00000001h, so if buf is an invalid handle it may destroy memory at [buf-4], or trigger a memory exception (for example, when buf=0).

A(37h) - calloc(sizx, sizy) ;SLOW!

Allocates xsiz*ysiz bytes by calling malloc(xsiz*ysiz), and, unlike malloc, it does additionally zerofill the memory via SLOW "bzero" function. Returns the address of the memory block (or zero if failed).

A(38h) - realloc(old_buf, new_size) ;SLOW!

If "old_buf" is zero, executes malloc(new_size), and returns r2=new_buf (or 0=failed). Else, if "new_size" is zero, executes free(old_buf), and returns r2=garbage. Else, executes malloc(new_size), bcopy(old_buf,new_buf,new_size), and free(old_buf), and returns r2=new_buf (or 0=failed).
Caution: The bcopy function is SLOW, and realloc does accidently copy "new_size" bytes from old_buf, so, if the old_size was smaller than new_size then it'll copy whatever garbage data - in worst case, if it exceeds the top of the 2MB RAM region, it may crash with a locked memory exception, although that'd happen only if SetMem(2) was used to restrict RAM to 2MBs.

A(39h) - InitHeap(addr, size)

Initializes the address and size of the heap - the BIOS does not automatically do this, so, before using the heap, InitHeap must be called by software. Usually, the heap would be memory region between the end of the boot executable, and the bottom of the executable's stack. InitHeap can be also used to deallocate all memory handles (eg. when a new exe file has been loaded, it may use it to deallocate all old memory).
The heap is used only by malloc/realloc/calloc/free, and by the "qsort" function.

B(00h) - alloc_kernel_memory(size)

B(01h) - free_kernel_memory(buf)

Same as malloc/free, but, instead of the heap, manages the 8kbyte control block memory at A000E000h..A000FFFFh. This region is used by the kernel to allocate ExCBs (4x08h bytes), EvCBs (N*1Ch bytes), TCBs (N*0C0h bytes), and the process control block (1x04h bytes). Unlike the heap, the BIOS does automatically initialize this memory region via SysInitMemory(addr,size), and does autimatically allocate the above data (where the number of EvCBs and TCBs is as specified in the SYSTEM.CNF file). Note: FCBs and DCBs are located elsewhere, at fixed locations in the kernel variables area.

Scratchpad Note

The kernel doesn't include any allocation functions for the scratchpad (nor do any kernel functions use that memory area), so the executable can freely use the "fast" memory at 1F800000h..1F8003FFh.

A(9Fh) - SetMem(megabytes)

Changes the effective RAM size (2 or 8 megabytes) by manipulating port 1F801060h, and additionally stores the size in megabytes in RAM at [00000060h].
Note: The BIOS bootcode accidently sets the RAM value to 2MB (which is the correct physical memory size), but initializes the I/O port to 8MB (which mirrors the physical 2MB within that 8MB region), so the initial values don't match up with each other.
Caution: Applying the correct size of 2MB may cause the "realloc" function to crash (that function may accidently access memory above 2MB).

BIOS Memory Fill/Copy/Compare (SLOW)

Like most A(NNh) functions, below functions are executed in uncached BIOS ROM, the ROM has very high waitstates, and the 32bit opcodes are squeezed through an 8bit bus. Moreover, below functions are restricted to process the data byte-by-byte. So, they are very-very-very slow, don't even think about using them.
Of course, that applies also for most other BIOS functions. But it's becoming most obvious for these small functions; memcpy takes circa 160 cycles per byte (reasonable would be less than 4 cycles), and bzero takes circa 105 cycles per byte (reasonable would be less than 1 cycles).

A(2Ah) - memcpy(dst, src, len)

Copies len bytes from [src..src+len-1] to [dst..dst+len-1]. Refuses to copy any data when dst=00000000h or when len>7FFFFFFFh. The return value is always the incoming "dst" value.

A(2Bh) - memset(dst, fillbyte, len)

Fills len bytes at [dst..dst+len-1] with the fillbyte value. Refuses to fill memory when dst=00000000h or when len>7FFFFFFFh. The return value is the incoming "dst" value (or zero, when len=0 or len>7FFFFFFFh).

A(2Ch) - memmove(dst, src, len) - bugged

Same as memcpy, but (attempts) to support overlapping src/dst regions, by using a backwards transfer when src\<dst (and, for some reason, only when dst>=src+len).
BUG: The backwards variant accidently transfers len+1 bytes from [src+len..src] down to [dst+len..dst].

A(2Dh) - memcmp(src1, src2, len) - bugged

Compares len bytes at [src1..src1+len-1] with [src2..src2+len-1], and (attempts) to return the difference between the first mismatching bytes, ie. [src1+N]-[src2+N], or 0 if there are no mismatches. Refuses to compare data when src1 or src2 is 00000000h, and returns 0 in that case.
BUG: Accidently returns the difference between the bytes AFTER the first mismatching bytes, ie. [src1+N+1]-[src2+N+1].
That means that a return value of 0 can mean absolutely anything: That the memory blocks are identical, or that a mismatch has been found (but that the NEXT byte after the mismatch does match), or that the function has failed (due to src1 or src2 being 00000000h).

A(2Eh) - memchr(src, scanbyte, len)

Scans [src..src+len-1] for the first occurence of scanbyte. Refuses to scan any data when src=00000000h or when len>7FFFFFFFh. Returns the address of that first occurence, or 0 if the scanbyte wasn't found.

A(27h) - bcopy(src, dst, len)

Same as "memcpy", but with "src" and "dst" exchanged. That is, the first parameter is "src", the refuse occurs when "src" is 00000000h, and, returns the incoming "src" value (whilst "memcpy" uses "dst" in that places).

A(28h) - bzero(dst, len)

Same as memset, but uses 00h as fixed fillbyte value.

A(29h) - bcmp(ptr1, ptr2, len) - bugged

Same as "memcmp", with exactly the same bugs.

BIOS String Functions

A(15h) - strcat(dst, src)

Appends src to the end of dst. Searches the ending 00h byte in dst, and copies src to that address, up to including the ending 00h byte in src. Returns the incoming dst value. Refuses to do anything if src or dst is 00000000h (and returns 0 in that case).

A(16h) - strncat(dst, src, maxlen)

Same as "strcat", but clipped to "MaxSrc=(min(0,maxlen)+1)" characters, ie. the total length is max "length(dst)+min(0,maxlen)+1". If src is longer or equal to "MaxSrc", then only the first "MaxSrc" chars are copied (with the last byte being replaced by 00h). If src is shorter, then everything up to the ending 00h byte gets copied, but without additional padding (unlike as in "strncpy").

A(17h) - strcmp(str1, str2)

Compares the strings up to including ending 00h byte. Returns 0 if they are identical, or otherwise [str1+N]-[str2+N], where N is the location of the first mismatch, the two bytes are sign-expanded to 32bits before doing the subtraction. The function rejects str1/str2 values of 00000000h (and returns 0=both are zero, -1=only str1 is zero, and +1=only str2 is zero).

A(18h) - strncmp(str1, str2, maxlen)

Same as "strcmp" but stops after comparing "maxlen" characters (and returns 0 if they did match). If the strings are shorter, then comparision stops at the ending 00h byte (exactly as for strcmp).

A(19h) - strcpy(dst, src)

Copies data from src to dst, up to including the ending 00h byte. Refuses to copy anything if src or dst is 00000000h. Returns the incoming dst address (or 0 if copy was refused).

A(1Ah) - strncpy(dst, src, maxlen)

Same as "strcpy", but clipped to "maxlen" characters. If src is longer or equal to maxlen, then only the first "maxlen" chars are copied (but without appending an ending 00h byte to dst). If src is shorter, then the remaining bytes in dst are padded with 00h bytes.

A(1Bh) - strlen(src)

Returns the length of the string up to excluding the ending 00h byte (or 0 when src is 00000000h).

A(1Ch) - index(src, char)

A(1Dh) - rindex(src, char)

A(1Eh) - strchr(src, char) ;exactly the same as "index"

A(1Fh) - strrchr(src, char) ;exactly the same as "rindex"

Scans for the first (index) or last (rindex) occurence of char in the string. Returns the memory address of that occurence (or 0 if there's no occurence, or if src is 00000000h). Char may be 00h (returns the end address of the string). Note that, despite of the function names, the return value is always a memory address, NOT an index value relative to src.

A(20h) - strpbrk(src, list)

Scans for the first occurence of a character that is contained in the list. The list contains whatever desired characters, terminated by 00h.
Returns the address of that occurence, or 0 if there was none. BUG: If there was no occurence, it returns 0 only if src[0]=00h, and otherwise returns the incoming "src" value (which is the SAME return value as when a occurence did occur on 1st character).

A(21h) - strspn(src, list)

A(22h) - strcspn(src, list)

Scans for the first occurence of a character that is (strspn), or that isn't (strcspn) contained in the list. The list contains whatever desired characters, terminated by 00h.
Returns the index (relative to src) of that occurence. If there was no occurence, then it returns the length of src. That silly return values do not actually indicate if an occurence has been found or not (unless one checks for [src+index]=00h or so).
"The strcspn() function shall compute the length (in bytes) of the maximum initial segment of the string pointed to by s1 which consists entirely of bytes not from the string pointed to by s2."
"The strspn() function shall compute the length (in bytes) of the maximum initial segment of the string pointed to by s1 which consists entirely of bytes from the string pointed to by s2."
Hmmmm, that'd be vice-versa?

A(23h) - strtok(src, list) ;first call

A(23h) - strtok(0, list) ;further call(s)

Used to split a string into fragments, list contains a list of characters that are to be treated as separators, terminated by 00h.
The first call copies the incoming string to a buffer in the BIOS variables area (the buffer size is 100h bytes, so the string should be max 255 bytes long, plus the ending 00h byte, otherwise the function destroys other BIOS variables), it does then search the first fragment, starting at the begin of the buffer. Further calls (with src=00000000h) are searching further fragments, starting at the buffer address from the previous call. The internal buffer is used only for strtok, so its contents (and the returned string fragments) remain intact until a new first call to strtok takes place.
The separate fragments are processed by searching the first separator, starting at the current buffer address, the separator is then replaced by a 00h byte, and the old buffer address is returned to the caller. Moreover, the function tries to skip all continously following separators, until reaching a non-separator, and does memorize that address for the next call (due to that skipping further calls won't return empty fragments, the first call may do so though). That skipping seems to be bugged, if list contains two or more different characters, then additional separators aren't skipped.

  ",,TEXT,,,END" with list=","  returns "", "TEXT", "END"
  ",,TEXT,,,END" with list=",." returns "", "", "TEXT", "", "", "END"

Once when there are no more fragments, then 00000000h is returned.

A(24h) - strstr(str, substr) - buggy

Scans for the first occurence of substr in the string. Returns the memory address of that occurence (or 0 if it was unable to find an occurence).
BUG: After rejecting incomplete matches, the function doesn't fallback to the old str address plus 1, but does rather continue at the current str address. Eg. it doesn't find substr="aab" in str="aaab" (in that example, it does merely realize that "aab"\<>"aaa" and then that "aab"\<>"b").

BIOS Number/String/Character Conversion

A(0Eh) - abs(val)

A(0Fh) - labs(val) ;exactly same as "abs"

Returns the absolute value (if val\<0 then R2=-val, else R2=val).

A(0Ah) - todigit(char)

Takes the incoming character, ANDed with FFh, and returns 0..9 for characters "0..9" and 10..35 for "A..Z" or "a..z", or 0098967Fh (9,999,999 decimal) for any other 7bit characters, or garbage for characters 80h..FFh.

A(25h) - toupper(char)

A(26h) - tolower(char)

Returns the incoming character, ANDed with FFh, with letters "A..Z" converted to uppercase/lowercase format accordingly. Works only for char 00h..7Fh (some characters in range 80h..FFh are left unchanged, others are randomly "adjusted" by adding/subtracting 20h, and by sign-expanding the result to 32bits).

A(0Dh) - strtol(src, src_end, base)

Converts a string to a number. The function skips any leading "blank" characters (that are, 09h..0Dh, and 20h) (ie. TAB, CR, LF, SPC, and some others) (some characters in range 80h..FFh are accidently treated as "blank", too).
The incoming base value should be in range 2..11, although the function does also accept the buggy values in range of 12..36 (for values other than 2..36 it defaults to decimal/base10). The used numeric digits are "0..9" and "A..Z" (or less when base is smaller than 36).
The string may have a negative sign prefix "-" (negates the result) (a "+" is NOT recognized; and will be treated as the end of the string). Additionally, the string may contain prefixes "0b" (binary/base2), "0x" (hex/base16), or "o" (octal/base8) (only "o", not "0o"), allowing to override the incoming "base" value.
BUG: Incoming base values greater than 11 don't work due to the prefix feature (eg. base=16 with string "0b11" will be treated as 11 binary, and base=36 with string "o55" will be treated as 55 octal) (the only workaround would be to add/remove leading "0" characters, ie. "b11" or "00b11" or "0o55" would work okay).
Finally, the function initializes result=0, and does then process the digits as "result=result*base+digit" (without any overflow checks) unless/until it reaches an unknown digit (or when digit>=base) (ie. the string may end with 00h, or with any other unexpected characters).
The function accepts both uppercase and lowercase characters (both as prefixes, and as numeric digits). The function returns R2=result, and [src_end]=end_address (ie. usually the address of the ending 00h byte; or of any other unexpected end-byte). If src points to 00000000h, then the function returns r2=0, and leaves [src_end] unchanged.

A(0Ch) - strtoul(src, src_end, base)

Same as "strtol" except that it doesn't recognize the "-" sign prefix (ie. works only for unsigned numbers).

A(10h) - atoi(src)

A(11h) - atol(src) ;exactly same as "atoi" (but slightly slower)

Same as "strtol", except that it doesn't return the string end address in [src_end], and except that it defaults to base=10, but still supports prefixes, allowing to use base2,8,16. CAUTION: For some super bizarre reason, this function treats "0" (a leading ZERO digit) as OCTAL prefix (unlike strtol, which uses the "o" letter as octal prefix) (the "0x" and "0b" prefixes are working as usually).

A(12h) - atob(src, num_dst)

Calls "strtol(str,src_end,10)", and does then exchange the two return values (ie. sets R2=[src_end], and [num_dst]=value_32bit).

A(0Bh) - atof(src) ;USES (ABSENT) COP1 FPU !!!

A(32h) - strtod(src, src_end) ;USES (ABSENT) COP1 FPU !!!

These functions are intended to convert strings to floating point numbers, however, the functions are accidently compiled for MIPS processors with COP1 floating point unit (which is not installed in the PSX, nor does the BIOS support a COP1 software emulation), so calling these functions will produce a coprocessor exception, causing the PSX to lockup via A(40h) SystemErrorUnresolvedException.


On other systems (eg. 8bit computers), "abs/atoi" (integer) and "labs/atol" (long) may act differently. However, on the Playstation, both use signed 32bit values.

BIOS Misc Functions

A(2Fh) - rand()

Advances the random generator as "x=x*41C64E6Dh+3039h" (aka plus 12345 decimal), and returns a 15bit random value "R2=(x/10000h) AND 7FFFh".

A(30h) - srand(seed)

Changes the current 32bit value of the random generator.

A(B4h) - GetSystemInfo(index) ;not supported by old CEX-1000 version

Returns a word, halfword, or string, depending on the selected index value:

  00h      Get Kernel BCD Date       (eg. 19951204h) (YYYYMMDDh)
  01h      Get Kernel Flags or so    (usually/always 000000003h)
  02h      Get Kernel Version String (eg. "CEX-3000/1001/1002 by K.S.",0)
  03h      Get whatever halfword     (usually 0)    ;PS2: returns cop0r15
  04h      Get whatever halfword     (usually 0)
  05h      Get RAM Size in kilobytes (usually 2048) ;=[00000060h] SHL 10
  06h..0Eh Get whatever halfwords    (usually 0,400h,0,200h,0,0,1,1,1)
  0Fh      N/A (returns zero) ;PS2: returns 0000h (effectively = same as zero)
  10h..FFFFFFFFh Not used (returns zero)

Note: The Date/Version are referring to the Kernel (in the first half of the BIOS). The Intro and Bootmenu (in the second half of the BIOS) may have a different version, there's no function to retrieve info on that portion, however, a version string for it can be usually found at BFC7FF32h (eg. "System ROM Version 4.5 05/25/00 E",0) (in many bios versions, the last letter of that string indicates the region, but not in all versions) (the old SCPH1000 does not include that version string at all).

B(56h) - GetC0Table()

B(57h) - GetB0Table()

Retrieves the address of the jump lists for B(NNh) and C(NNh) functions, allowing to patch entries in that lists (however, the BIOS does often jump directly to the function addresses, rather than indirectly via the list, so patching may have little effect in such cases). Note: There's no function to retrieve the address of the A(NNh) jump list, however, that list is usually/always at 00000200h.

A(31h) - qsort(base, nel, width, callback)

Sorts an array, using a super-slow implementation of the "quick sort" algorithm. base is the address of the array, nel is the number of elements in the array, width is the size in bytes of each element, callback is a function that receives pointers to two elements which need to be compared; callback should return return zero if the elements are identical, or a positive/negative number to indicate which element is bigger.
The qsort function rearranges the contents of the array, ie. depending on the callback result, it may swap the contents of the two elements, for some bizarre reason it doesn't swap them directly, but rather stores one of the elements temporarily on the heap (that means, qsort works only if the heap was initialized with InitHeap, and only if "width" bytes are free). There's no return value.

A(35h) - lsearch(key, base, nel, width, callback)

A(36h) - bsearch(key, base, nel, width, callback)

Searches an element in an array (key is the pointer to the searched element, the other parameters are same as for "qsort"). "lsearch" performs a slow linear search in an unsorted array, by simply comparing one array element after each other. "bsearch" assumes that the array contains sorted elements (eg. via qsort), which is allowing to skip some elements, and to jump back and forth in the array, until it has found the desired element (or the location where it'd be, if it'd be in the array). Both functions return the address of the element (or 0 if it wasn't found).

C(19h) - _ioabort(txt1,txt2)

Displays the two strings on the TTY (in some cases the BIOS does accidently pass garbage instead of the 2nd string though). And does then execute _ioabort_raw(1), see there for more details.

A(B2h) - _ioabort_raw(param) ;not supported by old CEX-1000 version

Executes "longjmp(ioabortbuffer,param)". Internally used to recover from failed I/O operations, param should be nonzero to notify the setjmp caller that the abort has occurred.

A(13h) - setjmp(buf)

This is a somewhat incomplete implementation of posix's setjmp, by storing the ABI-saved CPU registers in the specified buffer (30h bytes):

  00h 4    r31 (ra) (aka caller's pc)
  04h 4    r29 (sp)
  08h 4    r30 (fp)
  0Ch 4x8  r16..r23
  2Ch 4    r28 (gp)

That type of buffer can be used with "_ioabort", "longjmp", and also "HookEntryInt(addr)".
The "setjmp" function returns 0 when called directly. However, it may return again - to the same return address, and the same stack pointer - with another return value (which should be usually non-zero, to indicate that the state has been restored (eg. _ioabort passes 1 as return value).
Also noteworthy from what a compliant setjmp implementation should be doing is the absence of saving the state of cop0 and cop2, thus making this slightly unsuitable for a typical coroutine system implementation.

A(14h) - longjmp(buf, param)

Restores the R16-R23,GP,SP,FP,RA registers from a previously recorded jmp_buf buffer, and "returns" to that new RA address (rather than to the caller of the longjmp function). The "param" value is passed as "return value" to the code at RA, ie. usually to the caller of the original setjmp call. Noteworthy difference from a conformant longjmp implementation is that the "param" value won't be clamped to 1 if you pass 0 to it. So since setjmp returns 0 on the first call, the caller of longjmp must take care that "param" is non-zero, so the callsite of setjmp can make the difference between the first call and a rollback. See setjmp for further details.

A(53h) - set_ioabort_handler(src) ;PS2 only ;PSX: SystemError

Normally the _ioabort handler is changed only internally during booting, with this new function, games can install their own _ioabort handler. src is pointer to a 30h-byte "savestate" structure, which will be copied to the actual _ioabort structure.

A(06h) or B(38h) - exit(exitcode)

Terminates the program and returns control to the BIOS; which does then lockup itself via A(3Ah) _exit.

A(A0h) - _boot()

Performs a warmboot (resets the kernel and reboots from CDROM). Unlike the normal coldboot procedure, it doesn't display the "\<S>" and "PS" intro screens (and doesn't verify the "PS" logo in the ISO System Area), and, doesn't enter the bootmenu (even if the disk drive is empty, or if it contains an Audio disk). And, it doesn't reload the SYSTEM.CNF file, so the function works only if the same disk is still inserted (or another disk with identical SYSTEM.CNF, such like Disk 2 of the same game).

A(B5h..BFh) B(11h,24h..29h,2Ch..31h,5Eh..FFh) C(1Eh..7Fh) - N/A - Jump 0

These functions jump to address 00000000h. For whatever reason, that address does usually contain a copy of the exception handler (ie. same as at address 80000080h). However, since there's no return address stored in EPC register, the functions will likely crash when returning from the exception handler.

A(57h..5Ah,73h..77h,79h..7Bh,7Dh,7Fh..80h,82h..8Fh,B0h..B1h,B3h), and

C(0Eh..11h,14h) - N/A - Returns 0

No function. Simply returns with r2=00000000h.
Reportedly, A(85h) is CdStop, but that seems to be nonsense?

SYS(00h) - NoFunction()

No function. Simply returns without changing any registers or memory locations (except that, of course, the exception handler destroys k0).

SYS(04h..FFFFFFFFh) - calls DeliverEvent(F0000010h,4000h)

These are syscalls with invalid function number in R4. For whatever reason that is handled by issuing DeliverEvent(F0000010h,4000h). Thereafter, the syscall returns to the main program (ie. it doesn't cause a SystemError).

A(3Ah) - _exit(exitcode)

A(40h) - SystemErrorUnresolvedException()

A(A1h) - SystemError(type,errorcode) ;type "B"=Boot,"D"=Disk

These are used "SystemError" functions. The functions are repeatedly jumping to themselves, causing the system to hang. Possibly useful for debugging software which may hook that functions.

A(4Fh,50h,52h,53h,9Ah,9Bh) B(1Ah..1Fh,21h..23h,2Ah,2Bh,52h,5Ah) C(0Bh) - N/A

These are additional "SystemError" functions, but they are never used. The functions are repeatedly jumping to themselves, causing the system to hang.

BRK(1C00h) - Division by zero (commonly checked/invoked by software)

BRK(1800h) - Division overflow (-80000000h/-1, sometimes checked by software)

The CPU does not generate any exceptions upon divide overflows, because of that, the Kernel code and many games are commonly checking if the divider is zero (by software), and, if so, execute a BRK 1C00h opcode. The default BIOS exception handler doesn't handle BRK exceptions, and does simply redirect them to SystemErrorUnresolvedException().

BIOS Internal Boot Functions

A(45h) - init_a0_b0_c0_vectors

Copies the three default four-opcode handlers for the A(NNh),B(NNh),C(NNh) functions to A00000A0h..A00000CFh.

C(07h) - InstallExceptionHandlers() ;destroys/uses k0/k1

Copies the default four-opcode exception handler to the exception vector at 80000080h..8000008Fh, and, for whatever reason, also copies the same opcodes to 80000000h..8000000Fh.

C(08h) - SysInitMemory(addr,size)

Initializes the address (A000E000h) and size (2000h) of the allocate-able Kernel Memory region, and, seems to deallocate any memory handles which may have been allocated via B(00h).

C(09h) - SysInitKernelVariables()

Zerofills all Kernel variables; which are usually at [00007460h..0000891Fh].
Note: During the boot process, the BIOS accidently overwrites the first opcode of this function (by the last word of the A0h table), so, thereafter, this function won't work anymore (nor would it be of any use).

C(12h) - InstallDevices(ttyflag)

Initializes the size and address of the File and Device Control Blocks (FCBs and DCBs). Adds the TTY device by calling "KernelRedirect(ttyflag)", and the CDROM and Memory Card devices by calling "AddCDROMDevice()" and "AddMemCardDevice()".

C(1Ch) - AdjustA0Table()

Copies the B(32h..3Bh) and B(3Ch..3Fh) function addresses to A(00h..09h) and A(3Bh..3Eh). Apparently Sony's compiler/linker can't insert the addresses in the A0h table directly at compilation time, so this function is used to insert them during execution of the boot code.

BIOS More Internal Functions

Below are mainly internally used device related subfunctions.

Internal Device Stuff

  A(5Bh) dev_tty_init()                                      ;PS2: SystemError
  A(5Ch) dev_tty_open(fcb,and unused:"path\name",accessmode) ;PS2: SystemError
  A(5Dh) dev_tty_in_out(fcb,cmd)                             ;PS2: SystemError
  A(5Eh) dev_tty_ioctl(fcb,cmd,arg)                          ;PS2: SystemError
  A(5Fh) dev_cd_open(fcb,"path\name",accessmode)
  A(60h) dev_cd_read(fcb,dst,len)
  A(61h) dev_cd_close(fcb)
  A(62h) dev_cd_firstfile(fcb,"path\name",direntry)
  A(63h) dev_cd_nextfile(fcb,direntry)
  A(64h) dev_cd_chdir(fcb,"path")
  A(65h) dev_card_open(fcb,"path\name",accessmode)
  A(66h) dev_card_read(fcb,dst,len)
  A(67h) dev_card_write(fcb,src,len)
  A(68h) dev_card_close(fcb)
  A(69h) dev_card_firstfile(fcb,"path\name",direntry)
  A(6Ah) dev_card_nextfile(fcb,direntry)
  A(6Bh) dev_card_erase(fcb,"path\name")
  A(6Ch) dev_card_undelete(fcb,"path\name")
  A(6Dh) dev_card_format(fcb)
  A(6Eh) dev_card_rename(fcb1,"path\name1",fcb2,"path\name2")
  A(6Fh) ?   ;card ;[r4+18h]=00000000h  ;card_clear_error(fcb) or so
  A(96h) AddCDROMDevice()
  A(97h) AddMemCardDevice()
  A(98h) AddDuartTtyDevice()   ;PS2: SystemError
  A(99h) add_nullcon_driver()
  B(47h) AddDrv(device_info)  ;subfunction for AddXxxDevice functions
  B(48h) DelDrv(device_name_lowercase)
  B(5Bh) ChangeClearPAD(int)   ;pad AND card (ie. used also for Card)
  C(15h) _cdevinput(circ,char)
  C(16h) _cdevscan()
  C(17h) _circgetc(circ)    ;uses r5 as garbage txt for _ioabort
  C(18h) _circputc(char,circ)

Device Names

Device Names are case-sensitive (usually lowercase, eg. "bu" for memory cards). In filenames, the device name may be followed by a hexadecimal 32bit non-case-sensitive port number (eg. "bu00:" for selecting the first memory card slot). Accordingly, the device name should not end with a hexdigit (eg. "usb:" would be treated as device "us" with port number 0Bh).
Standard device names are "cdrom:", "bu00:", "bu10:", "tty00:". Other, nonstandard devices are:

  Castlevania is trying to access an unknown device named "sim:".
  Caetla (a firmware replacement for Cheat Devices) supports "pcdrv:" device.

BIOS PC File Server


Below BRK's are internally used in DTL-H2000 BIOS for two devices: "mwin:" (Message Window) and "sim:" (CDROM Sim).

Caetla Blurb

Caetla (a firmware replacement for Cheat Devices) supports "pcdrv:" device, the SN systems (=what?) device extension to access files on the drive of the pc. This fileserver can be accessed by using the kernel functions, with the "pcdrv:" device name prefix to the filenames or using the SN system calls.
The following SN system calls for the fileserver are provided. Accessed by setting the registers and using the break command with the specified field.
The break functions have argument(s) in A1,A2,A3 (ie. unlike normal BIOS functions not in A0,A1,A2), and TWO return values (in V0, and V1).

BRK(101h) - PCInit() - Inits the fileserver

No parameters.

BRK(102h) - PCCreat(filename, fileattributes) - Creates a new file on PC

  out: V0  0 = success, -1 = failure
       V1  file handle or error code if V0 is negative

Attributes Bits (standard MSDOS-style):

  bit0     Read only file (R)
  bit1     Hidden file    (H)
  bit2     System file    (S)
  bit3     Not used       (zero)
  bit4     Directory      (D)
  bit5     Archive file   (A)
  bit6-31  Not used       (zero)

BRK(103h) - PCOpen(filename, accessmode) - Opens a file on the PC

  out: V0  0 = success, -1 = failure
       V1  file handle or error code if V0 is negative

BRK(104h) - PCClose(filehandle) - Closes a file on the PC

  out: V0  0 = success, -1 = failure
       V1  0 = success, error code if V0 is negative

BRK(105h) - PCRead(filehandle, length, memory_destination_address)

  out: V0  0 = success, -1 = failure
       V1  number of read bytes or error code if V0 is negative.

Note: PCRead does not stop at EOF, so if you set more bytes to read than the filelength, the fileserver will pad with zero bytes. If you are not sure of the filelength obtain the filelength by PClSeek (A2=0, A3=2, V1 will return the length of the file, don't forget to reset the file pointer to the start before calling PCread!)

BRK(106h) - PCWrite(filehandle, length, memory_source_address)

  out: V0  0 = success, -1 = failure
       V1  number of written bytes or error code if V0 is negative.

BRK(107h) - PClSeek(filehandle, file_offset, seekmode) - Change Filepos

seekmode may be from 0=Begin of file, 1=Current fpos, or 2=End of file.

  out: V0  0 = success, -1 = failure
       V1  file pointer

BIOS TTY Console (std_io)

A(3Fh) - Printf(txt,param1,param2,etc.) - Print string to console

  in:  A0                     Pointer to 0 terminated string
       A1,A2,A3,[SP+10h..]    Argument(s)

Prints the specified string to the TTY console. Printf does internally use "putchar" to output the separate characters (and expands char 09h and 0Ah accordingly).
The string can contain C-style escape codes (prefixed by "%" each):

  c         display ASCII character
  s         display ASCII string
  i,d,D     display signed Decimal number (d/i=default32bit, D=force32bit)
  u,U       display unsigned Decimal number (u=default32bit, U=force32bit)
  o,O       display unsigned Octal number (o=default32bit, O=force32bit)
  p,x,X     display unsigned Hex number (p=lower/force32bit, x=lower, X=upper)
  n         write 32bit/16bit string length to [parameter] (default32bit)

Additionally, following prefixes (inserted between "%" and escape code):

  + or SPC  show leading plus or space character in positive signed numbers
  NNN       fixed width (for padding or so) (first digit must be 1..9) (not 0)
  .NNN      fixed width (for clipping or so)
  *         variable width (using one of the parameters) (negative=ending_spc)
  .*        variable width
  -         force ending space padding (in case of width being specified)
  #         show leading "0x" or "0X" (hex), or ensure 1 leading zero (octal)
  0         show leading zero's
  L         unknown/no effect?
  h,l       force 16bit (h=halfword), or 32bit (l=long/word)

The force32bit codes (D,U,O,p,l) are kinda useless since the PSX defaults to 32bit parameters anyways. The force16bit code (h) may be useful as "%hn" (writeback 16bit value), otherwise it's rather useless, unless signed 16bit parameters have garbage in upper 16bit, for unsigned 16bit parameters it doesn't work at all (accidently sign-expands 16bit to 32bit, and then displays that signed 32bit value as giant unsigned value). Printf supports only octal, decimal, and hex (but not binary).

A(3Eh) or B(3Fh) - puts(src) - Write string to TTY

  in: R4=address of string (terminated by 00h)

Like "printf", but doesn't resolve any "%" operands. Empty strings are handled in a special way: If R4 points to a 00h character then nothing is output (as one would expect it), but, if R4 is 00000000h then "\<NULL>" is output (only that six letters; without appending any CR or LF).

A(3Dh) or B(3Eh) - gets(dst) - Read string from TTY (keyboard input)

  in: r4=dst (pointer to a 128-byte buffer) - out: r2=dst (same is incoming r4)

Internally uses "getchar" to receive the separate characters (which are thus masked by 7Fh). The received characters are stored in the buffer, and are additionally sent back as echo to the TTY via std_out_putc.
The following characters are handled in a special way: 09h (TAB) is replaced by a single SPC. 08h or 7FH (BS or DEL) are removing the last character from the buffer (unless it is empty) and send 08h,20h,08h (BS,SPC,BS) to the TTY. 0Dh or 0Ah (CR or LF) do terminate the input (append 00h to the buffer, send 0Ah to the TTY, which is expanded to 0Dh,0Ah by the std_out_putc function, and do then return from the gets function).
The sequence 16h,NNh forces NNh to be stored in the buffer (even if NNh is a special character like 00h..1Fh or 7Fh). If the buffer is full (circa max 125 chars, plus one extra byte for the ending 00h), or if an unknown control code in range of 00h..1Fh is received without the 16h prefix, then 07h (BELL) is sent to the TTY.

A(3Bh) or B(3Ch) - getchar() - Read character from TTY

Reads one character from the TTY console, by internally redirecting to "read(0,tempbuf,1)". The returned character is ANDed by 7Fh (so, to read a fully intact 8bit character, "read(0,tempbuf,1)" must be used instead of this function).

A(3Ch) or B(3Dh) - putchar(char) - Write character to TTY

Writes the character to the TTY console, by internally redirecting to "write(1,tempbuf,1)". Char 09h (TAB) is expanded to one or more SPC characters, until reaching the next tabulation boundary (every 8 characters). Char 0Ah (LF) is expanded to 0Dh,0Ah (CR,LF). Other special characters (which should be handled at the remote terminal side) are 08h (BS, backspace, move cursor one position to the left), and 07h (BELL, produce a short beep sound).

C(13h) - FlushStdInOutPut()

Closes and re-opens the std_in (fd=0) and std_out (fd=1) file handles.

C(1Bh) - KernelRedirect(ttyflag) ;PS2: ttyflag=1 causes SystemError

Removes, re-mounts, and flushes the TTY device, the parameter selects whether to mount the real DUART-TTY device (r4=1), or a Dummy-TTY device (r4=0), the latter one sends any std_out to nowhere. Values other than r4=0 or r4=1 do remove the device, but do not re-mount it (which might result in problems).
Caution: Trying to use r4=1 on a PSX that does not has the DUART hardware installed causes the BIOS to hang (so one should first detect the DUART hardware, eg. by writing two different bytes to Port 1F802020h.1st/2nd access, and the read and verify that two bytes).

Activating std_io

The std_io functions can be enabled via C(1Bh) KernelRedirect(ttyflag), the BIOS is unable to detect the presence of the TTY hardware, by default the BIOS bootcode disables std_io by setting the initial KernelRedirect value at [A000B9B0h] to zero, this is hardcoded shortly after the POST(E) output:

  call    output_post_r4        ;\output POST(E)
  +mov    r4,0Eh                ;/
  mov     r1,0A0010000h         ;\set [0A000B9B0h]=0 ;TTY=dummy/off
  call    reset_cont_d_3        ; and call reset_cont_d_3
  +mov    [r1-4650h],0          ;/

assuming that R28=A0010FF0h, the last 3 opcodes of above code can be replaced by:

  mov     r1,1h                 ;\set [0A000B9B0h]=1 ;TTY=duart/on
  call    reset_cont_d_3        ; and call reset_cont_d_3
  +mov    [r28-4650h-0ff0h],r1  ;/

with that patch, the BIOS bootcode (and many games) are sending debug messages to the debug terminal, via expansion port, see:
EXP2 Dual Serial Port (for TTY Debug Terminal)
Note: The nocash BIOS automatically detects the DUART hardware, and activates TTY if it is present.

B(49h) - PrintInstalledDevices()

Uses printf to display the long and short names from the DCB of the currently installed devices. Doesn't do anything else. There's no return value.


Several BIOS functions are internally using printf to output status information, timeout, and error messages, etc. So, trying to close the TTY file handles (fd=0 and fd=1) would cause such functions to work unstable.

BIOS Character Sets

B(51h) - Krom2RawAdd(shiftjis_code)

  In: r4  = 16bit Shift-JIS character code
  Out: r2 = address in BIOS ROM of the desired character (or -1 = error)

r4 should be 8140h..84BEh (charset 2), or 889Fh..9872h (charset 3).

B(53h) - Krom2Offset(shiftjis_code)

  In: r4  = 16bit Shift-JIS character code
  Out: r2 = offset within charset (without charset base address)

This is a subfunction for B(51h) Krom2RawAdd(shiftjis_code).

Character Sets in ROM (112Kbytes)

The character sets are located at BFC64000h and up, intermixed with some other stuff:

  BFC64000h  Charset 1 (16x15 pix, letters with accent marks)    (NOT in JAPAN)
  BFC65CB6h  Garbage   (four-and-a-half reverb tables, ioports, printf strings)
  BFC66000h  Charset 2 (16x15 pix, various alphabets, english, greek, etc.)
  BFC69D68h  Charset 3 (16x15 pix, japanese or chinese symbols or so)
  BFC7F8DEh  Charset 4 (8x15 pix, mainly ASCII letters)
  BFC7FE6Fh  Charset 5 (8x15 pix, additional punctuation marks)    (NOT in PS2)
  BFC7FF32h  Version   (Version and Copyright strings)        (NOT in SCPH1000)
  BFC7FF8Ch  Charset 6 (8x15 pix, seven-and-a-half japanese chars) (NOT in PS2)
  BFC80000h  End       (End of 512kBYTE BIOS ROM)

Charset 1 (and Garbage) is NOT included in japanese BIOSes (in the SCPH1000 version that region contains uncompressed program code, in newer japanese BIOSes that regions are zerofilled)
Charset 1 symbols are as defined in JIS-X-0212 char(2661h..2B77h), and EUC-JP char(8FA6E1h..8FABF7h).
Version (and Copyright) string is NOT included in SCPH1000 version (that BIOS includes further japanese 8x15 pix chars in that region).
For charset 2 and 3 it may be recommended to use the B(51h) Krom2RawAdd(shiftjis_code) to obtain the character addresses. Not sure if that BIOS function (or another BIOS function) allows to retrieve charset 1, 4, 5, and 6 addresses?

BIOS Control Blocks

Exception Control Blocks (ExCB) (4 blocks of 8 bytes each)

  00h 4   ptr to first element of exception chain
  04h 4   not used (zero)

Event Control Blocks (EvCB) (usually 16 blocks of 1Ch bytes each)

  00h 4   class  (events are triggered when class and spec match)
  04h 4   status (0=free,1000h=disabled,2000h=enabled/busy,4000h=enabled/ready)
  08h 4   spec   (events are triggered when class and spec match)
  0Ch 4   mode   (1000h=execute function/stay busy, 2000h=no func/mark ready)
  10h 4   ptr to function to be executed when ready (or 0=none)
  14h 8   not used (uninitialized)

Thread Control Blocks (TCB) (usually 4 blocks of 0C0h bytes each)

  00h 4   status        (1000h=Free TCB, 4000h=Used TCB)
  04h 4   not used      (set to 1000h by OpenTh) (not for boot executable?)
  08h 80h r0..r31       (entries for r0/zero and r26/k0 are unused)
  88h 4   cop0r14/epc   (aka r26/k0 and pc when returning from exception)
  8Ch 8   hi,lo         (the mul/div registers)
  94h 4   cop0r12/sr    (stored/restored by exception, NOT init by OpenTh)
  98h 4   cop0r13/cause (stored when entering exception, NOT restored on exit)
  9Ch 24h not used      (uninitialized)

Process Control Block (1 block of 4 bytes)

  00h 4   ptr to TCB of current thread

The PSX supports only one process, and thus only one Process Control Block.

File Control Blocks (FCB) (16 blocks of 2Ch bytes each)

  00h 4  status (0=Free FCB) (nonzero=accessmode)
  04h 4  cdrom: disk_id (checksum across path table of the corresponding disk),
         memory card: port number (00h=slot1, 10h=slot2)
  08h 4  transfer address (for dev_in_out function)
  0Ch 4  transfer length  (for dev_in_out function)
  10h 4  current file position
  14h 4  device flags (copy of DCB[04h])
  18h 4  error  ;used by B(55h) - _get_error(fd)
  1Ch 4  Pointer to DCB for the file
  20h 4  filesize
  24h 4  logical block number (start of file) (for cdrom: at least)
  28h 4  file control block number (simply 0..15 for FCB number 0..15)

Device Control Blocks (DCB) (10 blocks of 50h bytes each)

  00h 4   ptr to lower-case short name ("cdrom", "bu", "tty") (or 0=Free DCB)
  04h 4   device flags (cdrom=14h, bu=14h, tty/dummy=1, tty/duart=3)
  08h 4   sector size  (cdrom=800h, bu=80h, tty=1)
  0Ch 4   ptr to upper-case long name  ("CD-ROM", "MEMORY CARD", "CONSOLE")
  10h 4   ptr to init()                                         (TTY only)
  14h 4   ptr to open(fcb,"path\name",accessmode)
  18h 4   ptr to in_out(fcb,cmd)                                (TTY only)
  1Ch 4   ptr to close(fcb)
  20h 4   ptr to ioctl(fcb,cmd,arg)                             (TTY only)
  24h 4   ptr to read(fcb,dst,len)
  28h 4   ptr to write(fcb,src,len)
  2Ch 4   ptr to erase(fcb,"path\name")
  30h 4   ptr to undelete(fcb,"path\name")
  34h 4   ptr to firstfile2(fcb,"path\name",direntry)
  38h 4   ptr to nextfile(fcb,direntry)
  3Ch 4   ptr to format(fcb)
  40h 4   ptr to cd(fcb,"path")                            (CDROM only)
  44h 4   ptr to rename(fcb1,"path\name1",fcb2,"path\name2")
  48h 4   ptr to remove()
  4Ch 4   ptr to testdevice(fcb,"path\name")

BIOS Versions

Kernel Versions

For the actual kernel, there seem to be only a few different versions. Most PSX/PSone's are containing the version from 1995 (which is kept 1:1 the same in all consoles; without any PAL/NTSC related customizations).

  28-Jul-1994  "DTL-H2000"                   ;v0.x (pre-retail devboard)
  22-Sep-1994  "CEX-1000 KT-3  by S.O."      ;v1.0 through v2.0
  no-new-date  "CEX-3000 KT-3  by K.S."      ;v2.1 only (old Port 1F801060h)
  04-Dec-1995  "CEX-3000/1001/1002 by K.S."  ;v2.2 through v4.5 (except v4.0)
  29-May-1997  "CEX-7000/-7001 by K.S.    "  ;v4.0 only (new Port 1F801010h)
  17-Jan-2000  "PS compatible mode by M.T."  ;v5.0 (Playstation 2)

The date and version string can be retrieved via GetSystemInfo(index).
The "CEX-7000/-7001" version was only "temporarily" used (when the kernel/gui grew too large they changed the ROM size from 512K to 1024K; but did then figure out that they could use a self-decompressing GUI to squeeze everything into 512K; but they did accidentally still use the 1024K setting) (newer consoles fixed that and switched back to the old version from 1995) (aside from the different date/version string, the only changed thing is the opcode at BFC00000h, which initializes port 1F801010h to BIOS ROM size of 1MB, instead of 512KB; no idea if that BIOS does actually contain additional data?).
The "CEX-3000 KT-3" version is already almost same as "CEX-3000/1001/1002", aside from version/date, the only differences are at offset BFC00014h..1Fh, and BFC003E0h (both related to Port 1F801060h).

Bootmenu/Intro Versions

This portion was updated more often. It's customized for PAL/NTSC displays, japanese/english language, and (maybe?) region/licence string checks. The SCPH1000 uses uncompressed Bootmenu/Intro code with "\<S>" intro, but without "PS" intro (or, "PS" is shown only on region matches?), newer versions are using selfdecompressing code, with both intro screens. The GUI in older PSX models looks like a drawing program for children, the GUI in newer PSX models and in PSone's looks more like a modernized bathroom furniture, unknown how the PS2 GUI looks like?
Games are communicating only with the Kernel, so the differences in the Bootmenu/Intro part should have little or effect on compatibility (although some I/O ports might be initialized differently, and although some games might (accidently) read different (garbage) values from the ROM).

  Ver  CRC32    Used in                      System ROM Version  Kernel
  0.xj 18D0F7D8 DTL-H2000                    (no version string) dtlh2000
  1.0j 3B601FC8 SCPH-1000 and DTL-H1000      (no version string) cex1000
  1.1j 3539DEF6 SCPH-3000 and DTL-H1000H     "1.1 01/22/95"      ""
  2.0a 55847D8C DTL-H1001                    "2.0 05/07/95 A"    ""
  2.0e 9BB87C4B SCPH-1002 and DTL-H1002      "2.0 05/10/95 E"    ""
  2.1j BC190209 SCPH-3500                    "2.1 07/17/95 J"    cex3000
  2.1a AFF00F2F SCPH-1001 and DTL-H1101      "2.1 07/17/95 A"    ""
  2.1e 86C30531 SCPH-1002 and DTL-H1102      "2.1 07/17/95 E"    ""
  2.2j 24FC7E17 SCPH-5000 and DTL-H1200      "2.2 12/04/95 J"    cex3000/100x
  2.2a 37157331 SCPH-1001 and DTL-H1201/3001 "2.2 12/04/95 A"    ""
  2.2e 1E26792F SCPH-1002 and DTL-H1202/3002 "2.2 12/04/95 E"    ""
  2.2v 446EC5B2 SCPH-5903 (VCD, 1Mbyte)      "2.2 12/04/95 J"    ""
  2.2d DECB22F5 DTL-H1100                    "2.2 03/06/96 D"    ""
  3.0j FF3EEB8C SCPH-5500                    "3.0 09/09/96 J"    ""
  3.0a 8D8CB7E4 SCPH-5501/7003               "3.0 11/18/96 A"    ""
  3.0e D786F0B9 SCPH-5502/5552               "3.0 01/06/97 E"    ""
  4.0j EC541CD0 SCPH-7000/9000               "4.0 08/18/97 J"    cex7000
  4.1w B7C43DAD SCPH-7000W                       ...XXX...
  4.1a 502224B6 SCPH-7001/7501/7503/9001     "4.1 12/16/97 A"    cex3000/100x
  4.1e 318178BF SCPH-7002/7502/9002          "4.1 12/16/97 E"    ""
  4.3j F2AF798B SCPH-100  (PSone)            "4.3 03/11/00 J"    ""
  4.4a 6A0E22A0 SCPH-101  (PSone)            "4.4 03/24/00 ..XXX..
  4.4e 0BAD7EA9 SCPH-102  (PSone)            "4.4 03/24/00 E"    ""
  4.5a 171BDCEC SCPH-101  (PSone)            "4.5 05/25/00 A"    ""
  4.5e 76B880E5 SCPH-102  (PSone)            "4.5 05/25/00 E"    ""
  5.0t B7EF81A9 SCPH10000 (Playstation 2)    "5.0 01/17/00 T"    PS compatible

The System ROM Version string can be found at BFC7FF32h (except in v1.0).

v2.2j/a/e use exactly the same GUI as v2.1 (only the kernel was changed). v2.2d is almost same as v2.2j (but with some GUI patches or so).
v4.1 and v4.5 use exactly the same GUI code for "A" and "E" regions (the only difference is the last byte of the version string; which does specify whether the GUI shall use PAL or NTSC).
v5.0 is playstation 2 bios (4MB) with more or less backwards compatible kernel.

Character Set Versions

The 16x15 pixel charsets at BFC66000h and BFC69D68h are included in all BIOSes, however, the 16x15 portion for letters with accent marks at BFC64000h is included only in non-japanese BIOSes, and in some newer japanese BIOSes (not included in v4.0j, but they are included in v4.3j).
The 8x15 pixel charset with characters 21h..7Fh is included in all BIOSes. In the SCPH1000, this region is followed by additional 8x15 punctuation marks at char 80h and up, however, this region is missing in PS2 BIOS. Moreover, some BIOSes include an incomplete 8x15 japanese character set (which ends abruptly at BF7FFFFFh), in newer BIOSes, some of theses chars are replaced by the version string at BFC7FF32h, and, the remaining 8x15 japanese chars were removed in the PS2 BIOS version.

BIOS Patches

The original PSX Kernel mainly consists of messy and unstable compiler generated code, and, to the worst, the \<same> author seems to have attempted to use assembler code in some places. In result, most commercial games are causing a greater mess by inserting patches in the kernel code...
Which has been a nasty surprise when making the nocash PSX bios; which obviously wasn't compatible with these patches. The only solutions would have been to insert hundreds of NOPs to make my bios \<exactly> as bloated as the original bios (which I really didn't want to do), or to create anti-patch-patches.

Patches and Anti-Patch-Patches

As shown below, all known patches are invoked by a B(56h) or B(57h) function call. In the nocash PSX bios, these two functions are examining the following opcodes, if the opcodes are a known patch, then the BIOS reproduces the desired behaviour, and does then continue normal execution after those opcodes. If the opcodes are unknown, then the BIOS simply locks up; and shows an error message with the address of that opcodes in the TTY window; info about any such unknown opcodes would be welcome!


If you want to (or need to) use patches, please use byte-identical opcodes as commercial games do (as shown below; only the "xxxx" address digits are don't care), so the nocash PSX bios (or other homebrewn BIOSes) can detect and reproduce them. Or alternately, don't use the BIOS, and access I/O ports directly, which is much better and faster anyways.


In newer Kernel version, the exception handler reads cop0r13/cause to r2, examines the Excode value in r2, and if the exception was caused by an interrupt, and if the next opcode (at EPC) is a GTE/COP2 command, then it does increment EPC by 4. The GTE commands are executed even if an interrupt occurs simultaneously, so, without adjusting EPC, the command would be executed twice. With some commands that'd just waste some clock cycles, with other commands it may cause data to be written twice to the GTE FIFOs, or may re-use the result from the 1st command execution as input to the 2nd execution.
The old "CEX-1000 KT-3" Kernel version did examine r2, but it "forgot" to previously load cop0r13 to r2, so it did randomly examine a garbage value. The patch inserts the missing opcode, used in elo2 at 80033740h, and in Pandemonium II at 8007F3FCh:

  240A00B0 mov  r10,0B0h                      ;\   00000000 nop
  0140F809 call r10                           ;    00000000 nop
  24090056 +mov  r9,56h                       ;/   241A0100 mov k0,100h
  3C0Axxxx mov  r10,xxxx0000h                 ;\   8F5A0008 mov k0,[k0+8h]
  3C09xxxx mov  r9,xxxx0000h                  ;    00000000 nop
  8C420018 mov  r2,[r2+06h*4] ;=C(06h)        ;    8F5A0000 mov k0,[k0]
  254Axxxx add  r10,xxxxh ;=@@new_data        ;    00000000 nop
  2529xxxx add  r9,xxxxh  ;=@@new_data_end    ;/   235A0008 addt k0,8h
          @@copy_lop:                         ;\   AF410004 mov [k0+4h],r1
  8D430000 mov  r3,[r10]                      ;    AF420008 mov [k0+8h],r2
  254A0004 add  r10,4h                        ;    AF43000C mov [k0+0Ch],r3
  24420004 add  r2,4h                         ;    AF5F007C mov [k0+7Ch],ra
  1549FFFC jne  r10,r9,@@copy_lop             ;    40026800 mov r2,cop0r13
  AC43FFFC +mov [r2-4h],r3                    ;/   00000000 nop

Alternately, same as above, but using k0/k1 instead of r10/r9, used in Ridge Racer at 80047B14h:

  240A00B0 mov  r10,0B0h                      ;\     00000000 nop
  0140F809 call r10                           ;      00000000 nop
  24090056 +mov r9,56h                        ;/     241A0100 mov  k0,100h
  3C1Axxxx mov  k0,xxxx0000h                  ;\     8F5A0008 mov  k0,[k0+8h]
  3C1Bxxxx mov  k1,xxxx0000h                  ;      00000000 nop
  8C420018 mov  r2,[r2+06h*4] ;=C(06h)        ;      8F5A0000 mov  k0,[k0]
  275Axxxx add  k0,xxxxh  ;=@@new_data        ;      00000000 nop
  277Bxxxx add  k1,xxxxh  ;=@@new_data_end    ;/     235A0008 addt k0,8h
          @@copy_lop:                         ;\     AF410004 mov  [k0+4h],r1
  8F430000 mov  r3,[k0]                       ;      AF420008 mov  [k0+8h],r2
  275A0004 add  k0,4h                         ;      AF43000C mov  [k0+0Ch],r3
  24420004 add  r2,4h                         ;      AF5F007C mov  [k0+7Ch],ra
  175BFFFC jne  k0,k1,@@copy_lop              ;      40026800 mov  r2,cop0r13
  AC43FFFC +mov [r2-4h],r3                    ;/     00000000 nop

Alternately, slightly different code used in metal_gear_solid at 80095CC0h, and in alone1 at 800A3ECCh:

  24090056 mov  r9,56h                        ;\
  240A00B0 mov  r10,0B0h                      ; B(56h) GetC0Table
  0140F809 call r10                           ;
  00000000 +nop                               ;/
  8C420018 mov  r2,[r2+06h*4] ;=00000C80h = exception_handler = C(06h)
  00000000 nop
  24420028 add  r2,28h
  00407821 mov  r15,r2
  3C0Axxxx lui  r10,xxxxh ;\@@ori_data        ;\
  254Axxxx add  r10,xxxxh ;/                  ;
  3C09xxxx lui  r9,xxxxh  ;\@@ori_data_end    ; @@ori_data:
  2529xxxx add  r9,xxxxh  ;/                  ;  AF410004 mov [k0+4h],r1
          @@verify_lop:                       ;  AF420008 mov [k0+8h],r2
  8D430000 mov  r3,[r10]                      ;  AF43000C mov [k0+0Ch],r3
  8C4B0000 mov  r11,[r2]                      ;  AF5F007C mov [k0+7Ch],ra
  254A0004 add  r10,4h                        ;  40037000 mov r3,cop0r14
  146B000E jne  r3,r11,@@verify_mismatch      ;  00000000 nop
  24420004 +add r2,4h                         ;
  1549FFFA jne  r10,r9,@@verify_lop           ;
  00000000 +nop                               ;/
  01E01021 mov  r2,r15
  3C0Axxxx lui  r10,xxxxh ;\@@new_data        ;\
  254Axxxx add  r10,xxxxh ;/                  ;
  3C09xxxx lui  r9,xxxxh  ;\@@new_data_end    ; @@new_data:
  2529xxxx add  r9,xxxxh  ;/                  ;  AF410004 mov [k0+4h],r1
          @@copy_lop:                         ;  AF420008 mov [k0+8h],r2
  8D430000 mov  r3,[r10]                      ;  40026800 mov r2,cop0r13
  00000000 nop                                ;  AF43000C mov [k0+0Ch],r3
  AC430000 mov  [r2],r3                       ;  40037000 mov r3,cop0r14
  254A0004 add  r10,4h                        ;  AF5F007C mov [k0+7Ch],ra
  1549FFFB jne  r10,r9,@@copy_lop             ;
  24420004 +add r2,4h                         ;/

Alternately, a bugged/nonfunctional homebrew variant (used by Hitmen's "minimum" demo):

  ;BUG1: 8bit "movb r6" should be 32bit "mov r6"
  ;BUG2: @@copy_lop should transfer 6 words (not 7 words)
  ;BUG3: and, asides, the minimum demo works only with PAL BIOS (not NTSC)
  0xxxxxxx call xxxxxxxxh               ;\B(56h) GetC0Table
  00000000 +nop                         ;/(mov r8,0B0h, jmp r8, +mov r9,56h)
  3C04xxxx mov  r4,xxxx0000h  ;\@@ori_data
  2484xxxx add  r4,xxxxh      ;/
  90460018 movb r6,[r2+06h*4] ;BUG1 ;exception_handler = C(06h)
  24870018 add  r7,r4,18h ;@@ori_end     ;\
  24C50028 add  r5,r6,28h ;C(06h)+28h    ;
  00A03021 mov  r6,r5                    ;                   @@ori_data:
          @@verify_lop:                  ;  80086520 AF410004 mov [k0+4h],r1
  8CA30000 mov  r3,[r5]                  ;  80086524 AF420008 mov [k0+8h],r2
  8C820000 mov  r2,[r4]                  ;  80086528 AF43000C mov [k0+0Ch],r3
  00000000 nop                           ;  8008652C AF5F007C mov [k0+7Ch],ra
  1462000C jne  r3,r2,@@verify_mismatch  ;  80086530 40037000 mov r3,cop0r14
  24840004 +add r4,4h                    ;  80086534 00000000 nop
  1487FFFA jne  r4,r7,@@verify_lop       ;                   @@ori_end:
  24A50004 +add r5,4h                    ;/
  00C02821 mov  r5,r6                    ;\                  @@new_data:
  3C04xxxx mov  r4,xxxx0000h ;\@@new_data;  80086538 AF410004 mov [k0+4h],r1
  2484xxxx add  r4,xxxxh     ;/          ;  8008653C AF420008 mov [k0+8h],r2
  2483001C add  r3,r4,1Ch ;@@bugged_end  ;  80086540 40026800 mov r2,cop0r13
          @@copy_lop:                    ;  80086544 AF43000C mov [k0+0Ch],r3
  8C820000 mov  r2,[r4]                  ;  80086548 40037000 mov r3,cop0r14
  24840004 add  r4,4h                    ;  8008654C AF5F007C mov [k0+7Ch],ra
  ACA20000 mov  [r5],r2                  ;                   @@new_end:
  1483FFFC jne  r4,r3,@@copy_lop         ;  80086550 00000000 nop  ;BUG2
  24A50004 +add r5,4h                    ;/                  @@bugged_end:


Because of a hardware glitch the card IRQ cannot be acknowledged while the external IRQ signal is still LOW, making it neccessary to insert a delay that waits until the signal gets HIGH before acknowledging the IRQ.
The original BIOS is so inefficient that it takes hundreds of clock cycles between the interrupt request and the IRQ acknowledge, so, normally, it doesn't require an additional delay.
However, the central mistake in the IRQ handler is that it doesn't memorize which IRQ has originally triggered the interrupt. For example, it may get triggered by a timer IRQ, but a newer card IRQ may occur during IRQ handling, in that case, the card IRQ may get processed and acknowledged without the required delay.
Used in Metal Gear Solid at 8009AA5Ch, and in alone1 at 800AE2F8h:

  24090056 mov  r9,56h    ;\                  ;        @@new_data:
  240A00B0 mov  r10,0B0h  ; B(56h) GetC0Table ;3C02A001 lui  r2,0A001h
  0140F809 call r10       ;                   ;2442DFAC sub  r2,2054h
  00000000 +nop           ;/                  ;00400008 jmp  r2 ;=@@new_cont_d
  8C420018 mov  r2,[r2+06h*4] ;\get C(06h)    ;00000000 +nop    ;=A000DFACh
  00000000 nop                ;/              ;00000000 nop
  8C430070 mov  r3,[r2+70h]   ;\              ;        @@new_data_end:
  00000000 nop                ; get           ;        @@new_cont_d:
  3069FFFF and  r9,r3,0FFFFh  ; early_card    ;8C621074 mov  r2,[r3+1074h]
  00094C00 shl  r9,10h        ; irq_handler   ;00000000 nop
  8C430074 mov  r3,[r2+74h]   ;               ;30420080 and  r2,80h ;I_STAT.7
  00000000 nop                ;               ;1040000B jz   r2,@@ret
  306AFFFF and  r10,r3,0FFFFh ;/              ;00000000 +nop
  012A1821 add  r3,r9,r10                     ;        @@wait_lop:
  24620028 add  r2,r3,28h ;=early+28h         ;8C621044 mov  r2,[r3+1044h]
  3C0Axxxx lui  r10,xxxxh ;\@@new_data        ;00000000 nop
  254Axxxx sub  r10,xxxxh ;/                  ;30420080 and  r2,80h ;JOY_STAT.7
  3C09xxxx lui  r9,xxxxh  ;\@@new_data_end    ;1440FFFC jnz  r2,@@wait_lop
  2529xxxx sub  r9,xxxxh  ;/                  ;00000000 +nop
          @@copy_lop:                         ;3C020001 lui  r2,0001h
  8D430000 mov  r3,[r10]                      ;8C42DFFC mov  r2,[r2-2004h]
  00000000 nop                                ;00000000 nop
  AC430000 mov  [r2],r3                       ;00400008 jmp  r2 ;=[0000DFFCh]
  254A0004 add  r10,4h                        ;00000000 +nop
  1549FFFB jne  r10,r9,@@copy_lop             ;        @@ret:
  24420004 +add r2,4h                         ;03E00008 ret
  3C010001 lui  r1,0001h      ;\[DFFCh]=r2    ;00000000 +nop
  0xxxxxxx call xxxxxxxxh     ; and call ...  ;
  AC22DFFC +mov [r1-2004h],r2 ;/              ;

Alternately, elo2 uses slightly different code at 8003961Ch:

  240A00B0 mov  r10,0B0h  ;\                  ;        @@new_data:
  0140F809 call r10       ; B(56h) GetC0Table ;3C02xxxx lui  r2,8xxxh
  24090056 +mov r9,56h    ;/                  ;2442xxxx sub  r2,xxxxh
  8C420018 mov  r2,[r2+06h*4] ;\get C(06h)    ;00400008 jmp  r2 ;=@@new_cont_d
  00000000 nop                ;/              ;00000000 +nop    ;=8xxxxxxxh
  8C430070 mov  r3,[r2+70h]   ;\              ;00000000 nop
  00000000 nop                ; get           ;        @@new_data_end:
  3069FFFF and  r9,r3,0FFFFh  ; early_card    ;        @@new_cont_d:
  8C430074 mov  r3,[r2+74h]   ; irq_handler   ;8C621074 mov  r2,[r3+1074h]
  00094C00 shl  r9,10h        ;               ;00000000 nop
  306AFFFF and  r10,r3,0FFFFh ;               ;30420080 and  r2,80h ;I_STAT.7
  012A1821 add  r3,r9,r10     ;/              ;1040000B jz   r2,@@ret
  3C0Axxxx mov  r10,xxxx0000h                 ;00000000 +nop
  3C09xxxx mov  r9,xxxx0000h                  ;        @@wait_lop:
  24620028 add  r2,r3,28h ;=early+28h         ;8C621044 mov  r2,[r3+1044h]
  254Axxxx sub  r10,xxxxh ;=@@new_data        ;00000000 nop
  2529xxxx sub  r9,xxxxh  ;=@@new_data_end    ;30420080 and  r2,80h ;JOY_STAT.7
          @@copy_lop:                         ;1440FFFC jnz  r2,@@wait_lop
  8D430000 mov  r3,[r10]                      ;00000000 +nop
  254A0004 add  r10,4h                        ;3C02xxxx lui  r2,8xxxh
  24420004 add  r2,4h                         ;8C42xxxx mov  r2,[r2-xxxxh]
  1549FFFC jne  r10,r9,@@copy_lop             ;00000000 nop
  AC43FFFC +mov [r2-4h],r3                    ;00400008 jmp  r2 ;=[8xxxxxxxh]
  3C018xxx mov  r1,8xxx0000h  ;\[...]=r2,     ;00000000 +nop
  0xxxxxxx call xxxxxxxxh     ; and call ...  ;        @@ret:
  AC22xxxx +mov [r1+xxxxh],r2 ;/              ;03E00008 ret
           ...                                ;00000000 +nop

Note: The above @@wait_lop's should be more preferably done with timeouts (else they may hang endless if a Sony Mouse is newly connected; the mouse does have /ACK stuck LOW on power-up).


Used to uninstall the "early_card_irq_vector" (the BIOS installs that vector from inside of B(4Ah) InitCARD2(pad_enable), and, without patches, the BIOS doesn't allow to uninstall it thereafter).
Used in Breath of Fire III (SLES-01304) at 8017E790, and also in Ace Combat 2 (SLUS-00404) at 801D23F4:

  240A00B0 mov  r10,0B0h          ;\
  0140F809 call r10               ; B(56h) GetC0Table
  24090056 +mov r9,56h            ;/
  3C0Axxxx mov  r10,xxxx0000h
  3C09xxxx mov  r9,xxxx0000h
  8C420018 mov  r2,[r2+06h*4] ;=00000C80h = exception_handler = C(06h)
  254Axxxx add  r10,xxxxh ;@@new_data
  2529xxxx add  r9,xxxxh  ;@@new_data_end
          @@copy_lop:             ;\  @@new_data:
  8D430000 mov  r3,[r10]          ;    00000000 nop
  254A0004 add  r10,4h            ;    00000000 nop
  24420004 add  r2,4h             ;    00000000 nop
  1549FFFC jne  r10,r9,@@copy_lop ;   @@new_data_end:
  AC43006C +mov [r2+70h-4],r3     ;/

Alternately, more inefficient, used in Blaster Master-Blasting Again (SLUS-01031) at 80063FF4h, and Raiden DX at 80029694h:

  24090056 mov  r9,56h            ;\
  240A00B0 mov  r10,0B0h          ; B(56h) GetC0Table
  0140F809 call r10               ;
  00000000 +nop                   ;/
  8C420018 mov  r2,[r2+06h*4] ;=00000C80h = exception_handler = C(06h)
  3C0Axxxx mov  r10,xxxx0000h ;\@@new_data
  254Axxxx add  r10,xxxxh     ;/
  3C09xxxx mov  r9,xxxx0000h  ;\@@new_data_end
  2529xxxx add  r9,xxxxh      ;/
          @@copy_lop:             ;\
  8D430000 mov  r3,[r10]          ;   @@new_data:
  00000000 nop                    ;    00000000 nop
  AC430070 mov  [r2+70h],r3       ;    00000000 nop
  254A0004 add  r10,4h  ;src      ;    00000000 nop
  1549FFFB jne  r10,r9,@@copy_lop ;   @@new_data_end:
  24420004 +add r2,4h   ;dst      ;/

Note: the above code is same as "patch_install_lightgun_irq_handler", except that it writes to r2+70h, instead of r2+80h.


Same purpose as the "early_card_irq_patch" (but for the command/status bytes rather than for the data bytes). The patch looks buggy since it inserts the delay AFTER the acknowledge, but it DOES work (the BIOS accidently acknowledges the IRQ twice; and the delay occurs PRIOR to 2nd acknowledge).
Used in Metal Gear Solid at 8009AAF0h, and in Legacy of Kain at 801A56D8h, and in alone1 at 800AE38Ch:

  24090057 mov  r9,57h   ;\                   ;         @@new_data:
  240A00B0 mov  r10,0B0h ; B(57h) GetB0Table  ; 3C08A001 lui  r8,0A001h
  0140F809 call r10      ;/                   ; 2508DF80 sub  r8,2080h
  00000000 +nop                               ; 0100F809 call r8 ;=A000DF80h
  8C42016C mov  r2,[r2+5Bh*4] ;B(5Bh)         ; 00000000 +nop
  00000000 nop                                ; 00000000 nop
  8C4309C8 mov  r3,[r2+9C8h]  ;blah           ;         @@new_data_end:
  3C0Axxxx lui  r10,xxxxh ;\@@new_data        ; 946F000A movh r15,[r3+0Ah]
  254Axxxx sub  r10,xxxxh ;/                  ; 3C080000 mov  r8,0h
  3C09xxxx lui  r9,xxxxh  ;\@@new_data_end    ; 01E2C025 or   r24,r15,r2
  2529xxxx sub  r9,xxxxh  ;/                  ; 37190012 or   r25,r24,12h
          @@copy_lop:                         ; A479000A movh [r3+0Ah],r25
  8D480000 mov  r8,[r10]                      ; 24080028 mov  r8,28h
  00000000 nop                                ;         @@wait_lop:
  AC4809C8 mov  [r2+9C8h],r8   ;B(5Bh)+9C8h.. ; 2508FFFF sub  r8,1h
  254A0004 add  r10,4h                        ; 1500FFFE jnz  r8,@@wait_lop
  1549FFFB jne  r10,r9,@@copy_lop             ; 00000000 +nop
  24420004 +add r2,4h                         ; 03E00008 ret  ;above delay is
           ...                                ; 00000000 +nop ;in UNCACHED RAM

Alternately, slightly different code used in elo2 at800396D4h, and in Resident Evil 2 at 800910E4h:

  240A00B0 mov  r10,0B0h ;\                   ;         @@swap_begin:
  0140F809 call r10      ; B(57h) GetB0Table  ; 3C088xxx lui  r8,8xxxh
  24090057 +mov r9,57h   ;/                   ; 2508xxxx sub  r8,xxxxh
  8C42016C mov  r2,[r2+5Bh*4] ;B(5Bh)         ; 0100F809 call r8 ;=8xxxxxxxh
  3C0Axxxx mov  r10,xxxx0000h                 ; 00000000 +nop
  3C09xxxx mov  r9,xxxx0000h                  ; 00000000 nop
  8C4309C8 mov  r3,[r2+9C8h] ;blah            ;         @@swap_end:
  254Axxxx sub  r10,xxxxh  ;=@@swap_begin     ;         ;- -  -
  2529xxxx sub  r9,xxxxh   ;=@@swap_end       ; 00000000 nop
          @@swap_lop:                         ; 240800C8 mov  r8,0C8h
  8C4309C8 mov  r3,[r2+9C8h] ;B(5Bh)+9C8h..   ;         @@wait_lop:
  8D480000 mov  r8,[r10]                      ; 2508FFFF sub  r8,1h
  254A0004 add  r10,4h                        ; 1500FFFE jnz  r8,@@wait_lop
  AD43FFFC mov  [r10-4h],r3                   ; 00000000 +nop
  24420004 add  r2,4h                         ; 03E00008 ret  ;above delay is
  1549FFFA jne  r10,r9,@@swap_lop             ; 00000000 +nop ;in CACHED RAM
  AC4809C4 +mov [r2+9C4h],r8                  ;


The "card_info" function sends an incomplete read command to the card; in order to receive status information. After receiving the last byte, the function does accidently send a further byte to the card, so the card responds by another byte (and another IRQ7), which is not processed nor acknowledged by the BIOS. This patch kills the opcode that sends the extra byte.
Used in alone1 at 800AE214h:

  24090057 mov  r9,57h                        ;\
  240A00B0 mov  r10,0B0h                      ; B(57h) GetB0Table
  0140F809 call r10                           ;
  00000000 +nop                               ;/
  240A0009 mov  r10,9h        ;=blah
  8C42016C mov  r2,[r2+5Bh*4] ;=B(5Bh)
  00000000 nop
  20431988 addt r3,r2,1988h   ;=B(5Bh)+1988h  ;\store a NOP,
  0xxxxxxx call xxxxxxxxh                     ; and call ...
  AC600000 +mov [r3],0        ;=nop           ;/


If a transmission error occurs (or if there's no controller connected), then the Pad handler handler does usually issue a strange chip select signal to the OTHER controller slot, and does then execute the bizarre_pad_delay function. The patch below overwrites that behaviour by NOPs. Purpose of the original (and patched) behaviour is unknown.
Used by Perfect Assassin at 800519D4h:

  240A00B0 mov  r10,0B0h                      ;\
  0140F809 call r10                           ; B(57h) GetB0Table
  24090057 +mov r9,57h                        ;/
  8C42016C mov  r2,[r2+5Bh*4] ;=B(5Bh)
  3C01xxxx mov  r1,xxxx0000h
  20430884 addt r3,r2,884h    ;B(5Bh)+884h
  AC23xxxx mov  [r1+xxxxh],r3 ;<--- SetPadEnableFlag()
  3C01xxxx mov  r1,xxxx0000h
  20430894 addt r3,r2,894h    ;B(5Bh)+894h
  2409000B mov  r9,0Bh        ;len
  AC23xxxx mov  [r1+xxxxh],r3 ;<--- ClearPadEnableFlag()
          @@fill_lop:                         ;\
  2529FFFF sub  r9,1h                         ;
  AC400594 mov  [r2+594h],0   ;B(5Bh)+594h..  ; erase error handling
  1520FFFD jnz  r9,@@fill_lop                 ;
  24420004 +add r2,4h                         ;/

Alternately, same as above, but with inefficient nops, used by Sporting Clays at 8001B4B4h:

  24090057 mov  r9,57h       ;\
  240A00B0 mov  r10,0B0h     ; B(57h) GetB0Table
  0140F809 call r10          ;
  00000000 +nop              ;/
  8C42016C mov  r2,[r2+5Bh*4]
  2409000B mov  r9,0Bh ;len
  20430884 addt r3,r2,884h
  3C01xxxx mov  r1,xxxx0000h
  AC23xxxx mov  [r1+xxxxh],r3 ;<--- SetPadEnableFlag()
  20430894 addt r3,r2,894h
  3C01xxxx mov  r1,xxxx0000h
  AC23xxxx mov  [r1+xxxxh],r3 ;<--- ClearPadEnableFlag()
          @@fill_lop:         ;\
  AC400594 mov  [r2+594h],0   ;
  24420004 add  r2,4h         ; erase error handling
  2529FFFF sub  r9,1h         ;
  1520FFFC jnz  r9,@@fill_lop ;
  00000000 +nop               ;/

Alternately, same as above, but without getting PadEnable functions, used in Pandemonium II (at 80083C94h and at 8010B77Ch):

  240A00B0 mov  r10,0B0h              ;\
  0140F809 call r10                   ; B(57h) GetB0Table
  24090057 +mov r9,57h                ;/
  8C42016C mov  r2,[r2+5Bh*4] ;=B(5Bh)
  2409000B mov  r9,0Bh        ;len            ;\
          @@fill_lop:                         ;
  2529FFFF sub  r9,1h                         ; erase error handling
  AC400594 mov  [r2+594h],0   ;B(5Bh)+594h..  ;
  1520FFFD jnz  r9,@@fill_lop                 ;
  24420004 +add r2,4h                         ;/


The normal BIOS functions are only allowing to READ from the controllers, but not to SEND data to them (which would be required to control Rumble motors, and to auto-activate Analog mode without needing the user to press the Analog button). Internally, the BIOS does include some code for sending data to the controller, but it doesn't offer a function vector for setting up the data source address, and, even if that would be supported, it clips the data bytes to 00h or 01h. The patch below retrieves the required SetPadOutput function address (in which only the src1/src2 addresses are relevant, the blah1/blah2 values aren't used), and suppresses clipping (ie. allows to send any bytes in range 00h..FFh).
Used in Resident Evil 2 at 80091914h:

  240A00B0 mov  r10,0B0h                      ;\
  0140F809 call r10                           ; B(57h) GetB0Table
  24090057 +mov r9,57h                        ;/
  8C42016C mov  r2,[r2+5Bh*4] ;B(5Bh)
  3C0Axxxx mov  r10,xxxx0000h
  3C09xxxx mov  r9,xxxx0000h
  3C01xxxx mov  r1,xxxx0000h
  204307A0 addt r3,r2,7A0h    ;B(5Bh)+7A0h
  254Axxxx add  r10,xxxxh  ;=@@new_data
  2529xxxx add  r9,xxxxh   ;=@@new_data_end
  AC23xxxx mov  [r1-xxxxh],r3 ;<--- SetPadOutput(src1,blah1,src2,blah2)
          @@double_copy_lop:                  ;\
  8D430000 mov  r3,[r10]                      ;           @@new_data:
  254A0004 add  r10,4h                        ;   00551024 and     r2,r21
  AC4303D8 mov  [r2+3D8h],r3  ;<--- here      ;   00000000 nop
  24420004 add  r2,4h                         ;   00000000 nop
  1549FFFB jne  r10,r9,@@double_copy_lop      ;   00000000 nop
  AC4304DC +mov [r2+4DCh],r3  ;<--- here      ;/          @@new_data_end:

Alternately, more inefficient (with NOPs), used in Lemmings at 80036618h:

  24090057 mov  r9,57h                        ;\
  240A00B0 mov  r10,0B0h                      ; B(57h) GetB0Table
  0140F809 call r10                           ;
  00000000 +nop                               ;/
  3C0Axxxx mov  r10,xxxx0000h
  254Axxxx add  r10,xxxxh    ;=@@new_data
  3C09xxxx movp r9,xxxx0000h
  2529xxxx add  r9,xxxxh     ;=@@new_data_end
  8C42016C mov  r2,[r2+5Bh*4] ;B(5Bh)
  00000000 nop
  204307A0 addt r3,r2,7A0h    ;B(5Bh)+7A0h
  3C01xxxx mov  r1,xxxx0000h
  AC23xxxx mov  [r1+xxxxh],r3 ;<--- SetPadOutput(src1,blah1,src2,blah2)
          @@double_copy_lop:                  ;\
  8D430000 mov  r3,[r10]                      ;           @@new_data:
  00000000 nop                                ;   00551024 and     r2,r21
  AC4303D8 mov  [r2+3D8h],r3                  ;   00000000 nop
  AC4304E0 mov  [r2+4E0h],r3                  ;   00000000 nop
  24420004 add  r2,4h                         ;   00000000 nop
  254A0004 add  r10,4h                        ;           @@new_data_end:
  1549FFF9 jne  r10,r9,@@double_copy_lop      ;
  00000000 +nop                               ;/


This patch suppresses automatic IRQ0 (vblank) acknowleding in the Pad/Card IRQ handler, that, even if auto-ack is enabled. Obviously, one could as well disable auto-ack via B(5Bh) ChangeClearPAD(int), so this patch is total nonsense. Used in Resident Evil 2 at 800919ACh:

  240A00B0 mov   r10,0B0h                      ;\
  0140F809 call  r10                           ; B(57h) GetB0Table
  24090057 +mov  r9,57h                        ;/
  8C42016C mov   r2,[r2+5Bh*4] ;=B(5Bh)
  240A0009 mov   r10,9h        ;len            ;\
  2043062C addt  r3,r2,62Ch    ;=B(5Bh)+62Ch   ;
          @@fill_lop:                          ;
  254AFFFF sub   r10,1h                        ;
  AC600000 mov   [r3],0                        ;
  1540FFFD jnz   r10,@@fill_lop                ;
  24630004 +add  r3,4h                         ;/

Alternately, same as above, but more inefficient, used in Sporting Clays at 8001B53Ch:

  24090057 mov   r9,57h                        ;\
  240A00B0 mov   r10,0B0h                      ; B(57h) GetB0Table
  0140F809 call  r10                           ;
  00000000 +nop                                ;/
  240A0009 mov   r10,9h    ;len
  8C42016C mov   r2,[r2+5Bh*4]
  00000000 nop
  2043062C addt  r3,r2,62Ch
          @@fill_lop:                          ;\
  AC600000 mov   [r3],0                        ;
  24630004 add   r3,4h                         ;
  254AFFFF sub   r10,1h                        ;
  1540FFFC jnz   r10,@@fill_lop                ;
  00000000 +nop                                ;/

Either way, no matter if using the patch or if using ChangeClearPAD(int), having auto-ack disabled allows to install a custom vblank IRQ0 handler, which is probably desired for most games, however, mind that the PSX BIOS doesn't actually support the same IRQ to be processed by two different IRQ handlers, eg. the custom handler may acknowledge the IRQ even when the Pad/Card handler didn't process it, so pad input may become bumpy.


Used in Sporting Clays at 80027D68h (when Konami Lightgun connected):

  240A00B0 mov  r10,0B0h     ;\
  0140F809 call r10          ; B(56h) GetC0Table
  24090056 +mov r9,56h       ;/
  3C0Axxxx mov  r10,xxxx0000h ;src
  3C09xxxx mov  r9,xxxx0000h  ;src.end
  8C420018 mov  r2,[r2+06h*4] ;C(06h)
  254Axxxx add  r10,xxxxh     ;src
  2529xxxx add  r9,xxxxh      ;src.end (=src+10h)
          @@copy_lop:              ;\    ;        @@src:
  8D430000 mov  r3,[r10]           ;     ;3C02xxxx mov  r2,xxxx0000h
  254A0004 add  r10,4h             ;     ;2442xxxx add  r2,xxxxh
  24420004 add  r2,4h              ;     ;0040F809 call r2  ;lightgun_proc
  1549FFFC jne  r10,r9,@@copy_lop  ;     ;00000000 +nop
  AC43007C +mov [r2+80h-4],r3      ;/             @@src_end:

Alternately, same as above, but more inefficient, used in DQM (Dragon Quest Monsters 1&2) at 80089390h (install) and 800893F8h (uninstall):

  24090056 mov  r9,56h        ;\
  240A00B0 mov  r10,0B0h      ; B(56h) GetC0Table
  0140F809 call r10           ;
  00000000 +nop               ;/
  8C420018 mov  r2,[r2+06h*4] ;=00000C80h = exception_handler = C(06h)
  3C0Axxxx mov  r10,xxxx0000h ;\@@new_data (3xNOP)
  254Axxxx add  r10,-xxxxh    ;/
  3C09xxxx mov  r9,xxxx0000h  ;\@@new_data_end
  2529xxxx add  r9,-xxxxh     ;/
          @@copy_lop:             ;\
  8D430000 mov  r3,[r10]          ; @@new_data: ;for (un-)install...
  00000000 nop                    ; 00000000 nop / 3C02xxxx mov r2,xxxx0000h
  AC430080 mov  [r2+80h],r3       ; 00000000 nop / 2442xxxx add r2,-xxxxh
  254A0004 add  r10,4h            ; 00000000 nop / 0040F809 call r2  ;proc
  1549FFFB jne  r10,r9,@@copy_lop ; @@new_data_end:
  24420004 +add r2,4h             ;/

Some lightgun games (eg. Project Horned Owl) do (additionally to above stuff) hook the exception vector at 00000080h, the hook copies the horizontal coordinate (timer0) to a variable in RAM, thus getting the timer0 value "closest" to the actual IRQ execution. Doing that may eliminate some unpredictable timing offsets that could be caused by cache hits/misses during later IRQ handling (and may also eliminate a rather irrelevant 1-cycle inaccuracy depending on whether EPC was pointing to a GTE opcode, and also eliminates constant cycle offsets depending on whether early_card_irq_handler was installed and enabled, and might eliminate timing differences for different BIOS versions).


Used in Spec Ops Airborne Commando at 80070AE8h, and also in the homebrew game Roll Boss Rush at 80010B68h and 8001B85Ch. Purpose is unknown (maybe to override improperly defined .EXE headers).

  8C030474 mov   r3,[200h+(9Dh*4)]      ;\get ptr to A(9Dh) GetConf (done so,
  00000000 nop                          ;/as there's no "GetA0Table" funtion)
  94620000 movh  r2,[r3+0h] ;lui msw    ;\
  84630004 movhs r3,[r3+4h] ;lw lsw+8   ; extract ptr to "boot_cnf_values"
  00021400 shl   r2,10h     ;msw*10000h ; (from first 2 opcodes of GetConf)
  2442FFF8 sub   r2,8h      ;undo +8    ;
  00431021 add   r2,r3      ;lsw        ;/
  AC450000 mov   [r2+0h],r5 ;num_TCB    ;\set num_EvCB,num_TCB,stacktop
  AC440004 mov   [r2+4h],r4 ;num_EvCB   ; (unlike A(9Ch) SetConf, without
  03E00008 ret                          ; actually reallocting anything)
  AC460008 +mov  [r2+8h],r6 ;stacktop   ;/

Cheat Devices

CAETLA detects the PSX BIOS version by checksumming BFC06000h..BFC07FFFh and does then use some hardcoded BIOS addresses based on that checksum. The reason for doing that is probably that the Pre-Boot Expansion ROM vector is called with the normal A0h/B0h/C0h vectors being still uninitialized.
Problems are that the hardcoded addresses won't work with all BIOSes (eg. not with the no$psx bios clone, probably also not with the newer PS2 BIOS), moreover, the checksumming can fail with patched original BIOSes (eg. no$psx allows to enable TTY debug messages and to skip the BIOS intro).
The Cheat Firmwares are probably also hooking the Vblank handler, and maybe also some other functions.
ACTION REPLAY (at least later versions like 2.81) uses the Pre-Boot handler to set a COP0 hardware breakpoint at 80030000h and does then resume normal BIOS booting (which will then initialize important things like A0h/B0h/C0h tables, and will then break when starting the GUI code at 80030000h).
XPLORER searches opcode 24040385h at BFC06000h and up, and does then place a COP0 opcode fetch breakpoint at the opcode address+10h (note: this is within a branch delay slot, which makes COP0 emulation twice as complicated). XPLORER does also require space in unused BIOS RAM addresses (eg. Xplorer v3.20: addr 7880h at 1F002280h, addr 017Fh at 1F006A58h).


Most games include two or three patches. The only game that I've seen so far that does NOT use any patches is Wipeout 2097.