In my previous article, I wrote a small routine to print out “Hello world” on the Atari 800. I used READ and POKE to get the program into a fixed location in memory (1536, or $0600 in hex), and then called it with X=USR(1536). Memory locations $0600 through $06FF (page six) is reserved by BASIC for machine language routines that the programmer might want to use.
255 bytes for all our machine-code support is pretty limiting, though, and this was not usually a BASIC developer’s first resort for machine code. Like on the other 8-bit home computer platforms we’ve worked with, you almost always would prefer your machine code to be in the same memory as the BASIC program’s workspace, but somehow protected from it.
Unfortunately for us, our options on the Atari are quite aggravatingly limited, because the system was very flexible. Where BASIC’s memory began or ended depended on what graphics mode you were in, whether any cartridges were inserted, how many disk drives were connected, and which version of DOS you had booted from. With BASIC, DOS, and the underlying OS all eating away at both edges of your RAM, it also wasn’t usually feasible to move some edge of the program away, either. If you were going to mix machine code in with your BASIC program, you would generally be best off if your program were relocatable—that is, if it refers to no fixed addresses inside itself. In this scheme, you would load machine code routines that got called by machine code routines into page six, and then BASIC would call into these relocatable routines that would run fine wherever you happened to be stashing them this time. Furthermore, if you don’t care about where a block of bytes is, there was a very easy way to declare a block of bytes to be in use and not otherwise touchable by BASIC: we can keep our program inside a string variable. BASIC will maintain the space for our routine inside its own memory manager, and we don’t have to worry about it being overwritten by accident. All we have to worry about is how to call it, how to get away with not using hardcoded addresses for anything, and how to load it in in the first place.
Calling the function is very easy, at least. Atari BASIC includes a primitive ADR, which returns the address of the contents of a string variable. We can use this to turn any string into a pointer to its contents.
Getting away with not using addresses is much harder, especially for something like Hello World—one way or another, we need a way to get the address of the string to print into the IO control block. For unconditional jumps and the like we can get good use out of the BNE and BEQ-like instructions, taking advantage of our knowledge of the flags to make relative unconditional jumps. And the fact that we’ll be only writing little snippets of machine code and weaving them together with BASIC means many of our operations simply won’t need long-range jumps or calls to subroutines on their own.
But there’s one extra trick up our sleeve that lets us get addresses of data more easily: the USR command can take arbitrary numbers of arguments. Remember how when we wrote our original program our first instruction pulled a value off the stack and threw it out? That value was the number of arguments passed to the routine. If we keep pulling values off of the stack, we’ll get each argument, left to right, high byte first. High byte first is a bit backwards, but that means we can take the string to print and its length as arguments and copy them from the stack directly into the I/O control block. This makes the main routine much shorter:
pla ldx #$00 lda #$0b sta $0342,x pla sta $0345,x ; High byte first! pla sta $0344,x pla sta $0349,x pla sta $0348,x jmp $e456
We can then define space for some strings and create our machine code routine and our data string. Our data string becomes a lot easier to create, too, since it is just a string and so we can just type it in. The one wobble we hit is that our string is supposed to end with an end-of-line character (which for some reason is character 155 in Atari’s ASCII variant instead of the 13 or 10 that it normally is), and so we have to append that on our own in line 60. Otherwise, things look very similar to our original program.
10 DIM A$(128),B$(38) 20 FOR I=1 TO 27 30 READ A:A$(I)=CHR$(A) 40 NEXT I 50 B$="Hello, relocatable world!" 60 B$(LEN(B$)+1)=CHR$(155) 70 X=USR(ADR(A$),ADR(B$),LEN(B$)) 80 DATA 104,162,0,169,11,157,66,3,104 90 DATA 157,69,3,104,157,68,3,104,157 100 DATA 73,3,104,157,72,3,76,86,228
(An aside, for those familiar with the other BASICs I’ve worked with on this blog: in Atari BASIC, you have to DIMension strings to their length, more like C than the BASICs we’re used to. This also means that we don’t have the ability to declare arrays of strings, or concatenate them in the ways we’re used to. Instead, subscript notation gets abused a bit to perform appends, and—unlike the strings of the other BASICs—our strings are mutable so we can just assign directly to each character as we see in line 30 above.)
Actually Gaining Something From This
That’s all well and good, one might think, but this doesn’t really save us much beyond just poking stuff into page 6 as needed. Why bother with this? This brings us to our final little bonus. Unlike most of the computer systems we’ve messed with here, it is possible to type all 256 possible character codes on the Atari keyboard, through a combination of shift states and input modes. So, as long as our program doesn’t have any newlines ($9B) or quotation marks ($22) in it, we can just type the whole program in as a string constant, for an effective preparation time of zero:
This is how you’d generally want to set up your machine code in a mixed BASIC/assembly language environment. However, I am lazy, and I’ve already typed these numbers in. Can’t I just get a program to write it for me? Well, yes, of course, but Atari BASIC gives us some interesting new options. In the Commodore and Apple II BASICs (and, for that matter, BBC BASIC on the Raspberry Pi), we needed to use a special program called a tokenizer to convert BASIC program text into the bytecoded format that the interpreter actually expects. Atari BASIC has a bytecode of its own—with some very strange properties that mean that how the program was typed in will affect what different byte values mean even for programs that are identical when listed—but Atari BASIC includes its own tokenizer and detokenizer. In addition to the traditional SAVE and LOAD commands to copy bytecoded BASIC programs to and from the disk, there is also LIST and ENTER, which will work with untokenized files. Furthermore, there is nothing special about files in Atari DOS; we are entirely within our rights to write a program that itself writes a program by PRINTing each line to a file. That program can then be ENTERed back into our memory as we wish. Better yet, ENTER, unlike LOAD, does not actually destroy the previously existing program, so we can use this to merge together individual bits of program into one full one.
For actually outputting the string itself, though, the PUT statement, which outputs a single arbitrary byte to a channel, is really a bit more convenient. I appended these lines to my first program in this post:
110 OPEN #1,8,0,"D:BUMB.LST" 120 PRINT #1;"10 DIM A$(128),B$(38)" 130 PRINT #1;"20 A$=";CHR$(34); 140 FOR I=1 TO 27 150 PUT #1,ASC(A$(I)) 160 NEXT I 170 PRINT #1;CHR$(34) 180 CLOSE #1
After that, all I had to do was put a blank formatted disk in the drive, RUN the program, ENTER "D:BUMB.LST" to merge in the lines it printed, and then delete all the bits of the program that created the file or interacted with DATA statements. That gave me the program in the screenshot.
A Quick Note on Filenames
The D: in the filename there means that this is a file on the disk drive. Other devices we have available to us are C: (the cassette drive), E: (the “editor”—this is actually IO Control Block 0, which we’ve been using for our basic text output), K: (the keyboard, for when you need to pull in data a keystroke at a time), and S: (the “screen”, which is different from the editor because it includes the graphical parts of the display). When BASIC has a graphics mode open, the S: device is opened for interaction with IO Control Block 6. We played around a bit with that last time, too.
We’ve now pretty much sorted out the techniques you’d use to mix machine code and BASIC. But BASIC required an optional cartridge and it also made big chunks of RAM on the 800XL permanently inaccessible. So next time we will look into how to write pure machine-code programs that can coexist with DOS irrespective of BASIC’s presence, and then look into taking full control of the machine so that not even DOS can vex us.