Category Archives: Color BASIC

POKE versus PRINT

See Also: part 1 and part 2 (coming soon).

This is a followup to a recent post I made about making a string in Color BASIC contain data from the text screen memory.

ASCII me no questions…

Color BASIC deals with a version of ASCII where specific numbers represent specific characters/letters:

https://en.wikipedia.org/wiki/ASCII

On the old school 8-bit home computers, not all of them used ASCII. Commodore used a variation called PETSCII, and the Atari 8-bits used ATASCII. While the trick discussed in this article might work on other systems that have a VARPTR or similar command, this discussion will be specifically about the character set in the Radio Shack Color Computer.

ASCII 65 is the uppercase letter ‘A’

PRINT CHR$(65)
A

If you POKE the value of 65 to the first position on the 32×16 text screen (location 1024), you will also see an uppercase 65.

POKE 1024,65

However, the embedded font data in the MC6847 VDG video generator chip does not follow ASCII for all of its characters. For example, CHR$(0) to CHR(31) are non printable characters. On the CoCo, two of them do something special — CHR$(8) will print a backspace and CHR$(13) will print an ENTER:

PRINT "HELLO";CHR$(8);"THERE";CHR$(13);"HOWDY"
HELLTHERE
HOWDY

It would have been nice if the CoCo could have done a beep for CHR$(7) like Apple 2s did, or clear the screen with CHR$(12) like many other systems did, but those are the only two that do anything other than “print nothing” on the CoCo.

If you POKE around a bit…

While you will not see anything if you PRINT those characters, if you POKE those values to the screen memory you will see something. For example, you could POKE characters 0 to 31 to the first row of the 32 column text screen like this:

FOR A=0 TO 31:POKE 1024+A,A:NEXT

The character set in the video chip has 0-31 representing reverse video characters “@” (AT sign) to “<-” (left arrow). We can expand that loop to POKE the first 128 characters onto the video screen:

FOR A=0 TO 127:POKE 1024+A,A:NEXT

But for PRINTing the ASCII characters, we have already established nothing shows up for characters 0-31, but things do PRINT when for 32-128:

FOR A=32 TO 127:PRINT CHR$(A);:NEXT

I put together this sloppy program that will show the differences, 32 characters at a time, of what you get when you PRINT the character values versus POKE the character values:

0 'POKEPRNT.BAS
10 CLS

20 PRINT@0,"PRINT 0-31:"
30 FOR A=0 TO 31:PRINT CHR$(A);:NEXT
40 PRINT@64,"POKE 0-31:"
50 FOR A=0 TO 31:POKE 1120+A,A:NEXT

60 PRINT@128,"PRINT 32-63:"
70 FOR A=32 TO 63:PRINT CHR$(A);:NEXT
80 PRINT@192,"POKE 32-63:"
90 FOR A=0 TO 31:POKE 1248+A,32+A:NEXT

100 PRINT@256,"PRINT 64-95:"
110 FOR A=64 TO 95:PRINT CHR$(A);:NEXT
120 PRINT@320,"POKE 64-95:"
130 FOR A=0 TO 31:POKE 1376+A,64+A:NEXT

140 PRINT@384,"PRINT 96-127:"
150 FOR A=96 TO 127:PRINT CHR$(A);:NEXT
160 PRINT@448,"POKE 96-127:"
170 FOR A=0 TO 31:POKE 1504+A,96+A:NEXT

999 GOTO 999

Looking at this, you can see only the characters 64-95 match between PRINT and POKE.

This means that the “copy screen to a string” concept from my earlier post doesn’t really do what we might expect. It does copy the data, but if we PRINT it back, we do not get back exactly what we started with.

This is the same thing that would happen if you tried to build a string by using PEEK from screen memory. This example prints stuff on the first line of the screen, then builds a string made of up characters using the PEEK value of that first line:

0 'PEEK2STR.BAS
10 CLS
20 PRINT "HELLO, WORLD! THIS IS A TEST."
30 FOR A=1024 TO 1024+31
40 A$=A$+CHR$(PEEK(A))
50 NEXT
60 PRINT "PEEKED STRING:"
70 PRINT A$

And running that shows this awfulness…

Yuck!

But that’s okay since there is not much use to copying TEXT data and then putting it back with PRINT. PRINT is fast, and we can easily PRINT that text data. Sure, there could be benefits if stuff being PRINTed is doing calculations and such to generate the output, but this trick won’t help there.

However, the semi graphics characters (128-256) are the same between PRINT and POKE.

0 'POKEPRT2.BAS
10 CLS
20 FOR A=0 TO 255
30 PRINT@A,CHR$(A);
40 POKE 1280+A,A
50 NEXT
60 GOTO 60

The top half is the PRINT CHR$ and the bottom half is the POKE:

Since there is no way on the CoCo to type those semi graphics characters into a string (pity, the later MC-10 could do this), we are forced to PRINT them like this:

PRINT CHR$(128);CHR$(128);CHR$(128)

That would print three black blocks. To speed things up, we could pre-generate a string of those three black blocks then we can PRINT that string very fast later:

A$=CHR$(128);CHR$(128);CHR$(128)
PRINT A$

And now you know why I chose to do a “splash screen” example for my demo in part 1. I initially tried it using the TEXT characters and quickly remembered why that can’t work (as explained here).

But it’s still a neat trick.

Bonus: This is stupid

For dumb fun, here is a program that makes A$ be whatever is on the first 32 character line of the screen.

0 'DUMBSTRN.BAS
10 A$="":A=VARPTR(A$):POKEA,32:POKEA+2,4:POKEA+3,0

When you RUN that, doing a PRINT A$ will show a 32 character line that is whatever was on the first line of the screen. If you do a “CLS” to clear the screen and show “OK” on the top line, then PRINT A$, you will see “OK” followed by 30 reverse @ symbols, which is CHR$(96) — but in video memory, a 96 is an empty block (space).

And with that, I’m going to stop now.

Unit next time…

Why is Microsoft BASIC INSTR like this?

UPDATE: I believe I have found the answer, and will share it in an upcoming post. Until then, keep those comments coming. I learn so much from all of you!


This topic has been discussed here years ago, but every time something reminds me about it, I get annoyed. While my annoyance is triggered by how it works in the CoCo’s Extended Color BASIC, past research showed the behavior was the same even in much later Microsoft Visual BASIC. But why?

INSTR is a command to return the index where a target string is found in a search string. From one of the Getting Started with Extended Color BASIC manuals, it is shown as this:

What the manual did not mention is that it can also return 1 when there is no match. See this example:

Looking for “B” in “ABC”? That’s at position 2. Good.

Looking for “X” in “ABC”? It is not there, so it returns 0. Good.

Looking for “A” in “ABC”? That’s at position 1. Good.

Looking for “” in “ABC”? Apparently “” is found at position 1. Don’t tell that to the “A” there.

Callbacks

I ran into this years ago when I was experimenting with various ways to handle key presses. You could have code block until a key was pressed, and then pass the key to INST and then use ON GOTO/GOSUB to get to the routine. Like this:

0 'INSTR.BAS
10 PRINT "A)BORT, R)ETRY, C)ONTINUE:";
20 A$=INKEY$:IF A$="" THEN 20
30 LN=INSTR("ARC",A$)
40 IF LN>0 THEN ON LN GOSUB 1000,2000,3000
50 GOTO 10

1000 ' ABORT
1010 PRINT"ABORT":STOP

2000 ' RETRY
2010 PRINT "RETRY":RETURN

3000 ' CONTINUE
3010 PRINT "CONT":RETURN

This was a great technique when dealing with a long list of menu options.

I had tried to optimize this by eliminating the A$ and embedding it inside the INSTR (someone in the comments may have suggested this to me; not sure if I am clever enough to have thought that up):

ON INSTR("ARC",INKEY$) GOSUB 1000,2000,3000

…but if I put that in my code replacing lines 20-40, running it immediately shows me “ABORT” as if INSTR returned 1.

Because INSTR returned 1.

The workaround suggested to me (again, from smart folks in the comments) was maybe to add a bogus value as the first search string character, and have that routine do nothing.

ON INSTR("*ARC",INKEY$) GOSUB 999,1000,2000,3000

However, for my example where I show the prompt again after it returns, it sticks in a loop printing the prompt over and over again. The code thinks the first option is being selected, then calls that routine (the empty routine that is just a RETURN in line 60) and then prints the prompt again.

0 'INSTR2.BAS
10 PRINT "A)BORT, R)ETRY, C)ONTINUE:";
20 ON INSTR("*ARC",INKEY$) GOSUB 60,1000,2000,3000
50 GOTO 10
60 RETURN

1000 ' ABORT
1010 PRINT"ABORT":STOP

2000 ' RETRY
2010 PRINT "RETRY":RETURN

3000 ' CONTINUE
3010 PRINT "CONT":RETURN

SO … it works, but the logic needs to be updated.

One quick solution is to not use RETURN and let each function decide where to go back to. When you GOSUB, BASIC has to scan forward (possibly starting at the top of the program if the line number is before the current line being parsed) to find the target. RETURN lets it “pop” back to right after the GOSUB, so that part is faster.

Also, GOSUB routines can be called from different places in the main code and they will return back to where they were called.

If these routines are never called from anywhere but the menu code, and the extra speed to GOTO back is not a problem, this this change makes it work. And, as a bonus, the fake first GOTO line can just be back to the ON INSTR again since it doesn’t need to do anything:

0 'INSTR3.BAS
10 PRINT "A)BORT, R)ETRY, C)ONTINUE:";
20 ON INSTR("*ARC",INKEY$) GOTO 20,1000,2000,3000

1000 ' ABORT
1010 PRINT"ABORT":STOP

2000 ' RETRY
2010 PRINT "RETRY":GOTO 10

3000 ' CONTINUE
3010 PRINT "CONT":GOTO 10

I am sure there are many other ways to solve this problem.

But why do we have to?

Why does INSTR behave like this? What is the benefit of not returning 0?

Hmmm, A.I. did not exist when I was first exploring this. Maybe I’ll ask one of the ‘bots and see what it knows.

Until next time…

Copy screen memory to a string in Color BASIC???

See Also: part 1 and part 2 (coming soon).

Updates:

  • 2026-05-03 – Corrected hex value of 64 (thanks MiaM).

Today I was tagged in a Facebook post by MC-10 (well, and CoCo) programmer, Jim Gerrie. He shared a snipped of code he was trying to get working. The concept was to have stuff on the 32 column text screen get copied into a normal string and then be able to PRINT it back.

Jim’s post (with the code that wasn’t working) with code for the MC-10 was this:

10 CLEAR1200:DIMJ,K,A$,B$:GOSUB100
20 A$=””:K=VARPTR(A$):POKEK,255:POKEK+1,0:POKEK+2,64:B$=A$
50 CLS:PRINTB$;
60 GOTO60
100 CLS1:PRINT”THIS IS LINE ONE”
110 PRINT”THIS IS LINE TWO”
112 PRINT”THIS IS LINE THREE”
113 PRINT”THIS IS LINE FOUR”
114 PRINT”THIS IS LINE FIVE”
115 PRINT”THIS IS LINE SIX”
116 PRINT”THIS IS LINE SEVEN”
117 PRINT”THIS IS LINE EIGHT”:RETURN

Can someone explain why this program doesn’t work on my TRS-80 MC-10?! It should reassign the memory pointer of string variable A$ to the beginning of screen memory (highbyte 64 lowbyte 0) so that I can then just assign A$ to B$, which will allow me to “capture” the first 255 bytes of screen mem.

It works on the TRS-80 MODEL I/III (using its screen start at highbyte 60 lowbyte 0)!

Any help greatly appreciated.

– Jim Gerrie in the Facebook TRS-80 MC-10 Group.

I could immediately see what the program was attempting to do, and it was something that never occurred to me to try. The concept is “simple” now that I see it:

  1. Stuff is placed on the screen (CLS, PRINT, etc.)
  2. A string (A$) is declared (line 20) and then VARPTR is used to get the memory location of the 5-byte string descriptor for that string. At this point, A$ is zero bytes long, but it will point to somewhere inside the program memory just after the first quote in A=”” because that is where the string begins (even if it is zero bytes long).
  3. The string descriptor is modified using POKE to change the length of the string to 255 bytes, then the start location of the string from that location inside program memory to be the start of the text screen. That was the first bug. The location being POKEd was off by one and was not modifying the string start address properly.
  4. After this, A$ is copied into a normal string, thus saving the contents of the first string (screen memory) into the new string (in normal reserved string memory). This is where the second bug was.

Before continuing, If you need a refreshed on VARPTR, start with this article. It will show how the five byte string descriptor is used. Here is a refresh:

STRING DESCRIPTOR (5 BYTES)
0 - LENGTH OF STRING
1 - NOT USED FOR STRINGS
2 - MSB OF ADDR OF STRING IN MEMORY
3 - LSB OF ADDR OF STRING IN MEMORY
5 - ALWAYS 0 FOR A STRING

The first POKE at the address returned by VARTPR (K) was fine, setting the length to 255. But the next two pokes were at K+1 and K+2. For Color BASIC, they should have been at K+2 and K+3. Also, the values being poked were 0 and 64, which is backwards. From a quick search, the MC-10s text screen starts at the 16K mark, $4000 (16384). To verify this, I went to the online MC-10 emulator here:

https://mc-10.com

…and then did POKE 16384,42. That indeed placed an inverted “*” in the top left of the text screen.

The MC-10 is a big endian processor, so the memory location should be MSB ($40) then LSB ($00). $40 in decimal is 64 in decimal (no HEX support on the MC-10, I don’t think). So the actual pokes to make a string start at the top left corner of the text screen should have been 64 and 0 rather than 0 and 64. (That is 64*256+0 to make 16384.)

Adjusting those POKEs to be at the proper spot in VARPTR and swapping the values to MSB/LSB was the first fix.

At that point, I still wasn’t getting it working. This was due to the initial string being a “hard coded” string in BASIC. When A$=”” was declared in BASIC, it made a string that pointed into the program space. I have not looked into why, but forcing the string to be in RAM by doing A$=””+”” was all I needed to change to make this work. (NOTE TO SELF: Explore this and understand what was different about the program-space string.)

Jim posted a corrected version for the MC-10:

0 CLEAR1200:DIMC1,M$,I$,AA$,BB$:GOSUB100:GOTO20
8 M$="":C1=VARPTR(M$):POKEC1,255:POKEC1+2,64:POKEC1+3,0:AA$=M$+""
9 M$="":C1=VARPTR(M$):POKEC1,255:POKEC1+2,65:POKEC1+3,0:BB$=M$+"":RETURN
20 GOSUB8:CLS:PRINTAA$" "BB$;
60 GOTO60
100 CLS8:PRINT"THIS IS LINE ONE"
110 PRINT"THIS IS LINE TWO"
112 PRINT"THIS IS LINE THREE"
113 PRINT"THIS IS LINE FOUR"
114 PRINT"THIS IS LINE FIVE"
115 PRINT"THIS IS LINE SIX"
116 PRINT"THIS IS LINE SEVEN"
117 PRINT"THIS IS LINE EIGHT"
118 PRINT"THIS IS LINE NINE"
119 PRINT"THIS IS LINE TEN"
120 PRINT"THIS IS LINE ELEVEN"
121 PRINT"THIS IS LINE TWELVE"
122 PRINT"THIS IS LINE THIRTEEN"
123 PRINT"THIS IS LINE FOURTEEN"
124 PRINT"THIS IS LINE FIFTEEN"
125 PRINT"THIS IS LINE SIXTEEN";:RETURN

Meanwhile, I had come up with a silly program that would create some kind of image on the CoCo screen, then capture it in two strings so it could be quickly restored later. Well, almost the entire screen — a string is limited to 255 bytes so two strings captures 510 bytes of the 32×16 screen. If one were to use this trick, it could be adjusted to capture just the number of lines on the screen needed (like, the first 5 lines, or lines 10-20, etc.).

My example looked like this:

0 'SAVESCR1.BAS
10 CLEAR511:DIMS1$,S2$
11 ' 0 = LEN OF STRING
12 ' 1 = NOT USED FOR STRING
13 ' 2 = MSB OF ADDRESS
14 ' 3 = LSB OF ADDRESS
15 ' 4 = ALWAYS 0
16 ' 1024 = 4*256+0
17 ' 1279 = 4*256+255
20 REM DRAW SCREEN
25 CLS0:C=0:FOR I=0 TO 29 STEP 2
30 SET(I*2,0,C):SET(63-I*2,30,C)
35 SET(0,30-I,C):SET(63,I,C)
40 C=C+1:IF C>7 THEN C=0
50 NEXT
55 PRINT@266,"THIS";CHR$(128)"IS";CHR$(128);"COOL";
60 'SAVE SCREEN
65 GOSUB 1000
70 'WAIT FOR KEY
75 GOSUB 5000
80 'CLEAR SCREEN
85 CLS 5
90 'WAIT FOR KEY
95 GOSUB 5000
100 'RESTORE SCREEN
105 GOSUB 2000
110 GOTO 70

999 GOTO 999

1000 ' SAVE SCREEN
1005 Z$="":K=VARPTR(Z$):POKEK,255:POKEK+2,4:POKEK+3,0:S1$=Z$+""
1010 Z$="":K=VARPTR(Z$):POKEK,255:POKEK+2,4:POKEK+3,255:S2$=Z$+""
1015 RETURN

2000 ' RESTORE SCREEN
2005 PRINT@0,S1$;S2$;:RETURN

5000 ' WAIT FOR KEY
5005 IF INKEY$="" THEN 5005
5010 RETURN

I made a “save screen” subroutine at line 1000. My program starts and makes a simple splash screen (colored pixels set around the screen and a text message in the center), then calls the save screen routine which makes a temporary Z$ then modifies it to point to the start of the text screen (1024 – so values 4*256+0) and be 255 bytes long. It then copies that string to S1$. It repeats the process for the next part of the screen, which begins at 1024+255 (1279, so 4*256+255). That is saved in S2$.

Then the main program waits for a keypress, then does a CLS 5 to erase the screen to white, then after another keypress, it calls the “restore screen” subroutine which just prints the two saved strings starting at position 0 on the top left corner of the screen.

AND IT WORKS!

Now blasting bytes to the screen can be as fast as printing a string. I can think of some interesting uses for this, such as “drawing” various levels slowly during initialization, and capturing them to strings so they can be displayed later very fast.

And that gives me an idea for an old project I was playing with some years ago.

But that will have to wait for the next installment…

ASCII Kong text data

Just posting this so I can share the link… This was a planning file from my “ASCII Kong” program I am toying with:

  ----------------------------
12345678901234567890123456789012
H H BONUS
H H 3900
H HXXXXXXX
OO H H H
OO.....H H H
XXXXXXXXXXXXXXXXXXXXXXXXXX
H
XXXXXXXXXXXXXXXXXXXXXXXXXX
H H
XXXXXXXXXXXXXXXXXXXXXXXXXX
H H H
XXXXXXXXXXXXXXXXXXXXXXXXXX
H H
XXXXXXXXXXXXXXXXXXXXXXXXXX
U H
XXXXXXXXXXXXXXXXXXXXXXXXXXXX
12345678901234567890123456789012
----------------------------
-----
@
<=>
/ \
-----
@
/=
|\
-----
@
=\
/|
-----

.----------|----------|----------
1....XX.... .XX.......
2....XX.... .XX.......
3..XXXXXX.. ..XXX.....
4.X..XX..X. .X.XXX....
5...X..X... X..X.XX...
6..X....X.. ..XX..XX..

EXEC versus USR: Who called me?

NOTE: I originally started writing this in November 2025, but kept thinking I’d do more work on it. I haven’t gotten around to it, so here you go…


Here is a Color BASIC 6809 assembly quickie… (That ended up not being very quick by the time I finished working through all of this…)

Recently I began working on an assembly language Color BASIC extension that makes certain characters move the cursor around the screen rather than just printing those characters (similar to what my VIC-20 could do). Initially, I created the 6809 assembly routine you could load into memory and EXEC. Next I decided to let it be called from DEF USR so I could pass in parameters and return a status code like A=USR0(-1). Next next I decided I wanted it to still work with EXEC so the user could use it either way–just use defaults with EXEC, or customize things using USR.

Then I ran into a snag…

USRx(n) or EXEC?

If the USR routine ONLY expected a number parameter, the code to handle both USR and EXEC seems easy. When calling a routine with EXEC, the D register will be zero (it seems). If it wasn’t zero, I could then do the JSR INTCNV call which would process the parameter in BASIC and put it in the D register.

My startup code looked something like this:

; lwasm execdreg.asm -fbasic -oexecdreg.bas --map
; decb copy -2 execdreg.bin drive0.dsk,EXECDREG.BIN

; Show if routine is being called with USRx(n) or EXEC

ORGADDR equ $3e00 ; Where program loads in memory.

; Absolute addresses of ROM calls.
CHROUT equ $A002
INTCNV equ $B3ED
GIVABF equ $B4F4

org ORGADDR

; This code expects to have been called by USRx(x) or EXEC xxxx.
start cmpd #0 ; called from EXEC?
beq fromexec ; if yes, goto fromexec
fromusr jsr INTCNV ; else, get USR number parameter in D
pshs d ; save D
leax usrmsg,pcr ; display "called from USR" message
bsr print
puls d ; restore D
addd #1 ; add one to D
jmp GIVABF ; return back to USR call.

fromexec leax execmsg,pcr ; display "called from EXEC" message
bsr print
rts

; PRINT subroutine. Prints the 0-terminated string pointed to by X plus CR.
print lda ,x+
beq printdone
jsr [CHROUT]
bra print
printdone lda #13
jsr [CHROUT]
rts

usrmsg fcc "CALLED FROM USR"
fcb 0

execmsg fcc "CALLED FROM EXEC"
fcb 0

end

When the routine starts, it checks to see what D is set to. If 0, it assumes it was called from EXEC and jumps to code that just prints “FROM EXEC” then ends.

If not 0, it assumes it was called from USR and the code calls the ROM INTCVT routine to parse the parameter and place it in D, then it prints “FROM USR”, increments D (just so we can verify it passed something back), and returns it back to BASIC.

Here it is in operation:

And all was right in the world… Until I tried just using EXEC by itself. After using it first with the address (“EXEC &H3E00”) BASIC will remembers that address so when you just type “EXEC” next it uses the previous address:

EXEC &H3E00
FROM EXEC

EXEC
?TM ERROR

Making the user always have to provide the EXEC address each time is not optimal. My solution was clearly not a solution.

But wait! There’s more…

I also learned about Sean Conner documenting how USR can also take a string parameter instead of just a number. If you are interested in USR, be sure to check out that link. He also has a cool 6809 compiler (“a09”) I just started playing with. It has some unique features not available in other compilers I have tried.

USRx(n) or USRx(“STRING”)

With this new knowledge, I had an idea to make my USR routine also be able to take a string for a special configuration function. I could let the user specify the four characters that will move the cursor by doing something like A=USR0(“udlr”). But, if you pass in a string and it calls INTCNV, that routine will check the parameter type and, if not a number, return with a ?TM ERROR (type mismatch).

This required me to learn how to tell whether USR was being called with a number or a string.

Under Extended Color BASIC (the original Color BASIC did things differently, see Sean’s page for details), the ROM code sets up some registers when calling the USR function. Sean documented these in his excellent blog post on USR. Basically, register A would be 0 if the USR parameter was a number, or 255 if it was a string. If it was a string, register X would have the address of the string descriptor (the location in memory that VARPTR returns) and register B would be the length of the string.

That is really convenient. Now you can have code that detects if it is being called from USR with a number or a string. My test code looked like this:

; lwasm execdreg2.asm -fbasic -oexecdreg2.bas --map
; decb copy -2 execdreg2.bin drive0.dsk,EXECDRG2.BIN

; Show if USR is being called with a number or a string.

ORGADDR equ $3e00 ; Where program loads in memory.

; Absolute addresses of ROM calls.
CHROUT equ $A002
INTCNV equ $B3ED
GIVABF equ $B4F4

org ORGADDR

; This code expects to have been called by USRx(x) or USRx("STRING")
start tsta ; A=0 is USR(0), A=255 is USR("...")
bne usrstring ; if not 0, goto usrstring
usrnumber pshs d,x ; save D and X
leax numbermsg,pcr ; display "number" message
bsr print
puls d,x ; restore D and X
jsr INTCNV ; else, get USR number parameter in D
addd #1 ; add one to D
jmp GIVABF ; return back to USR call.

usrstring leax stringmsg,pcr ; display "string" message
bsr print
ldd #123 ; load D with return value
jmp GIVABF ; return back to USR call.

; PRINT subroutine. Prints the 0-terminated string pointed to by X plus CR.
print lda ,x+
beq printdone
jsr [CHROUT]
bra print
printdone lda #13
jsr [CHROUT]
rts

stringmsg fcc "STRING"
fcb 0

numbermsg fcc "NUMBER"
fcb 0

end

And here it is in operation:

Now I know how to detect a USRx(number) versus EXEC, and how to detect a USRx(number) versus a USRx(string). But, this has the same problem if called by EXEC with no address:

EXEC &3E00
NUMBER

EXEC
NUMBER
?TM ERROR

It appears that using EXEC with the address after it sets registers up differently than using EXEC with no address (where it uses the last address EXEC used). While both end up at the code path for USRx(number), is seems that plain EXEC thinks it is returning an invalid type and the ?TM ERROR is displayed.

EXEC or EXEC xxxx or USRx(n) or USRx(“STRING”)

Can both routines be combined? On the CoCo mailing list, this all started when I asked: Is there a way to tell if a routine was called from USR versus EXEC? It was Sean’s reply that got me going down this rabbit hole:

Maybe.

Address $9D contains the address EXEC uses to jump to your code, so that
should be called address.  Also, X will also be this address (implementation
detail).

For Color BASIC, you need to know you are running under Color BASIC. 
Address $112 is the address for USR, so this should point to your code. 
Also, upon calling, X should be equal to $AA2F and B should be 6 (both are
implementation details).

For Extended Color BASIC, you need to know you are running under Extended
Color BASIC (16 bits at $8000 are $4558).  Addresses $013E through $0150
contain the USRn addresses, so one of these 10 addresses should point to
your code.  Also, A will equal the contents of address $06.  If A=0, then
X=$4F; if A=255, then X is pointing elsewhere (the string descriptor).

For Disk Extended Color BASIC, you need to know you are running under Disk
Extended BASIC (16 bits at $C000 are $444B).  The USRn addresses are now
$095F through $0971, but other than that, it’s the same as Extended Color
BASIC.

Based on all that, I think the best method might be (completely untested):

mycode cmpx #mycode
beq called_by_exec
; otherwise, assume called by USR/USRn

Good luck.

-spc

– Sean Conner via the CoCo Mailing List

This gave me a lot of think about. I did some tests to see what register X looked like when being called by EXEC with or without an address, as well as looking at what was stored in the $9D memory location which is the address EXEC (with no address after it) will use. I created a simple program that would print the value of the X register and the value of $9D so I could test it and see what the pattern was. This code uses an undocumented ROM call that will print the value of the D register. (I learned about this call from Sean’s pages.)

; lwasm showstuff.asm -fbasic -oshowstuff.bas --map
; decb copy -2 showstuff.bin drive0.dsk,SHOWSTUF.BIN

ORGADDR equ $3e00 ; Where program loads in memory.

; Absolute addresses of items in RAM variables.
EXECJP equ $9d location of jump address for EXEC

; Absolute addresses of ROM calls.
REGDOUT EQU $BDCC ; Convert the value in ACCD into a decimal
; number and send it to CONSOLE OUT.

org ORGADDR

start tfr x,d ; X=D
jsr REGDOUT
lda #32 ; space
jsr [CHROUT]
ldd EXECJP ; load D with EXEC address
jsr REGDOUT
rts

end

Now I could load this into memory, set up a DEFUSR0=&H3E00 and do some tests:

15872 ($3E00) is the start of my user program. EXEC with that address will have both X and the $9D memory location containing that value.

EXEC without an address will have 43947 ($ABAB) in X, and 15872 ($3E00) as the address of the last EXEC address specified. But what is $ABAB? Looking at the Color BASIC Unravelled book, that address is where the EXEC token is:

ABAB     FDB EXEC

I did not dive into this, but I expect X was is used for the token scanning and since that was the last thing it found (no address after it to parse) that is what was in the register when it jumps to the user code.

When I tested A=USR0(0), I got a 79 in register X, and $9D still had the last EXEC address used. It then errored out with a ?TM ERROR due to this code not setting up a clean return back to a USR call.

And lastly, A=USR0(“STRING”) put 425 in register X, and $9D was still the last EXEC address used.

Now, had I done the USR calls first, that $9D would not be set up yet and it would look like this:

46154 ($B44A) appears to be the default value EXEC will use. By default, EXEC points to the routine that prints ?FC ERROR:

B44A     FDB LB44A   ARGUMENT OF EXEC COMMAND - SET TO โ€˜FCโ€™ ERROR

So on a power cycle, typing EXEC is the same as typing EXEC &HB44A:

EXEC &HB44A
?FC ERROR

Having this value there is not useful for any of my checks since all that means is that the user has not done an EXEC with an address yet.

BUT, now that I see what happens with register X, I should be able to check it, and the $9D exec location and determine if I am being called by EXEC, EXEC xxxx, or a USRx command. Here is my test program:

; lwasm whocalled.asm -fbasic -owhocalled.bas --map
; decb copy -2 whocalled.bin drive0.dsk,WHOCALLD.BIN

ORGADDR equ $3e00 ; Where program loads in memory.

; Absolute addresses of items in RAM variables.
EXECJP equ $9d location of jump address for EXEC

; Absolute addresses of ROM calls.
CHROUT equ $A002

org ORGADDR

; This code expects to have been called by USRx(x).
start cmpx #start ; called by "EXEC xxxx"?
beq fromexec ; if yes, goto fromexec
cmpx #$abab ; called by "EXEC"?
bne fromusr ; if no, must be USR. goto fromusr
ldx EXECJP ; get EXEC address
cmpx #start ; called by "EXEC xxxx"?
beq fromexec ; if yes, goto from exec
fromusr leax usrmsg,pcr
lbsr print
rts
fromexec leax execmsg,pcr
lbsr print
rts

; PRINT subroutine. Prints the 0-terminated string pointed to by X plus CR.
print lda ,x+
beq printdone
jsr [CHROUT]
bra print
printdone lda #13
jsr [CHROUT]
rts

usrmsg fcc "FROM USR"
fcb 0

execmsg fcc "FROM EXEC"
fcb 0

end

And here is what it does:

I now have code that can properly (?) detect if it was called from EXEC xxxx, EXEC, or USR. This demo does not handle detecting a string parameter to USR, but … I think it proves it is possible to do it.

With a few more lines of assembly, I came up with this test program:

; lwasm whocalled2.asm -fbasic -owhocalled2.bas --map
; decb copy -2 whocalled2.bin drive0.dsk,WHOCALL2.BIN

ORGADDR equ $3e00 ; Where program loads in memory.

; Absolute addresses of items in RAM variables.
EXECJP equ $9d location of jump address for EXEC

; Absolute addresses of ROM calls.
CHROUT equ $A002
INTCNV equ $B3ED
GIVABF equ $B4F4

org ORGADDR

; This code can be called by USRx(n), USRx("STRING"), EXEC addr or EXEC.
start cmpx #start ; called by "EXEC xxxx"?
beq fromexec ; if yes, goto fromexec
cmpx #$abab ; called by "EXEC"?
bne fromusr ; if no, must be USR. goto fromusr
ldx EXECJP ; get EXEC address
cmpx #start ; called by "EXEC"?
beq fromexec ; if yes, goto from exec
fromusr tsta ; A=0?
beq donumber ; if yes, number passed in. goto donumber.
inca ; inc A so if 255 (string) it will be 0 now.
beq dostring ; if A=0 (was 255), string. goto dostring.
bra unknown ; else, goto unknown (this should never happen).

donumber leax numbermsg,pcr ; show "number" message
bsr print
jsr INTCNV ; get number that was passed in
addd #1 ; add 1 to D
jmp GIVABF ; return new number back to BASIC

dostring leax stringmsg,pcr ; show "string" message
bsr print
ldd #12345 ; load D with a return value
jmp GIVABF ; return that number back to BASIC

fromexec leax execmsg,pcr ; show "from exec" message
lbsr print
rts

unknown leax unknownmsg,pcr ; this should never happen
lbsr print ; show "unknown" message
rts

; PRINT subroutine. Prints the 0-terminated string pointed to by X plus CR.
print lda ,x+
beq printdone
jsr [CHROUT]
bra print
printdone lda #13
jsr [CHROUT]
rts

execmsg fcc "FROM EXEC"
fcb 0

numbermsg fcc "FROM USR(NUMBER)"
fcb 0

stringmsg fcc "FROM USR(STRING)"
fcb 0

unknownmsg fcc "UNKNOWN"
fcb 0

end

And here is what I get after loading this into memory:

DEF USR0=&H3E00
OK

A=USR0(42)
FROM USR(NUMBER)
PRINT A
43

A=USR0("STRING")
FROM USR(STRING)
PRINT A
12345

EXEC &H3E00
FROM EXEC

EXEC
FROM EXEC

I think we may have a winner! The important parts are:

start       cmpx    #start      ; called by "EXEC xxxx"?
beq fromexec ; if yes, goto fromexec
cmpx #$abab ; called by "EXEC"?
bne fromusr ; if no, must be USR. goto fromusr
ldx EXECJP ; get EXEC address
cmpx #start ; called by "EXEC"?
beq fromexec ; if yes, goto from exec
  • If X is the address of the user program, it was called by “EXEC xxx”
  • If not, then if X is NOT $ABAB, it was called by USR
  • Else, it was $ABAB, so the EXECJP ($9D) is checked to see if it is the address of the user program. If it is, it is from EXEC.

I hope that makes sense. If not, think of it like this:

  • X=program start – it was called from EXEC xxxx
  • X=$ABAB and EXECJP=program start – it was called by EXEC.
  • Anything else is USR.

Now what I need from you is to double check my work and tell me if I got this right, and if this method can be relied on.

Comments if ya got ’em!

Until next time…

Color BASIC, Juan Castro and โ€œforbiddenโ€ variables

See Also: Two Letter Variables

Over the years I have shared many tidbits about Color BASIC.

This is another one.

A recent post by Juan Castro to the Groups.IO Color Computer mailing list caught my attention, mostly because he called me out by name in the subject line:

https://groups.io/g/ColorComputer/message/312

Color BASIC variable name limits

As a reminder, Color BASIC allows 1 or 2 character variable names. They must start with a letter (A-Z) and the second character can be either letter (A-Z) or number (A-0). BUT, the BASIC interpreter does let you type longer names for variables, but it only honors the first two characters. Here is a screenshot from a past blog post here (which I’d link to if I was not so lazy):

Color BASIC variables may be very long, but only the first two characters are used.

This is a reminder that, if you try to use variables longer than two characters, you have to make sure you always keep the first two characters unique since “LONGVARIABLE” and “LOST” and “LO” are all the same variable to BASIC.

…but not all variable name limits are the same.

To break the rule I just said, in Color BASIC, some variable names are forbidden. A forbidden variable is one you cannot use because it is already reserved for a keyword or token. For example, FOR is a keyword:roar

FOR I=1 TO 10
PRINT I
NEXT I

Because of this, even though BASIC only honors the first two characters of a variable name, you still cannot use “FOR” as a variable.

FOR=42
?SN ERROR

But you can use “FO”, since that is not long enough to be recognized as a BASIC token or keyword.

FO=42
PRINT FO
42

There are a number of two-character tokens, such as “TO” in the FOR/NEXT statement (“FOR I=1 TO 10”), and “AS” in the Disk BASIC FIELD statement (“FIELD #1,5 AS A$”), as well as “FN” which is used in DEF FN.

AS=42
?SN ERROR

FN=42
?SN ERROR

TO=42
?SN ERROR

This means if you wrote something for Color BASIC or Extended Color BASIC that uses “AS” as a variable, that would not work under Disk Extended Color BASIC.

BASIC ignores spaces

In recent years, someone pointed me to the fact that when scanning a BASIC line (either type in directly or when parsing a line of code in a program), spaces get ignored by the scanner. This means:

N M = 42
PRINT N M
42

That one surprised me when I learned it. This is probably why, when printing two variables, a semicolon is required between them:

N = 10
M = 20
PRINT N;M
10 20

And if you had done this (remember to CLEAR between these tests so variables are erased each time):

N = 10
M = 20
NM = 30
PRINT N M
30
PRINT N;M;N M
10 20 30

By the way, if you have ever wondered about that space printed in front of numeric variables when you do things like “PRINT X”, I covered why this happens in an earlier blog and included a simple patch to BASIC that removes that feature.

How to turn a forbidden variable into a non-forbidden one for fun and profit

Well, Juan Casto showed that using this “BASIC ignores spaces” quirk as a way to use forbidden variables. From his post:

Now it seems obvious. BASIC’s interpreter looks for keywords like “FOR” and will not recognize “F O R” or “FO R” as that token. The detokenizer honors the spaces.

But when it comes to variables, the spaces are ignored by the parser, so “T O” will not match as a token for “TO”, but will be processed as a variable “TO”.

Go figure.

Admittedly, space in two-character variable names look silly, but now I can finally update my old *ALLRAM* BBS to use the variable “TO$” for who a message is to:

FR$="ALLEN HUFFMAN"
T O$="JUAN CASTRO"
SB$="THAT'S REALLY COOL"

Though why would you want to do that… (Please click on that link. That’s a web domain I own… ;)

Not invented here

I suspect this was discovered by the early pioneers of BASIC, likely soon after the original Color Computer was released in 1980. If you know of a reference to this behavior from some early newsletter or magazine article, please let me know.

And as to Juan … thanks for sending me down a BASIC rabbit hole again…

Until next time…

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…

Tackling the Logiker 2025 Vintage Computing Christmas Challenge โ€“ part 1

See Also: Prologue and part 1.

Rules to the Challenge

โ„๏ธ Vintage Computing Christmas Challenge 2025 โ„๏ธ – Logiker

The Pattern

Observations

  1. Since this is a symmetrical pattern, if we can figure out how to draw one quadrant, we can draw the others.
  2. The pattern is 19 characters wide, which contains a center column of asterisks, and a left and right column that are spaces except for the center row of asterisks.
  3. “As if they had planned it,” this means the pattern in each quadrants is 8 characters, matching the number of bits in a byte.

I typed it up to figure out what the bit pattern would be. (Actually, I typed up a bit of it, then pasted that into Copilot and had it tell me the bit pattern.)

.        *
.------*- = 2
.-*-*---* = 81
.--**---- = 48
.-***--*- = 114
.----*--* = 9
.-----*-- = 4
.*-*---*- = 162
.-*-*---* = 81
*******************

That’s a mess, but in the left the “.” would represent the blank space down the left side up to the row of 19 asterisks. After that is the 8-bit pattern with “-” representing a space in the pattern (0 bit) and the “*” representing the asterisk (1 bit).

This let me quickly cobble together a proof-of-concept:

1 READ V
2 A$=STRING$(19,32):MID$(A$,10,1)="*"
3 FOR B=0 TO 7
4 IF V AND 2^B THEN MID$(A$,9-B,1)="*":MID$(A$,B+11,1)="*"
5 NEXT
6 PRINT A$:A$(L)=A$
7 L=L+1:IF L<8 THEN 1
8 PRINT STRING$(18,42)
9 FOR B=7 TO 0 STEP -1:PRINT A$(B):NEXT
10 DATA 2,81,48,114,9,4,162,81
  • Line 10 are the 8 rows of byte data for a quadrant of the snowflake.
  • Line 1 reads the first value from the DATA statement.
  • Line 2 builds a string of 19 spaces, then sets the character at position 10 (in the center) to an asterisk. Every row has this character set.
  • Line 3 begins a loop representing each bit in the byte (0-7).
  • Line 4 checks the read DATA value and ANDs it with the bit value (2 to the power of the the FOR/NEXT loop value). If it is set, the appropriate position in the left side of the string is set to an asterisk, and then the same is done for the right side. To mirror, the left side is center-minus-bit, and the right side is center-plus-bit.
  • Line 5 is the NEXT to continue doing the rest of the bits.
  • Line 6 prints the completed string, then stores that string in an A$() array. L has not been used yet so it starts at 0.
  • Line 7 increments L, and as long as it is still ess than 8 (0-7 for the first eight lines of the pattern) it goes back to line 1 to continue with the next DATA statement.
  • Line 8 once 8 lines have been done, the center row of 19 asterisks is printed.
  • Line 9 is a loop to print out the A$() lines we saved, backwards. As they were built in line 6, they went from 0 to 7. Now we print them backwards 7 to 0.

…and there we have a simple way to make this pattern, slowly:

Logiker 2025 pattern on a CoCo.

On a CoCo 3, adding a WIDTH 40 or WIDTH 80 before it would show the full pattern:

Logiker 2025 pattern on a CoCo 3.

My example program can be made much smaller by packing lines together and removing unnecessary spaces. One minor optimization I already did was doing the bits from 0 to 7 which removed the need to use “STEP -1” if counting backwards. Beyond that, this is the raw proof-of-concept idea of using bytes.

Other options folks have used in past challenges included rune-length type encoding (DATA showing how many spaces, then how many asterisks, to make the pattern) so that probably is worth investigating to see if it helps here.

Then, of course, someone will probably figure out a math pattern to make this snowflake.

What thoughts do you have?

PEEK versus ARRAY in BASIC?

Hat tip to Erico Monteiro for sending me down another quick benchmarking rabbit hole…

NOTE: This technique will work poorly for ASCII TEXT characters, since the PEEK value is not the same as the PRINT CHR$ value for some characters. It works fine with the graphics blocks (128-255). See the example at the end.

In general, I expect PEEK to be faster than looking up a variable. PEEK only has to process whatever is in the parentheses:

V=PEEK(1024)

Parsing the decimal 1024 can be slow. Using hex is faster (&H400). Using a variable can be even faster (unless there are a ton of variables BASIC has to scan to before finding the target one):

V=PEEK(L)

Erico just showed me technique using an array to store all the characters on the CoCo’s 32 column screen. PRINT@ can be used to put characters on the screen quickly, and when you want to PRINT@ the character somewhere else, you can PRINT@ whatever character used to be there by taking it from the array.

I expected PEEK would be faster than accessing elements of an array so I did a test where I looped through 512 characters using PEEK versus an array:

0' peek-vs-array.bas

10 TIMER=0:FORA=1024 TO 1536
20 Z=PEEK(A)
30 NEXT:PRINT TIMER

40 DIMB(511):TIMER=0:FOR A=0 TO 511
50 Z=B(A)
60 NEXT:PRINT TIMER

At line 10, I loop through all the locations of the 32×16 screen. One by one, Z is set to the value of that location. The value of the loop (1024-1536) matches the POKE/PEEK memory location of the screen.

At line 40, I have an array B() that would be loaded with all the bytes in the screen. The elements of the array (0-511) match the PRINT@ location of the screen.

My results:

123
127

Very close, though the array access is slightly slower. I confirmed PEEK is indeed faster … in this example.

Let’s pretend the loop is a “background” screen and we would PEEK and POKE to restore it versus PRINT@ from the array. (In this example, I am just getting what is there and printing that same thing there again, just for timing.)

0' peek-vs-array2.bas

10 TIMER=0:FOR A=1024 TO 1536
20 Z=PEEK(A):POKEA,Z
30 NEXT:PRINT TIMER

40 DIMB(511):TIMER=0:FOR A=0 TO 511
50 Z=B(A):PRINT@A,CHR$(Z);
60 NEXT:PRINT TIMER

And my results:

210
258

PEEK is faster here, too.

But I have now seen “real code” using this to put a CHR$() player graphic on the screen, and erase it as it moved across the screen (restoring the background as it goes) and the array was faster.

This is another example of why benchmarking a specific item is not always useful. For example, if using PRINT to put things on the screen, you are using the 0-511 location which matches the 0-511 array. If using PEEK, you have one location that is the display screen and another that would be the “saved” background screen. Each time you want to update something, you have to take the location (offset) and add it to the background (to get that one) and the foreground (to get that one). That doubling of math could make it slower versus PRINT@ using 0-511 and CHR$(B(x)) using the same 0-511.

So while PEEK is faster by itself, if you do that and need more math to use it, ARRAYs could win.

0' peek-vs-array3.bas

10 'FOREGROUND 0400-05FF (512)
11 'BACKGROUND 3E00-3FFF (512)
20 TIMER=0:FOR A=0 TO 511
30 Z=PEEK(&H3E00+A):POKE&H400+A,Z
40 NEXT:PRINT TIMER

50 DIM B(511):TIMER=0:FOR A=0 TO 511
60 Z=B(A):PRINT@A,CHR$(Z);
70 NEXT:PRINT TIMER

The first test loops 0-511 then uses “math” to calculate the background memory location, and more “math” to calculate the foreground memory location. Twice the math, twice the slowness.

The second does not match because the array and PRINT@ location.

333
259

Array is faster than two maths.

But, we can cut that math in half but have the FOR loop go through screen memory, then only add to that to get the background. &H3E00 (15872) background minus &H400 (1024) foreground is &H3A00 (14848). I am using hex because it is quicker for BASIC to parse an &H value than a decimal value.

0' peek-vs-array4.bas

10 'FOREGROUND 0400-05FF (512)
11 'BACKGROUND 3E00-3FFF (512)
20 TIMER=0:FOR A=1024 TO 1536
30 Z=PEEK(&H3A00+A):POKEA,Z
40 NEXT:PRINT TIMER

50 DIM B(511):TIMER=0:FOR A=0 TO 511
60 Z=B(A):PRINT@A,CHR$(Z);
70 NEXT:PRINT TIMER

Now that the first version only maths one time, it gets faster:

267
259

…but the array still beats it. The slower array/PRINT without math is faster than the faster PEEK/POKE with math.

How would you optimize this further? Comments if you got ’em…

Bonus Code

Use line 110 for keyboard control, or line 115 for random movement.

10 'ARRAYBAK.BAS
20 DIMB(511)
30 'KALEIDOSCOPE
40 CLS0:FORA=0TO42:X=RND(32)-1:Y=RND(16)-1:C=RND(8):SET(X,Y,C):SET(63-X,Y,C):SET(X,31-Y,C):SET(63-X,31-Y,C):NEXT
50 'SAVE SCREEN DATA TO ARRAY
60 FORA=0TO511:B(A)=PEEK(&H400+A):NEXT
70 'L=PRINT@ LOCATION
80 L=111
90 'MAIN LOOP
100 PRINT@L,"<O>";
110 'ONINSTR(" WASD",INKEY$)GOTO110,120,130,140,150
115 ONRND(4)GOTO120,130,140,150
120 NL=L-&H20:GOTO160
130 NL=L-1:GOTO160
140 NL=L+&H20:GOTO160
150 NL=L+1
160 IFNL<.THEN110
170 IFNL>&H1FC THEN110
180 PRINT@L,CHR$(B(L))CHR$(B(L+1))CHR$(B(L+2));:L=NL:GOTO100

Double Bonus

10 'ARRAYBAK.BAS
20 'L=NUMBER OF BLOCKS (0-7)
30 N=7
40 DIMB(511):DIML(N),C(N)
50 'KALEIDOSCOPE
60 CLS0:FORA=0TO42:X=RND(32)-1:Y=RND(16)-1:C=RND(8):SET(X,Y,C):SET(63-X,Y,C):SET(X,31-Y,C):SET(63-X,31-Y,C):NEXT
70 'SAVE SCREEN DATA TO ARRAY
80 FORA=0TO511:B(A)=PEEK(&H400+A):NEXT
90 'RANDOMIZE SHIP POSITIONS
100 FORA=0TON:L(A)=RND(510)-1:C(A)=143+16*A:NEXT
110 'MAIN LOOP
120 FORA=0TON
130 ONRND(4)GOTO140,150,160,170
140 NL=L(A)-&H20:GOTO180
150 NL=L(A)-1:GOTO180
160 NL=L(A)+&H20:GOTO180
170 NL=L(A)+1
180 IFNL<.THENNL=NL+&H1FC:GOTO210
190 IFNL>&H1FC THENNL=NL-&H1FC
200 L=L(A):PRINT@L,CHR$(B(L));:PRINT@NL,CHR$(C(A));:L(A)=NL
210 NEXT:GOTO120

Hacking the Color BASIC PRINT command โ€“ part 7

See Also: part 1, part 2, part 3, part 4, part 5, part 6 and part 7 (and maybe more to come…)

Source code for this series:

https://github.com/allenhuffman/CoCo_BASIC/tree/main/basic_assembly/consmove

After the last post in this series, I took a detour and learned more about how the DEF USR function works in Color BASIC. I am still not confident I am doing it properly, but for now I have “working” code that lets me toggle it on/off using the EXEC command or use DEF USR and call features using a number parameter like A=USR0(x) (returning a status variable) and even passing in a string (used for remapping the cursor movement characters) like A=USR0(“ABCD”)

Every new bit of education (most of it from Sean Conner‘s website) has me repeating: “Wish I knew then what I know now.”

Today I present the current version of the program. Other than trying to clean up the assembly source code a bit, the main thing I have done is try to make the code position independent. Normally, you build code that loads at a specific address in memory and it runs from there. If you try to move that code elsewhere in memory, it fails because the code itself is hard-coded to jump to specific memory locations.

The 6809 processor supports Position Independent Code (PIC) with instructions that are “relative” — instead of “jump to memory location XYX” you can “branch to X spots lower/earlier in the code.” These relative branches mean no matter where the code is loaded in memory, they still work.

Accessing data in the code is done in similar manner. Instead of “load register A with the value at address XYZ”, it can become “load register A with the value that is X bytes lower/earlier in the code.”

Microware’s OS-9 operating system required position independent code since you could load many programs in memory at the same time, and the OS would decide where to put them. I know I used to write quite a bit of assembly code in my CoCo OS-9 days, so at one point I knew how to do this. I mean, I wrote a full Space Invaders-type game in position independent 6809 assembly code back then!

https://github.com/allenhuffman/Invaders09

Since I no longer remembered how to do it, I am having to re-learn PIC and specifically learn how to do it with code that has to used fixed memory locations when going in and out of the Color BASIC ROM code.

Therefore, proceed with caution! My code may still have spots I missed so for now let’s just load and run it at the intended memory location of &H3E00. (You can change the source code to load wherever you want, of course.)

Here is my current version:

; lwasm consmove7.asm -fbasic -oconsmove7.bas --map --list
; decb copy -2 consmove7.bin drive0.dsk,CONSMOVE.BIN

; Allow embedded characters to move the cursor in a PRINT

;USR SET 0 ; Support DEF USR. Comment out for just EXEC

ORGADDR equ $3e00 ; Where program loads in memory

;------------------------------------------------------------------------------
; Definitions
;------------------------------------------------------------------------------
UPCHAR equ 'u ; default character for up
DOWNCHAR equ 'd ; default character for down
LEFTCHAR equ 'l ; default character for left
RIGHTCHAR equ 'r ; default character for right

;------------------------------------------------------------------------------
; Absolute addresses of items in RAM variables
;------------------------------------------------------------------------------
; Direct Page
CURLIN equ $68 ; PV CURRENT LINE # OF BASIC PROGRAM, $FFFF = DIRECT
DEVNUM equ $6f ; device number being used for I/O
CURPOS equ $88 ; location of cursor position in RAM
EXECJP equ $9d ; location of jump address for EXEC
; Others
RVEC3 equ $167 ; console out RAM hook
RVEC12 equ $182 ; inputting a BASIC line
VIDRAM equ $400 ; VIDEO DISPLAY AREA

;------------------------------------------------------------------------------
; Absolute addresses of ROM calls
;------------------------------------------------------------------------------
CHROUT equ $A002
INTCNV equ $B3ED
GIVABF equ $B4F4

org ORGADDR

;------------------------------------------------------------------------------
; This code can be called by EXEC, EXEC xxxx, USRx(n) or USRx("STRING")
;------------------------------------------------------------------------------
start leay start,pcr ; Y=start
cmpx #start ; X=start? (called by "EXEC xxxx")
beq toggle ; if yes, goto toggle
cmpx #$abab ; X=ABAB? (called by "EXEC")
bne fromusr ; if no, goto fromusr
ldx <EXECJP ; else, load X with EXECJP address
cmpx #start ; X=start? (called by "EXEC xxxx")
beq toggle ; if yes, goto toggle
; else, must be USR
fromusr tsta ; compare A to 0
beq donumber ; if A=0, number passed in. goto donumber
inca ; inc A so if 255 (string) it will be 0 now
beq dostring ; if A=0 (was 255), string. goto dostring
bra unknown ; else, goto unknown (this should never happen)

;------------------------------------------------------------------------------
; Restore default up, down, left and right characters
;------------------------------------------------------------------------------
defaults lda #UPCHAR
sta up,pcr
lda #DOWNCHAR
sta down,pcr
lda #LEFTCHAR
sta left,pcr
lda #RIGHTCHAR
sta right,pcr
lbra exitsuccess ; TODO: reorganize to use just "bra"

;------------------------------------------------------------------------------
; A=USRx("STRING")
; X will be VARPTER, B will be string length
;------------------------------------------------------------------------------
dostring tstb ; B=0?
beq defaults ; if yes, goto defaults
cmpb #4 ; is B=4? (4 characters - up, down, left, right.)
bne exiterror ; if no, goto exiterror
ldy 2,x ; load Y with address of string data
ldd ,y++ ; load D with UP and DOWN characters, inc Y twice
std up,pcr ; store them at up and down
ldd ,y ; load D with LEFT and RIGHT characters
std left,pcr ; store them at left and right
bra exitsuccess ; goto exitsuccess

;------------------------------------------------------------------------------
; A=USRx(0)
; INTCNV will get the number parameter into the D register
;------------------------------------------------------------------------------
donumber jsr INTCNV ; get passed in value in D
cmpd #0 ; is D=0? USRx(0)
beq toggle ; if yes, goto toggle
cmpd #1 ; is D=1? USRx(1)
beq install ; if yes, goto install
cmpd #-1 ; is D=-1? USRx(-1)
beq uninstall ; if yes, goto uninstall

;------------------------------------------------------------------------------
; This should never happen
;------------------------------------------------------------------------------
unknown leax msgunknown,pcr ; load X with address of "unknown" message
bsr print ; call the print subroutine
bra exiterror ; goto exiterror

;------------------------------------------------------------------------------
; EXEC would start here
;------------------------------------------------------------------------------
toggle lda savedrvec3,pcr ; test if we have already installed
bne uninstall ; if not 0, then gotouninstall
; else fall through to install
install lda savedrvec3,pcr ; test if we have already installed
bne installed ; if not 0, already installed

; Hijack the CONOUT routine
lda RVEC3 ; get RAM hook op code
sta savedrvec3,pcr ; save it
ldx RVEC3+1 ; get RAM hook address
stx savedrvec3+1,pcr ; save it

lda #$7e ; op code for JMP
sta RVEC3 ; store it in RAM hook
leax newrvec3,pcr ; address of new code
stx RVEC3+1 ; store it in RAM hook

; Hijack the LINE INPUT routine
lda RVEC12 ; get RAM hook op code
sta savedrvec12,pcr ; save it
ldx RVEC12+1 ; get RAM hook address
stx savedrvec12+1,pcr ; save it

lda #$7e ; op code for JMP
sta RVEC12 ; store it in RAM hook
leax newrvec12,pcr ; address of new code
stx RVEC12+1 ; store it in RAM hook

installed leax msginstalled,pcr ; load X with address of "installed" message
bsr print ; call the print subroutine
bra exitsuccess ; goto exitsuccess

;------------------------------------------------------------------------------

exiterror ldd #-1 ; return -1 as an error code
bra return ; goto return
exitsuccess ldd #0 ; return 0 as an error code
return jmp GIVABF ; return value back to USRx()

;------------------------------------------------------------------------------
; PRINT subroutine. Prints the 0-terminated string pointed to by X plus CR
;------------------------------------------------------------------------------
print lda ,x+
beq printdone
jsr [CHROUT]
bra print
printdone lda #13
jmp [CHROUT] ; JMP CHROUT will do an rts.
;rts

;------------------------------------------------------------------------------
; Uninstall hooks and restore original ones
;------------------------------------------------------------------------------
uninstall lda savedrvec3,pcr ; get saved RAM hook op code
beq uninstalled ; if zero, already uninstalled
sta RVEC3 ; restore RAM hook op code
ldx savedrvec3+1,pcr ; get saved RAM hook address
stx RVEC3+1 ; restore RAM hook address

lda savedrvec12,pcr ; get saved RAM hook op code
sta RVEC12 ; restore RAM hook op code
ldx savedrvec12+1,pcr ; get saved RAM hook address
stx RVEC12+1 ; restore RAM hook address

clr savedrvec3,pcr ; zero out to mark unused

uninstalled leax msguninstalled,pcr
bsr print
bra exitsuccess

;------------------------------------------------------------------------------
; Data storage for the string messages
;------------------------------------------------------------------------------
msginstalled fcc "ON"
fcb 0

msguninstalled fcc "OFF"
fcb 0

msgunknown fcc "UNK"
fcb 0

;------------------------------------------------------------------------------
; Do this only if DEVNUM is 0 (console)
;------------------------------------------------------------------------------
newrvec3 tst <DEVNUM ; is DEVNUM 0?
bne savedrvec3 ; not device #0 (console)

; Do this only if NOT in Direct mode
pshs a ; save A
lda CURLIN ; GET CURRENT LINE NUMBER (CURLIN)
inca ; TEST FOR DIRECT MODE
puls a ; restore A
beq savedrvec3 ; if 0, in direct mode

leas 2,s ; remove PC from stack since we won't
; return there

; Now this is the start of what Color BASIC ROM does for PUTCHR:
; PUT A CHARACTER ON THE SCREEN
;LA30A
PSHS X,B,A ; SAVE REGISTERS
LDX CURPOS ; POINT X TO CURRENT CHARACTER POSITION

;checkup
cmpa up,pcr ; is it the up character?
bne checkdown ; if no, goto checkdown
cmpx #VIDRAM+32 ; compare X to start of second line
blt cantmove ; if less than, goto cantmove
leax -32,x ; move up one line
bra cursormoved ; goto checksdone

checkdown cmpa down,pcr ; is it the down character?
bne checkleft ; if no, goto checkleft
cmpx #VIDRAM+512-32 ; compare X to start of bottom line
bge cantmove ; if greater or equal, goto cantmove
leax 32,X ; move down one line
bra cursormoved ; goto checksdone

checkleft cmpa left,pcr ; is it the left character?
bne checkright ; if no, goto checkright
cmpx #VIDRAM ; top left of screen?
beq cantmove ; if yes, goto cantmove
leax -1,X ; move left one character
bra cursormoved ; goto checksdone

checkright cmpa right,pcr ; is it the right character?
bne goLA30E ; if no, goto goLA30E
cmpx #VIDRAM+511 ; is it bottom right of screen?
beq cantmove ; if yes, goto cantmove
leax 1,x ; increment X, skipping that location
bra cursormoved ; goto checksdone

; This is the next instruction after PSHS X,B,A / LDX CURPOS in the ROM.
goLA30E jmp $A30E ; jump back into Color BASIC ROM code

; This is the STX CURPOS / check for scroll routine in the ROM.
cursormoved jmp $A344 ; jump back into Color BASIC ROM code.

; This is the PULS A,B,X,PC at the end of this routine in the ROM.
cantmove jmp $A35D ; jump back into Color BASIC ROM code

savedrvec3 fcb 0 ; call regular RAM hook
fcb 0
fcb 0
rts ; just in case..

;------------------------------------------------------------------------------
; William Astle: "RVEC12 would be right. You can clobber X in this case. You
; would check 2,s to see if it's $AC7F. If it is, you just set CURLIN to $FFFF
; This works around the unfortunate ordering of the instructions in the
; immediate mode loop."
;------------------------------------------------------------------------------
newrvec12 ldx 2,s ; load X with address we were called from
cmpx #$ac7f ; compare X to $AC7F
bne savedrvec12 ; if not that, goto savedrvec12 to return
ldx #$ffff ; else, load X with $ffff (directo mode)
stx <CURLIN ; update CURLINE

savedrvec12 fcb 0 ; call regular RAM hook
fcb 0
fcb 0
rts ; just in case..

;------------------------------------------------------------------------------
; Placed at the end of the program memory for easy patching in the BASIC
; loader DATA statements
;------------------------------------------------------------------------------
up fcb UPCHAR
down fcb DOWNCHAR
left fcb LEFTCHAR
right fcb RIGHTCHAR

end

I compiled it using the lwasm assembler written by William Astle. It can generate a BASIC loader program, which is what this is:

0 'CONSMOVE7
5 CLEAR 200,&3E00
10 READ A,B
20 IF A=-1 THEN 70
30 FOR C = A TO B
40 READ D:POKE C,D
50 NEXT C
60 GOTO 10
70 END
80 DATA 15872,16246,49,141,255,252,140,62,0,39,100,140,171,171,38,7,158,157,140,62,0,39,88,77,39,56,76,39,29,32,72,134,117,167,141,1,80,134,100,167,141,1,75,134,108,167,141,1,70,134,114,167,141,1,65,22,0,130,93,39,226,193,4,38,118,16,174,2,236
90 DATA 161,237,141,1,43,236,164,237,141,1,39,32,106,189,179,237,16,131,0,0,39,20,16,131,0,1,39,20,16,131,255,255,39,107,48,141,0,152,141,85,32,72,166,141,0,238,38,93,166,141,0,232,38,52,182,1,103,167,141,0,223,190,1,104,175,141,0,217,134,126
100 DATA 183,1,103,48,141,0,117,191,1,104,182,1,130,167,141,0,213,190,1,131,175,141,0,207,134,126,183,1,130,48,141,0,185,191,1,131,48,141,0,73,141,13,32,5,204,255,255,32,3,204,0,0,126,180,244,166,128,39,6,173,159,160,2,32,246,134,13,110,159,160
110 DATA 2,166,141,0,139,39,28,183,1,103,174,141,0,131,191,1,104,166,141,0,139,183,1,130,174,141,0,133,191,1,131,111,141,0,109,48,141,0,7,141,200,32,192,79,78,0,79,70,70,0,85,78,75,0,13,111,38,86,52,2,150,104,76,53,2,39,77,50,98,52,22,158,136
120 DATA 161,141,0,87,38,10,140,4,32,45,57,48,136,224,32,49,161,141,0,72,38,10,140,5,224,44,41,48,136,32,32,33,161,141,0,57,38,9,140,4,0,39,25,48,31,32,18,161,141,0,43,38,9,140,5,255,39,10,48,1,32,3,126,163,14,126,163,68,126,163,93,0,0,0,57,174
130 DATA 98,140,172,127,38,5,142,255,255,159,104,0,0,0,57,117,100,108,114,-1,-1

You can RUN this on a CoCo or CoCo emulator to get the code loaded into memory, Then, you have two ways you can use it:

The EXEC method

Once in memory (be sure to CLEAR 200,&H3E00 to keep BASIC from overwriting this memory), you can start it by typing:

EXEC

You will see it print the word “ON” indicating that it has installed the patch. If you was to deinstall it, type EXEC again, and you will see “OFF”. Using EXEC, this is all the control you have – on or off.

The characters that move the cursor default to the lowercase letters “u” (up), “d” (down), “l” (left) and “r” (right). If you want to change them, they are the last for (non negative) numbers in the DATA statements:

120 DATA 161,141,0,87,38,10,140,4,32,45,57,48,136,224,32,49,161,141,0,72,38,10,140,5,224,44,41,48,136,32,32,33,161,141,0,57,38,9,140,4,0,39,25,48,31,32,18,161,141,0,43,38,9,140,5,255,39,10,48,1,32,3,126,163,14,126,163,68,126,163,93,0,0,0,57,174
130 DATA 98,140,172,127,38,5,142,255,255,159,104,0,0,0,57,117,100,108,114,-1,-1

The numbers 117, 100, 108 and 114 are the ASCII characters for lowercase “u”, “d”, “l” and “r”. You can change them, then RUN the program and it will use the four letters you want to move the cursor.

Now, once it is installed, any PRINT done from the program (not from direct mode) will move the cursor when it sees one of those letters. Type in:

10 CLS 0
20 PRINT@200,"XXXXdllllXrrXdllllXXXX";
30 GOTO 30

…and you will see a box made of “X” characters, with the center still showing the CLS background color. Neat.

For more control, there is another way to use this…

The DEF USR method

You can add this routine as a USRx() function by typing:

DEF USR0=&H300

Now you can toggle it ON/OFF (same as typing EXEC) by doing:

A=USR0(0)

Each time you do that you will see “ON” or “OFF” printed to the screen, indicating the status.

If you want to install it rather than toggle, pass in a 1:

A=USR0(1)

That will always print “ON”. It checks and if it was already installed, it just skips re-installing and prints ON.

To disable it, use -1:

A=USR0(-1)

And, if you want to customize the four characters used to move the cursor, you can pass in a four character string. If you wanted it to use uppercase letters, you could type:

A=USR0(“UDLR”)

A will come back with 0 if it worked, or -1 if it did not. If you do not pass in all four characters, you will be a -1 error value back.

And, if you want to restore back to the defaults of lowercase “udlr”, pass in the empty string:

A=USR0(“”)

What I need from you

What I need from you: Code inspection. Ideally, I want to provide this as a fully position independent program so it can be loaded anywhere in memory. Rather than give a 16K and 32K version that load at different addresses, I just want to provide one and the user can load it in memory wherever they want.

Also, if you see issues with how I am interfacing with the BASIC ROM, please let me know. I have only lightly tested this and “it seems to work for me.” Which is not at all testing ;-)

The smaller version.

As a bonus, here is a smaller version with all the USR stuff removed. If you just wanted to play with this and didn’t care about the extra features, start with this one. This one is less than 255 bytes, so I moved the start address up to $3F00 for a 16K system or you could have it at $7F00 for a 32K system.

; lwasm consmove7-exec.asm -fbasic -oconsmove7-exec.bas --map --list
; decb copy -2 consmove7-exec.bin drive0.dsk,CONSMVEX.BIN

; Allow embedded characters to move the cursor in a PRINT
; This is the small version that only supports EXEC.

; Smaller, so it can load at $3f00 (16K) or $7f00 (32K).
ORGADDR equ $3f00 ; Where program loads in memory

;------------------------------------------------------------------------------
; Definitions
;------------------------------------------------------------------------------
UPCHAR equ 'u ; default character for up
DOWNCHAR equ 'd ; default character for down
LEFTCHAR equ 'l ; default character for left
RIGHTCHAR equ 'r ; default character for right

;------------------------------------------------------------------------------
; Absolute addresses of items in RAM variables
;------------------------------------------------------------------------------
; Direct Page
CURLIN equ $68 ; PV CURRENT LINE # OF BASIC PROGRAM, $FFFF = DIRECT
DEVNUM equ $6f ; device number being used for I/O
CURPOS equ $88 ; location of cursor position in RAM
; Others
RVEC3 equ $167 ; console out RAM hook
RVEC12 equ $182 ; inputting a BASIC line
VIDRAM equ $400 ; VIDEO DISPLAY AREA

org ORGADDR

;------------------------------------------------------------------------------
; This code can be called by EXEC
;------------------------------------------------------------------------------
start

;------------------------------------------------------------------------------
; EXEC would start here
;------------------------------------------------------------------------------
toggle lda savedrvec3,pcr ; test if we have already installed
bne uninstall ; if not 0, then gotouninstall
; else fall through to install
install lda savedrvec3,pcr ; test if we have already installed
bne installed ; if not 0, already installed

; Hijack the CONOUT routine
lda RVEC3 ; get RAM hook op code
sta savedrvec3,pcr ; save it
ldx RVEC3+1 ; get RAM hook address
stx savedrvec3+1,pcr ; save it

lda #$7e ; op code for JMP
sta RVEC3 ; store it in RAM hook
leax newrvec3,pcr ; address of new code
stx RVEC3+1 ; store it in RAM hook

; Hijack the LINE INPUT routine
lda RVEC12 ; get RAM hook op code
sta savedrvec12,pcr ; save it
ldx RVEC12+1 ; get RAM hook address
stx savedrvec12+1,pcr ; save it

lda #$7e ; op code for JMP
sta RVEC12 ; store it in RAM hook
leax newrvec12,pcr ; address of new code
stx RVEC12+1 ; store it in RAM hook

installed rts

;------------------------------------------------------------------------------
; Uninstall hooks and restore original ones
;------------------------------------------------------------------------------
uninstall lda savedrvec3,pcr ; get saved RAM hook op code
beq uninstalled ; if zero, already uninstalled
sta RVEC3 ; restore RAM hook op code
ldx savedrvec3+1,pcr ; get saved RAM hook address
stx RVEC3+1 ; restore RAM hook address

lda savedrvec12,pcr ; get saved RAM hook op code
sta RVEC12 ; restore RAM hook op code
ldx savedrvec12+1,pcr ; get saved RAM hook address
stx RVEC12+1 ; restore RAM hook address

clr savedrvec3,pcr ; zero out to mark unused

uninstalled rts

;------------------------------------------------------------------------------
; Do this only if DEVNUM is 0 (console)
;------------------------------------------------------------------------------
newrvec3 tst <DEVNUM ; is DEVNUM 0?
bne savedrvec3 ; not device #0 (console)

; Do this only if NOT in Direct mode
pshs a ; save A
lda CURLIN ; GET CURRENT LINE NUMBER (CURLIN)
inca ; TEST FOR DIRECT MODE
puls a ; restore A
beq savedrvec3 ; if 0, in direct mode

leas 2,s ; remove PC from stack since we won't
; return there

; Now this is the start of what Color BASIC ROM does for PUTCHR:
; PUT A CHARACTER ON THE SCREEN
;LA30A
PSHS X,B,A ; SAVE REGISTERS
LDX CURPOS ; POINT X TO CURRENT CHARACTER POSITION

;checkup
cmpa up,pcr ; is it the up character?
bne checkdown ; if no, goto checkdown
cmpx #VIDRAM+32 ; compare X to start of second line
blt cantmove ; if less than, goto cantmove
leax -32,x ; move up one line
bra cursormoved ; goto checksdone

checkdown cmpa down,pcr ; is it the down character?
bne checkleft ; if no, goto checkleft
cmpx #VIDRAM+512-32 ; compare X to start of bottom line
bge cantmove ; if greater or equal, goto cantmove
leax 32,X ; move down one line
bra cursormoved ; goto checksdone

checkleft cmpa left,pcr ; is it the left character?
bne checkright ; if no, goto checkright
cmpx #VIDRAM ; top left of screen?
beq cantmove ; if yes, goto cantmove
leax -1,X ; move left one character
bra cursormoved ; goto checksdone

checkright cmpa right,pcr ; is it the right character?
bne goLA30E ; if no, goto goLA30E
cmpx #VIDRAM+511 ; is it bottom right of screen?
beq cantmove ; if yes, goto cantmove
leax 1,x ; increment X, skipping that location
bra cursormoved ; goto checksdone

; This is the next instruction after PSHS X,B,A / LDX CURPOS in the ROM.
goLA30E jmp $A30E ; jump back into Color BASIC ROM code

; This is the STX CURPOS / check for scroll routine in the ROM.
cursormoved jmp $A344 ; jump back into Color BASIC ROM code.

; This is the PULS A,B,X,PC at the end of this routine in the ROM.
cantmove jmp $A35D ; jump back into Color BASIC ROM code

savedrvec3 fcb 0 ; call regular RAM hook
fcb 0
fcb 0
;rts ; just in case..

;------------------------------------------------------------------------------
; William Astle: "RVEC12 would be right. You can clobber X in this case. You
; would check 2,s to see if it's $AC7F. If it is, you just set CURLIN to $FFFF
; This works around the unfortunate ordering of the instructions in the
; immediate mode loop."
;------------------------------------------------------------------------------
newrvec12 ldx 2,s ; load X with address we were called from
cmpx #$ac7f ; compare X to $AC7F
bne savedrvec12 ; if not that, goto savedrvec12 to return
ldx #$ffff ; else, load X with $ffff (directo mode)
stx <CURLIN ; update CURLINE

savedrvec12 fcb 0 ; call regular RAM hook
fcb 0
fcb 0
;rts ; just in case..

;------------------------------------------------------------------------------
; Placed at the end of the program memory for easy patching in the BASIC
; loader DATA statements
;------------------------------------------------------------------------------
up fcb UPCHAR
down fcb DOWNCHAR
left fcb LEFTCHAR
right fcb RIGHTCHAR

end

And here is a BASIC loader for that version:

0 'CONSMVEX.BAS
5 CLEAR 200,&H3F00
10 READ A,B
20 IF A=-1 THEN 70
30 FOR C = A TO B
40 READ D:POKE C,D
50 NEXT C
60 GOTO 10
70 END
80 DATA 16128,16339,166,141,0,186,38,59,166,141,0,180,38,52,182,1,103,167,141,0,171,190,1,104,175,141,0,165,134,126,183,1,103,48,141,0,65,191,1,104,182,1,130,167,141,0,160,190,1,131,175,141,0,154,134,126,183,1,130,48,141,0,132,191,1,131,57,166
90 DATA 141,0,121,39,28,183,1,103,174,141,0,113,191,1,104,166,141,0,120,183,1,130,174,141,0,114,191,1,131,111,141,0,91,57,13,111,38,86,52,2,150,104,76,53,2,39,77,50,98,52,22,158,136,161,141,0,85,38,10,140,4,32,45,57,48,136,224,32,49,161,141,0
100 DATA 70,38,10,140,5,224,44,41,48,136,32,32,33,161,141,0,55,38,9,140,4,0,39,25,48,31,32,18,161,141,0,41,38,9,140,5,255,39,10,48,1,32,3,126,163,14,126,163,68,126,163,93,0,0,0,174,98,140,172,127,38,5,142,255,255,159,104,0,0,0,117,100,108,114
110 DATA -1,-1

Until next time…