The F8 has no internal stack for the program counter, so you must be careful when calling subroutines. Using PI/POP only works for one level of subroutines, because the return address for the first PI opcode will be overwritten by subsequent PI opcodes. Here's a single-level example:
prog: ; ...code pi sub ; Pushes address of next instruction to PC1 ; address of sub is stored in PC0 (jump to subroutine) ; ...code continues
sub: ; ... often used code pop ; Move return address from PC1 to PC0
To have 2 levels of subroutines, you can use the K register to save the first return address:
prog: ; ...do something... pi sub1 ; Address of next instruction stored in PC1 ; sub1 is stored in PC0 (jump to subroutine) ; ...do more...
sub1: lr k,p ; Copy PC1 to K, original jump address to K ; ...do something... pi sub2 ; Pushes address of next instruction to PC1 ; sub1 is stored in PC0 (jump to subroutine) ; ...do more... pk ; Store address of next instruction in PC1 ; Copy value in K to PC0 (jump back to main)
sub2: ; ...do something... pop ; Move return address from PC1 to PC0
That's as deep as the processor allows you to go without writing additional code to save return addresses. In the Channel F BIOS, there are routines which create a simulated stack for the K register. The routine at $0107 (known as PUSHK or CALL) can push K to the stack and the routine at $011E (known as POPK or RTRN) can pop K from the stack. For example:
prog: ; ...do something... pi sub1 ; ...do more...
sub1: lr k,p pi PUSHK ; ...do something... pi sub2 ; ...do more... pi POPK pk
sub2: lr k,p pi PUSHK ; ...do something... pi sub3 ; ...do more... pi POPK pk
sub3: ; ...do something... pop
By using PUSHK/POPK, you can have more than 2 levels of subroutine calls. However, a lot of overhead is added to the code by manipulating the stack. When inside a pushk/popk subroutine it's still possible to use plain pi/pop as it only affects PC0 and PC1. Whenever calling a subroutine one level deep, it's best to use the PI/POP combination; for two levels of subroutines, it's best to use the second example above. If using the K register in a subroutine only simple PI/POP is usable to get there, not to destroy contents of K.
;==================; ; Register K Stack ; ;==================; ; the K stack is an emulated stack using the register r59 ; as a stack pointer, which holds the register number for ; the top of the stack, which first points at r40. when ; pushk is called, K is pushed to the first two registers ; on the stack, and the pointer is increased. ; ; the K stack can hold 9 copies of K (r40-58) before ; the stack pointer itself is overwritten (r59) ;--------; ; Push K ; ;--------; ; pushes register K (r12-13) onto a stack using r59 as ; the stack pointer ; ; modifies: r7 pushk: ; backup the ISAR lr A, IS lr 7, A ; get the top of the stack lisu 7 lisl 3 ; r59, stack pointer lr A, S lr IS, A ; load the referenced register ; push K onto the stack lr A, Ku lr S, A ; push high byte of K lr A, IS inc lr IS, A ; increase ISAR (they could've used lr I, A) lr A, Kl lr S, A ; push low byte of K ; adjust pointer to the top of the stack lr A, IS inc lr IS, A ; increase ISAR to the top of the stack lr A, IS ; redundantly, get back the register number lisu 7 lisl 3 lr S, A ; save the register number to the stack pointer ; restore the ISAR lr A, 7 lr IS, A ; return from the subroutine pop ;--------; ; Pop K ; ;--------; ; retrieves a 16-bit value from the K stack and ; stores it in K, using r59 as the stack pointer ; ; modifies: r7 popk: ; backup the ISAR lr A, IS lr 7, A ; retrieve K from the stack lisu 7 lisl 3 lr A, S ; load the stack pointer ai $ff ; "subtract" 1 to get the first register on the stack lr IS, A ; set the ISAR to this register lr A, S lr Kl, A ; load lower byte of K lr A, IS ai $ff ; get previous register (they could've used lr A, D) lr IS, A lr A, S lr Ku, A ; load upper byte of K ; adjust pointer to the top of the stack lr A, IS lisu 7 lisl 3 lr S, A ; save the register number to the stack pointer ; restore the ISAR lr A, 7 lr IS, A ; return from the subroutine pop ;===================;
Also consider using macros- you have a lot more code space than the original Channel F programmers, so you might as well use it; the time you save can be considerable.
Blackbird is writing more efficient versions of PUSHK/POPK (Snippet:KStack). Another idea is to write a version that uses the Schach RAM at $2800 that MESS emulates. That would free up more scratchpad registers and possibly also be quicker.
Here's a trick from the Guide: if a subroutine will be called frequently, it's quicker to load its address into the K register and call it using PK than to use PI multiple times. You'll use 4.5 cycles instead of 6.5 cycles to do the same thing.
It's also possible to change the Program Counter (PC0) with "lr P,Q" but there's no opcode for the other direction, address could be copied from A to Q in two steps or K to Q in four steps via Accumulator one byte at the time.