Coding standards and talking myself into snake_case…

Obviously, things done “today” can be quite different than how things were done 30 years ago, especially in regards to computers. Is it strange that out of all the places I’ve worked where I wrote code that only one of them had an actual “coding standard” we had to follow? (And that was at a giant mega-corporation.) All the others seemed to have folks who just carried on how things were, for the most part, or were given the freedom to code as they wanted as long as they followed some specific system (such as “Clean Code” at a startup I worked at).

My day job has been undertaking an official coding standard. I started by documenting how things were done in the millions of lines of code we maintain. Over my years here, I have introduced some things from the mega-corporation’s standards into our code base, such as prefixing static variables with “s_” or globals with “g_” or similar.

But why reinvent the wheel? I thought it would make a lot more sense to find an existing standard that applied to embedded C programming and just adopt it. That led me to this:

https://barrgroup.com/embedded-systems/books/embedded-c-coding-standard1

This document clearly states the “why” for any requirement it has, and I have learned a few things. Unlike most standards that are mostly cosmetic (how to name functions or variables, how wide is a tab, etc.), this one focuses on bug detection, bug reduction and making code easier to review.

Which brings me to snake case…

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

Most places I have worked use camelCase, where all words are ran together (no spaces) with the first letter in lowercase, then all subsequent words starting with uppercase. itWorksButCanBeHardToRead.

The mega-corp standard I worked under would refer to “camel case starting with an uppercase letter,” which I have since learned is known as PascalCase. ItCanHaveTheSameProblem when the eyes have to un-smush all the letters.

snake_case is using lowercase words separated by underlines. A variant, SCREAMING_SNAKE_CASE uses all uppercase. (I never knew the name of that one, but most places I worked use that for #defines and macros and such.)

…and I expect someone will comment with even more naming conventions than I have encountered.

Do. Or do not. There is no try.

Beyond “how stuff looks,” some suggestions actually matter. Something I only started doing in recent times is when you put the constant on the left side of a comparison like this:

if (42 == answer)
{
    // Ultimate!
}

I saw this for the first time about a decade ago. All the code from a non-USA office we interacted with had conditions written “backwards” like that. I took an instant dislike to it since it did not read naturally. I mean who says “if 225 pounds or more is your weight, you can’t ride this scooter”?

However, all it takes is a good argument and I can flip on a dime. This flip was caused by finding multiple bugs in code where the programmer accidentally forgot one of the equals:

if (answer = 42)
{
// Ultimate?
}

If you use a modern compiler and have compiler warnings enabled, it should catch that unintended variable assignment. But, if you are using a “C-Like” embedded compiler that is missing a lot of modern features, it probably won’t. Or, if even you are running GCC with defaults:

https://onlinegdb.com/pv5Yz24t2

And thus, I broke my habit of making code that “reads more naturally” and started writing comparisons backwards since the compiler will fail if you do “42 = answer”.

This week, I learned they refer to this as Yoda conditions. Of course they do.

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

Snakes. Why did it have to be snakes?

This week I read one sentence that may make me get away from camelCase and PascalCase and go retro with my naming convention. It simply had to do with readability.

is_adc_in_error is very easy to read. I’d say much easer than IsAdcInError which looks like some gibberish and requires you to focus on it. If anyone else is going to review the code, being able to “parse” it easier is a benefit.

I am almost convinced, even if I personally dislike it.

Give me some reasons to stick with camelCase or PascalCase, and let’s see if any of them are more than “’cause it looks purtier.” (For what its worth, I’ve seen camelCase for global functions, and CamelCase for static functions.)

Awaiting your responses…

Open Micro Works Digisector DS-69 digitizer .PIX files in GIMP

Step 1: Rename the .PIX file so it has the extension .data. This is needed for GIMP to recognize it as a “raw” data file.

Step 2: Open this image in GIMP by expanding “Select File Type” and choosing Raw image data. That should allow the .data file to show up in the browser to open it.

Step 3: The file will open and you must adjust settings to tell GIMP more about the image. Under Pixel format, select Grayscale 4-bit. For the Width and Height, set them to 256 (if it is a 32K file) or 128 (if it is 8K). Now you should be able to Open the image.

Step 4: With the image open, you will need to Invert it to get the colors correct (Colors -> Invert) and rotate the image clockwise (Image -> Transform -> Rotate 90 clockwise).

Step 5: That should give you a 256×256 or 128×128 16-greyscale image you can now save out in whatever format you wish. GIMP can save based on the extension you give it when exporting. (File -> Export As… then change the extension to .PNG or .GIF or whatever.)

Tada!

Neat.

Or, I had A.I. write this quick conversion script… It can convert one file at a time, or run it in a directory with .PIX files and it will do them all. It currently only supports the 128×128 16-grey and 256×256 16-grey photos. I recall there was a 64-grey mode, so if I find one of those images, I will update the script to do them, too.

#!/usr/bin/env python3
import sys
import glob
from PIL import Image

def convert_pix(pix_file):
    with open(pix_file, 'rb') as f:
        data = f.read()

    if len(data) == 32768:
        width, height = 256, 256
    elif len(data) == 8192:
        width, height = 128, 128
    else:
        print(f"Invalid file size for {pix_file} (expected 8192 or 32768 bytes)")
        return

    pixels = []
    for byte in data:
        pixels.append(byte >> 4)
        pixels.append(byte & 0x0F)

    # Create image
    img = Image.new('P', (width, height))
    img.putdata(pixels)

    # Rotate right 90 degrees (CW)
    img = img.rotate(-90)

    # Invert colors
    inverted_pixels = [15 - p for p in img.getdata()]
    img.putdata(inverted_pixels)

    # Set greyscale palette
    palette = []
    for i in range(16):
        v = i * 255 // 15
        palette.extend([v, v, v])
    img.putpalette(palette)

    # Save as PNG
    output_file = pix_file.replace('.PIX', '.png').replace('.pix', '.png')
    img.save(output_file)
    print(f"Converted {pix_file} ({width}x{height}) to {output_file}")

def main():
    if len(sys.argv) == 1:
        pix_files = glob.glob('*.PIX') + glob.glob('*.pix')
        if not pix_files:
            print("No .PIX files found in current directory")
            sys.exit(1)
    else:
        pix_files = sys.argv[1:]

    for pix_file in pix_files:
        convert_pix(pix_file)

if __name__ == "__main__":
    main()

You can find it on my GitHub along with documentation on what all it needs to run:

https://github.com/allenhuffman/DS69-PIX-to-PNG

Good luck!

Wanted: disassembly of Sub-Etha Software’s MultiBoot

Updates:

  • 2025-11-20 – Thanks to a comment from Jerry Stratton, I have the start of a disassembly for the “DOS” code which would load at $2600. Article updated with what I have, so far.
  • 2025-11-21 – Updating assembly to include the raw bytes that were used.
  • 2025-11-21 – Updating the assembly again, using a tool John Linville pointed me to.
  • 2025-11-21 – Removing asm from this page and linking to the GitHub link instead.

To this date I still think one of the most useful products Sub-Etha Software ever created was MultiBoot. For CoCo hard drive users of the era, most of us had to boot from a floppy disk. That boot disk would contain the necessary drivers to access the hard drive. With boot files, “one size does not fit all” so most of us had a stack of them — one for running a BBS with serial drivers installed, one with the Sierra Online drivers for playing those games, one with all the graphics drivers for that stuff, etc.

MultiBoot allowed putting multiple boot files on one floppy disk. Type “DOS” and a menu is displayed. Choose the one you want, then that boot file is made active and the process continues.

MultiBoot was written by myself and Sub-Etha co-founder, Terry S. Todd. I wrote the OS-9 frontend in C, and Terry wrote the actual MultiBoot code in RS-DOS assembly. He would then provide that code to me as data, then my C front end program would “install” it by copying it out to the boot sector of the boot disk.

That code looks like this in the MultiBoot source code:

char mb_sec34_1[] = { /* Terry's RS-DOS DOS startup code... 50 bytes */
   79,83,134,13,151,211,204,56,0,221,209,12,211,142,0,234,204,2,0,237,132,
   134,33,214,211,237,2,220,209,237,4,173,159,192,4,109,6,38,8,76,129,61,37,
   221,126,57,0,126,215,9
};

char mb_sec33_15[] = { /* Terry's RS-DOS MultiBoot code V1.12... 512 Bytes */
   142,56,0,16,142,38,0,236,129,237,161,140,57,0,37,247,23,1,152,23,1,171,
   32,32,32,32,32,32,32,32,77,117,108,116,105,66,111,111,116,32,86,49,46,49,
   50,13,32,98,121,32,84,101,114,114,121,32,84,111,100,100,32,38,32,65,108,
   108,101,110,32,72,117,102,102,109,97,110,13,32,32,32,32,32,67,111,112,121,
   114,105,103,104,116,32,40,67,41,32,49,57,57,51,32,98,121,13,32,32,32,32,
   32,32,32,83,117,98,45,69,116,104,97,32,83,111,102,116,119,97,114,101,13,
   0,16,142,5,192,16,159,136,23,1,53,32,32,32,32,32,85,115,101,32,85,80,47,
   68,79,87,78,32,116,111,32,115,99,114,111,108,108,13,91,69,78,84,69,82,93,
   32,83,101,108,101,99,116,115,32,32,32,91,66,82,69,65,75,93,32,81,117,105,
   116,115,0,246,58,235,23,1,16,206,4,164,246,58,255,16,39,0,209,240,58,235,
   193,8,37,2,198,8,52,20,223,136,189,58,208,53,20,48,136,32,51,200,32,90,
   38,238,182,58,234,176,58,235,198,32,61,195,4,163,31,1,134,106,167,132,16,
   190,58,253,49,63,38,5,50,98,22,0,151,173,159,160,0,39,241,198,96,231,132,
   198,255,129,94,39,17,129,10,39,36,129,13,39,62,129,3,38,197,15,113,126,
   140,27,182,58,234,39,187,247,1,85,74,183,58,234,177,58,235,36,3,122,58,
   235,22,255,126,182,58,234,76,177,58,255,36,160,247,1,86,183,58,234,182,
   58,235,139,7,177,58,234,36,228,124,58,235,32,223,246,58,234,92,141,112,
   48,27,52,16,142,0,234,204,2,0,237,132,204,0,1,237,2,204,1,218,237,4,173,
   159,192,4,109,6,38,79,16,174,4,49,168,21,53,64,198,5,166,192,167,160,90,
   38,249,134,3,167,132,173,159,192,4,109,6,38,50,126,38,2,134,87,183,149,
   201,134,16,183,255,34,204,63,0,253,255,188,189,246,82,126,169,40,174,228,
   166,128,39,6,173,159,160,2,32,246,175,228,57,166,128,39,251,173,159,160,
   2,32,246,126,215,9,142,59,0,93,39,236,48,136,32,90,38,250,57,0,0,0,0,0,
   0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,66,0
};

Are there any 6809 disassembler gurus out there that might take this data and reverse it back into 6809 assembly source code?

Sadly, the only source code from Terry that I have found is that for his MultiBasic product. I cannot even find the master disks for his InfoPatch, and I do not think I ever had sources to ShadowBBS, OS9Term or anything else he wrote.

Thank you for your attention to this matter.

Disassemblies

The “DOS” code would load into memory at &H2600. The first two bytes must be “OS”, and indeed that matches the first two bytes in the data. Per a suggestion from Jerry Stratton, I wrote a program to POKE this data into memory, then used EDTASM Z-BUG to disassemble and do a bit of cleanup.

Where the “??” comment is was something Z-BUG could not decode. There is a value of 2 there. No cluck. BUT, I see a reference to DSKCON (C004) so I am betting if I look up how that works some of this code might make sense.

Help is appreciated!

NOTE: This is now updated using the f9dasm disassembler that John Linville pointed me to:

https://github.com/Arakula/f9dasm

Closer!

DOS Assembly (f9dasm)

https://github.com/allenhuffman/SubEthaSoftware/blob/main/OS-9/MultiBoot/asm/dos.asm

Multiboot Assembly (f9dasm)

https://github.com/allenhuffman/SubEthaSoftware/blob/main/OS-9/MultiBoot/asm/multiboot.asm

Updates

Tim Lindner pointed me to his online javascript disassembler. It did a great job right in the web browser:

https://www.macmess.org/follow9.html

Wanted: disassembly of Sub-Etha Software’s OS9Term

OS9Term was an RS-DOS terminal program written by Sub-Etha co-founder Terry S. Todd. It emulated the OS-9 text screen control codes. This not only included things like color, move cursor position, and underline, but also the overlay windows! OS-9 Level 2 “windows” on the text screen were all created by sending a series of escape code bytes. Terry’s program would let you dial in to an OS-9 machine using a modem and actually run text-mode windowing applications. I recall modifying my EthaWin interface with an option so it would not create its own window on startup. This allowed me to run the Towel disk utility remotely if I wanted. (EthaWin supported a mouse, but was designed so all functions – including the menu system – could be operated from the keyboard.)

It was a neat product, though I do not recall us selling very many copies. Too little, too late, I suppose.

The DSK image for this is up on the Color Computer Archive site:

https://colorcomputerarchive.com/repo/Disks/Applications/OS9Term%20%28Sub-Etha%20Software%29%20%28Coco%203%29.zip

As I went to find that link, I see this .zip has a Play Now button ;-) So here is a screenshot:

I would love to have a disassembly of this code, if anyone out there might be able and willing to take on such a task. The code is not all Terry’s. This program uses the bitbanger serial routines written by Ultimaterm’s Ken Johnston. At some point, Terry got in contact with him, and he provided his routines to Terry. Sub-Etha’s launch product, Shadow BBS, used the remote terminal driver written by Ken.

I don’t even know where to begin with a task like this, so I thought I’d post and see if anyone out there had some suggestions.

Thank you for your attention to this matter.

EXEC dispatch table for 6809 assembly

I am writing this so one of the 6809 experts who reads this can chime in and tell me a better way…

Often I post things so they can get in the search engines in case anyone else looks for that topic later. This is one of those.

Using DEF USR is a great way to put up to ten “easy to execute” routines in an assembly language program. Each of those routines can also do different things based on the numeric (or string) parameter passed in to the USR() call.

If you aren’t trying to be that fancy, but do want multiple functions for whatever reason, what methods are there? Please leave a comment with the best ways to call multiple functions using EXEC from Color BASIC.

Dispatch table

One method that comes to mind is using a dispatch table at the start of the machine language program. If the code is built to compile at &H3F00, then doing an EXEC &H3F00 will run that program. If there are more functions, you have to figure out where they are located and provide those address to the user. This is fine, until you make a change to the code and then those locations shift.

Instead, the start of the program could begin with a series of “branch always” instructions. For example:

            org     $7f00

start1 bra install
start2 bra uninstall

The branch always instruction is one byte, and it is followed by a second byte which is how many bytes away the function is. This makes each entry take two bytes. Thus, install is at &H7F00 and uninstall is at &H7F02. A whole series of functions could be done this way, and the user just has to remember which is which — &H7F00, &H7F02, &H7F04, etc. Having every two bytes be an entry makes it easy to remember.

; lwasm dispatch.asm -fbasic -odispatch.bas --map
; a09 -fbasic -odispatch.bas dispatch.asm

ORGADDR equ $3f00 ; Where program loads in memory

org ORGADDR

;------------------------------------------------------------------------------
; Absolute addresses of ROM calls
;------------------------------------------------------------------------------
CHROUT equ $A002

;------------------------------------------------------------------------------
; This code can be called by EXEC/EXEC xxxx.
;------------------------------------------------------------------------------
; Dispatch table at the start of the program.
start1 bra install
start2 bra uninstall

install leax <msginst,pcr ; X points to message
bra print ; print will do the RTS
;rts

uninstall leax <msguninst,pcr ; X points to message
;bra print ; print will do the RTS
;rts

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

;------------------------------------------------------------------------------
; Data storage for the string messages
;------------------------------------------------------------------------------
msginst fcc "INSTALLED"
fcb 0

msguninst fcc "UNINSTALLED"
fcb 0

end

One potential issue is that branch can only jump so far. If large functions are being called, you might find they cannot be reached from this dispatch table. One option would be to switch to “long branch”, but then you add more bytes and your dispatch table might be every three bytes – &H7F00, &H7F03, &H7F06, &H7F09, &H7F0C, etc.

That is a fine solution though every 2 may “look” nicer than every 3.

As a workaround, the dispatch table could remain short branches, but they go to a longer one just below it:

            org     $7f00

start1 bra install
start2 bra uninstall

; If a short branch cannot reach, it can call a second long branch:
uninstall lbra realuninstall

Above, perhaps “install” is within reach of the “bra”, but “uninstall” is too far away. Simply make the “bra uninstall” branch to a spot with a long branch. A few more bytes, a few more clock cycles, but now the dispatch table can remain “every 2 bytes”.

But there has to be a better way…

Leave your suggestions in the comments.

Until next time…

Bonus

Here is a BASIC loader for that example. RUN it, then EXEC &H7F00 or &H7F02 and be amazed. (Loader generated using Sean Conner’s a09 assembler.)

10 DATA32,2,32,5,48,140,21,32,3,48,140,26,166,128,39,6,173,159,160,2,32,246,134,13,110,159,160,2,73,78,83,84,65,76,76,69,68,0,85,78,73,78,83,84,65,76,76,69,68,0
20 CLEAR200,16127:FORA=16128TO16177:READB:POKEA,B:NEXT:

PEEK versus ARRAY in BASIC?

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

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

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

V=PEEK(1024)

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

V=PEEK(L)

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

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

0' peek-vs-array.bas

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

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

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

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

My results:

123
127

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

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

0' peek-vs-array2.bas

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

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

And my results:

210
258

PEEK is faster here, too.

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

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

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

0' peek-vs-array3.bas

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

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

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

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

333
259

Array is faster than two maths.

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

0' peek-vs-array4.bas

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

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

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

267
259

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

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

Bonus Code

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

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

Double Bonus

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

Wanted: Sub-Etha Software’s InfoPatch for Infocom

Sub-Etha Software sold a product called InfoPatch. It would take a stock TRS-80 Color Computer Infocom text adventure game and patch it to work on the CoCo 3 using the 80 column screen. As a bonus, it would speed up disk access by setting it to 6ms step rate.

I cannot find any master copies or documentation for this product, but I know we sold it. Here is what I wrote in my 1993 Middle America Fest trip report (from my book CoCoFest Chronicles).

Sub-Etha Software — There we were with our normal line of software including MiniBanners, CheckBook+, InfoPatch, N*Johnson Software, Carl England Utilities, and the new OSK products such as Etha-GUI and the new Write-Right “what you see is what you get” word processor which was the big hit for us. (Joel, you did a great job on this and I think you left many people quite impressed!)

– CoCoFest Chronicles, page 48.

I also mentioned it 1993 2nd Annual “Last” Chicago CoCoFest report:

Oh, just to get a fair plug in…”the same old stuff” includes MiniBanners, CheckBook+, Etha-GUI, and the recent InfoPatch which converts old CoCo 1/2 Infocom text adventures to run in 80 columns on a CoCo 3. We also had Carl England’s disk utilities and some old N*Johnson software. We promise MORE NEW ITEMS for the next Fest!

– CoCoFest Chronicles, page 65.

Here is text from an ad for the product:

InfoPatch by Terry Todd
Patch classic Infocom(tm) text games to run on 80 columns,
upper/lowercase, 6ms disk on CoCo 3.
RS-DOS Req: CoCo 3, Infocom Disk..........$ 9.95

It was still being advertised in 1995. From the Spring 1995 FARNA catalog:

As I go through my Sub-Etha archives, I find this ad copy from 1993 which notes this patch did not work on Seastalker:

NEW!  InfoPatch - Clever hack which patches old CoCo 1/2 Infocom text adventures (like The Hitchhiker's Guide to the Galaxy, Zork, and others) to work in 80 columns, double speed more on a CoCo 3. (Does not work with Seastalker!)  See Adventure Survivors newsletter.
Req: CoCo 3, Disk Drive, Infocom Game Disk ........................ $ 9.95

And, it appears we launched this product in 1992. From that edition of the “What is Sub-Etha Software” flyer:

A few Sub-Etha ads ran in the Underground and just in time for the ’92 Atlanta CoCoFest Terry would return (but Mark would not be able to attend). Terry’s new offering was InfoPatch – a program to modify Infocom text adventures to run in 80 columns on a CoCo 3. This was also the show that gave life to the “PVC Nightmare” which spawned from the lack of fancy backdrops. Terry and I set out to a late-night hardware store and bought tubing which we hacked up in the parking lot with a newly acquired saw (to make it fit into my Honda) and would later reassemble in the show area and drape a background from it. It was very unstable, but, then again, the same is often said about us. This was our first ‘Fest with a full booth!

– What is Sub-Etha Software flyer, October 1994.

We sold many copies of it. Did you buy one? If so, any chance you could find it and send me a disk image of it? While the source code is likely lost forever, I’d really like to get this item preserved in the Color Computer Archive site.

Thank you for your attention to this matter.

More Sub-Etha Software MM/1 source code on GitHub

I have located source code to some of the MM/1 programs I wrote. It is now uploaded to my GitHub page:

https://github.com/allenhuffman/SubEthaSoftware

This includes…

  • MiniBanners – ported from CoCo BASIC09 to the MM/1 BASIC by Joel Hegberg (he had an MM/1 and I didn’t)
  • MegaBanners – the updated banner program that made use of Joel’s excellent MFONTS library.
  • Towel – the EthaWin library disk utility.
  • TCWin – my termcap windowing system that simulated many of the CoCo and MM/1’s CGFX text windowing calls. This allowed me to port EthaWin stuff over to OSK and OS-9000 boxes running on terminals. (I even ported it to DOS at one point, but lost all that work in a laptop hard drive crash.)

If you have access to an MM/1, I’d love to know if any of this software can be built and ran.

Hacking the Color BASIC PRINT command – part 7

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

Source code for this series:

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

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

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

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

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

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

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

https://github.com/allenhuffman/Invaders09

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

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

Here is my current version:

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

; Allow embedded characters to move the cursor in a PRINT

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

ORGADDR equ $3e00 ; Where program loads in memory

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

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

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

org ORGADDR

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

clr savedrvec3,pcr ; zero out to mark unused

uninstalled leax msguninstalled,pcr
bsr print
bra exitsuccess

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

msguninstalled fcc "OFF"
fcb 0

msgunknown fcc "UNK"
fcb 0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

end

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

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

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

The EXEC method

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

EXEC

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

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

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

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

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

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

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

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

The DEF USR method

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

DEF USR0=&H300

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

A=USR0(0)

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

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

A=USR0(1)

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

To disable it, use -1:

A=USR0(-1)

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

A=USR0(“UDLR”)

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

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

A=USR0(“”)

What I need from you

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

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

The smaller version.

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

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

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

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

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

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

org ORGADDR

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

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

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

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

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

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

installed rts

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

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

clr savedrvec3,pcr ; zero out to mark unused

uninstalled rts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

end

And here is a BASIC loader for that version:

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

Until next time…

Dissecting my MiniBanners program – part 1

See Also: part 1 (with more parts to come).

This series of blog posts will discuss my CoCo program, MiniBanners. It will discuss the things that changed between versions, and dissect the code to explore how it worked. The code referenced in this blog post can be found in my new SubEthaSoftware repository on GitHub:

https://github.com/allenhuffman/SubEthaSoftware/tree/main

Part 1 – Origin and versions

MiniBanners is a banner printing program I wrote and sold through Sub-Etha Software. It made its debut at the 1990 Atlanta CoCoFest. It was written in BASIC and ran on a Color Computer 3 using the 40 column text screen. It “required” a CoCo 3 only because I was making use of CoCo 3 fonts that could be LOADM’d and used on the graphics screens with the HPRINT command.

Unlike most (all?) commercial banner programs of the era, MiniBanners did not require a graphics printer. It would work on small printers like the Radio Shack TP-10 (32 columns) or wider ones (132 columns). It would use any ASCII character to make the banners with. Some printers had a black graphics block they could print (Tandy printers, TP-10, etc.), so it could use that. For non-dot matrix printers, it could use an ASCII “X” or “#” or whatever character the user wanted. This let it print to basically any printer – dot matrix, thermal and even daisy wheel. (Remember those?)

Another feature was the ability to print multi-line banners. You could specify the height of each line and either print a one-line banner using the full 80 column width of a printer, or make it four lines with each line of text being 20 characters tall.

I would even switch printer ribbons and print “multi color” banners, but that was not anything that the program directly helped with. Here is a photo of some of the banners we used at a CoCoFest (except for the top one; I think that was done on an Apple 2 using PrintShop or whatever they had back then).

While our initial lineup of programs were all for Disk BASIC (RS-DOS as we called it back then), so many people at the CoCoFest told us “I’d buy it if it ran under OS-9” that it was soon ported over to BASIC09 as an OS-9 program. If I can find versions of that code, I’ll include it in this series in a later installment.

It all began as a one (or two) liner…

I recall finding a free BASIC banner printing program that had an awful font. I wanted to modify it to make the letters it printed look better. I decided to use the font data that was part of the CoCo 3 ROM. This data starts in memory at &HF09D. Here is a snippet from the disassembly:

* SPECIAL CHARACTERS AND NUMBERS
SF09D FCB $00,$00,$00,$00,$00,$00 BLANK
FCB $00,$00
FCB $10,$10,$10,$10,$10,$00 !
FCB $10,$00
FCB $28,$28,$28,$00,$00,$00 "
FCB $00,$00
FCB $28,$28,$7C,$28,$7C,$28 #
FCB $28,$00
FCB $10,$3C,$50,$38,$14,$78 $
FCB $10,$00

Rather than rebuild the DATA in the program I was playing with, I decided to just write my own. I quickly had a simple program that would input a line and then print out a banner using this font data. If I recall, I had the whole program fitting in one or two lines.

I had planned to submit it to Rainbow magazine for their “one/two liner” thing they printed in each issue, but I do not recall actually doing that.

It was this simple program inspired me to create a larger program with more features (such as multi-line banners). MiniBanners became Sub-Etha Software’s third product (after ShadowBBS and MultiBasic, both written by cofounder Terry Todd).

It pretended to be in assembly…

I knew most folks would not buy a program written in BASIC. Ads of the time would exlaim “100% machine language.” Because of this, I decided to disguise that MiniBanners was in BASIC by turning it into a “.BIN” file you had to LOADM and EXEC to run, rather than LOAD and RUN to run like a BASIC program.

In case anyone got suspicious and took a disk editor to my .BIN program, I also “scrambled” the contents so they would not look obviously like a BASIC program. You wouldn’t be able to see the standard patterns for line numbers, strings, etc.

I did this by using a short assembly language routine I called SCRAM.ASM. It would take the BASIC program in memory, then “scramble” it by inverting the bits in each byte using the COM op code. That data was then added to an assembly loader routine that would descramble it and set it up to run like a normal BASIC. The end result was you would LOADM”MINIBAN.BIN”:EXEC and have a program running as if it were a machine language program.

In a later installment, I plan to explore that assembly code and try to re-learn how it worked.

For now, let’s discuss the different versions of the RS-DOS BASIC version of MiniBanners.

Version 0.0 – 9/20/1990

According to comments in the code, work began on MiniBanners on September 20, 1990. The goal was to have it ready for the first Atlanta CoCoFest which would be held about two weeks later on the weekend of October 6-7, 1990. The whole program was created in about ten days, including writing the code and manual, and having the manual printed and getting master disks copied (with serial numbers) and labeled for sale at the Fest.

Since this program only came to exist after we had submitted our booth information for the CoCoFest show guide, our entry there has no mention of MiniBanners:

Our entry in the 1990 Atlanta CoCoFest show guide.

Version 1.0 – 10/1990

This was the initial release version sold at that CoCoFest. It ran only on a 40 column screen.

Version 1.1 – 10/1990

This version added support for 80 columns with a new menu option of “W” to toggle between 40 and 80.

It also made specifying the baud rate easier. The user could now just type in 600, 1200, etc. and it would be calculated. In version 1.0, you had to know the POKE value for the desired baud rate. Examples offered in v1.0 were 180 for 600 baud, 88 for 1200 baud, and 41 for 2400 baud. In 1.1, you could just enter 1200 or 2400 and a routine would figure out the POKE value.

I have no idea where I got this from, but I know I didn’t come up with it ;-)

875 REM * Baud Rate Routine
880 POKE150,INT(.2175+5.7825*2^(5-(LOG(BD/600)/LOG(2)))-4.5):RETURN

It also appears the “instant showing screen” trick was not used in version 1.0, as I find routines in 1.1 that set the text palette to the background color. Then, in the GOSUB routine that does input, it sets it back to white. Clever.

50 REM * Main Menu
55 PALETTE8,BG:CLS:PRINTTAB(TB+9)"/) MiniBanners! "VR$" (\":ATTR1,1:PRINT:ATTR0,0:LOCATE0,22:ATTR1,1:PRINT:ATTR0,0:PRINTTAB(TB+13)"Enter Function";
60 LOCATETB+11,6:PRINT"[1] Print a Banner":PRINT:PRINTTAB(TB+11)"[2] Select Font ("FT$")":PRINT:PRINTTAB(TB+11)"[3] Configuration"
65 LOCATETB+11,17:PRINT"[W] Toggle 40/80":LOCATETB+11,19:PRINT"[X] Exit to Basic"
70 LOCATETB+15,3:PRINT"[D]rive:"DR:DRIVEDR
75 GOSUB1055:A=INSTR("123DWX",A$):IFA=0THENSOUND200,1:GOTO75
...
1050 REM * Inkey$
1055 PALETTE8,FG
1060 A$=INKEY$:IFA$=""THEN1060ELSESOUND1,1:LT=ASC(A$):IFLT>96THENA$=CHR$(LT-32)
1065 RETURN

There are also some new POKEs added when setting up the screen:

PALETTE1,FG:PALETTE9,BG:POKE63395,9:POKE63468,9:POKE63503,0:POKE63567,0:POKE63644,0:POKE63771,0

Let’s see if we can figure out what they do. Consulting Super Color BASIC Unravelled should help, or the source code listing at the toolshed repository on GitHub:

https://github.com/n6il/toolshed/blob/master/cocoroms/coco3.asm

First, let’s convert those decimal values into HEX so we can find them in the assembly listing.

POKE63395,9 - F7A3
POKE63468,9 - F7EC
POKE63503,0 - F80F
POKE63567,0 - F84F
POKE63644,0 - F89C
POKE63771,0 - F91B

At the time I was writing MiniBanners, my Sub-Etha Software co-founder, Terry, was deep into patching BASIC for his MultiBasic product. He had picked up copies of the Unravelled series and would tell me neat POKEs to try. I expect he provided me with these POKEs because, at the time, I had no idea how the BASIC ROMs worked beyond using a few documented ROM calls in small assembly routines I wrote.

POKE63395,9 – F7A3 takes me to this bit in the source code:

SF79F	LDA	>H.CRSATT	GET THE CURSOR ATTRIBUTES RAM IMAGE
ORA #$40 FORCE THE UNDERLINE ATTRIBUTE

I expect this location is changing the #$40 from an underline to a 9. The original code was setting bits. $40 would be 01000000. 9 would be 00001001. Since these deal with the cursor attributes, I guess we need to figure out what those 8 bits do. The Unravelled book explains this on page 8:

Bit 7 BLINK 1=Character blinks
Bit 6 UNDLN 1=Character is underlined
Bit 5 FGND2 Foreground Color (MSB)
Bit 4 FGND1 Foreground Color
Bit 3 FGND0 Foreground Color (LSB)
Bit 2 BGND2 Background Color (MSB)
Bit 1 BGND1 Background Color
Bit 0 BGND0 Background Color (LSB)
Figure 4 - Attribute byte

The original code was setting bit 6, the underline bit. That make the BASIC cursor be an underline. But the new code is setting bits 0 and 3 which are setting the foreground color to 100 (4) and background to 100 (4). Or maybe that is reversed (since I see MSB/LSB) and they are both being set to 1. Maybe someone can explain it, but I think this is just setting it to a solid color block.

POKE63468,9 – F7EC looks to be the same attribute, but this time involved when printing a backspace.

* DO A HI-RES BACKSPACE HERE
SF7E2 PSHS B,A
LDA #SPACE SPACE CHARACTER
LDB >H.CRSATT GET THE ATTRIBUTES RAM IMAGE
STD ,X SAVE A SPACE ON THE SCREEN AT THE OLD CURSOR POSITION
ORB #$40 FORCE THE UNDERLINE ATTRIBUTE

POKE63503,0 – F80F is also related to the cursor attribute:

SF807	PSHS	B,A
LDA #$20 GET THE CURSOR CHARACTER
LDB >H.CRSATT GET THE CURSOR ATTRIBUTES RAM IMAGE
ORB #$40 FORCE THE UNDERLINE ATTRIBUTE

POKE63567,0 – F84F is just a bit further down, same thing:

	LDB	>H.CRSATT	ACCB ALREADY CONTAINS THIS VALUE
ORB #$40 FORCE THE UNDERLINE ATTRIBUTE

POKE63644,0 – F89C is likely this, again:

	LDB	>H.CRSATT	GET THE CURSOR ATTRIBUTES RAM IMAGE
ORB #$40 FORCE THE UNDERLINE ATTRIBUTE

POKE63771,0 – F91B is this a bit further down:

	LDA	>H.CRSATT	GET THE CURSOR ATTRIBUTES RAM IMAGE
ORA #$40 FORCE UNDERLINE ATTRIBUTE

I therefore conclude that these POKEs are just changing the cursor from an underline (which screams “I am a BASIC program!”) to a solid block. Running these POKEs on a CoCo 3 emulator confirms. Thanks, Terry!

I find a POKE 282,0 added, which is done before calling a line input routine. That address is:

CASFLG RMB 1 UPPER CASE/LOWER CASE FLAG: $FF=UPPER, 0=LOWER

It appears to change the input to lowercase before the user types in the line to be printed. I suppose this saved the user from having to SHIFT-0 before entering their message.

Another line was added, which checks to see if the printer is ready. If the printer is ready, it just goes on and prints. If it is not, it displays the “Ready Printer or Press [X]” message. Nice.

156 IF(PEEK(&HFF22)AND1)>0THENLOCATETB+6,23:ATTR1,1,B:PRINT" Ready Printer or Press [X]";:ATTR0,0:GOSUB1055:IFA$="X"THEN55ELSE156

There is an error handling routine which got updated with the wording changed from “eliminate” to “exterminate” for some reason. Maybe I thought “exterminate” was more clever for a bug?

735 LOCATETB+6,4:PRINT"[ Error"ERNO"reported at"ERLIN"]":PRINT:PRINTTAB(TB)" Honestly, I thought I had it bug-free!":PRINT:PRINTTAB(TB)"   If this was something other than a":PRINTTAB(TB)"   Disk Full or I/O Error, please let"
736 PRINTTAB(TB)" us know so we may exterminate it."

I also find a few bug fixes. MiniBanners ran in the “double speed” mode using POKE 65497,0. This would mess up disk I/O unless you switched back to low speed with POKE 65496,0 before doing it. I found I had added the slow/fast POKEs in some places that had been missing it, such as this line which loaded a font:

765 A$=FT$:EX$="FNT":GOSUB840:IFA=0THENRETURNELSEPOKE65496,0:LOADMA$+"."+EX$:POKE65497,0

It also looks like I added a “are you sure” confirmation when you exited the program:

95 LOCATETB+5,23:PRINT"Exit Banners? [Y]es or [N]o :";:GOSUB1055:IFA$<>"Y"THEN55

There may have been some other things I missed, but those were the main ones that I noticed.

Version 1.2 – 4/11/1991

This update had the note “fix reset when exiting” but I find nothing in the code that changed from 1.1 other than the version (“1.2”) and the year (“1991”). I may be missing the actual fixed version, and now I wonder what the problem was when exiting. The exit code looks like:

95 LOCATETB+5,23:PRINT"Exit Banners? [Y]es or [N]o :";:GOSUB1055:IFA$<>"Y"THEN55
96 CLS:PRINTTAB(TB+4)"Thank You for using MiniBanners!":ATTR1,1:PRINT:ATTR0,0:PRINT
97 POKE65496,0:POKE150,18:POKE282,255:POKE63395,64:POKE63468,64:POKE63503,64:POKE63567,64:POKE63644,64:POKE63771,64:NEW

It has the “are you sure” confirmation added in 1.1, then I see it POKEs back to slow speed, POKEs to reset the printer baud rate, and POKEs to restore the cursor before erasing the program using NEW. I expect somewhere in the program I did something else that I forgot to restore, and that is what 1.2 was supposed to address. Maybe I will find another copy of this file somewhere in my archives. If so, I’ll update this post at that time.

Looking at this now, I probably should have just reset the machine, or added something to erase the program from memory. After existing this way, someone could have done two POKEs to change the end location of BASIC in memory and recovered the program and had the source code ;-)

Up next … dissecting the code.

Until then…