Tuesday, November 11, 2025
Extending the syntax when calling assembly language subroutines for Color BASIC
A few days ago on the Color Computer mailing list, Allen Huffman asked:
The BASIC ROM has the
USRfunction:
DEF USR0=&H3F00
A=USR0(42)It accepts one parameter.
Since it jumps from BASIC into the
USRassembly, couldn’t that code just parse a “,” and more numbers, allowing it to accept whatever needed to be passed in?
A=USR0(1,2,3,4)
[Coco] USRx() and adding more parameters?
Since we're talking about a 45 year old computer and with zero chance of a newer version of BASIC coming out any time sooner, the answer is “yes,” if you don't mind digging through the Unravelled Series (a series of books giving a disassembly of the Color Computer BASIC ROMs) and calling a bunch of nearly undocumented routines.
We'll start with the following code that implements a 16-bit version of PEEK,
using some include files I wrote:
include "basic.i" .opt basic defusr0 peekw org $7F00 peekw jsr INTCVT ; convert parameter to integer tfr x,d ; transfer into an index register ldd ,x ; read 16 bits from memory jmp GIVABF ; return value to BASIC end
Now,
a 16-bit version of PEEK is nice,
but it would also be nice to have a 16-bit version of POKE.
But USRn only takes one parameter;
if we want an aditional value,
we're going to have to extend the BASIC parser.
Fortunately,
due to the way Color BASIC works,
this is easy and there are quite a few routines we can call.
The first call is to a routine I'm calling CB.evalcomma
(as in the Unravelled Series it goes by the name of LB26D).
We need the comma,
otherwise BASIC will return a syntax error without it.
The next call we need is to a routine I'm calling CB.eval,
which just evaluates an expression.
And that's pretty much all that is needed to parse the additional syntax needed for this.
The code looks like:
include "basic.i" include "basic-internal.i" .opt basic defusr1 pokew org $7F00 pokew jsr INTCVT ; convert parameter to integer pshs d ; save the address jsr CB.evalcomma ; parse past comma jsr CB.eval ; parse expression jsr INTCVT ; convert expression to integer puls x ; get address std ,x ; write data into address rts ; return to BASIC
And to use this:
DUMMY=USR1(&H76),1234
As it was pointed out,
the code that handles USRn parses an expression in parentheses,
so a modified USRn that parses would have to look something like A=USR0(1),2,3,4.
It's unfortunate that we can't just call USR1() without using the result,
but that's a limitation of ColorBASIC which requires the results of USRn to be used.
Other than that,
we have successfully extended BASIC to modify how USRn works.
This can also work with EXEC,
the other way assembly code is called in BASIC
(I'll leave that as an exercise for the reader).
But there are issues:
DUMMY=USR0(&H76),&HA000 ?FC ERROR OK
The issue—INTCVT checks if the value being converted is between -32,768 and 32,767
(a signed 16-bit value).
The value of &HA000 is 40,960
(which still fits in 16-bits, but is unsigned)
and thus,
we get the ?FC ERROR from BASIC.
This issue also affects the PEEKW function as we can't easily peek ROM addresses.
If we wanted to peek the 16-bits at address 40,960,
we would need to pass in the value of -24,576.
That will work,
but the value returned would be -24,117,
which is $A1CB,
the address that is stored at address 40,960
(or $A000 in hexadecimal).
It would be nice if we could correct these issues.
In looking through the Unravelled Series,
I did find two routines that help.
The first is CB.uintcvt.
Like INTCVT this returns a 16-bit value in the D register,
but doesn't signal an error if the value is outside -32,768 and 32,767.
The other routine is CB.addrcvt,
which returns the 16-bit value in the X register.
We can thus rewrite our POKEW function as:
include "basic.i" include "basic-internal.i" .opt basic defusr1 pokew org $7F00 pokew jsr CB.addrcvt ; convert parameter to an address pshs x ; save the address jsr CB.evalcomma ; parse past comma jsr CB.eval ; parse expression jsr CB.uintcvt ; convert expression to integer puls x ; get address std ,x ; write data into address rts ; return to BASIC
And it all works as expected.
But that still leaves the PEEKW function returning negative values.
Again,
it would be nice if we could return an unsigned result to BASIC.
I scanned the Unravelled Series but I could not find a routine to call.
Perhaps I didn't look hard enough,
but I did come up with a workaround.
It's not pretty,
but it works.
include "basic.i" .opt basic defusr0 peekw org $7F00 peekw jsr INTCVT ; convert parameter to integer tfr x,d ; transfer into an index register ldd ,x ; read 16 bits from memory bmi .neg jmp GIVABF ; return value to BASIC .neg std FP0 ; return negative value as unsigned lda #$90 ; to BASIC sta FP0EXP clr FP0+2 clr FP0+3 clr FP0+4 rts end
For positive values,
I still use GIVABF.
For negative values,
I construct the floating point value “by hand” since all integer values from 32,768 to 65535 use the same floating point exponent.
All I can say is “it works.”
And given how seldom I've wanted to return an unsigned value,
I can't say that it's a bad solution for this one case.
Then I got to thinking—could I combine the two routines into one?
One function that can either peek or poke a 16-bit value?
Where X=USR0(&H76) would read a 16-bit value,
and X=USR0(&H76),&HA000 would write a 16-bit value while returning the original 16-bit value?
I mean,
why not make the return value for poking memory do something?
But this means peeking ahead for a comma and deciding what to do.
Fortunately,
there are enough examples of this in ColorBASIC that's it's relatively straight forward—the next character in the input can be found by the pointer
stored at address CB.charad,
and wouldn't you know it,
the MC6809 can read through a pointer at an address to get it:
ldb [CB.charad] ; CB.charad contains an address cmpb #',' ; is it a comma?
So now we can combine the two routines:
include "dp.i" include "basic.i" include "basic-internal.i" .opt basic defusr0 peekpokew org $7FD0 peekpokew jsr CB.addrcvt ; return parameter in X ldd ,x ; read 16-bit at address pshs x,d ; save address and data ldb #',' ; check for comma cmpb [CB.charad] bne .return ; if none, just a peek jsr CB.evalcomma ; else parse the comma jsr CB.eval ; evaluate the expression jsr CB.uintcvt ; convert to 16-bit unsigned std [2,s] ; store expr in address .return puls x,d ; restore data (and stack) tsta ; test data < 0 bmi .neg ; if so, handle jmp GIVABF ; return pos via BASIC .neg std FP0 ; return negative as unsigned lda #$90 sta FP0EXP clr FP0+2 clr FP0+3 clr FP0+4 rts .pcle $8000 end
The PCLE directive stands for “PC register Less Than or Equal to” and is there to ensure our code doesn't run into ROM.
If it does, it generates an error:
pw.asm:30: error: E0106: PC 8030 exceeds given limit 8000
and one can adjust the origin appropriately.
There are also some other functions I found,
CB.evalopar which parses an open parenthesis,
and CB.evalcpar which parses a close parenthesis.
Then there's ECB.evalpoint which parses two expressions in parentheses:
(x,y) and places the results in the variables ECB.horbeg
(horizontal beginning)
and the second into ECB.verbeg
(verical beginning)—nice for parsing X,Y coordinates for graphics,
and ECB.evalrect which parses a pair of points:
(x1,y1)-(x2,y2) and places the second set of values into ECB.horend and ECB.verend.
This can lead to some weird looking BASIC code:
X=USR3(4),(X1,Y1)-(X2,Y2)
but it works.
![Glasses. Titanium, not steel. [Self-portrait with my new glasses]](https://www.conman.org/people/spc/about/2025/0925.t.jpg)