Tuesday, November 26, 2024
The definitive guide to writing assembly language subroutines for Extended Color BASIC
And in keeping with documenting 40 year old technology, I'm documenting how to call assembly language subroutines for Extended Color BASIC.
One major difference between Color BASIC and Extended Color BASIC is how to define the address to call.
No longer do you have to poke the address into memory,
but use the BASIC command DEFUSR
n
(where n is between 0 and 9).
And you can define up to 10 such routines.
From that point on,
existing code written for Color BASIC will just work.
VARTYP
will still have the value type,
FP0
will still be a floating point value or contain an address to a string descriptor,
and all the functions defined for Color BASIC still function the same.
But there are some major differences.
First off,
the registers are defined upon entry now.
The A
register contains the contents of VARTYP
and the condition codes are set appropriately,
so that one can immedately do a conditional branch to check the type
(BEQ
for a number, BMI
for a string).
The X
register either points to FP0
(technically, one byte prior to FP0
for internal reasons) if the parameter is a number,
or it points to the string descriptor if the parameter is a string.
And if the parameter is a string,
the B
register has the length of the string.
So the chksum
function can be rewritten to read:
CHKSTR equ $B146 GIVBF equ $B4F3 org $7F00 checksum jsr CHKSTR lda ,x ldx 2,x clrb .sum addb ,x+ deca bne .sum comb jmp GIVBF end
Not a big change, but no longer do we have to load the string descriptor pointer from memory.
The biggest change however,
is the ability to return a string from an assembly language subroutine.
No longer do you have to modify the passed in string descriptor,
you can return a new string.
To do so,
you call RSVPSTR
to reserve the space,
put the length into the BASIC variable STRDES
(name from the Unravelled Series and located at address $0056),
the pointer into STRDES
+2,
and call GIVSTR
(name I came up with since it's not named in the Unravelled series and located at address $B54C).
Here is the revised ROT-13 code for Extended Color BASIC:
rot13 jsr CHKSTR tfr x,y ; save string descriptor jsr RSVPSTR ; B is already set stb STRDES ; save new string length stx STRDES + 2 ; and the space for it ldy 2,y ; get stirng data, which RSVPSTR may have moved .loop lda ,y+ ; ROT-13 blah blah ... cmpa #'A' blo .out cmpa #'Z' bhi .out adda #13 cmpa #'Z' bls .out suba #26 .out sta ,x+ decb bne .loop jmp GIVSTR ; return new string to BASIC end
And this:
110 X$ = USR0("ABCXYZ") 120 PRINT X$
will work as expected—X$
will be the ROT-13 version of the string literal.
And with that,
we're done.
That's all there is to interfacing an assembly language subroutine to Color BASIC and Extended Color BASIC.
I'm not sure why this wasn't documented in Getting Started With Extended Color BASIC—perhaps no one talked about the changes to Extended Color BASIC to the documentation department, or there wasn't time before shipping the updated BASIC, or what. It's clear from the Unravelled series that the change was a deliberate change to make it easier to interface with BASIC and cache useful values in the various registers, but for some reason, it never got documented. What is documented will work, you can pass in a string as:
110 X$="ABCXYZ" : X=USR0(VARPTR(X$))
but it's not needed as I've shown. And here's one last example, defining multiple subroutines that show handling of various parmaters of strings and numbers and returning strings or numbers (here are links to basic.i and basic-fp.i):
include "basic.i" include "basic-fp.i" .opt basic strspace 300 .opt basic defusr0 num2num .opt basic defusr1 fp2fp .opt basic defusr2 num2str .opt basic defusr3 str2num .opt basic defusr4 str2str .opt basic defusr5 son2son .opt basic defusr6 son2nos org $7F00 ;*************************************************************************** num2num jsr CHKNUM ; check for numeric argument jsr INTCVT ; convert to integer coma ; 1s complement comb jmp GIVABF ; return it to BASIC ;*************************************************************************** fp2fp jsr CHKNUM ; check for numeric argument ldx #.pi ; * 3.1415926 jmp CB.FMULx .pi .float 3.1415926 ;*************************************************************************** num2str jsr CHKNUM ; check for number jsr INTCVT ; convert to integer bmi .minus ; if negative, return 'MINUS' beq .zero ; if zero, return "ZERO" ldx #.textplus ; else return "PLUS" bra .return .minusinf ldx #.textminus bra .return .zero ldx #.textzero .return ldb ,x+ ; get length stb STRDES + _STRLEN ; save length stx STRDES + _STRPTR ; save text jmp GIVSTR ; return string to BASIC .textminus ascii 'MINUS'c ; a "counted" string .textzero ascii 'ZERO'c ; where the first byte is the length .textplus ascii 'PLUS'c ; of the string ;*************************************************************************** str2num jsr CHKSTR ; check for string lda _STRLEN,x ; get length ldx _STRPTR,x ; get string data clrb ; clear checksum .sum addb ,x+ ; add next byte deca ; if more, do more bne .sum comb ; complement checksum jmp GIVBF ; return it to BASIC ;*************************************************************************** str2str jsr CHKSTR ; check for string tfr x,y ; save descriptor jsr RSVPSTR ; reserve space for return string stb STRDES + _STRLEN ; save length stx STRDES + _STRPTR ; and allocated space ldy _STRPTR,y ; get original string .loop lda ,y+ ; get character cmpa #'A' ; < 'A', just store blo .store cmpa #'Z' ; if <= 'Z', convert to lower case bls .lower cmpa #'a' ; if between 'a' and 'z', blo .store ; convert to upper case cmpa #'z' bhi .store anda #$5F bra .store .lower ora #$20 .store sta ,x+ ; save character in new string decb ; if more, do more bne .loop jmp GIVSTR ; return new string to BASIC ;*************************************************************************** son2son beq .num ; if 0, number (string or number to string or number) lsrb ; cut length in half stb _STRLEN,x rts .num ldx #.one ; add 1.0 to number jmp CB.FADDx ; and return new value to BASIC .one .float 1.0 ;*************************************************************************** son2nos bmi .string ; if minus, string (string or number to number or string) ldb #4 ; return "0" to BASIC, len of 1 ldx #num2str.textzero+1 ; text of "ZERO" stb STRDES + _STRLEN stx STRDES + _STRPTR jmp GIVSTR .string tfr a,b ; transfer type to B jmp GIVBF ; and return it to BASIC ;*************************************************************************** end
And the resulting BASIC file output from my assembler:
10 DATA189,177,67,189,179,237,67,83,126,180,244,189,177,67,142,127,20,126,186,202,130,73,15,218,104,189,177,67,189,179,237,43,7,39,10,142,127,67,32,8,142,127,57,32,3,142,127,62,230,128,215,86,159,88,126,181,76,4,45,73,78,70,4,90,69,82,79,4,43,73,78 20 DATA70,189,177,70,166,132,174,2,95,235,128,74,38,251,83,126,180,243,189,177,70,31,18,189,181,109,215,86,159,88,16,174,34,166,160,129,65,37,18,129,90,35,12,129,97,37,10,129,122,34,6,132,95,32,2,138,32,167,128,90,38,227,126,181,76,39,4,84,231,132 30 DATA57,142,127,148,126,185,194,129,0,0,0,0,43,12,198,4,142,127,63,215,86,159,88,126,181,76,31,137,126,180,243 40 CLEAR300,32511:FORA=32512TO32683:READB:POKEA,B:NEXT:DEFUSR0=32512:DEFUSR1=32523:DEFUSR2=32537:DEFUSR3=32584:DEFUSR4=32601:DEFUSR5=32648:DEFUSR6=32665