573in1/src/ps1/system.s

291 lines
7.6 KiB
ArmAsm

# ps1-bare-metal - (C) 2023 spicyjpeg
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
.set noreorder
.set noat
## Exception handler
.set BADV, $8
.set SR, $12
.set CAUSE, $13
.set EPC, $14
.set COP0_CAUSE_EXC_BITMASK, 31 << 2
.set COP0_CAUSE_EXC_SYS, 8 << 2
.section .text._exceptionVector, "ax", @progbits
.global _exceptionVector
.type _exceptionVector, @function
_exceptionVector:
# This 16-byte stub is going to be relocated to the address the CPU jumps to
# when an exception occurs (0x80000080) at runtime, overriding the default
# one installed by the BIOS. We're going to fetch a pointer to the current
# thread, grab the EPC (i.e. the address of the instruction that was being
# executed before the exception occurred) and jump to the exception handler.
# NOTE: we can't use any registers other than $k0 and $k1 here, as doing so
# would destroy their contents and corrupt the current thread's state.
lw $k0, %gprel(currentThread)($gp)
j _exceptionHandler
mfc0 $k1, EPC
nop
.section .text._exceptionHandler, "ax", @progbits
.global _exceptionHandler
.type _exceptionHandler, @function
_exceptionHandler:
# Save the full state of the thread in order to make sure the interrupt
# handler callback (invoked later on) can use any register. The state of
# $hi/$lo is saved after all other registers in order to let the multiplier
# finish any ongoing calculation.
sw $at, 0x04($k0)
sw $v0, 0x08($k0)
sw $v1, 0x0c($k0)
sw $a0, 0x10($k0)
sw $a1, 0x14($k0)
sw $a2, 0x18($k0)
sw $a3, 0x1c($k0)
sw $t0, 0x20($k0)
sw $t1, 0x24($k0)
sw $t2, 0x28($k0)
sw $t3, 0x2c($k0)
sw $t4, 0x30($k0)
sw $t5, 0x34($k0)
sw $t6, 0x38($k0)
sw $t7, 0x3c($k0)
sw $s0, 0x40($k0)
sw $s1, 0x44($k0)
sw $s2, 0x48($k0)
sw $s3, 0x4c($k0)
sw $s4, 0x50($k0)
sw $s5, 0x54($k0)
sw $s6, 0x58($k0)
sw $s7, 0x5c($k0)
sw $t8, 0x60($k0)
sw $t9, 0x64($k0)
sw $gp, 0x68($k0)
sw $sp, 0x6c($k0)
sw $fp, 0x70($k0)
sw $ra, 0x74($k0)
mfhi $v0
mflo $v1
sw $v0, 0x78($k0)
sw $v1, 0x7c($k0)
# Check bits 2-6 of the CAUSE register to determine what triggered the
# exception. If it was caused by a syscall, increment EPC to make sure
# returning to the thread won't trigger another syscall.
mfc0 $v0, CAUSE
lw $v1, %gprel(interruptHandler)($gp)
# int code = CAUSE & COP0_CAUSE_EXC_BITMASK;
andi $v0, COP0_CAUSE_EXC_BITMASK
beqz $v0, .LisInterrupt
li $at, COP0_CAUSE_EXC_SYS
beq $v0, $at, .LisSyscall
lw $a0, %gprel(interruptHandlerArg0)($gp)
.LisOtherException: # if ((code != INT) && (code != SYS)) {
# If the exception was not triggered by a syscall nor by an interrupt call
# _unhandledException(), which will then display information about the
# exception and lock up.
sw $k1, 0x00($k0)
mfc0 $a1, BADV # _unhandledException((CAUSE >> 2) & 0x1f, BADV)
srl $a0, $v0, 2
jal _unhandledException
addiu $sp, -8
lw $k0, %gprel(nextThread)($gp)
b .Lreturn
addiu $sp, 8
.LisInterrupt: # } else {
# If the exception was caused by an interrupt, check if the interrupted
# instruction was a GTE opcode and increment EPC to avoid executing it again
# if that is the case. This is a workaround for a hardware bug.
# if ((code == INT) && ((*EPC >> 25) == 0x25)) EPC += 4;
lw $v0, 0($k1)
li $at, 0x25
srl $v0, 25
bne $v0, $at, .LskipEPCIncrement
lw $a0, %gprel(interruptHandlerArg0)($gp)
.LisSyscall:
# if (code == SYS) EPC += 4;
addiu $k1, 4
.LskipEPCIncrement:
# Save the modified EPC and invoke the interrupt handler, which will
# temporarily use the current thread's stack.
sw $k1, 0x00($k0)
# interruptHandler(interruptHandlerArg0, interruptHandlerArg1);
lw $a1, %gprel(interruptHandlerArg1)($gp)
jalr $v1
addiu $sp, -8
lw $k0, %gprel(nextThread)($gp)
addiu $sp, 8
.Lreturn: # }
# Grab a pointer to the next thread to be executed and restore its state.
# currentThread = nextThread;
sw $k0, %gprel(currentThread)($gp)
lw $v0, 0x78($k0)
lw $v1, 0x7c($k0)
mthi $v0
mtlo $v1
lw $k1, 0x00($k0)
lw $at, 0x04($k0)
lw $v0, 0x08($k0)
lw $v1, 0x0c($k0)
lw $a0, 0x10($k0)
lw $a1, 0x14($k0)
lw $a2, 0x18($k0)
lw $a3, 0x1c($k0)
lw $t0, 0x20($k0)
lw $t1, 0x24($k0)
lw $t2, 0x28($k0)
lw $t3, 0x2c($k0)
lw $t4, 0x30($k0)
lw $t5, 0x34($k0)
lw $t6, 0x38($k0)
lw $t7, 0x3c($k0)
lw $s0, 0x40($k0)
lw $s1, 0x44($k0)
lw $s2, 0x48($k0)
lw $s3, 0x4c($k0)
lw $s4, 0x50($k0)
lw $s5, 0x54($k0)
lw $s6, 0x58($k0)
lw $s7, 0x5c($k0)
lw $t8, 0x60($k0)
lw $t9, 0x64($k0)
lw $gp, 0x68($k0)
lw $sp, 0x6c($k0)
lw $fp, 0x70($k0)
lw $ra, 0x74($k0)
jr $k1
rfe
## Delay functions
.set IO_BASE, 0xbf801000
.set TIMER2_VALUE, IO_BASE | 0x120
.set TIMER2_CTRL, IO_BASE | 0x124
.set TIMER2_RELOAD, IO_BASE | 0x128
.section .text.delayMicroseconds, "ax", @progbits
.global delayMicroseconds
.type delayMicroseconds, @function
delayMicroseconds:
# Calculate the approximate number of CPU cycles that need to be burned,
# assuming a 33.8688 MHz clock (1 us = 33.8688 = ~33.875 cycles).
# cycles = ((us * 271) + 4) / 8;
sll $a1, $a0, 8
sll $a2, $a0, 4
addu $a1, $a2
subu $a1, $a0
addiu $a1, 4
sra $a0, $a1, 3
# Compensate for the overhead of calculating the cycle count, entering the
# loop and returning.
addiu $a0, -(6 + 1 + 2 + 4 + 2)
# TIMER2_CTRL = 0;
lui $v1, %hi(IO_BASE)
sh $0, %lo(TIMER2_CTRL)($v1)
# Wait for up to 0xff00 cycles at a time (resetting the timer and waiting
# for it to count up each time), as the counter is only 16 bits wide. We
# have to wait 0xff00 cycles rather than 0x10000 since the counter wraps
# around rather than saturating on overflow.
li $a1, 0xff00
slt $v0, $a1, $a0
beqz $v0, .LshortDelay
li $a2, 0xff00 + 3
.LlongDelay: # for (; cycles > 0xff00; cycles -= (0xff00 + 3)) {
# TIMER2_VALUE = 0;
sh $0, %lo(TIMER2_VALUE)($v1)
li $v0, 0
.LlongDelayLoop:
# while (TIMER2_VALUE < 0xff00);
nop
slt $v0, $v0, $a1
bnez $v0, .LlongDelayLoop
lhu $v0, %lo(TIMER2_VALUE)($v1)
slt $v0, $a1, $a0
bnez $v0, .LlongDelay
subu $a0, $a2
.LshortDelay: # }
# Run the last busy loop once less than 0xff00 cycles are remaining.
# TIMER2_VALUE = 0;
sh $0, %lo(TIMER2_VALUE)($v1)
li $v0, 0
.LshortDelayLoop:
# while (TIMER2_VALUE < cycles);
nop
slt $v0, $v0, $a0
bnez $v0, .LshortDelayLoop
lhu $v0, %lo(TIMER2_VALUE)($v1)
# return;
jr $ra
nop
.section .text.delayMicrosecondsBusy, "ax", @progbits
.global delayMicrosecondsBusy
.type delayMicrosecondsBusy, @function
delayMicrosecondsBusy:
# cycles = ((us * 271) + 4) / 8:
sll $a1, $a0, 8
sll $a2, $a0, 4
addu $a1, $a2
subu $a1, $a0
addiu $a1, 4
sra $a0, $a1, 3
# Compensate for the overhead of calculating the cycle count and returning.
addiu $a0, -(6 + 1 + 2)
.Lloop:
# while (cycles > 0) cycles -= 2;
bgtz $a0, .Lloop
addiu $a0, -2
# return;
jr $ra
nop