Bankswitching
The sorbus computer only provides a ROM area of $E000-$FFFF, so bankswitching is required to provide more than 8kB. OSI BASIC alone requires >7.5kB. So bankswitching is required to access more than just the 8kB of ROM banked into memory.
The bankswitching is implemented in the same fashion as on the first bankswitching cartridges for the Atari 2600 VCS: the whole memory block is switched (on the Sorbus by writing a value to a specific register, see below).
The BIOS Region
The last page of memory ($FF00-$FFFF) has a special function: this is identical in every bank (or at least similar in specific cases like the NMOS 6502 monitor). There also is a function (BRK based) provided to copy the data from ROM to RAM, so kernel functionality can be executed even from RAM.
Bankswitching Register
Bankswitching is implemented using a simple register @ $DF00, with the following values:
- $00: switch area to RAM (required for CP/M, helpful for other things)
- $01: main kernel bank with BRK handler, etc.
- every valid bank >$02: helper function, BASIC, etc.
- invalid banks will switch to bank $01
Overview
| Method | From Bank | To Bank | Note |
|---|---|---|---|
| BRK | any + RAM | kernel($01) | RAM only works when BIOS is copied to RAM |
| bankstart | kernel($01) | any | starts payload at $E000, e.g. BASIC |
| bankjsr | kernel($01) | tools($02) | very transparent design, expandable to other banks |
| bankjmp | any | any | very quirky in usage, cannot be used for $E000 |
| bootblock | kernel($01) | RAM | used for CP/M and other bootblocks |
So, every method has been taylored to a specific usecase. bankjsr and
bankjmp require a jumptable at the beginning of the destination bank,
because it's not possible to hand labels between kernel banks.
Bankswitching Variant 1: BRK Software Interrupts
The kernel uses the software interrupt BRK with an operand to trigger kernel functions. The code that handles that is non-trivial but allows for more freedom within the development of the kernel, as things can be moved around with less implications.
Using the function to wait for a keypress and return it converted to uppercase runs like this:
brk #$01 ; typically written as "int CHRINUC" in source
; read IRQ vector jumping to
jmp (UVIRQ) ; User Vector for IRQ ($DF78) normally to IRQCHECK
IRQCHECK:
sta BRK_SA ; let's figure out the source of the IRQ
pla ; get processor status from stack
pha ; and put it back
and #$10 ; check for BRK bit
bne _isbrk
lda BRK_SA ; restore saved accumulator
jmp (UVNBI) ; user vector for non-BRK IRQ ($DF7C)
_isbrk:
lda BANK ; get current bank
sta BRK_SB ; save it
stx BRK_SX
sty BRK_SY
tsx
lda $0103,x ; offset of current stack pointer is 1+2
sta TMP16+1 ; at offset 0 is processor status (got called via BRK)
lda $0102,x ; get the return address from stack
sta TMP16+0 ; and write it to temp vector
bne :+
dec TMP16+1
:
dec TMP16+0
lda (TMP16) ; finally get BRK operand!
; needs to be read without bank change
sta ASAVE ; misuse asave to store BRK operand for user handler
ldx #$01 ; will be restored by brkjump
stx BANK ; switch to first ROM bank
jsr brkjump ; to call the subroutine selector
brkjump:
cmp #jumptablesize
bcc @okay ; sanity check: if BRK operand out of scope
lda #$00 ; reset, user BRK can lda (TMP16) to get BRK operand
@okay:
asl ; BRK >=$80 will always be =$00
tax
lda @jumptable+1,x ; get address from jump table
pha
lda @jumptable+0,x
pha
tsx
lda $0105,x ; get processor state from stack
pha ; and save it for rti below
ldx BRK_SX ; get stored X
lda BRK_SA ; get stored A
rti ; jump to handling subroutine
; here's the routine to be called
chrinuc:
; wait for character from UART and make it uppercase
jsr CHRIN
bcs chrinuc
jsr uppercase
sta BRK_SA ; when called via BRK, this is required
rts
; now return from brkjump
php
ldy BRK_SY ; get stored Y
pla
sta BRK_SY ; use stored Y now for storing P
ldx BRK_SX ; get stored X
lda BRK_SB ; get stored BANK
sta BANK ; restore saved bank
pla ; drop old P from stack
lda BRK_SY ; get P from store
pha ; put it on the stack, so flags can be returned
lda BRK_SA ; get stored accumulator
rti ; return to calling code
As an interrupt is intended to be handled without changing the registers, this is here the case as well (except for processor status). If an interrupt want to "return" a value, this has be written specifically into the memory addresses where those values are stored: BRK_SA, BRK_SX and BRK_SY.
This also shows that BRKs cannot be nested. However they are used in other banks than the kernel to call functions within the kernel bank.
Bankswitching Variant 2: Core Functionality
This is very blunt. The assumption is that at every bank at $E000 some basic function is provided:
- $01: kernel reset
- $02: handler for 65816 native interrupts
- $03: OSI BASIC
So, a short routine in the BIOS is implemented for jumping to a specific bank. This includes extra code to switch 65816 to 8-bit mode, so the kernel routines for native interrupts and reset are guaranteed to work. (Even when a softreset via jmp ($fffc) is performed in native mode.)
_unhandled:
sep #$30 ; set MX to 8-bit mode, like 65C02
; 65(S)C02 will execute: NOP #$30
; 65CE02 will execute LDA ($30,S),Y
ldx #UNH65816_BANK
bra bankstart
_reset: ; $FFFC points here
sep #$30 ; set MX to 8-bit mode, like 65C02
; 65(S)C02 will execute: NOP #$30
; 65CE02 will execute LDA ($30,S),Y
ldx #KERNEL_BANK ; reset routine is per definition in kernel
bankstart:
stx BANK ; select bank from X
jmp $E000 ; jump to table index, table will be used top down
The accumulator is left untouched, just in case it is required to "pass something along". The "bankgotoE000" function can also be used to switch from any bank to any bank, with the exception that to bank $00 is not guarateed to work as that is RAM.
Running a bootblock uses a different approach: writing a small stub at the beginning of the stack ($0100) and jumping there. This also includes a small routine at @ $0100 directly which copies the BIOS from ROM to RAM.
Also a valid bootblock typically starts like this:
jmp realstart ; jump to read start
.byte "SBC23" ; magic value for a valid boot block
The jmp at the beginning is just a suggestion, any three byte code can go in there.
Bankswitching Variant 3: Kernel Jumps To Subroutine In Other Bank
Here the intention is different: code from bank $01 jumps into a subroutine into bank $02 and returns from there. It should be possible to expand this to other banks as well, but this is not implemented right now.
firstbankjsr:
subroutine1: ; bank 2: $E003
nop
subroutine2: ; bank 2: $E006
nop
subroutine3: ; bank 2: $E009
sta ASAVE
php
pla
; stack now contains: RETL RETH
sta PSAVE
phx
; stack now contains: X RETL RETH
; we want to get these: ^^^^ ^^^^
; RET points at "yy" the the byte sequence 20 xx yy
; however, we want "xx", so decrement by 1
tsx
lda $0103,x
sta TMP16+1
lda $0102,x
bne :+
dec TMP16+1
:
dec
sta TMP16+0
; now we know where the call originated from
lda (TMP16)
sec
sbc #(<firstbankjsr)-1 ; adjust for start of nopslide and JSR "offset"
; here some further evaluation could take place, if a jsr to a bank other
; than TOOLS_BANK is required
sta TMP16+0
asl ; A needs to be < $FF/3 = $55, so no clc needed
adc TMP16+0
dec
sta TMP16+0 ; TMP16+0 now holds $ff,$02,...
plx
; stack now contains: RETL RETH
lda #>(banksubret-1)
pha
lda #<(banksubret-1)
pha
; stack now contains: banksubretL banksubretH RETL RETH
lda #>ROMSTART
pha
lda TMP16+0
pha
; stack now contains: banksubL banksubH banksubretL banksubretH RETL RETH
lda PSAVE
pha
; stack now contains: P banksubL banksubH banksubretL banksubretH RETL RETH
phx
; stack now contains: X P banksubL banksubH banksubretL banksubretH RETL RETH
ldx #TOOLS_BANK
lda ASAVE
jmp banksubgo
; PC now at banksubgo
banksubgo:
stx BANK
plx
; stack now contains: P banksubL banksubH banksubretL banksubretH RETL RETH
plp
; stack now contains: banksubL banksubH banksubretL banksubretH RETL RETH
rts
; PC now at banksub
; stack now contains: banksubretL banksubretH RETL RETH
banksub:
; [... running subroutine ...]
rts
; PC now at banksubret
; stack now contains: RETL RETH
banksubret:
php
; stack now contains: P RETL RETH
phx
; stack now contains: X P RETL RETH
ldx #KERNEL_BANK
; called by kernel switch to bank for subroutine call
; php and pha are done there
banksubgo:
stx BANK
plx
; stack now contains: P RETL RETH
plp
; stack now contains: RETL RETH
rts
Similar to the BRK routine everything is preserved during bankswitching: all registers, as well as processor status. Just like it is expected from a "normal" JSR. But as there a few memory addresses are modified as part of the setup process. However, those can be used within the subroutines.
Bankswitching Variant 4: Kernel Jumps To Other Bank Without Return
This can be implemented by "hijacking" parts of the Variant 3. Basic idea is to reuse "banksubgo", a nice implementation is still work be done.
bankjmp:
; A=entrypoint: $E0xx : xx = A
; X=bank
; ASAVE=A on entry
; PSAVE=X on entry
php
; stack now contains: P
sta TMP16+0 ; saving A=entrypoint-1
pla
; stack now contains: (nothing)
sta TMP16+1 ; saving P
lda #>ROMSTART
pha
; stack now contains: JMPH
lda TMP16+0
dec ; adjusting for RTS
pha
; stack now contains: JMPL JMPH
lda TMP16+1 ; getting P
pha
; stack now contains: P JMPL JMPH
lda PSAVE ; getting X for function call
pha
; stack now contains: X P JMPL JMPH
lda ASAVE ; getting A for function call
jmp banksubgo
banksubgo:
stx BANK
plx
; stack now contains: P RETL RETH
plp
; stack now contains: RETL RETH
rts
The "banksubgo" part is the same code used in "banksubret" above.