Radio Shack introduced the Color Computer in 1980. It came 4K or RAM, and Microsoft Color BASIC in the ROM. It could be expanded to 16K RAM, which allowed adding a second ROM for Extended BASIC. A plug-in disk interface cartridge came later, with it’s own ROM containing Disk BASIC.
I’ve often wondered what Microsoft used to write the CoCo BASIC ROMs in.
EDTASM+
Around 1982, Radio Shack released the EDTASM+ ROM-Pak for the Color Computer. It was a 6809 assembler for machine language, as well as a debugger. It could load and save files (source code and final binaries) to cassette tape.
There was also Disk EDTASM+, which added some extra features — though the most important one was probably that it could load and save to a disk drive, making that process far faster.
While the CoCo 1 and 2 were basically the same machine, just with redesigned circuit boards and enclosures, the 1986 CoCo 3 was quite different. It could operate in a double speed more, and provided up to 80 columns of text versus the original CoCo’s 32 columns. It also came with 128K — double what the CoCo 1/2 could handle — and could be expanded to 512K (though third party folks figured out how to do 1 an 2 megabyte upgrades).
Unfortunately, Radio Shack never released an update to the EDTASM+ ROM-Pak or disk software. It was still limited to the small memory and screen size of the original 1980 CoCo hardware.
Folks came up with various patches. I had one that patched my Disk EDTASM+ to run in 80 columns on the CoCo 3, in double speed more (faster assembling!) while setting the disk drive step rate to 6ms. It was a much nicer experience coding with long lines!
After this I moved on to OS-9, and used the Microware assemblers (asm and rma) from OS-9 Level 1 and Level 2. I am not sure I touched EDTASM+ again until I played with it on JS Mocha, decades later.
Hitatchi 6309
Hitcachi made a clone of the 6809. This replacement chip had some undocumented features such as more registers and more op codes. EDTASM+ couldn’t help with that, but there were some OS-9 compilers that were updated to support it.
That’s when folks like Robert Gault came to our rescue with enhancements for the original EDTASM+. Robert added support for the 6309, and many new features — including CoCo 3 compatibility.
His EDTASM+ looks like this on a CoCo 3 in 80 column mode:
Robert Gault’s EDTASM+ update.
If you notice the copyright date, you’ll see he has continued to update and support it. Today he offers it in a variety of versions that run on the original CoCo 1/2, a CoCo 3, certain emulators, RGB-DOS support (for hard drive users), CoCoSDC (the modern SD card floppy replacement) as well as supporting things like DriveWire.
You can pick up your own copy for $35 as follows:
EDTASM6309 $35
Robert Gault
832 N.Renaud
Grosse Pointe Woods, MI 48236
USA
e-mail: robert.gault@att.net
There are a number of new features added. Here is the list provided in the README.txt file:
CHANGES TO EDTASM (Tandy Version)
1) Tape is no longer supported; code has been removed.
2) Buffer size increased to over 42K bytes.
3) Directory obtainable from both Editor and ZBUG; V command.
4) Multiple FCB and FDB data per line.
5) FCS supported.
6) SET command now works properly.
7) Screen colors remain as set from Basic before starting EDTASM.
8) Symbol table printed in five names per line on Coco3.
9) On assembly with /NL option, actual errors are printed.
10) Warning error on long branch where short is possible.
11) ZBUG now defaults to numeric instead of symbolic mode.
12) RGB DOS users now have support for drive numbers higher than 3.
13) Hitachi 6309 opcodes are now supported for both assembly and disassembly
including latest discoveries.
14) HD6309 detection is included and if present incorporates a ZBUG error trap
for illegal opcodes and enables monitoring and changing the E,F,V registers
from ZBUG.
15) Coco 3 users can now safely exit to Basic and use their RESET button from
EDTASM.
16) Keyboard now has auto repeat keys when keys are held down.
17) Lower case is now supported for commands, opcodes, options, and symbols.
Take care when loading or saving files or using symbols, ex. NAME does not
equal name, \.A not= \.a, etc.
18) Local names are now supported. Format is A@-Z@ and a@-z@ for 52 local
symbols. New sets of locals are
started after each blank line in the source code. Local
symbols do not appear in or clutter symbol table.
19) Local symbols can only be accessed from ZBUG in expanded form:
ex. A@00023 not A@.
20) Now reads source code files that don't have line numbers. Writes normal
source files with line numbers ( W filename ) or without line numbers
( W# filename ).
21) Macro parameters now function correctly from INCLUDEd files.
22) While in the Editor, the U key will backup one screen in your source file.
23) DOS.BAS can be used to program the F1 and F2 keys on a Coco3. See below.
24) Coco3 WIDTH80 now uses 28 lines of text.
Coco 1&2 versions do require 64K RAM, the Coco 3 version will work with 128K
of RAM. You can assemble 6309 code even if your Coco has a 6809 cpu.
It also adds some new commands:
V - obtains a directory from either Editor or ZBUG modes.
U - scrolls backwards through source code.
FCS - is used exactly like FCC but automatically add $80 to the last character
in the string.
FCB, FDB - for multiple entries per line entries should be seperated by a
comma. Make sure that the comment field for that line DOES NOT CONTAIN ANY
COMMAS or an error will result.
New ‘V’ directory command in Robert Gault’s EDTASM+ update.
If you are wanting to do some CoCo assembly language programming, I highly recommend you sending $35 to Robert and pick up a copy of his version. EDTASM+ is tricky to learn, and his updates make it a bit less tricky.
This website, “designed and coded” by Gwilym Thomas, is amazing. If has a spot where you can enter 6809 assembly source code, then you can compile and run it!
It even has a few sample programs you can select and try out.
While it runs, you see the registers update, as well as a source-level debugger showing what op codes are currently executing. You can set break points, and memory watch points, too.
It also provides text output in the form of the MC6847 VDG chip (used by the CoCo, and a few other systems). The graphics mode is different VDG. While it supports some similar resolutions, it also adds a 16-color display.
The screen memory is mapped to $400 (1024) just like the CoCo, so you can run stuff like this:
If you past that in to the Assembly language input window and then click Assemble source code, you will see the text characters in the Text screen preview window cycling through values. Neat!
The graphics screen starts just past the text screen at $600 (1536). I think that might be where it started on a non-Disk Extended Color BASIC system. (See my article about memory on the CoCo for more details.)
The documentation notes this about the modes:
The graphics screen is a memory-mapped display of 6144 bytes of RAM beginning at address $0600. There are 3 graphics colour modes, in which either 1, 2, or 4 bits represent a single pixel in 2, 4, or 16 colours respectively. Addresses increase left to right, top to bottom as for the text screen.
Columns and rows are zero-base with (0, 0) at the (left, top). Sequences of bits (1, 2, or 4) from high to low represent pixels from left to right. The 2 colour mode has 256 pixels by 192, the 4 colour 128 by 192, each line being 32 bytes. The 16 colour mode has 128 pixels by 96, each line being 64 bytes.
Example: in 4 colour (2 bit) mode pixel (93, 38) would be in byte $0600+(3832)+trunc (93/4), because there are 4 pixels in a byte. The colour value (0..3) would be stored in bits 5 & 4, ie. shifted left ((4-1)2)-((93 mod 4)*2 times).
Changing screen modes is NOT done via simulated VDG registers. Instead, it has code that looks like this:
ldd #$0204 ; select 4 colour graphics mode
swi3
I have not been able to find details on what values represent what mode. Also, the documentation says there is keyboard input:
Click the text screen panel then start typing for the emulator to receive keyboard input. Remember that (due to limitations of the emulated hardware) when lower case characters are printed to the screen they will appear in inverse video.
As far as the 6809 assembler goes, it does not parse all of the extensions that the LWTOOLS’ lwasm assembler supports, so I have been modifying my projects to be compatible with the emulator’s assembler. This has let me, with minor changes for things like ROM calls, test and debug my code in a way that is impossible on actual hardware.
2021-9-17 – Added revisions suggested by William “Lost Wizard” Astle.
Hello, folks who actually know how to program 6809 assembly.
Can you make this more better?
The idea is to look at each byte on the 32-column VDG screen (from 1024 to 1535) and if the high bit is set (>128, thus a semigraphics block character), add 16 to it. That moves it to the next color. If it rolls over, adjust it back to the proper range.
i.e., if the block is 250, and it adds 16, you end up with 10. Setting the high bit again bumps it back to the 128-255 range. Initially, I did this using a compare to #128, then using ADDA of #128. Then I looked up the BIT test. Rather than compare to decide if I need to do something, I just do the OR to set that high bit back. Seems to work. Maybe faster than checking each byte?
Does this make sense?
Please and thank you.
William Astle Updates
William Astle immediately left these comments:
I’d use “BPL” instead of “BITA/BEQ”. (Bit 7 is the sign bit and LD and ST set flags.) Also, I’d do “LDA ,X+” and then if updating the screen, do “STA -1,X”. That removes the need for the “LEAX”.
William Astle
BPL is “Branch if PLus”. When a register value is used to represent values that can be negative (-127 to 128 versus 0 to 255), the high bit is used to indicate negative. I expect BPL is basically “branch if the high bit is not set.”
When I “LDA ,X”, I am loading register A with whatever is pointed to by X. Adding the plus “LDA ,X” will load A then increment X. Since STA allows an offset, so you can do things like “STA 5,X” to start the value in register A 5 locations past wherever X points. Doing a -1 stores it one byte earlier. That eliminates me needing to use “LEAX” to add one to X.
Previously, a code snippet shared by Jim McClellan on Facebook motivated me to benchmark various methods of reading the CoCo keyboard in BASIC:
10 KBD=PEEK(135) AND PEEK(65282) 20 PRINT KBD 30 GOTO 10
Unfortunately, as I started my stream of consciousness article, I discovered that this sample code did not work on the Xroar emulator I was using. I recalled there had been some differences to Color BASIC’s keyboard scanning code in later versions, and thought this might be why.
I think it would only work on Color Basic 1.2 or 1.3. The reason for that is Color Basic 1.2 introduced an optimization where it only does the full matrix scan if no keys (or joystick buttons!) are pressed. It does this by writing a 0 to FF02 to “strobe” all columns. That means there will be at least one 0 bit in FF00 representing a key pressed if anything is down at all. If it detects nothing, it just exits, leaving the column strobe set to 0.
Prior to Color Basic 1.2, the matrix scan was always run. In that case, if no keys are down, it will have strobed all columns in sequence (by writing a zero bit to successive bits of FF02). This zero bit is shifted over by a ROL instruction so it eventually shifts off into carry and the final result in FF02 is FF. Eventually, most versions will eventually strobe no columns by writing FF to FF02 which is used to detect joystick buttons. The debounce check also puts a nonzero, non FF value in FF02 corresponding with the column where a key was detected. Depending on the Color Basic version, the final results of KEYIN and FF02 are (assuming I read the code correctly):
1.0: FF if no key jor joystick button, non-FF if key down (or debounce fails)
1.1: FF if no key or joystick button, non-FF if key down (or debounce fails)
1.2: 0 if no key or joystick button, FF if key or joystick button, not 0 or FF if debounce fails
1.3: same as 1.2; no modifications to KEYIN
Coco3: always FF (the check for a key optimization is removed) except when debounce fails
Basically, this trick only works on Color Basic 1.2 and 1.3. And even then, it isn’t actually reliable since you won’t get FF if debounce fails. Instead, you’ll potentially get a value with a single zero bit which will corrupt your key code. The odds of the timing being just right for that to happen are really small. However, it is possible.
So my considered opinion based on the variances between ROM versions is that this trick must not be used.
As a side note: aside from the system initialization code, KEYIN has the most significant changes between Color Basic versions.
William Astle
With this note, I now have to change my original plan for this article. I was going to see how much faster this code would be with normal tricks (HEX versus decimal constants, variables instead of constants, etc.) to see which approach would make any game using this code faster.
But since this sample only works with specific versions of Color BASIC, I now need to come up with a more portable non-portable way to do this. This sent me back to some other Facebook posts from Ben Jimenez and Jim Gerrie back in April 2020:
When I run this under Xroar using Color BASIC 1.1, I see that it will print a repeating series of numbers as I hold down each arrow key. I also notice that it repeats for some other keyboard presses, but not all.
But why? A new mystery!
I suspect this is because those POKEs must have something to do with the row or column the four arrow keys are in the keyboard matrix:
1 2 3 4 5 6 7 8
| | | | | | | |
1 --- @ --- A --- B --- C --- D --- E --- F --- G
| | | | | | | |
2 --- H --- I --- J --- K --- L --- M --- N --- O
| | | | | | | |
3 --- P --- Q --- R --- S --- T --- U --- V --- W
| | | | | | | |
4 --- X --- Y --- Z -- UP -- DWN - LFT - RGT - SPACE
| | | | | | | |
5 --- 0 -- 1! -- 2" -- 3# -- 4$ -- 5% -- 6& -- 7'
| | | | | | | |
6 -- 8( -- 9) -- :* -- ;+ -- ,< -- -= -- .> -- /?
| | | | | | | |
7 -- ENT - CLR - BRK - ALT - CTL - F1 -- F2 - SHIFT
All four arrow keys are in row 4, and they repeat. I am betting that the other keys that repeat are the ones in the same columns UP, DOWN, LEFT and RIGHT are in (col 4-7). A quick test shows that @, A and B to not repeat (they use columns 1, 2 and 3), but C, D E and F do (columns 4-7) and G does not (column 8).
Mystery solved.
Whatever those POKEs are doing, they are resetting something involving columns 4, 5, 6 and 7. This sends me back into the Color BASIC Unraveled disassembly to see what memory locations 341 (&H155) to 344 (&H158) are:
0152 KEYBUF RMB 8 KEYBOARD MEMORY BUFFER
Eight bytes, and we are setting four of them to 255 (all bits set to 1). No help so far. I start making notes:
Next, I search the code to see where this KEYBUF is being used, and then things get confusing. The disassembly has multiple code listings that reference this — different code for different versions of the ROM. Great.
KEYIN konfusion
As William Astle referenced, this buffer is used by the KEYIN routine and there are multiple implementations. The code is described as:
KEYIN
SCAN THE KEYBOARD FOR A KEY DEPRESSION - Return zero
flag = 1 if no new key down. Return the ASCII value
of the key in ACCA if a new key is depressed.
Ah, this looks familiar. As soon as I read this, I remembered something from learning CoCo 6809 programming using the EDTASM cartridge.
POLCAT ROM routine digression
Microsoft, thinking ahead to potential changes in the ROM code, set aside a few “documented ROM calls” that assembly language programmers could use. If you wrote an assembly program for Color BASIC 1.0 and wanted to use the KEYIN routine in ROM by jumping directly to its address in the ROM, that program would not work in later Color BASIC 1.2 versions that had KEYIN at a different address.
Instead, Microsoft placed a few hooks in ROM starting at location &HA000. Each entry was the address of a routine elsewhere in the ROM. By using a “jump by reference” assembly instruction, you could end up at wherever those addresses point. POLCAT was the entry for getting a keystroke, and it points to KEYIN:
0002 A000 A1 CB POLCAT FDB KEYIN GET A KEYSTROKE
0003 A002 A2 82 CHROUT FDB PUTCHR OUTPUT A CHARACTER
0004 A004 A7 7C CSRDON FDB CASON TURN ON CASSETTE MOTOR, START READING
0005 A006 A7 0B BLKIN FDB GETBLK READ A BLOCK FROM CASSETTE
0006 A008 A7 F4 BLKOUT FDB SNDBLK WRITE A BLOCK TO CASSETTE
0007 A00A A9 DE JOYIN FDB GETJOY READ JOYSTICKS
0008 A00C A7 D8 WRTLDR FDB WRLDR TURN ON MOTOR AND WRITE $55’S TO CASSETTE
That was the official documented way to see if a key was pressed. The implementation of what POLCAT points to changes between ROM versions, so while this compatible call would return a key, the way that it obtained that key changed.
Here’s a short assembly program using POLCAT (KEYIN).
Using the Color BASIC POLCAT ROM routine.
When I assemble that in EDTASM (“A/IM/WE/AO”) and execute from ZBUG (“Z”, “G START”), I can start pressing keys and see them POKEd to the top left corner of the screen, and moving forward as I press different keys. Holding down a key does not repeat. Thus, the KEYIN routine does not support keyboard repeat.
But I digress.
When Jim’s original code PEEKs two values from memory location 135 (last key pressed) and 65282 (&HFF02, the PIA chip that reads the eight keyboard columns of the keyboard matrix), it apparently relying on code in the Color BASIC ROM that is resetting things so it always updates. In the 1.1 ROM I am using, the ROM code is different so that method does not work.
However, the second bit of code (from either Jim or Ben) using the four POKEs and INKEY$ does appear to work on 1.1 as well as 1.2.
Dam this stream of consciousness!
Sorry to disappoint, but I’ll let someone else write an article explaining the differences in KEYIN. I can already see this is over my head without spending substantial time diffing through the various ROM versions…
At this point, I see WHAT works, but cannot explain WHY. In the next installment, I’ll move beyond the WHY and get back to the original goal of trying to make the WHAT work as fast as possible.
Welcome to the first installment of my new column, Allen’s Old CoCo Notebook. I will be sharing scans of pages from my original notebook I used during my earliest days with the Color Computer, and hopefully figuring out what all the stuff on them means.
Today’s installment deals with this page of hand written 6809 assembly:
CoCo Notebook: unknown 6809 disassembly
If memory serves, seeing the use of greater than “>” and less than “<” tells me this was a disassembly of some code, most likely done via Radio Shack’s EDTASM+ (either the cartridge that I started out with, or the disk version that I used when I got a disk drive).
But what does it do? First, let me try to transcribe it, hopefully without typos.
The lack of memory addresses prevents me from knowing if this was a disassembly of a routine in the Color BASIC ROMs, but there are at least a few addresses that might offer some clues. I will consult the Color BASIC Unravelled book series and see what is at those locations.
CLRA
COMB
NEG <0
LDU #16A
Address $16a is indeed an addressed used by Color BASIC. It is an entry inside the RAM vector table At that location are three bytes which point to the “CONSOLE IN” input routine. Where it goes depends on what level and version of BASIC is installed. It seems to be loading that address into the U (user stack?) register of the 6809.
PULA A,X
STA >263C
STX >263D
PULA A,X is pulling whatever is on the stack (pointed to by U) into the A and X register. I believe this would load the 8-bit value at $16A into A, and the following two bytes into the 16-bit X register. At the CONSOLE IN location should be a “JMP $xxxx” instruction, so this seems to be loading the JMP and address into registers.
It then stores them at $263c (JMP) and $263d. These locations are in the lower 32K address space so this is likely memory being used by some machine language program loaded into RAM — possibly its own dispatch table. That location should low look like:
$263c JMP $263d $xx $263e $yy
…where $xxyy is the address for CONSOLE IN in the ROM. This code is saving the current vector, likely so it can be restored when the program ends or, more likely, used for something else later.
LDX #261D
STX >16B
Next it loads the address $261d into register X. This is likely the address of some routine in the above-mentioned machine language program.
It stores that address at $16b, which is the address portion of the 3-byte CONSOLE IN vector entry. Thus, originally that looked like:
$16a JMP $16b $xx $16c $yy
…and it is replaced with:
$16a JMP $16b $26 $16c $1d
This code is preserving where CONSOLE IN currently goes, then pointing it to a new routine. I am guessing that after this routine does whatever it does, it then calls the saved ROM CONSOLE IN code to complete the task.
I am now guessing that this may be part of a remote terminal driver — a program like REMOTE from a 1983 Rainbow magazine that would patch the input/output of BASIC to the serial port and allow for running a BBS.
LDD <8A
STD >2600
JMP >AC7C
In the Color BASIC Unravelled book, the entry for memory location $8a says “PV DUMMY – THESE TWO BYTES ARE ALWAYS ZERO.” The same comment is found in Extended Color BASIC Unravelled, and Disk Extended Color BASIC Unravelled. But, for whatever reason, this code is loading these two bytes and saving them at location $2600.
It then jumps to $ac7c, which is in ROM space. That location says “THIS IS THE MAIN LOOP OF BASIC WHEN IN DIRECT MODE.” It looks like we have patched CONSOLE IN and then returned control to BASIC.
The next bit of code is an an unknown address, but since it is after a JMP it is probably the start of a new routine.
CLR <70
STX ,S
LDX >263F
This code sets the byte at $70 to zero. Or something. The comment for that location is “PV START OF STRING STORAGE (TOP OF FREE RAM)”.
It then saves X on the stack (or wherever the S stack pointer is pointing?), then loads X (indirect) with whatever two bytes are pointed to by $263f. Earlier we saved the CONSOLE IN entry at $263c to $263e, so this seems to be directly after those three bytes.
LDA ,X+
STX >263F
TSTA
BNE 3F3A
Here it is loading A with a byte from X, which currently points to $263f. It then increments X by one. X must be a pointer to the next byte to be processed.
TSTA will see if A is zero. If it is not equal, it will branch (BNA) to $3f3a. This appears to be some kind of loop that goes through that memory until it finds a zero, updating the pointer along the way. I am betting this is some kind of buffer routine.
If A is zero, we continue to this next bit of code:
A is loaded with an 8-bit value that $263d points to, which is the saved address of BASIC’s CONSOLE IN routine. It stores it at >16A, the first byte of the RAM vector for CONSOLE IN.
It then stores X at $16b and $16c. That restores the CONSOLE IN RAM vector back to what it was. This must be shutdown code.
The code that follows is probably cleanup code — though loading A with #0d (13, a carriage return) looks interesting.
…and then I stopped disassembling.
But why?
But why was I doing this? If it was code from Rainbow (REMOTE), I would have had a copy and not need to disassemble.
Ah, this looks promising. And RSOUT is defined as:
RSOUT EQU $8E0C
And that address is somewhere in Extended Color BASIC. Turning to the Extended BASIC Unravelled book shows that to be “SEND CHAR IN ACCA OUT OVER RS232 OUTPUT.”
Bingo! The branch in my disassembly ends up at the CoCo ROM’s serial output routine.
It looks like I may have been disassembling REMOTE by Dan Downard. I believe it was the original version because of the date (1983) and because the next update (REMOTE2) moved its location to $7d00, requiring 32K.
Now I can look through the published source for REMOTE and find what they code was.
Moments later…
Unfortunately, I cannot find any matches for this code in the tiny ~130 byte REMOTE program. It looks like the branch location that matched up with REMOTE may have been a coincidence, or this code might have been a modified version of REMOTE. I recall at some point I customized one of the later versions (maybe after 1986) for some reason, but I had a printer by then and was not using this notebook.
Well, mystery not solved.
Do you recognize this code? Any clue what it goes to? Can you correct some of the things in my interpretation I did not understand?
Microsoft Visual Studio Code for Mac (left) with extension to color code BASIC code for easy loading into Xroar emulator (right).
As mentioned elsewhere, there are some CoCo-related extensions available for the Microsoft Visual Studio Code editor. I did not realize that MS released this editor for Mac and Linux as well as Windows.
Previously, we finally got to do something semi-useful with assembly: we replaced a slow full-screen scrolling routine in BASIC with a turbo-charged assembly routine, all called via the DEFUSR command.
Today, let’s apply this concept a bit further with the shell of a Pac-Man style video game written in BASIC, but enhanced with assembly.
In my Optimizing Color BASIC, part 3 article, I set the groundwork for writing a game in BASIC that involved moving a character around the screen and detecting collisions with enemy characters. Today I will combine that with the previous maze demo and create the world’s easiest Pac-Man game (no enemies, and no bothersome dots to eat).
The Maze
A few years ago, I started toying with a video output project for the Arduino computers. I began by simply bouncing a circle around the screen and then, for some reason, turned that in to an animated Pac-Man. This led me to digging in to some wonderful websites that had reverse engineered the original Pac-Man source code to explain how everything worked. You can find the series here:
Although I have yet to finish the game, I learned quite a bit about how Pac-Man works, including how the ghosts behave. I don’t know if BASIC would be fast enough to handle the logic of four ghosts and all the other stuff, but it sure would be fun to try — it would be much easier to write it in BASIC than C, I think.
But I digress.
The reason I mention this series is so I can show this picture:
Pac-Man!
For the Arduino project, I started with a screen shot of the original game and downsized it to fit the low resolution, black and white Arduino TVOut graphics library. It ended up looking like this:
Arduino Pac-Man!
Pac-Man was designed on a tile system. The original game resolution was 224×288. The screen was made up of 8×8 tiles, 28 across and 36 down. Without the score lines at the top and the players left lines at the bottom, the playfield itself was 28×31. The maze tiles looked like this:
Pac-Man maze tiles.
…and since the CoCo’s screen is 32×16, if we used one character per tile, we could replicate the same horizontal dimensions, but we’d need to scroll up and down to get to all 31 lines of the maze.
I was initially working on this for a 4K programming challenge I started (and have yet to complete). Using ASCII, the make looks like this:
XXXXXXXXXXXXXXXXXXXXXXXXXXXX
X XX X
X XXXX XXXXX XX XXXXX XXXX X
X X X X X XX X X X X X
X XXXX XXXXX XX XXXXX XXXX X
X X
X XXXX XX XXXXXXXX XX XXXX X
X XXXX XX XXXXXXXX XX XXXX X
X XX XX XX X
XXXXXX XXXXX XX XXXXX XXXXXX
X XXXXX XX XXXXX X
X XX XX X
X XX XXX--XXX XX X
XXXXXX XX X X XX XXXXXX
X X
XXXXXX XX X X XX XXXXXX
X XX XXXXXXXX XX X
X XX XX X
X XX XXXXXXXX XX X
XXXXXX XX XXXXXXXX XX XXXXXX
X XX X
X XXXX XXXXX XX XXXXX XXXX X
X XXXX XXXXX XX XXXXX XXXX X
X XX XX X
XXX XX XX XXXXXXXX XX XX XXX
XXX XX XX XXXXXXXX XX XX XXX
X XX XX XX X
X XXXXXXXXXX XX XXXXXXXXXX X
X XXXXXXXXXX XX XXXXXXXXXX X
X X
XXXXXXXXXXXXXXXXXXXXXXXXXXXX
It may look odd presented as Xs. and the aspect ratio is different, but it’s the exact Pac-Man layout used in the arcade. Here is the full play field that will scroll on the CoCo’s 32×16 screen:
Pac-Man full maze.
Since the original Pac-Man played on a monitor that was turned sideways, it was taller than it was wider. Most home ports either shrink the screen down, or flatten it out. By scrolling, maybe we can keep the aspect ratio similar.
And this is how my ASCII Pac-Man maze came to be.
As I referenced at the top of this article, I have been covering ways to Optimize Color BASIC in another article series. A recent part discussed reading the keyboard and moving a character around the screen. I took some of this code and used it to place a character in the Pac-Man maze and move it around. I also added collision detection making sure the player could not run through any of the walls.
Today I would like to present my work-in-progress Pac-Man maze, entirely in BASIC, and the changes I made to integrate the screen moving assembly routines. The assembly calls (and all the DATA statements) are in this listing, but are commented out. The ‘commented-out ines in red are what lines to uncomment to see the assembly-enhanced version, and any line just in red is the BASIC version that would need to be commented out.
The Listing
Here is the current listing, with comments to follow explaining how it works. I have been writing this on my Mac in a text editor, then loading it in to the XRoar emulator for testing. Because of this, you will notice I put spaces between program sections to make them easier to see. When this loads in to an emulator as an ASCII program, those empty lines are ignored. It works out nice.
0 REM
1 REM PAC-MAZE 1.00
2 REM BY ALLEN C. HUFFMAN
3 REM WWW.SUBETHASOFTWARE.COM
4 REM
6 REM
7 REM
8 REM
9 'CLEAR200,&H3F00
10 DIM MZ$(30)
15 REM
16 REM READ MAZE IN TO ARRAY
17 REM
20 FOR A=0 TO 30:READ MZ$(A):NEXT
21 'GOSUB2000:DEFUSR0=&H3F00
25 REM
26 REM UP+DOWN+LEFT+RGHT CHARS
27 REM
30 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9)
35 REM
36 REM PLAYER/WALL/BG CHARS
37 REM
40 PC=159 'PAC-MAN CHAR
41 WC=ASC("X") 'WALL CHAR
42 BG=96 'BACKGRND CHAR
50 REM
51 REM INITIALIZATION
52 REM
60 ST=7 'SCRN START LINE
61 PM=1360 'PAC-MAN START LOC
62 DR=0 'CURRENT DIRECTION
63 DN=0 'NEXT DIRECTION
80 REM
81 REM DRAW INITIAL MAZE
82 REM
90 CLS:FOR A=0 TO 15:PRINT @A*32+2,MZ$(A+ST);:NEXT
100 REM
101 REM MAIN LOOP
102 REM
110 POKE PM,PC
120 A$=INKEY$:IF A$="" THEN 140
130 KB=INSTR(KB$,A$):IF KB=0 THEN 140 ELSE DN=KB
135 REM TRY NEXT DIRECTION
140 ON DN GOSUB 500,600,700,800
145 REM THEN TRY CURRENT DIR
150 IF DR<>DN THEN ON DR GOSUB 500,600,700,800
160 GOTO 100
500 REM
501 REM UP
502 REM
510 IF PEEK(PM-32)<>BG THEN RETURN
520 POKE PM,BG:DR=1
530 IF PM<1183 AND ST>0 THEN ST=ST-1:GOSUB 950 ELSE PM=PM-32
540 RETURN
600 REM
601 REM DOWN
602 REM
610 IF PEEK(PM+32)<>BG THEN RETURN
620 POKE PM,BG:DR=2
630 IF PM>1376 AND ST<15 THEN ST=ST+1:GOSUB 900 ELSE PM=PM+32
640 RETURN
700 REM
701 REM LEFT
702 REM
710 IF PEEK(PM-1)<>BG THEN RETURN
720 POKE PM,BG:DR=3
730 PM=PM-1:RETURN
800 REM
801 REM RIGHT
802 REM
810 IF PEEK(PM+1)<>BG THEN RETURN
820 POKE PM,BG:DR=4
830 PM=PM+1:RETURN
900 REM
901 REM SCROLL SCREEN UP
902 REM
910 FOR A=0 TO 15:PRINT @A*32+2,MZ$(A+ST);:NEXT
915 'Z=USR0(1):PRINT@482,MZ$(ST+15);
920 RETURN
950 REM
951 REM SCROLL SCREEN DOWN
952 REM
960 FOR A=0 TO 15:PRINT @A*32+2,MZ$(A+ST);:NEXT
965 'Z=USR0(2):PRINT@2,MZ$(ST);
970 RETURN
999 GOTO 999
1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1001 DATA "X XX X"
1002 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1003 DATA "X X X X X XX X X X X X"
1004 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1005 DATA "X X"
1006 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1007 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1008 DATA "X XX XX XX X"
1009 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
1010 DATA " X XXXXX XX XXXXX X "
1011 DATA " X XX XX X "
1012 DATA " X XX XXX--XXX XX X "
1013 DATA "XXXXXX XX X X XX XXXXXX"
1014 DATA "< X X >"
1015 DATA "XXXXXX XX X X XX XXXXXX"
1016 DATA " X XX XXXXXXXX XX X "
1017 DATA " X XX XX X "
1018 DATA " X XX XXXXXXXX XX X "
1019 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
1020 DATA "X XX X"
1021 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1022 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1023 DATA "X XX XX X"
1024 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1025 DATA "XXX XX XX XXXXXXXX XX XX XXX"
1026 DATA "X XX XX XX X"
1027 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1028 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
1029 DATA "X X"
1030 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1100 REM
1101 REM MAZE ARRAY TO GRAPHICS
1102 REM
1110 FOR R=0 TO 30
1120 DIM P,PL,PS,C:P=VARPTR(MZ$(R))
1130 PL=PEEK(P):PS=PEEK(P+2)*256+PEEK(P+3)
1140 FOR C=PS TO PS+PL-1
1150 PRINT CHR$(PEEK(C));
1155 IF PEEK(C)=ASC("X") THEN POKEC,175
1160 NEXT:PRINT
1170 NEXT
2000 REM
2001 REM LOAD ASSEMBLY ROUTINE
2002
2010 READ A,B
2020 IF A=-1 THEN 2070
2030 FOR C = A TO B
2040 READ D:POKE C,D
2050 NEXT C
2060 GOTO 2010
2070 RETURN 'END
2080 DATA 16128,16217,189,179,237,90,39,14,90,39,28,90,39,42,90,39,55,204,255,255,32,67,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,47,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,30,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,14
2090 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,204,0,0,126,180,244,-1,-1
As listed, this will do the game entirely in BASIC. Using the arrow keys, you can move around the yellow PAC-BLOCK and explore the maze. When you get near the top or bottom of the maze, the screen will sluggishly scroll so you can access the rest of the maze.
Give that a try and explore the top and bottom of the maze so you can get an idea of the speed BASIC scrolls at.
Then, to make it use the assembly language routines:
Uncomment line 9. This protects memory beyond &H3F00 for the assembly language code.
Uncomment line 21. This will GOSUB to the routine that reads in the assembly language and POKEs it in to memory starting at &H3F00.
Comment line 910 (BASIC redraw/scroll up code).
Uncomment line 915. This calls the assembly routine to scroll the screen up, then redraws a new line at the bottom.
Comment line 960 (BASIC redraw/scroll down code).
Uncomment line 965. This calls the assembly routine to scroll the screen down, then redraws a new line at the top.
Make those changes and re-run the program then move from top to bottom and see how much faster the scree “scrolls.”
Assembly!
And, the assembly could be made almost twice as fast, and the BASIC code could be optimized to be faster, too.
But before we do that, let’s dig in to how the code actually works.
Dissection
20-21 read in all the maze lines in to an array called MZ$. The maze strings are in the DATA statements starting at line 1000.
30 builds a string that contains the ASCII characters for Up, Down, Left and Right. It is much faster to use INSTR and parse through a string rather than have to build one with CHR$() inside the INSTR call every time.
40-42 sets some default variables:
PC is the character of Pac-Man to POKE to the screen (159 is a yellow block).
WC is what character to use for wall detection (an ASCII “X” letter). The move code will PEEK screen memory, and not let you move in any direction that contains an “X”.
BG is the background character (a space) that will be used to erase Pac-Man before moving him.
60-63 initialize some game play variables:
ST is which of the 31 lines of the maze should be the first line to display. Thus, ST=7 means we will initially draw lines 7-22 on the screen to display that middle section of the maze.
PM is the memory location where Pac-Man will be POKEd. The screen memory starts at 1024, so this default is somewhere in the middle of the screen under the ghost house.
DR is the direction Pac-Man is currently moving.
DN is the next direction the Pac-Man will try to move at an intersection. Like the arcade, this version will let you press UP while Pac-Man is moving left, and as soon as there is an opening in the wall, the direction will turn UP.
90 draws the initial 16 maze lines that will fit on the screen.
110 POKEs the Pac-Man character on to the screen (showing the yellow block).
120-130 wait for one of the four keys in KB$ (up, down, left or right) to be pressed. If no key is pressed, it skips to line 140, else it sets DN (direction next) to match the key that was pressed.
140 uses DN (next direction) to call a routine to try to move Pac-Man up, down, left or right.
150 assumes that if DN and DR don’t match, a new direction has been pressed, so it will use DR (current direction) to call the up, down, left or right routine.
160 goes back to 100 to keep doing this forever.
510 is the UP routine. It will PEEK the memory location 32 bytes higher in memory (one line up from the current Pac-Man PM location) and if it is NOT the background character (ie, not some place we can move), it returns.
520 POKEs the background character where Pac-Man is, erasing him, then sets DR (direction) to 1 for up.
530 checks to see if the Pac-Man location is before a certain spot on the screen and that the screen is starting at a line later than the first one. If so, then the screen is allowed to scroll up (start line ST is decremented). A GOSUB to 950 will handle scrolling the screen. Otherwise, we don’t need to scroll and can just subtract 32 from the Pac-Man location, moving him up one line.
540 returns us back to the main loop.
610-640 is the same process for moving Pac-Man down, but we check for locations at the bottom of the screen and memory +32 from Pac-Man.
710-730 is the same code for moving Pac-Man left. We never scroll left or right so we don’t have to do as much here.
810-830 is the same code for moving Pac-Man right.
910-920 is the routine to scroll the screen up:
910 scrolls the screen up in BASIC by redrawing all 16 lines of the maze.
915 uses the assembly language routine to move the screen up, then PRINTs the next line at the bottom that would be displayed.
960-970 are the same thing for scrolling down.
1000-1030 is the 31 line maze.
2000-2090 is the assembly language loader generated by lwasm and renumbered to fix. It READs in the assembly from DATA statements and POKEs it in to memory.
In part 4 of this article I presented an example of using BASIC to scroll a PAC-MAN style maze that was too tall to fit on 16 the line screen.
I also presented some assembly code that would scroll the screen much faster than BASIC could ever hope to.
Today, let’s combine these two items and try to create a fast-scrolling maze playfield.
Let’s get started!
START SHOUTING AT ME! (revisited)
But before we get started, let’s revisit the uppercase routine I presented in Part 3.
Simon Jonassen is well-known in the CoCo Community for doing some amazing things on the original CoCo 1 and 2 hardware (and, lately, the CoCo 3 as well). He is quite the master of optimization, and has created some stunning sound players that allow the original CoCo to have cool background music while doing other things (if only the game programmers of 1980 knew about this!). He also has a cool web-based CoCo semigraphics editor. He provided a few enhancements:
* 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
; ldb ,x * load string len to B
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
; bra return 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
This code is 46 bytes long, compared to my original which was 49 bytes. The changes are:
Move the initial LDB with string length to after the string length check, since it’s only needed if we get past that check and have a string.
Change my LDA ,Y to LDA ,Y+ to increment Y there and not need the LEAY 1,Y later.
Changed my “characters left” check from BEQ EXIT and BRA LOOP to BNE LOOP since it can just fall through and continue otherwise.
Change a BRA RETURN to JMP GIVABF, since the branch would just end up at a JMP, and doing a JMP is faster than branching to a JMP.
Minor changes, but every little bit helps.
Simon also pointed out an embarrassing oversight in my very first example shown in part 1:
ORGADDR EQU $3f00
GIVABF EQU $B4F4 * 46324
INTCNV EQU $B3ED * 46061
org ORGADDR
start jsr INTCNV * get passed in value in D
tfr d,x * transfer D to X so we can manipulate it
leax 1,x * add 1 to X
tfr x,d * transfer X back to D
return jmp GIVABF * return to caller
He reminded me about the “addd” instruction which can add to D. For some reason, I was thinking I needed to use LEA to add to a 16-bit register, and since “LEAD 1,D” wasn’t a thing, I did the whole transfer to X, add one to X, transfer back to D thing.
He said I should just do this:
* ADDONE.ASM v1.01
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* 1.01 made less stupid per Simon Jonassen
*
* DEFUSRx() add one routine
*
* INPUT: integer to add one to
* RETURNS: value +1
*
* EXAMPLE:
* CLEAR 200,&H3F00
* DEFUSR0=&H3F00
* A=USR0(42)
* PRINT A
*
ORGADDR EQU $3f00
INTCNV EQU $B3ED * 46061
GIVABF EQU $B4F4 * 46324
org ORGADDR
start jsr INTCNV * get passed in value in D
; tfr d,x * transfer D to X so we can manipulate it
; leax 1,x * add 1 to X
; tfr x,d * transfer X back to D
addd #1 * add 1 to D
return jmp GIVABF * return to caller
* lwasm --decb -o -9 addone2.bin addone2.asm
* lwasm --decb -f basic -o addone2.bas
* decb copy -2 -r addone2.bin ../Xroar/dsk/DRIVE0.DSK,ADDONE2.BIN
See what happens when people who actually know 6809 assembly language look at my code? Thanks, Simon!
Moving Day
My simple examples have been building up to slight less-simple ones that do something more useful, like moving data that would take days to move in BASIC. Previously, I presented a PAC-MAN maze that could “scroll” up and down the screen by PRINTing the whole screen each time with just the lines of the maze that should be visible. I also presented some assembly code that could be used to move the screen up, down, left or right.
Today, the first thing I want to do is integrate that assembly routine in to the PAC-MAN maze code. Instead of redrawing the entire screen each time, BASIC will only need to redraw the top or bottom line depending on which was the screen just scrolled. If my math is correct, printing one line instead of sixteen lines should be at least twice faster.
First, let’s revisit the screen moving assembly code, which, thanks to comments from L. Curtis Boyle, now has a smarter routine for checking which direction the user passed in to scroll (though it could still be thrown off by values larger than 255):
* SCRNMOVE.ASM v1.01
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* DEFUSRx() screen moving function
*
* INPUT: direction (1=up, 2=down, 3=left, 4=right)
* RETURNS: 0 on success
* -1 if invalid direction
*
* 1.01 better param parsing per L. Curtis Boyle
*
* EXAMPLE:
* CLEAR 200,&H3F00
* DEFUSR0=&H3F00
* A=USR0(1)
*
ORGADDR EQU $3f00
INTCNV EQU $B3ED * 46061
GIVABF EQU $B4F4 * 46324
UP EQU 1
DOWN EQU 2
LEFT EQU 3
RIGHT EQU 4
SCREEN EQU 1024 * top left of screen
END EQU 1535 * bottom right of screen
org ORGADDR
start jsr INTCNV * get incoming param in D
; cmpb #UP decb * decrement B
beq up * if one DEC got us to zero
; cmpb #DOWN decb * decrement B
beq down * if two DECs...
; cmpb #LEFT decb * decrement B
beq left * if three DECs...
; cmpb #RIGHT decb * decrement B
beq right * if four DECs...
error ldd #-1 * load D with -1 for error code
bra exit
up ldx #SCREEN+32
loopup lda ,x
sta -32,x
leax 1,x
cmpx #END
ble loopup
bra return
down ldx #END-32
loopdown lda ,x
sta 32,x
leax -1,x
cmpx #SCREEN
bge loopdown
bra return
left ldx #SCREEN+1
loopleft lda ,x
sta -1,x
leax 1,x
cmpx #END
ble loopleft
bra return
right ldx #END-1
loopright lda ,x
sta 1,x
leax -1,x
cmpx #SCREEN
bge loopright
return ldd #0 * return code (0=success)
exit jmp GIVABF * return to BASIC
* lwasm --decb -9 -o scrnmove2.bin scrnmove2.asm
* lwasm --decb -f basic -o scrnmove2.bas scrnmove2.asm
* decb copy -2 -r scrnmove2.bin ../Xroar/dsk/DRIVE0.DSK,SCRNMOVE2.BIN
The generated BASIC program looks like:
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,16217,189,179,237,90,39,14,90,39,28,90,39,42,90,39,55,204,255,255,32,67,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,47,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,30,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,14
90 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,204,0,0,126,180,244,-1,-1
Let’s take the original maze program and modify it to use the assembly routines instead:
0 REM MAZETEST.BAS
10 DIM MZ$(31)
20 FOR A=0 TO 30:READ MZ$(A):NEXT
30 CLS
40 REM SCROLL MAZE DOWN
50 FOR ST=0 TO 15
60 FOR LN=0 TO 15
70 PRINT @LN*32,MZ$(LN+ST);
80 NEXT:NEXT
90 REM SCROLL MAZE UP
100 FOR ST=15 TO 0 STEP-1
110 FOR LN=0 TO 15
120 PRINT @LN*32,MZ$(LN+ST);
130 NEXT:NEXT
140 GOTO 40
999 GOTO 999
1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X XX X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X XX XX XX X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
2100 DATA " X XXXXX XX XXXXX X "
2110 DATA " X XX XX X "
2120 DATA " X XX XXXXXXXX XX X "
2130 DATA "XXXXXX XX X X XX XXXXXX"
2140 DATA " X X "
2150 DATA "XXXXXX XX X X XX XXXXXX"
2160 DATA " X XX XXXXXXXX XX X "
2170 DATA " X XX XX X "
2180 DATA " X XX XXXXXXXX XX X "
2190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
3200 DATA "X XX X"
3210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
3220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
3230 DATA "X XX XX X"
3240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
3250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
3260 DATA "X XX XX XX X"
3270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
3280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
3290 DATA "X X"
4200 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
The maze is 31 lines tall. The fake scrolling is done by redrawing the entire screen line-by-line. The screen is 16 lines tall, so initially we draw maze lines 0-15. Then we redraw maze lines 1-16, giving the appearance that the screen is scrolling up and a line has scrolled off the top of the screen. This repeats for lines 2-17, 3-18 and so on until we’ve drawn the last 16 lines of 15-30.
After “scrolling” all the way to the bottom of the maze, a second block of FOR/NEXT loops reverses the process, starting with maze lines 15-30, then 15-30 and so on until it is back to displaying the top lines 0-15.
The scrolling is done by the FOR/NEXT loops using the LN variables in lines 60-80 and 110-130.
Rather than redrawing all sixteen lines each time, we could use the assembly routine to move the screen, and then we’d just draw one line – top or bottom, depending on which was the screen scrolled.
In effect, we’d replace this:
40 REM SCROLL MAZE DOWN
50 FOR ST=0 TO 15
60 FOR LN=0 TO 15
70 PRINT @LN*32,MZ$(LN+ST);
80 NEXT:NEXT
90 REM SCROLL MAZE UP
100 FOR ST=15 TO 0 STEP-1
110 FOR LN=0 TO 15
120 PRINT @LN*32,MZ$(LN+ST);
130 NEXT:NEXT
…with this:
35 FOR LN=0 TO 15:PRINT @LN*32,MZ$(LN+ST);:NEXT
40 REM SCROLL MAZE DOWN
50 FOR ST=0 TO 15
60 Z=USR0(1)70 PRINT @480,MZ$(ST+15);80 NEXT
90 REM SCROLL MAZE UP
100 FOR ST=15 TO 0 STEP-1
110 Z=USR0(2)120 PRINT @0,MZ$(ST);130 NEXT
Line 35 was added to initially draw the screen. After that, the assembly routine can move it up or down, and let BASIC redraw just the one line that needs to be drawn.
This, of course, requires the assembly routine to be loaded. We can take the BASIC loader of that and renumber it so we can call it from our test program. Here is the scrnmove2.asm updated code from the top of this article, renumbered and changed in to a subroutine:
5000 REM ASSEMBLY ROUTINE
5010 READ A,B
5020 IF A=-1 THEN 5070
5030 FOR C = A TO B
5040 READ D:POKE C,D
5050 NEXT C
5060 GOTO 5010
5070 RETURN5080 DATA 16128,16217,189,179,237,90,39,14,90,39,28,90,39,42,90,39,55,204,255,255,32,67,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,47,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,30,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,14
5090 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,204,0,0,126,180,244,-1,-1
Now, I can add this to the end of the mazetest.bas program and set it up so the USR0() calls will work:
Now the program will use the CLEAR command to protect memory starting at &H3F00 (where the assembly will load), then after it reads all the maze strings in to memory (those DATA statements appear first), it will GOSUB 5000 and that READs the assembly code statements and POKEs them in to memory starting at &H3F00. The DEFUSR call is then done to make USR0(x) work.
With just a few lines changed, and getting our assembly routine in memory, now the maze scrolling is very fast! And, if we optimized the BASIC code around it, it could be even faster since most of the time is spent processing the BASIC program.
Here is the full listing:
0 REM MAZETST2.BAS - W/ASM!
10 CLEAR 200,&H3F00:DIM MZ$(31)
20 FOR A=0 TO 30:READ MZ$(A):NEXT
25 GOSUB5000:DEFUSR0=&H3F00
30 CLS
40 REM SCROLL MAZE DOWN
50 FOR ST=0 TO 15
60 FOR LN=0 TO 15
70 PRINT @LN*32,MZ$(LN+ST);
80 NEXT:NEXT
90 REM SCROLL MAZE UP
100 FOR ST=15 TO 0 STEP-1
110 FOR LN=0 TO 15
120 PRINT @LN*32,MZ$(LN+ST);
130 NEXT:NEXT
35 FOR LN=0 TO 15:PRINT @LN*32,MZ$(LN+ST);:NEXT
40 REM SCROLL MAZE DOWN
50 FOR ST=0 TO 15
60 Z=USR0(1)
70 PRINT @480,MZ$(ST+15);
80 NEXT
90 REM SCROLL MAZE UP
100 FOR ST=15 TO 0 STEP-1
110 Z=USR0(2)
120 PRINT @0,MZ$(ST);
130 NEXT
140 GOTO 40
999 GOTO 999
1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X XX X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X XX XX XX X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
2100 DATA " X XXXXX XX XXXXX X "
2110 DATA " X XX XX X "
2120 DATA " X XX XXXXXXXX XX X "
2130 DATA "XXXXXX XX X X XX XXXXXX"
2140 DATA " X X "
2150 DATA "XXXXXX XX X X XX XXXXXX"
2160 DATA " X XX XXXXXXXX XX X "
2170 DATA " X XX XX X "
2180 DATA " X XX XXXXXXXX XX X "
2190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
3200 DATA "X XX X"
3210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
3220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
3230 DATA "X XX XX X"
3240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
3250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
3260 DATA "X XX XX XX X"
3270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
3280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
3290 DATA "X X"
4200 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
5000 REM ASSEMBLY ROUTINE
5010 READ A,B
5020 IF A=-1 THEN 5070
5030 FOR C = A TO B
5040 READ D:POKE C,D
5050 NEXT C
5060 GOTO 5010
5070 RETURN
5080 DATA 16128,16217,189,179,237,90,39,14,90,39,28,90,39,42,90,39,55,204,255,255,32,67,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,47,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,30,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,14
5090 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,204,0,0,126,180,244,-1,-1
Try the original BASIC-only version and then this new assembly-enhanced version and see what you think.
Next time, I will share a version of this scrolling maze that has a character you can control and move through the maze.
JohnStrong (StrongWare) chimed in on Facebook with another improvement to the screen clearing assembly code. He suggested using a 16-bit register to blast bytes to the screen instead of doing it 8-bits at a time. It looks like this:
* CLEARX.ASM v1.02
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* 1.01 use TSTA instead of CMPD per L. Curtis Boyle
* 1.02 use STDD for 16-bit copy per John Strong
*
* DEFUSRx() clear screen to character routine
*
* INPUT: ASCII character to clear screen to
* RETURNS: 0 is successful
* -1 if error
*
* EXAMPLE:
* CLEAR 200,&H3F00
* DEFUSR0=&H3F00
* A=USR0(42)
* PRINT A
*
ORGADDR EQU $3f00
INTCNV EQU $B3ED * 46061
GIVABF EQU $B4F4 * 46324
org ORGADDR
start jsr INTCNV * get passed in value in D
* D is made up of A and B, so if
* A has anything in it, it must be
* greater than 255.
tsta * test for zero
bne error * branch if it is not zero
ldx #$400 * load X with start of
loop stb ,x+ * store B register at X and increment X tfr b,a * transfer B to A
loop std ,x++ * store D (A and B) then increment X twice
cmpx #$600 * compare X to end of screen
bne loop * if not there, keep looping
bra return * done
error ldd #-1 * load D with -1 for error code
return jmp GIVABF * return to caller
* lwasm --decb -o clearx3.bin clearx3.asm
* lwasm --decb -f basic -o clearx3.bas clearx3.asm
* decb copy -2 -r clearx3.bin ../Xroar/dsk/DRIVE0.DSK,CLEARX3.BIN
This change takes the byte to clear the screen to (in register B) and duplicates it (to register A), which affects the 16-bit register D since it is made up of A and B combined. Thus, if the desired byte is $2A (42), register D ends up being $2A2A.
Then, instead of copying one byte at a time, the loop copies two bytes at a time. The end result should be a faster screen clear. This ends up being two bytes larger than the second version, but still one byte smaller than my original version:
So far, we have looked at interfacing assembly language with BASIC to do some useless things (add one to a number), questionably useful things (clear screen to any given character), and actually useful things (high speed uppercasing of text).
In this installment, we will try to do something else actually useful: move the screen around.
But first, let me digress a bit.
The cross compiler I use, lwtools by Lost Wizard Enterprises, is able to compile code to run under COLOR BASIC or OS-9/NitrOS-9. It also has some other options I just learned about (thanks, William!) that I wanted to mention.
Previously, I shared a small bit of assembly that would clear the 32-column screen to any specified character:
ORGADDR EQU $3f00
GIVABF EQU $B4F4 * 46324
INTCNV EQU $B3ED * 46061
org ORGADDR
start jsr INTCNV * get passed in value in D
* D is made up of A and B, so if
* A has anything in it, it must be
* greater than 255.
tsta * test for zero
bne error * branch if it is not zero
ldx #$400 * load X with start of screen
loop stb ,x+ * store B register at X and increment X
cmpx #$600 * compare X to end of screen
bne loop * if not there, keep looping
bra return * done
error ldd #-1 * load D with -1 for error code
return jmp GIVABF * return to caller
NOTE: This article is using version 2, from the previous article, and does not include John Strong’s updates.
I have been compiling these to .BIN files, copying them over to a disk image, and then loading them in the XRoar emulator. It turns out, the lwasm also has another output option: BASIC. It will actually generate a short BASIC program that will POKE that assembly code in to memory! You use the format (-f) option like this:
lwasm --decb -f basic -o clearx2.bas clearx2.asm
This would assemble clearx2.asm and output it as a BASIC program! It looks like this:
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,16151,189,179,237,77,38,12,142,4,0,231,128,140,6,0,38,249,32,3,204
,255,255,126,180,244,-1,-1
The assembly is turned in to data statements, and it appears this is even capable of handling programs with multiple ORG statements. The DATA begins with the start memory location and the end memory location for a block of code, and then the actual code bytes. Clever.
This would be an easy way to add assembly code to your BASIC program without needing to LOADM/CLOADM a separate .BIN file. It will also give us a simple way to test this code in the XRoar emulator without copying files to a disk image (more on this in a moment).
But I digress.
Scroll With It, Baby
In all the examples I have shown so far, any parameter passed in was used to do something — a value to add to, a character to clear the screen to, or a string to print in uppercase.
The USR command allows for up to 10 functions to be defined (USR0 through USR9). This lets you easily have ten different assembly routines to call. However, you can also just use the parameter passed in to handle multiple functions.
Suppose you wanted to write a simple maze game using the 32 column text screen. You could limit your maze to be 32×16 (the size of the screen), or you could try to have a much larger maze and scroll it within the viewable screen area.
Scrolling UP is easy … you just print something at the bottom of the screen, and BASIC moves the whole screen up. Try this:
10 PRINT TAB(RND(30));".":GOTO 10
That code will tab over a random number of spaces (0 to 30) and print a period. Over and over and over. If you run this, you see a cheesy scrolling star field (if stars were black and space was nuclear green).
Scrolling stars!
There was a famous Commodore BASIC program that did something similar using the PETASCII slash characters to generate a maze. There has even been an entire book written about this one liner:
The CoCo does not have the Commodore character set, but we do have “/” and “\” so we could try this:
10 PRINT CHR$(47+(RND(2)-1)*45);:GOTO 10
This will print either CHR$(47) (a slash) or randomly add 45 to print CHR$(92) (a backslash). We get a similar endless maze that scrolls up, but doesn’t look nearly as nice as the one on the Commodore.
Scrolling maze… Sorta.
See? Easy.
I expect I wasn’t the only kid who wrote simple space games like this, with the ship at the top and objects traveling up the screen towards it.
I think I may be digressing again, so let me get back to the main point.
If we wanted to scroll in the other direction, we could try to do it in BASIC by copying every byte down one line. Here is an attempt to do that by using PEEK and POKE:
10 CLS
20 REM SCROLL UP
30 FOR A=1 TO 100
40 PRINT TAB(RND(30));"."
50 NEXT
60 REM SCROLL DOWN
70 FOR A=1 TO 100
80 PRINT@0,TAB(RND(30));"."
90 GOSUB 2000 'DOWN
100 NEXT
999 GOTO 999
2000 REM SCROLL DOWN
2010 FOR Z=1535-32 TO 1024 STEP-1
2020 POKE Z+32,PEEK(Z)
2030 NEXT
2040 RETURN
XROAR TIP: If you want to try this out in the XRoar emulator, save the above listing out as a text file with the extension of .asc (“scrolldown.asc”). If you do that, in XRoar you can do “File -> Load” and point it to this file. Then, that file will act like a cassette with an ASCII program on it! You can then type “CLOAD” and load the program, without needing to transfer it to a disk image.
This program will let the stars scroll up the screen (100 lines worth) using normal PRINT, then it will try to make them scroll down the screen (100 times) using a PEEK/POKE subroutine.
Scrolling down is painfully slow this way. You can see this would be no way to write a game.
Side Note: If I were trying to write a “space ship flying through space” game, I would just draw the individual stars and other objects, moving them each time, instead of redrawing the entire screen. But that’s not the point of this silly code.
And, if we wanted to also scroll the screen left and right, we’d need similar (and painfully slow) code. Here is a brute-force BASIC program that attempts to move the screen in each direction using POKE and PEEK:
10 CLS
20 FOR A=1 TO 14
30 PRINT @32*A+A,"SCROLLING IS HARD"
40 NEXT
50 GOSUB 1000 'UP
60 GOSUB 2000 'DOWN
70 GOSUB 3000 'LEFT
80 GOSUB 4000 'RIGHT
999 GOTO 999
1000 REM SCROLL UP
1010 FOR A=1024+32 TO 1535
1020 POKE A-32,PEEK(A)
1030 NEXT
1040 RETURN
2000 REM SCROLL DOWN
2010 FOR A=1535-32 TO 1024 STEP-1
2020 POKE A+32,PEEK(A)
2030 NEXT
2040 RETURN
3000 REM SCROLL LEFT
3010 FOR A=1024+1 TO 1535-1
3020 POKE A,PEEK(A+1)
3030 NEXT
3040 RETURN
4000 REM SCROLL RIGHT
4010 FOR A=1535-1 TO 1024 STEP-1
4020 POKE A+1,PEEK(A)
4030 NEXT
4040 RETURN
If you run this, you see it prints a message down the screen, then SLOWLY moves every byte up, then back down, then left, then right. It is very slow. It also leaves leftover characters on the edge of the screen, with the idea being you would be drawing new characters over there if you were making a maze or something scroll.
It’s not elegant, nor is it pretty. Or useful.
Obviously, doing this to scroll a screen is not practical. Clever programmers will try to make large strings and then just print them in the proper position. It’s mush faster letting the BASIC ROM do the work for you. Here’s an example that will scroll a PAC-MAN maze up and down the screen:
10 DIM MZ$(31)
20 FOR A=0 TO 30:READ MZ$(A):NEXT
30 CLS
40 REM SCROLL MAZE DOWN
50 FOR ST=0 TO 15
60 FOR LN=0 TO 15
70 PRINT @LN*32,MZ$(LN+ST);
80 NEXT:NEXT
90 REM SCROLL MAZE UP
100 FOR ST=15 TO 0 STEP-1
110 FOR LN=0 TO 15
120 PRINT @LN*32,MZ$(LN+ST);
130 NEXT:NEXT
140 GOTO 40
999 GOTO 999
1000 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
1010 DATA "X XX X"
1020 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1030 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1040 DATA "X XXXX XXXXX XX XXXXX XXXX X"
1050 DATA "X X"
1060 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1070 DATA "X XXXX XX XXXXXXXX XX XXXX X"
1080 DATA "X XX XX XX X"
1090 DATA "XXXXXX XXXXX XX XXXXX XXXXXX"
2100 DATA " X XXXXX XX XXXXX X "
2110 DATA " X XX XX X "
2120 DATA " X XX XXXXXXXX XX X "
2130 DATA "XXXXXX XX X X XX XXXXXX"
2140 DATA " X X "
2150 DATA "XXXXXX XX X X XX XXXXXX"
2160 DATA " X XX XXXXXXXX XX X "
2170 DATA " X XX XX X "
2180 DATA " X XX XXXXXXXX XX X "
2190 DATA "XXXXXX XX XXXXXXXX XX XXXXXX"
3200 DATA "X XX X"
3210 DATA "X XXXX XXXXX XX XXXXX XXXX X"
3220 DATA "X XXXX XXXXX XX XXXXX XXXX X"
3230 DATA "X XX XX X"
3240 DATA "XXX XX XX XXXXXXXX XX XX XXX"
3250 DATA "XXX XX XX XXXXXXXX XX XX XXX"
3260 DATA "X XX XX XX X"
3270 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
3280 DATA "X XXXXXXXXXX XX XXXXXXXXXX X"
3290 DATA "X X"
4200 DATA "XXXXXXXXXXXXXXXXXXXXXXXXXXXX"
If you run this, you will see an ASCII maze that is 31 lines tall get scrolled up and down the 16 line screen. Using PRINT to blast out a string of bytes is much faster than PEEK and POKE.
Fancy BASIC programmers would use this trick, storing all their characters in strings and printing them on the screen. If you want to add left and right scrolling, you could do that with longer strings and MID$ to just print the middle 32 characters of the string.
But I digress. Again.
While there are ways to do simulate screen scrolling somewhat fast in BASIC, assembly language will still be much faster. I present this simple code that has assembly versions of the BASIC code I presented earlier. Instead of having four different subroutines to GOSUB to, you can call it by using USR0(z) and giving it a direction code (1=up, 2=down, 3=left and 4=right).
It looks like this:
* SCRNMOVE.ASM v1.00
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* DEFUSRx() screen moving function
*
* INPUT: direction (1=up, 2=down, 3=left, 4=right)
* RETURNS: 0 on success
* -1 if invalid direction
*
* EXAMPLE:
* CLEAR 200,&H3F00
* DEFUSR0=&H3F00
* A=USR0(1)
*
ORGADDR EQU $3f00
INTCNV EQU $B3ED * 46061
GIVABF EQU $B4F4 * 46324
UP EQU 1
DOWN EQU 2
LEFT EQU 3
RIGHT EQU 4
SCREEN EQU 1024 * top left of screen
END EQU 1535 * bottom right of screen
org ORGADDR
start jsr INTCNV * get incoming param in D
cmpb #UP
beq up
cmpb #DOWN
beq down
cmpb #LEFT
beq left
cmpb #RIGHT
beq right
bra error
up ldx #SCREEN+32
loopup lda ,x
sta -32,x
leax 1,x
cmpx #END
ble loopup
bra return
down ldx #END-32
loopdown lda ,x
sta 32,x
leax -1,x
cmpx #SCREEN
bge loopdown
bra return
left ldx #SCREEN+1
loopleft lda ,x
sta -1,x
leax 1,x
cmpx #END
ble loopleft
bra return
right ldx #END-1
loopright lda ,x
sta 1,x
leax -1,x
cmpx #SCREEN
bge loopright
bra return
error ldd #-1 * load D with -1 for error code
bra exit
return ldd #0
exit jmp GIVABF
* lwasm --decb -9 -o scrnmove.bin scrnmove.asm
* lwasm --decb -f basic -o scrnmove.bas scrnmove.asm
* decb copy -2 -r scrnmove.bin ../Xroar/dsk/DRIVE0.DSK,SCRNMOVE.BIN
If I use the “-f basic” option, I can produce a BASIC loader with DATA statements that contain the assembly language routines. I then renumbered them and made them a subroutine so at the top of the example program I can GOSUB to it, then install and use the routine.
1 CLEAR 200,&H3F00
2 GOSUB 1000
3 DEFUSR0=&H3F00
10 CLS
20 FOR A=1 TO 14
30 PRINT @32*A+A,"SCROLLING IS HARD"
40 NEXT
50 Z=USR0(1) 'UP
60 Z=USR0(2) 'DOWN
70 Z=USR0(3) 'LEFT
80 Z=USR0(4) 'RIGHT
999 GOTO 999
1000 REM LOAD ASM ROUTINE
1010 READ A,B
1020 IF A=-1 THEN 1070
1030 FOR C = A TO B
1040 READ D:POKE C,D
1050 NEXT C
1060 GOTO 1000
1070 RETURN
1080 DATA 16128,16225,189,179,237,193,1,39,14,193,2,39,27,193,3,39,40,193,4,39,52,32,66,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,54,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,37,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,21
1090 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,32,5,204,255,255,32,3,204,0,0,126,180,244,-1,-1
If you run this, you will see the screen jump and then it will look like the original example looked…it just happens almost instantly instead of taking minutes.
Now let’s try the star scrolling example again. Instead of GOSUBing to slow BASIC routines, we will use the assembly scroll up and down routines:
1 CLEAR 200,&H3F00
2 GOSUB 1000
3 DEFUSR0=&H3F00
5 SP$=STRING$(31," ")
10 CLS
20 REM SCROLL UP
30 FOR A=1 TO 100
35 PRINT @32*15,SP$;
40 PRINT @32*15,TAB(RND(30));".";
45 Z=USR0(1) 'UP
50 NEXT
60 REM SCROLL DOWN
70 FOR A=1 TO 100
80 PRINT@0,TAB(RND(30));"."
90 Z=USR0(2) 'DOWN
100 NEXT
110 GOTO 20
999 GOTO 999
1000 REM LOAD ASM ROUTINE
1010 READ A,B
1020 IF A=-1 THEN 1070
1030 FOR C = A TO B
1040 READ D:POKE C,D
1050 NEXT C
1060 GOTO 1000
1070 RETURN
1080 DATA 16128,16225,189,179,237,193,1,39,14,193,2,39,27,193,3,39,40,193,4,39,52,32,66,142,4,32,166,132,167,136,224,48,1,140,5,255,47,244,32,54,142,5,223,166,132,167,136,32,48,31,140,4,0,44,244,32,37,142,4,1,166,132,167,31,48,1,140,5,255,47,245,32,21
1090 DATA 142,5,254,166,132,167,1,48,31,140,4,0,44,245,32,5,204,255,255,32,3,204,0,0,126,180,244,-1,-1
You will notice scrolling up and down now go at the same speed, but it is slightly slower than the normal BASIC PRINT scroll up. This is because of line 35 and 75 that use a PRINT statement to erase a line before the screen scrolls. This is because my simple assembly routines don’t bother to do this (neither did the BASIC version).
If the usage is known, the assembly can easily be made to clear out whichever roll of column is being moved. Doing it inside the routine will be much faster than using a PRINT command (and, PRINT doesn’t help us if the screen is scrolling left or right).
Can we do better? I think so.
Next time … let’s make another pass over this screen scrolling routine and see if we can make it do something more useful.
A quick update on some code listed in the previous installment. I mentioned that the user was passing in an integer that represented which character (a byte) the screen would be cleared to. It would be passed in as a 16-bit value (register D). Since the screen characters were one byte, a check was added in case the value passed in was larger than 255 (cmpd #255).
In the comments, Justin chimed in:
You could also just do a clra to force the issue and avoid the compare and branch. – Justin
Justin’s suggestion would make the code smaller and ignore the first byte of register D. Thus, if the user did pass in anything higher, it would just chop off the excess. In binary, if the user passed in a value from 0-255, only bits would be set in register B. When the value was larger than 255, it would start setting bits in register A:
Reg A | Reg B
0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0 = Reg D is 0
0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 1 = Reg D is 1
0 0 0 0 0 0 0 0|1 1 1 1 1 1 1 1 = Reg D is 255
0 0 0 0 0 0 0 1|0 0 0 0 0 0 0 0 = Reg D is 256
If we just “clra”, we ensure the routine will never get a value greater than 8-bits. However, the user will get unexpected results. If they tried to pass in 256 (see above), register A would be cleared, and the value the routine would use would be 0. “Garbage in, garbage out!”
However, if error checking is desired, we still need to do a compare. L. Curtis Boyle suggested:
You could use TSTA. instead of CMPA #$00 to save a byte. – L. Curtis B.
I looked up the TST instruction, and it seems to test a byte in memory location or the A or B register and set some condition code register (CC) bits. If the high bit 7 is set, the CC register’s N bit will be set (testing for a negative value). If any bits are set, the CC register’z Z (zero) bit will be set (not zero). Hopefully I have that correct. The key point here is you can use TST to check for zero, and TSTA is a smaller instruction than CMPD. Here is the code:
* CLEARX.ASM v1.01
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* 1.01 use TSTA instead of CMPD per L. Curtis Boyle
*
* DEFUSRx() clear screen to character routine
*
* INPUT: ASCII character to clear screen to
* RETURNS: 0 is successful
* -1 if error
*
* EXAMPLE:
* CLEAR 200,&H3F00
* DEFUSR0=&H3F00
* A=USR0(42)
* PRINT A
*
ORGADDR EQU $3f00
GIVABF EQU $B4F4 * 46324
INTCNV EQU $B3ED * 46061
org ORGADDR
start jsr INTCNV * get passed in value in D
cmpd #255 * compare passed in value to 255
bgt error * if greater, error
* D is made up of A and B, so if * A has anything in it, it must be * greater than 255. tsta * test for zero bne error * branch if it is not zero
ldx #$400 * load X with start of screen
loop stb ,x+ * store B register at X and increment X
cmpx #$600 * compare X to end of screen
bne loop * if not there, keep looping
bra return * done
error ldd #-1 * load D with -1 for error code
return jmp GIVABF * return to caller
* lwasm --decb -o clearx2.bin clearx2.asm
* decb copy -2 -r clearx2.bin ../Xroar/dsk/DRIVE0.DSK,CLEARX2.BIN
When I build this in to a .BIN file, the original showed 37 bytes, and this version shows 34 bytes. Here are the hex bytes that were generated:
It appears to save three bytes. Curtis mentioned saving one byte which I think is the case between a “CMPA #0” and “TSTA”.
Best of all, with this change, it still works and rejects larger values:
CLEARX2: Electric Boogaloo
Thanks, Justin and Curtis, for those suggestions.
By the way, I know programmers often don’t bother with error checking. I mean, our code is perfect, right? And, clearing a screen is hardly anything that requires error checking. And while I agree, I noticed that even COLOR BASIC has error checking for it’s CLS command:
CLS with a value greater than 255 returns a Function Call error.
And, since the CoCo’s VDG chip supported nine colors, you only get colors for CLS 0 through CLS 8. If you try to clear to any value between 9 and 255, you get an easter egg:
CLS 9 through 255 present a Microsoft easter egg.
Bonus Question: There is also an additional CLS easter egg in the CoCo 3’s BASIC, Do you know what it is?
But I digress…
String Theory
You can really speed up a BASIC program by using assembly routines. For instance, while BASIC has great string manipulation routines, doing something simple like converting a string to uppercase can be painfully slow.
Suppose you were trying to write a text-based program and you wanted it to work on all Color Computer models. The original Color Computer 1 and early Color Computer 2 models could not display true lowercase – they displayed inverse characters instead. Later Tandy-branded CoCo 2s and the CoCo 3 could support lowercase.
To work on all systems, you might simply choose to put all your menu text in UPPERCASE. Or, you might store every string twice with an uppercase and mixed case version, and use a variable to know which one to print:
IF UC=1 PRINT "ENTER YOUR NAME:" ELSE PRINT "Enter your name:"
That would be one brute-force way to do it, but if your program used many strings, it would needlessly increase the size of your program. Instead, it might make sense to store all the strings in mixed case, and convert them to uppercase on output if needed.
Here is a very simple brute-force subroutine that does just this:
BASIC uppsercase subroutine.
And it works just fine…
Output of BASIC uppercase subroutine.
…but it’s slow. If no conversion is needed, the mixed case text instantly appears, but when conversion is needed, it crawls through the line character-by-character at speeds we haven’t seen text display at since the days of dial-up BBSes.
In an earlier series of articles, we discussed word-wrap routines in BASIC. Several folks contributed their versions, and we ranked them based on code size, RAM size and speed. There were many different approaches to the same thing, and the same applies to uppercasing a string, so please don’t take my brute-force example as the best way it can be done. It certainly isn’t, and can surely be improved.
But even the fastest BASIC routine won’t compare to doing the same thing in assembly.
Unfortunately, the USRx() command only allows you to pass in a numeric value, and not a string, so we can’t simply do something like:
A$=USR0("Convert this to all uppercase.") 'THIS WILL NOT WORK!
Pity. But, I was able to find the solution, and it involves another BASIC command known as VARPTR. This command gets the address of a variable in memory. You may recall that Darren Atkinson (creator of the CoCoSDC interface) used VARPTR in his version of the word-wrap routine:
This is our solution to passing in a string to USRx(). We can pass in the address of the string, and then the assembly code can figure it out from there. Here is how it works:
A$="This is a string in memory"
X = VARPTR(A$)
PRINT "A$ IS LOCATED AT ";X
If you run that code, you will see the address of that string in memory. We just need to understand how a string is stored.
The address does not point to the actual string data. Instead, it points to a few bytes of information that describe the string and where it is.
The first byte where the string is stored will be the size of that string:
A$="THIS IS A STRING IN MEMORY"
X = VARPTR(A$)
PRINT "A$ IS LOCATED AT";X
PRINT "A$ IS";PEEK(X);"LONG"
I forget what the second byte is used for, but bytes three and four are the actual address of the string character data:
PRINT "STRING DATA IS AT";PEEK(X+2)*256+PEEK(X+3)
On my system, it looks like this:
VARPTR of a string.
Once you know the actual starting place for the string data, you can see what that is in memory. In my case, the string length was 26 bytes, and the data started at 32709. I could use a FOR/NEXT loop and display the contents of that memory:
VARPTR string data example.
You will notice that the string information (length of string, location of string characters) is nowhere near the actual string data is. This is because the string characters could actually be in your program code, rather than in string memory. For example:
10 A$="THIS STRING IS IN THE PROGRAM"
Somewhere in RAM will be a string identification block with the length of the string and an address that points inside the program space. This makes it sort of an “embedded string” that lives inside your program. However, if you manipulate this string, BASIC will then make a copy of it in other memory and make the pointer go there. Thus, if you have a 10 character string like this:
10 A$="1234567890"
…and inside your code you do something like this:
20 A$=A$+"!"
…at that point, BASIC will no longer be pointing A$ to inside your code. It will be copied (pluy the “!”) to a new memory location inside of string space:
Here you can see that the location of the string (initially inside the program code space) moves to higher string memory RAM:
VARPTR shows you where the string moves to.
You won’t see memory decrease when this happens, because print MEM is showing you available program space. Strings live in a special section at the end of program memory. You may have seen the CLEAR command uses to reserve space for strings like this:
CLEAR 200
I believe 200 is the default if you don’t specify. In this case, the string started out inside the program’s code space and was not using any of that 200 bytes, and then after altering the string, it was copied in to the 200 bytes of string space.
Thus, if you want to see the impact, try running with “CLEAR 0” so there is NO ROOM for strings!
5 CLEAR 0 ' NO STRING SPACE
Now when we run that program, we see that the initial string works, because it is stored inside the program space, but the moment we try to add one character to it, there is no string memory available to copy the string to and it fails with an ?OS ERROR (out of string space).
?OS ERROR showing strings move from code space to string space.
This is something to be aware of if you are ever writing large programs with many strings. Rather than do something like this:
…which would then allocate string space to hold the length of A$, B$ and C$, you could keep those strings in program code space by just printing them out each time:
40 PRINT A$;B$;C$
The trick is to avoid BASIC having to allocate string memory and copy things over. If you need to do this, you can re-use a temporary string:
40 TS$=A$+B$+C$:GOSUB 1000:TS$=""
I think something like that would create a temporary string (TS) and copy all those code space strings over, then you could use it, and then setting it back to “” at the end would release that memory. If string memory is limited, tricks like this can really help out.
But I digress.
Now that we know how strings are stored, we can create an assembly routine that will serve as a UPPERCASE PRINT command.
Our assembly routine will be passed the address of the string, and then use byte 1 to get the length, and bytes 3 and 4 to get the location of the actual string characters. We can then walk through that memory and use the CHROUT ROM routine to output each character one-by-one, the same way BASIC does for PRINT.
Here is the routine:
* UCASE.ASM v1.00
* by Allen C. Huffman of Sub-Etha Software
* www.subethasoftware.com / alsplace@pobox.com
*
* 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
dir
GIVABF EQU $B4F4 * 46324
INTCNV EQU $B3ED * 46061
CHROUT EQU $A002
org ORGADDR
start jsr INTCNV * get passed in value in D
tfr d,x * move value (varptr) to X
foo ldb ,x * load string len to B
ldy 2,x * load string addr to Y
beq null * exit if strlen is 0
ldx #0 * clear X (count of chars conv)
loop 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
beq exit * if 0, go to exit
bra loop * go to loop
exit tfr x,d * move chars conv count to D
bra return * return D to caller
null ldd #-1 * load -2 as error
return jmp GIVABF * return to caller
* lwasm --decb -o ucase.bin ucase.asm
* decb copy -2 -r ucase.bin ../Xroar/dsk/DRIVE0.DSK,UCASE.BIN
W will call our assembly routine like this:
A$="Convert this to uppercase."
A=USR0(VARPTR(A$))
And it should work on upper and lowercase strings automatically:
Uppercase output routine in assembly.
Now our uppercasing output routine is lightning fast.