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 26, 2024

Interfacing 6809 assembly with Color BASIC is now easier

A typical way to extend Color BASIC is to write some assembly:

INTCVT		equ	$B3ED	; put argument into D
GIVABF		equ	$B4F4	; return D to BASIC

		org	$7F00

swapbyte	jsr	INTCVT	; get argument
		exg	a,b	; swap bytes
		jmp	GIVABF	; return it to BASIC

		end

Then assemble the code, transcribe the resulting object code into DATA statements in a BASIC program, write the loop to poke the data into memory and define the “user-defined machine language” subroutine. The result of all this work would look something like this:

10 DATA189,179,237,30,137,126,180,244
20 CLEAR200,32511:FORA=32512TO32519:READB:POKEA,B:NEXT:POKE275,127:POKE276,0

Line 10 contains the object code for the above subroutine. Line 20 starts with reserving memory with the CLEAR statement. The first value, 200, is the number of bytes BASIC can use for dynamic strings (200 being the default value of Color BASIC in general). The second number is the highest address Color BASIC can use; any address above that is off-limits to it. This memory, at the top of RAM, is where we're storing our assembly subroutine (and yes, we don't need that much memory). There's the loop to load the object code into memory, and the final two statements, POKE275,127:POKE276,0, informs Color BASIC were our assembly subroutine resides in memory.

This is tedious.

So I decided to add support for a “basic” format that writes the code for us. Just by adding the following to our source code:

		.opt	basic usr swapbyte

and using the -fbasic format, my 6809 assembler will now generate the BASIC code automatically. Of course, there are options to control the line numbers that are generated—these are just the default values.

Extended Color BASIC changed how you define an assembly subroutine, and also expanded BASIC to support up to 10 such subroutines. I support that as well. It's just a variation on the .OPT directive:

		.opt	basic defusr0 swapbyte
		.opt	basic defusr1 peekw

INTCVT		equ	$B3ED	; put argument into D
GIVABF		equ	$B4F4	; return D to BASIC

		org	$7F00

swapbyte	jsr	INTCVT	; get argument
		exg	a,b	; swap bytes
		jmp	GIVABF	; return to BASIC

peekw		jsr	INTCVT	; get address
		tfr	d,x	; transfer to X
		ldd	,x	; load word from given address
		jmp	GIVABF	; return to BASIC

		end

This defines two assembly subroutines, and this will generate the following BASIC code:

10 DATA189,179,237,30,137,126,180,244,189,179,237,31,1,236,132,126,180,244
20 CLEAR200,32511:FORA=32512TO32529:READB:POKEA,B:NEXT:DEFUSR0=32512:DEFUSR1=32520

The only difference, aside from the extra object code, are the calls to DEFUSRn, which are variables used to define the address of each subroutine. Disk Extended Color BASIC has a command MERGE, which is used to merge two BASIC files into one. This is useful if you need to update the assembly subroutines later on in an existing BASIC program, saving me the trouble of adding such functionality to my assembler.

My assembler isn't the first to do this—after I started coding, I found out that the LW Tool Chain (another 6809 assembler and linker) does this as well, although the online manual doesn't mention it.


The definitive guide to writing assembly language subroutines for Color BASIC

There's nothing quite like documenting 40 year old technology, but hey, retro-computing is now popular, so why not?

Anyway, since I've modified my assembler to make it easier to write assembly subroutines for Color BASIC, I've been doing a deep dive into the nuances of doing so. This post will cover the method for plain Color BASIC; Extended Color BASIC, which does things a bit differently, will be covered in another post.

The information in Getting Started With Color BASIC is a bit light. It covers how to use POKE to load the object code into memory, and how to define the address for use by the USR function by poking that address into memory, but that's it. It gives one sample program:

LOOP1	JSR	[POLCAT]	;POLL FOR A KEY
	BEQ	LOOP1		;IF NONE, RETRY
	CMPA	#10		;CTRL KEY (DN ARW)?
	BNE	OUT		;NO, SO EXIT
LOOP2	JSR	[POLCAT]	;YES, SO GET NEXT KEY
	BEQ	LOOP2		;IF NONE, RETRY
	CMPA	#65		;IS IT A-Z?
	BLT	OUT		;IF <A, EXIT
	SUBA	#64		;CONVERT TO CTRL A/Z
OUT	TFR	A,B		;GET RETURN BYTE READY
	CLRA			;ZERO MSB
	JMP	GIVABF		;RETURN VALUE TO BASIC
POLCAT	EQU	40960
GIVABF	EQU	46324

It shows how to return a value to Color BASIC, but doesn't fully explain the BASIC call:

110 A = USR(0) 'CALL THE SUBROUTINE AND GIVE RESULT TO A

Why the 0 to USR? How do we get it? There is no explanation.

The book TRS-80 Color Computer Assembly Language Programming goes into more depth, explaining how to retrieve the argument and even how to pass in a string and not just a numeric parameter (although it uses a function only available in Extended Color BASIC). Neither go into any real depth on how this all works.

I'm going into that depth.

First off, Color BASIC only supports two data types—numeric (or float) and strings. Numbers are in the Microsoft BASIC floating point format, which are five bytes in length. Strings are stored in two parts—the first is a “string descriptor,” which is also five bytes (to keep the same size as number). Only three bytes are used, one byte for the length (0 to 255) and two bytes for the second part of the string, a pointer to the actual contents. This is done for a few reasons. One, the string can be defined anywhere in memory, not just the string pool used for dynamic strings. Second, the string pool can be subject to garbage collection which can change the location of string data. So while the descriptor doesn't change location, the pointer to the actual string contents might!

Now, when you call the assmbly language subroutine via USR(), the BASIC variable FP0 (located at address $0050) contains the result of the expression given to USR(). This is a floating point value. You can use the function INTCVT (located at address $B3ED, which is mentioned in Getting Started With Extended Color BASIC), to convert this into the 16-bit D register. The CPU registers themselves have no defined value upon input. To return a 16-bit value, you can call GIVABF (located at address $B4F4) with the value in the D register. You can also call GIVBF (located at address $B4F3) with an 8-bit unsigned value in the B register (not documented by Tandy—more on this in a bit). Furthermore, no CPU registers need to be saved by the assembly language subroutine. Putting this together, we can write a simple subroutine such as:

INTCVT		equ	$B3ED	; put argument into D
GIVABF		equ	$B4F4	; return D to BASIC

		org	$7F00

swapbyte	jsr	INTCVT	; get argument
		exg	a,b	; swap bytes
		jmp	GIVABF	; return it to BASIC

		end

And while both Getting Started With Extended Color BASIC and TRS-80 Color Computer Assembly Language Programming both mention passing strings to an assembly language subroutine, they both state you must pass in a pointer to the string descriptor with VARPTR (this function will return the address of both string and numeric variables), this isn't completely true. Color BASIC will call the generic expression parsing routine for the parameter to USR() and this can be either a numberic expression or a string expression! In either case, the variable FP0 will contain the result of the expression, and the variable VARTYP (located at address $0006) will contain a 0 for a numerica value, or 255 for a string value. In the case of a string value, the location FP0+2 will contain the address of the string descriptor. This means you can pass a string expression to USR():

FP0		equ	$0050
GIVBF		equ	$B4F3

		org	$7F00

checksum	ldx	FP0 + 2		; get string descriptor
		lda	,x		; get length
		ldx	2,x		; get pointer to data
		clrb			; clear checksum
.sum		addb	,x+		; add in next character
		deca			; decrement length
		bne	.sum		; continue if more data
		comb
		jmp	GIVBF		; return checksum to BASIC
		end

Of course, this routine assumes a string was correctly passed in. If you do pass in a number to USR() all you'll get is a nonsensical result. It would be nice to do some error checking, and while you could do something like:

VALTYP		equ	$0006

checksum	tst	VALTYP
		beq	.error
		...
.error		ldd	#-1
		jmp	GIVABF

There are two functions I found via the Unravelled Series (a collection of books that give a source listing of the BASIC ROM contents—this is also where I found GIVBF) that can help with error checking. They're not named in the Unravelled series (they're just named after their memory address) but I've come to call CHKNUM (located at address $B143) to ensure the given parameter is a number, and CHKSTR (located at address $B146) to ensure the given parameter is a string. If either function fails, the function instead returns a TM (type mismatch) error to BASIC and the program stops running. So we can rewrite our checksum function as:

FP0		equ	$0050
CHKSTR		equ	$B146
GIVBF		equ	$B4F3

		org	$7F00

checksum	jsr	CHKSTR		; check parameter is a string
		ldx	FP0 + 2		; get string descriptor
		lda	,x		; get length
		ldx	2,x		; point to string data
		clrb			; clear checksum
.sum		addb	,x+		; add in next character
		deca			; decrement length
		bne	.sum		; continue if more data
		comb
		jmp	GIVBF		; return checksum to BASIC
		end

This is nice, but what if we want to return a new string? This isn't so straightforward in plain Color BASIC. Color BASIC expects a numeric result from USR(), and if we attempt to return a string, we get an error. So something like:

110 A$ = USR("SOME STRING")

is right out.

But not all is lost. We can modify the string descriptor. For example:

silly_example	jsr	CHKSTR		; just assume this is defined
		ldx	FP0 + 2		; and FP0, but get string descriptor
		ldb	#.textlen	; new string length
		stb	,x		; save it
		ldd	#.text		; get new text 
		std	2,x		; point to it
		clrb			; and return a value to BASIC
		jmp	GIVBF
.text		fcc	/HELLO, WORLD!/
.textlen	equ	* - .text
		end

So, calling this with:

110 X$="THIS IS A STRING"
120 PRINT X$
130 X=USR(X$)
140 PRINT X$

will return in:

THIS IS A STRING
HELLO, WORLD!

And again, that's fine. But if you want to modify the passed in string? You could set aside memory for this. For example, to ROT-13 a string:

rot13		jsr	CHKSTR		; ensure a string
		ldy	FP0 + 2		; get string descriptor
		ldb	,y		; get length
		ldx	#buffer		; tmp space
		ldu	2,y		; get original string
		stx	2,y		; save pointer to new string in descriptor

.loop		lda	,u+		; get character
		cmpa	#'A'		; if < 'A', no processing
		blo	.out
		cmpa	#'Z'		; if > 'Z', no processing
		bhi	.out
		adda	#13		; ROT-13 the character
		cmpa	#'Z'
		bls	.out
		suba	#26
.out		sta	,x+		; save character in new string
		decb			; continue if more
		bne	.loop
		jmp	GIVBF		; return result to BASIC

buffer		rmb	255		; maximum length of string
		end

But that will fail if you attempt to ROT-13 multiple strings at the same time. A better way is to call RSVPSTR (again, found on the Unravelled series and given a name my be and located at address $B56D) which will reserve space from the dynamic string pool maintained by BASIC. It expects the amount of space in the B register, and if it returns (it can error out with an “OS” (out of string space) error), it returns the length in the B register, and the space in the X register. So now our function looks like:

rot13		jsr	CHKSTR		; ensure a string
		ldy	FP0 + 2		; get string descriptor
		ldb	,y		; get length
		jsr	RSVPSTR		; reserve new string of said length
		ldu	2,y		; get original string
		stx	2,y		; save pointer to new string in descriptor

.loop		lda	,u+		; get character
		cmpa	#'A'		; if < 'A', no processing
		blo	.out
		cmpa	#'Z'		; if > 'Z', no processing
		bhi	.out
		adda	#13		; ROT-13 the character
		cmpa	#'Z'
		bls	.out
		suba	#26
.out		sta	,x+		; save character in new string
		decb			; continue if more
		bne	.loop
		jmp	GIVBF		; return result to BASIC

buffer		rmb	255		; maximum length of string
		end

The only downside is that you have to use a string variable when calling the routine. You could give it a string literal:

100 X=USR("THIS IS A STRING")

While that won't crash, you won't have access to the newly created string either. Just something to keep in mind.

One other thing to keep in mind—don't change the actual string data itself, for doing so will cause undefined results. For instance, if you call USR() with a string literal:

110 X=USR("HELLO, WORLD!")

The pointer in the string descriptor points directly into the source code! So you can change the contents of the descriptor, but not the string itself.

Also to keep in mind, when you call USR() with a number, you don't have to convert it to an integer. You could call into some Color BASIC floating point routines if you know where they are. So, for example:

CHKNUM		equ	$B143
FNULx		equ	$BACA

		org	$7F00

twopi		jsr	CHKNUM		; check for number input
		ldx	#.pi
		jmp	FMULx

.pi		.float	3.14159265358979323846
		end

To aid in writing such code, I have written definitions for interfacing with Color BASIC and a file that points to floating point routines within BASIC. Note that these files assume you are using my assembler but it should be easy to adapt to other assemblers.

And that's pretty much it for calling an assembly language subroutine in plain Color BASIC. You can pass in numbers or strings, but you can only return numbers. And if you want a new string, you have to pass in a string variable. You are also restricted to only one such function. A fair start, but things get eaiser with Extended Color BASIC.


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 DEFUSRn (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

Obligatory Picture

An abstract representation of where you're coming from]

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-2024 by Sean Conner. All Rights Reserved.