Interfacing assembly with BASIC via DEFUSR, part 8

See also: part 1, part 2, part 3, part 4, part 5, part 6, part 7 and part 8.

Just when I thought I was out, they pull me back in.

In part 3 I showed a simple assembly language routine that would uppercase a string.

In part 5, this routine was made more better by contributions from commenters.

Today, I revisit this code and update it to use “what I now know” (thank you, Sean Conner) about being able to pass strings into a USR function without using VARPTR.

First, here is the code from part 5:

* UCASE.ASM v1.01
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* 1.01 a bit smaller per Simon Jonassen
*
* DEFUSRx() uppercase output function
*
* INPUT: VARPTR of a string
* RETURNS: # chars processed
*
* EXAMPLE:
* CLEAR 200,&H3F00
* DEFUSR0=&H3F00
* A$="Print this in uppercase."
* PRINT A$
* A=USR0(VARPTR(A$))
*
ORGADDR EQU $3f00

GIVABF EQU $B4F4 * 46324
INTCNV EQU $B3ED * 46061
CHROUT EQU $A002

opt 6809 * 6809 instructions only
opt cd * cycle counting

org ORGADDR

start jsr INTCNV * get passed in value in D
tfr d,x * move value (varptr) to X
ldy 2,x * load string addr to Y
beq null * exit if strlen is 0
ldb ,x * load string len to B
ldx #0 * clear X (count of chars conv)

loop lda ,y+ * get next char, inc Y
; lda ,y * load char in A
cmpa #'a * compare to lowercase A
blt nextch * if less, no conv needed
cmpa #'z * compare to lowercase Z
bgt nextch * if greater, no conv needed
lcase suba #32 * subtract 32 to make uppercase
leax 1,x * inc count of chars converted
nextch jsr [CHROUT] * call ROM output character routine
; leay 1,y * increment Y pointer
cont decb * decrement counter
bne loop * not done yet
; beq exit * if 0, go to exit
; bra loop * go to loop

exit tfr x,d * move chars conv count to D
jmp GIVABF * return to caller

null ldd #-1 * load -2 as error
return jmp GIVABF * return to caller

* lwasm --decb -o ucase2.bin ucase2.asm -l
* lwasm --decb -f basic -o ucase2.bas ucase2.asm -l
* lwasm --decb -f ihex -o ucase2.hex ucase2.asm -l
* decb copy -2 -r ucase2.bin ../Xroar/dsk/DRIVE0.DSK,UCASE2.BIN

In the header comment you can see an example of the usage, and that it involved using VARPTR on a string to get the string’s descriptor location in memory, then pass that address into the USR function.

See also: Color BASIC and VARPTR

Now that I know we can just pass a string in directly, I thought it would be fun (?) to update this old code to use that method. Here is what I came up with. Note that I changed the “*” comments to “;” since the a09 assembly does not support those. If you wanted to run this in EDTASM, you would have to change those back.

; UCASE3.ASM v1.02
; by Allen C. Huffman of Sub-Etha Software
; www.subethasoftware.com / alsplace@pobox.com
;
; 1.01 a bit smaller per Simon Jonassen
; 1.02 converted to allow passing a string in to USR
;
; DEFUSRx() uppercase output function
;
; INPUT: string
; RETURNS: # chars converted or -1 if error
;
; EXAMPLE:
; CLEAR 200,&H3F00
; DEFUSR0=&H3F00
; A$="Print this in uppercase."
; PRINT A$
; A=USR0(A$)
; PRINT "CHARS CONVERTED:";A
; A=USR0("This is another test");
; PRINT "CHARS CONVERTED:";A
;
ORGADDR EQU $3f00

CHROUT EQU $A002
CHKSTR EQU $B146 ; Undocumented ROM call
INTCNV EQU $B3ED ; 46061
GIVABF EQU $B4F4 ; 46324

org ORGADDR

start jsr CHKSTR ; ?TM ERROR if not a string.
; X will be VARPTR, B will be string length
tstb
beq reterror ; exit if strlen is 0
ldy 2,x ; load string addr to Y
ldx #0 ; clear X (count of chars conv)

loop lda ,y+ ; get next char, inc Y
cmpa #'a ; compare to lowercase A
blo nextch ; if less, no conv needed
cmpa #'z ; compare to lowercase Z
bhi nextch ; if greater, no conv needed
suba #32 ; subtract 32 to make uppercase
leax 1,x ; inc count of chars converted
nextch jsr [CHROUT] ; call ROM output character routine
decb ; decrement counter
bne loop ; not done yet

tfr x,d ; move chars conv count to D
bra return

reterror ldd #-1 ; load -1 as error
return jmp GIVABF ; return to caller

end

; lwasm --decb -o ucase3.bin ucase3.asm -l -m
; lwasm --decb -f basic -o ucase3.bas ucase3.asm -l -m
; lwasm --decb -f ihex -o ucase3.hex ucase3.asm -l -m
; decb copy -2 -r ucase3.bin ../Xroar/dsk/DRIVE0.DSK,UCASE3.BIN
; a09 -fbasic -oucase3_a09.bas ucase3.asm

Here are the changes… In the original version, I have this:

start       jsr     INTCNV  * get passed in value in D
tfr d,x * move value (varptr) to X
ldy 2,x * load string addr to Y
beq null * exit if strlen is 0
ldb ,x * load string len to B
ldx #0 * clear X (count of chars conv)

That first jsr INTCNV expects a number parameter and, if not a number, it exits with ?TM ERROR. If it gets past that, the number is in the D register and it gets transferred over to X. In this case, the number is the value returned by VARPTR:

A=USR0(VARPTR(A$))

That value is the address of the 5-byte string descriptor that contains the address of the actual string data and the length of that data. Y is loaded with 2 bytes in from wherever X points which makes Y contain the address of the string data.

After this is a bug, I think. Looking at the comments, I think that “beq null” should be one line lower, like this:

start       jsr     INTCNV  * get passed in value in D
tfr d,x * move value (varptr) to X
ldy 2,x * load string addr to Y
ldb ,x * load string len to B
beq null * exit if strlen is 0
ldx #0 * clear X (count of chars conv)

That way, Y is loaded with the address of the string data, then b is loaded with the length of that data, and the branch-if-equal check is now checking B. If the length is 0, it is an empty string so no processing can be done on it. (That’s a bug, right?)

The new code is this:

start       jsr     CHKSTR      ; ?TM ERROR if not a string.
; X will be VARPTR, B will be string length
tstb
beq reterror ; exit if strlen is 0
ldy 2,x ; load string addr to Y
ldx #0 ; clear X (count of chars conv)

The first line is something I learned from Sean Conner‘s excellent writeup on USR. That is an undocumented ROM call which checks is a variable is a string. If it isn’t, it will return back to BASIC with a ?TM ERROR. By having that check there, if the user tries to pass in a number, that error will be seen. As a bonus, if you try to EXEC that code, that, too, will show ?TM ERROR.

After that, B should be the length of the string so tstb checks that to be 0 (empty string) then the rest of the code is similar.

As I write this, I could have altered the order of my new code to do the tstb/beq after the ldy and then it would be closer to how the original worked. But since the original appears buggy, I won’t worry about that.

Now if I load this and set it up, I should see this:

DEF USR0=&H3F00

A=USR0(42)
?TM ERROR

A=USR0("This is a test")
THIS IS A TEST

Also, I notice that the value I return can be -1 if you pass in an empty string…

A=USR0("")
OK
PRINT A
-1

…and if it is non-empty, it is only the count of the characters that had to be converted. So “Hello World” converts the “ello” and “orld” for a return value of 8. It does not touch the uppercase “H” and “W” or the space.

I am not sure that is really useful. The code could be modified to return the length of the string it processed, but at least this way you know that a positive non-zero return value means it did do some work.

Spot any bugs? Comment, if you will.

Until next time…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.