Difference between revisions of "Subroutines"
| (One intermediate revision by the same user not shown) | |||
| Line 64: | Line 64: | ||
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. | 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. | ||
| + | |||
| + | You can use the pushk ($0107) and popk ($011E) routines from BIOS or use these slightly faster versions: | ||
<pre> | <pre> | ||
| Line 88: | Line 90: | ||
; modifies: r7 | ; modifies: r7 | ||
| + | ; 26% faster than BIOS (6 cycles) | ||
pushk: | pushk: | ||
| − | + | lr A, IS ; backup ISAR in r7 | |
| − | + | lr 7,A | |
| − | + | lisu 7 ; get the top of the stack | |
| − | + | lisl 3 ; r59, stack pointer | |
| − | + | lr A,S | |
| − | + | lr IS, A ; load the referenced register | |
| − | + | lr A,KU | |
| − | + | lr I,A ; push high byte of K | |
| − | + | lr A,KL | |
| − | + | lr I,A ; push low byte of K | |
| − | + | lr A,IS | |
| − | + | lisu 7 | |
| − | + | lisl 3 | |
| − | + | lr S,A ; save the register number to the stack pointer | |
| − | + | lr A,7 ; restore the ISAR | |
| − | + | lr IS,A | |
| − | + | pop | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
;--------; | ;--------; | ||
| Line 131: | Line 119: | ||
; modifies: r7 | ; modifies: r7 | ||
| + | ; 18% faster than BIOS (4.5 cycles) | ||
popk: | popk: | ||
| − | + | lr A,IS ; backup ISAR in r7 | |
| − | + | lr 7,A | |
| − | + | lisu 7 ; r59, stack pointer | |
| − | + | lisl 3 | |
| − | + | lr A,S ; A = stack pointer, points to next free | |
| − | + | ai $ff ; A = last pushed byte, KL | |
| − | + | lr IS,A ; ISAR -> KL | |
| − | + | lr A,D ; read KL, then ISAR -> KU | |
| − | + | lr KL,A | |
| − | + | lr A,S ; read KU | |
| − | + | lr KU,A | |
| − | + | lr A,IS ; ISAR now points to new stack pointer | |
| − | + | lisu 7 | |
| − | + | lisl 3 | |
| − | + | lr S,A ; store new stack pointer in r59 | |
| − | + | lr A,7 ; restore original ISAR | |
| − | + | lr IS,A | |
| − | + | pop | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
;===================; | ;===================; | ||
Latest revision as of 00:41, 5 August 2026
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.
You can use the pushk ($0107) and popk ($011E) routines from BIOS or use these slightly faster versions:
;==================;
; 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
; 26% faster than BIOS (6 cycles)
pushk:
lr A, IS ; backup ISAR in r7
lr 7,A
lisu 7 ; get the top of the stack
lisl 3 ; r59, stack pointer
lr A,S
lr IS, A ; load the referenced register
lr A,KU
lr I,A ; push high byte of K
lr A,KL
lr I,A ; push low byte of K
lr A,IS
lisu 7
lisl 3
lr S,A ; save the register number to the stack pointer
lr A,7 ; restore the ISAR
lr IS,A
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
; 18% faster than BIOS (4.5 cycles)
popk:
lr A,IS ; backup ISAR in r7
lr 7,A
lisu 7 ; r59, stack pointer
lisl 3
lr A,S ; A = stack pointer, points to next free
ai $ff ; A = last pushed byte, KL
lr IS,A ; ISAR -> KL
lr A,D ; read KL, then ISAR -> KU
lr KL,A
lr A,S ; read KU
lr KU,A
lr A,IS ; ISAR now points to new stack pointer
lisu 7
lisl 3
lr S,A ; store new stack pointer in r59
lr A,7 ; restore original ISAR
lr IS,A
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.