The Boston Diaries

The ongoing saga of a programmer who doesn't live in Boston, nor does he even like Boston, but yet named his weblog/journal “The Boston Diaries.”

Go figure.

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 USR function:

DEF USR0=&H3F00

A=USR0(42)

It accepts one parameter.

Since it jumps from BASIC into the USR assembly, 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.

Obligatory Picture

[Self-portrait with my new glasses]

Obligatory Contact Info

Obligatory Feeds

Obligatory Links

Obligatory Miscellaneous

Obligatory AI Disclaimer

No AI was used in the making of this site, unless otherwise noted.

You have my permission to link freely to any entry here. Go ahead, I won't bite. I promise.

The dates are the permanent links to that day's entries (or entry, if there is only one entry). The titles are the permanent links to that entry only. The format for the links are simple: Start with the base link for this site: https://boston.conman.org/, then add the date you are interested in, say 2000/08/01, so that would make the final URL:

https://boston.conman.org/2000/08/01

You can also specify the entire month by leaving off the day portion. You can even select an arbitrary portion of time.

You may also note subtle shading of the links and that's intentional: the “closer” the link is (relative to the page) the “brighter” it appears. It's an experiment in using color shading to denote the distance a link is from here. If you don't notice it, don't worry; it's not all that important.

It is assumed that every brand name, slogan, corporate name, symbol, design element, et cetera mentioned in these pages is a protected and/or trademarked entity, the sole property of its owner(s), and acknowledgement of this status is implied.

Copyright © 1999-2025 by Sean Conner. All Rights Reserved.