- 2022-08-02 – Minor assembly optimization using “TST” to replace “PSHS B / LDB DEVNUM / PULS B”, contributed by L. Curtis Boyle in the comments.
Since I wrote part 1, I have learned a bit more about using the Color BASIC RAM hooks. One thing I learned is that the BREAK CHECK RAM hook cannot be used to disable BREAK. This is because other parts of the BASIC ROM jump directly to the break check and do not call the RAM hook. Ah, well. If I really need to disable the break key, at least I know how to do it thanks to the 500 POKES, PEEKS ‘N EXECS for the TRS-80 Color Computer book.
I did want to revisit using the CONSOLE OUT RAM hook and do something perhaps almost useful. The MC6487 VDG chip used in the Color Computer lacks true lowercase, and displays those characters as inverse uppercase letters. Starting with later model CoCo’s labeled as “TANDY” instead of “TRS-80”, a new version of the VDG was used that did include true lowercase, but by default, BASIC still showed them as inverse uppercase.
I remembered having a terminal program for my CoCo 1 that would show all text in uppercase. This made the screen easier to read when calling in to a B.B.S. running on a system that had real lowercase. I thought it might be fun to make a quick assembly program that would intercept all characters going to the screen and translate any lowercase letters to uppercase.
Let’s start by looking at the code:
* lwasm consout2.asm -fbasic -oconsout2.bas --map * Convert any lowercase characters written to the * screen (device #0) to uppercase. DEVNUM equ $6f RVEC3 equ $167 console out RAM hook org $3f00 init lda RVEC3 get op code sta savedrvec save it ldx RVEC3+1 get address stx savedrvec+1 save it lda #$7e op code for JMP sta RVEC3 store it in RAM hook ldx #newcode address of new code stx RVEC3+1 store it in RAM hook rts done newcode * Do this only if DEVNUM is 0 (console) *pshs b save b *ldb DEVNUM get device number *puls b restore b tst DEVNUM is DEVNUM 0? bne continue not device #0 (console) uppercase cmpa #'a compare A to lowercase 'a' blt continue if less than, goto continue cmpa #'z compare A to lowercase 'z' bgt continue if greater than, goto continue suba #32 a = a - 32 continue savedrvec rmb 3 call regular RAM hook rts just in case...
The first thing to point out are the EQUates at the start of the code. They are just labels for two locations in BASIC memory we will be using: The CONSOLE OUT RAM hook entry, and the DEVNUM device number byte. DEVNUM is used by BASIC to know what device the output is going to.
- -3 – used by the DLOAD command in CoCo 1/2 Extended Color BASIC
- -2 – printer
- -1 – casette
- 0 – screen and keyboard
- 1-15 – disk
The BASIC ROM will set DEVNUM to the device being used, and routines use that to know what to do with the date being written. For example:
Device #0 may seem unnecessary, since it is assumed if #0 is not present:
PRINT "THIS GOES TO THE SCREEN" PRINT #0,"SO DOES THIS"
10 INPUT "NAME";A$ 10 INPUT #0,"NAME";A$
But, it is very useful if you are writing code that you want to be able to output to the screen, a printer, a cassette file, or disk file. For example:
10 REM DEVICE0.BAS 20 DN=0 30 PRINT "OUTPUT TO:" 40 PRINT"S)CREEN, T)APE OR D)ISK:" 50 A$=INKEY$:IF A$="" THEN 50 60 LN=INSTR("STD",A$) 70 ON LN GOSUB 100,200,300 80 GOTO 30 100 REM SCREEN 110 DN=0:GOSUB 400 120 RETURN 200 REM TAPE 210 PRINT "SAVING TO TAPE" 220 OPEN"O",#-1,"FILENAME" 230 DN=-1:GOSUB 400 240 CLOSE #-1 250 RETURN 300 REM DISK 310 PRINT "SAVING TO DISK" 320 OPEN"O",#1,"FILENAME" 330 DN=1:GOSUB 400 340 CLOSE #1 350 RETURN 400 REM OUTPUT HEADER TO DEV 410 PRINT #DN,"+-----------------------------+" 420 PRINT #DN,"+ SYSTEM SECURITY REPORT: +" 430 PRINT #DN,"+-----------------------------+" 440 RETURN
That is a pretty terrible example, but hopefully shows how useful device number 0 can be. In this case, the routine at 400 is able to output to tape, disk or screen (though in the case of tape or disk, code must open/create the file before calling 400, and then close it afterwards).
Installing the new code.
The code starts out by saving the three bytes currently in the RAM hook:
init lda RVEC3 get op code sta savedrvec save it ldx RVEC3+1 get address stx savedrvec+1 save it
The three bytes are saved elsewhere in program memory, where they are reserved using the RMB statement in the assembly source:
savedrvec rmb 3 call regular RAM hook
More on that in a moment. Next, the RAM hook bytes are replaced with three new bytes, which will be a JMP instructions (byte $7e) and the two byte location of the “new code” routine in memory:
lda #$7e op code for JMP sta RVEC3 store it in RAM hook ldx #newcode address of new code stx RVEC3+1 store it in RAM hook rts done
There is not much to it. As soon as this code executes, the Color BASIC ROM will start calling the “newcode” routine every time a character is being output. After that RAM hook is done, the ROM continues with outputting to whatever device is selected.
Color BASIC came with support for screen, keyboard and cassette.
Extended BASIC used the RAM hook to patch in support for the DLOAD command (which uses device #-3).
Disk BASIC used the RAM hook to patch in support for disk devices.
And now our code uses the RAM hook to run our new code, and then we will call whatever was supposed to be there (which is why we save the 3 bytes that were in the RAM hook before we change it).
Now we look at “newcode” and what it does.
Most printers print lowercase.
Since a printer might print lowercase just fine, our code will not want to uppercase any output going to a printer. Likewise, we may want to write files to tape or disk using full upper or lowercase. Also, you can save binary data to a file on tape or disk. Translating lowercase characters to uppercase would be a bad thing if the characters being sent were actually supposed to be raw binary data.
Thus, DEVNUM is needed so the new code will ONLY translate if the output is going to the screen (device #0). That’s what happens here:
newcode * Do this only if DEVNUM is 0 (console) tst DEVNUM is DEVNUM 0? bne continue not device #0 (console)
If that value at DEVNUM is not equal to zero, the code just skips the lowercase-to-uppercase code.
uppercase cmpa #'a compare A to lowercase 'a' blt continue if less than, goto continue cmpa #'z compare A to lowercase 'z' bgt continue if greater than, goto continue suba #32 a = a - 32
For characters going to device #0, A will be the character to be output. This code just looks at the value of A and compares it to a lowercase ‘a’… If lower, skip doing anything else. If it wasn’t lower, it then compares it to lowercase ‘z’. If higher, skip doing anything. Only if it makes it past both checks does it subtract 32, converting ‘a’ through ‘z’ to ‘A’ through ‘Z’.
Lastly, when we are done (either converting to uppercase, or skipping it because it was not the screen), we have this:
continue savedrvec rmb 3 call regular RAM hook rts just in case...
The double labels — continue and savedrvec — will be at the same location in memory. I just had two of them so I was brancing to “continue” so it looked better than “bra savedrvec”, or better than saving the vector bytes as “continue”.
By having those three remembered (RMB) bytes right there, whatever was in the original RAM hook is copied there and it will now be executed. When it’s done, we RTS back to the ROM.
When this code is built and ran, it immediately starts working. Here is a BASIC loader that will place this code in memory:
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,16165,182,1,103,183,63,38,190,1,104,191,63,39,134,126,183,1,103,142,63,24,191,1,104,57,13,111,38,10,129,97,45,6,129,122,46,2,128,32,16169,16169,57,-1,-1
If you RUN that code, you can then to a CLEAR 200,&H3F00 to protect it from BASIC, and then EXEC &H3F00 to initialize it. Nothing will appear to happen, but if you try to do something like this:
PRINT "Lowercase looks weird on a CoCo"
…you will see “LOWERCASE LOOKS WEIRD ON A COCO”. To test it further, switch to lowercase (SHIFT-0 on a real CoCo, or SHIFT-ENTER on the XRoar emulator) and type a command like LIST.
If the code is working, it should still type out as “LIST” on the screen, and then give a “?SN ERROR” since it’s really lowercase, and BASIC does not accept lowercase commands.
No going back.
One warning: There is no uninstall routine. Once it’s installed, do not run it again or it will replace the modified RAM hook (that points to “newcode”) with a new RAM hook (which points to “newcode”) and then at the end of the newcode routine it will then jump to the saved RAM hook that points to “newcode”. Enjoy life in the endless loop!
To make this a better patch, ideally the code should also reserve a byte that represents “is it already installed” and check that first. The first time it’s installed, that byte will get set to some special value. If it is ran again, it checks for that value first, and only installs if the value is uninitialized. It’s not perfect, but it would help prevent running this twice.
An uninstall could also be written, which would simple restore the savedrvec bytes back in the original RAM hook.
But I’ll leave that as an exercise for you, if you are bored.
Until next time…
One quick optimization note – where you are preserving B because you use LDB to check the current device number. Since you only care if it is 0, and the TST instruction conveniently does that check for you without modifying any registers, your “PSHS B / LDB DEVNUM / PULS B” can be replace with just “TST DEVNUM”.
That’s something I need to make a habit of. Thanks!
Updated. Which meant updating source code, source code comments, and regenerating the .bas loaded. Ugh. Thanks!
lol… you’re welcome!
savedrvec is an ideal location to use for an initializatiom check.
Replace rmb with fdb $0000
And then your init section can start with
Good idea. Then 00s there would mean first time running, do initialization. I suppose it could also reverse the process, and copy those values back to the vector, and reset SAVEDRVEC to 00s to uninstall, which solves the “no going back” part. I’ll try to add that, and credit your comment.
Another approach to the init test could be to use it to toggle the ram hook diversion on or off.
Call first time, $00 in the save area,
Copy hook and patch to new vector.
Save area <>$00, copy back to ram hook and clear save area.
You mean like EXEC to install, EXEC again to remove?
I think that’s how it should work.
Of course, the next step is building a ram hook manager!
For example, if the code was relocatable, it’d be possible to disable the ram hook modification, move the code to elsewhere in RAM, and then change the hook and the init code would patch the hook to the proper location.
I can’t remember how the LOADM/CLOADM command worked, but I know you could use a number and change the loading address.
Yes, typing (C)LOADM “HOOK”, 10
would load the assembled code 10 bytes higher in memory. I’m not sure if the EXEC address is adjusted or not but I think it’s more likely that EXEC would start at the right place.
I experimented with CLOADM/LOADM. I created a program with ORG 0 to see if I could load it anywhere I wanted. But the moment I didn’t add an offset, it would run a great chance of crashing BASIC as it wiped out low memory system variables :) I suppose you build the program for a small memory system, like 4K or 16K, at the very end, and use offsets to load it at the end of 32K. Not really sure how this was used, but I do recall seeing instructions to add the number for some programs back in the day.