And, yet, there is something about this book which makes it more than "simply a map." After all, if this were "simply" a memory map, I might "simply" use it to learn that "SSKCTL" is the "serial port control" and that it is at location $232. But what does that mean? Why would I want to control the serial port? How would I control it?
The value of this book, then, lies not so much in the map itself as it does in the explanations of the various functions and controls and the implications thereof. Even though I consider myself reasonably familiar with the Atari (and its ROM-based operating system), I expect to use this book often.
Until now, if I needed to use an exotic location somewhere in the hardware registers, I would have to first locate the proper listing, then find the right routine within the listing, figure out why and how the routine was accessing the given register, and finally try to make sure that there were no other routines that also accessed this same register. Whew! Now, I will open this book, turn to the right page, find out what I need to know, and start programming.
Okay. So much for this introduction. And if you are comfortable programming your "home" language, the language you know best, and two or three other languages, you don't need any more from me. So good luck and bon voyage.
The title of this section is perhaps a little misleading (on purpose, of course, as those of you who read my column "Insight: Atari" in COMPUTE! Magazine can attest). The "common problem" we will discuss here is not a bug-type problem. Rather, it is a task-type problem which occurs in many common programs. Or perhaps we could approach it as a quiz. Why not?
Quiz: Devise a set of routines which will (1) alter the current cursor
position (in any standard OS graphics mode) to that horizontal and
vertical position specified by the variables "H" and "V"
and (2) retrieve the current cursor position in a like manner. To receive
full credit for this problem, implement the routine in at least seven
different computer languages.
Well, our first task will be to decide what seven languages we will use. First step in the solution: find out what languages are available on the Atari computers. Here's my list:
Anyway, let's tackle these languages one at a time.
Actually, the first part of this problem set is done for you in Atari BASIC: the POSITION statement indeed does exactly what we want (POSITION H,V will do the assigned task). But that's cheating, since the object of these problems is to discover how to do machine level access without such aids.
Step 1 is to look at the memory map and discover that COLCRS, at locations 85 and 86, is supposed to be the current graphics cursor column (COLumn of CuRSor). Also, ROWCRS (ROW of CuRSor) at location 84 is the current graphics cursor row.
Let's tackle the row first. Assuming that the row number is in the variable "V" (as specified above), then we may set the row cursor via "POKE 84,V". And, in a like manner, we may say "V=PEEK(84)" to assign the current position to "V". Now that's fairly straightforward: to change a single memory location, use "POKE address, value"; to retrieve the contents of a single memory location, use "PEEK(address)". Virtually anyone who has programmed in BASIC on an Atari is at least familiar with the existence of PEEK and POKE, since that is the only method of accessing certain functions of the machine (and since the game programs published in magazines are loaded with PEEKs and POKEs).
But now let's look at the cursor column, specified as being locations 85 and 86, a "two byte" value. What does that mean? How can something occupy two locations? Actually, it all stems from the fact that a single location (byte, memory cell, character, etc.( in an Atari computer can store only 256 different values (usually numbered 0 to 255). If you need to store a bigger number, you have to use more bytes. For example, two contiguous bytes can be used to store 65536 different values, three bytes can store 16,777,216 different values, etc.
Since the Atari graphics mode can have as many as 320 columns, we can't use a single one-byte location to store the column number. Great! We'll simply use two bytes and tell BASIC that we want to talk to a bigger memory cell. What's that? You can't tell BASIC to use a bigger memory cell? Oops.
Ah, but have no fear. We can still perform the task; it just takes a little more work in BASIC. The first sub-problem is to break the column number (variable "H") into two "pieces," one for the first byte and one for the second. The clearest way to accomplish this is with the following code:
H1 = INT(H/256)
H2 = H - 256* H1
Because of the nature of machine language "arithmetic," numbers designed to be two-byte integers must usually be divided as shown: the "high order byte" must be obtained by dividing the number by 256, and any fractional part of the quotient must be discarded. the "low order byte" is actually the remainder after all units of 256 have been extracted (often designated as "the number modulo 256").
So, if we have obtained "H1" and "H2" as above, we can change the cursor row as follows:
POKE 85,H2
POKE 86,H1
Notice the reversal of the order of the bytes! For the Atari (and many other microcomputers), the low order (or least significant) byte comes first in memory, followed by the high order (or most significant) byte.
Now, suppose we wish to avoid the use of the temporary variables "H1" and "H2" and further suppose that we would now like to write the entire solution to the first problem here. Voila:
POKE 84,V
POKE 86,INT(H/256)
POKE 85,H - 256 * INT(H/256)
And we wrote those last two lines in "reverse" order so that we could offer a substitute last line, which will not be explained here but which should become clear a few paragraphs hence:
POKE 85,H - 256 * PEEK(86)
Whew! All that to solve just that first problem! Cheer up, it does get easier. In fact, we already mentioned above that you can retrieve the current row via "PEEK(84)". But how about the column?
Again, we must remember that the column number might be big enough to require two adjacent bytes (locations, memory cells, etc.). Again, we could construct the larger number via the following:
H2 = PEEK(85)
H1 = PEEK(86)
H = H2 + 256 * H1
Do you see the relationship between this and the POKEs? To "put it back together," we must multiply the "high order byte" by 256 (because, remember, it is actually the number of 256's we could obtain from the larger number) before adding it to the "low order byte."
Again, let us summarize and simplify. The following code will satisfy the second problem requirement for BASIC:
V = PEEK(84)
H = PEEK(85) + 256 * PEEK(86)
Okay, we did it. For two languages. And if you are only interested in BASIC, you can quit now. But if you are even a little curious, stick with us. It gets better.
Problem 1.
POKE 84,V
DPOKE 85,H
Problem 2.
V = PEEK(84)
H = DPEEK(85)
As you can see, for the single memory cell situations, BASIC A+ functions exactly the same as the Atari and Microsoft BASICs. But for the double-byte problems, BASIC A+ has an extra statement and an extra function, designed specifically to interface to the double-byte "words" of the Atari's 6502 processor.
DPOKE (Double POKE) performs exactly the equivalent of the two POKEs required by Atari BASIC. DPEEK (Double PEEK) similarly combines the functions of both the Atari BASIC PEEKs. And that's it. Simple and straightforward.
Again, I think I will show the solutions before explaining:
Problem 1.
V @ 84 c!
H @ 85 !
Problem 2.
84 c @ H !
85 @ V !
Now, if you are not a Forth user, that may all look rather cryptic (looks like a secret code to me), but let's translate it into pseudo-English. The first line of the first problem might be read like this:
V means the location (or variable) called "V"
@ means fetch the contents of that location
84 means use the number 84
c! means store the character (byte) that we fetched first into the
location that we fetched second
or, in shorter form,
"V is to be fetched as the data and 84 is to be used as the address
of a byte-sized memory store."
And, again, the only difference between this and the next line is that "@" (instead of "c@") implies a double-byte fetch (again, as does DPEEK of BASIC A+).
Neither is there space here nor is it apprpriate now to discuss the foibles of Forth's reverse Polish notation and its stacking mechanism, but even dyed-in-the-wool algorithmic language freaks (like me) can appreciate its advantages in situations such as those demonstrated here.
C, somewhat like Forth, is fairly intimately tied to the machine level. For example, there are operators in C which will increment or decrement a memory location, just as there are such instructions in the assembly language of most modern microprocessors.
Unlike Forth, however, C requires the user to declare the he/she is going beyond the scope of the language structures in order to "cheat" and access the machine level directly. In standard C (i.e., as found on UNIX), we could change the current cursor row via something like this:
* ((char *) 84) = V;
Which, I suppose, is just as cryptic as Forth to the uninitiated. If you remember that parentheses imply precedence, just as in BASIC, you could read the above as "Use the expression '84' as a pointer to a character (i.e., the address of a byte -- specified by 'char *') and store V ('=') indirectly (the first '*') into that location." Whew! Even experienced C users (well, some of us) often find themselves putting in extra parantheses to be sure the expression means what they want it to.
Anyway, that '(char *)' is called "type casting" and is a feature of more advanced C compilers than those available for the Atari. But, to be fair, it is really a poor way of doing the job, anyway. So let's do it "right":
Problem 1.
char *pc; /* pc is a pointer to a byte */
int *pi; /* pi is a pointer to a double byte */
pc = 84; pi = 85;
...
*pc = V; *pi = H;
Problem 2.
char *pc;
int *pi;
pc = 84; pi = 85;
...
V = *pc; H = *pi;
As with the Pascal solutions, in the following section, we must declare the "type" of a variable, rather than simply assuming its existence (as in BASIC) or declaring its existence (as in Forth). The theory is that this will let the compiler detect more logic errors, since you aren't supposed to do the wrong thing with the wrong variable type. (In practice, the C compilers available for the Atari, including our own C/65, are "loose" enough to allow you to cheat most of the time.)
Here, the declarations establish that "pc" (program counter) will always point to (i.e., contain the address of) a byte-sized item. Now, actually, these variables point to nothing until we put an address into them, which we proceed to do via "pc = 84" and "pi = 85".
And, finally, the actual "assignments" to or from memory are handled by the last line in each problem solution. Now, all this looks very complicated and hardly worthwhile, but the advantage of C is, once we have made all our declarations, that we can use the variables and structures wherever we need them in a program module, secure in the knowledge that our code is at least partially self-documented.
Anyway, Atari Pascal does provide a method to access individual memory cells. I am not sure that the method I will show here is the best or easiest way, but it appears to work. Again, the solution is presented first:
Note: the code in this first part is common to both problems,
for H and V.
(* in the "type" declarations section *)
charaddr = record
row : char;
end;
wordaddr = record
col : integer;
end;
(* in the "var" declarations section *)
pc : ^charaddr;
pw : ^wordaddr;
rowcrs : absolute[84]^charaddr;
colcrs : absolute[85]^wordaddr;
Problem 1.
(includes the above common code)
(* execution code in the procedure *)
pc := rowcrs;
pw := colcrs;
pc^.row := V;
pw^.col := H;
Problem 2.
(includes the above common code)
(* again, procedure execution code *)
pc := rowcrs;
pw := colcrs;
V := pc^.row;
H := pw^.col;
Did you get lost? Don't feel bad. I really felt that this could be written in a simpler fashion, but I wanted to present a version which I felt reasonbly sure would work under most circumstances.
The type declarations are necessary simply to establish record formats which can be pointed to (and it was these record formats which I felt to be redundant). Then the variables which indeed point to these record formats are declared. Most importantly, the "absolute" type allows us to inform the Pascal compiler that we have a constant which really is (honest, really, please let it be) the address of one of those record formats we wanted to point to. (And it is this "absolute" type which is the extension of Pascal which is not in the standard.)
Once we have made all our declarations, the code looks surprisingly like the C code: assign the absolute address to the pointer and then fetch or store via the pointer. The overhead of the record element reference (the ".row" and ".col") is the only real difference (and perhaps unneeded, as I stated).
However, when using PILOT on an Atari computer, the worst anyone can do is to crunch their own copy of their own disk or cassette. So Atari has thoughtfully provided a way to access memory cells from PILOT; and they have done it in a fashion that is remarkably reminiscent of BASIC. Once more, the solution is given first:
Problem 1.
C:@B84 = #V
C:@B86 = #H/256
C:@B85 = #H\256
Problem 2.
C:#V = @B84
C:#H = @B85 + (256 * @B86)
The trick to this is that Atari PILOT uses the "@B" operator to indicate a memory reference. When used on the left side of the equals sign in a C: (compute) statement, it implies a store (just as does POKE in BASIC). When used on the right side of an equals sign (or, for that matter, in Jump tests, etc.), it implies a memory fetch (just as does PEEK in BASIC).
If you have already examined the BASIC code, you will probably note a marked similarity between it and this PILOT example. Again, we must take the larger number apart into its two components: the number of units of 256 each (#H?256) and the remainder. Notice that with PILOT we do not need to (nor can we) specify "INT(#H/256)". There is no INT function simply because all arithmetic in Atari PILOT is done with double-byte integers already. Sometimes, as in this instance, that can be an advantage. Other times, the lack of floating point will preclude PILOT being used for several applications.
Notice the last line of the solution to problem 1: the use of the "\" (modulo) operator is essentially just a convenient shorthand available in several languages. In PILOT,
"#H\256"
is exactly equivalent to
"#H - (256 * (#H/256))".
Atari PILOT is much more flexible and usable than the original, so why not take advantage of all its features? Experiment. You will be glad you did.
For the purposes of the example solutions, we will presume that somewhere in our program we have coded something equivalent to the following:
V *= * + 1 ;reserve one byte for V
H *= * + 2 ;reserve two bytes for H
Those values do not give values to V and H; they simply assign memory space to hold the eventual values (somewhat like DIMensioning an array in Atari BASIC, which does not put any particular values into the array). If we wished not only to reserve space for the "variables" V and H but also to assign an initial value to them, we could code this instead:
V .BYTE 3 ;assign initial value of 3 to byte V
H .WORD 290 ;assign initial value of 290 to word H
Anyway, given that H and V have been reserved and have had some value(s) placed in them, here are the solutions to the problems:
Problem 1.
LDA V ;get the contents of V
STA 84 ;and store them in ROWCRS
LDA H ;then get the first byte of H
STA 85 ;and store in first byte of COLCRS
LDA H + 1;what's this? the second byte of H!
STA 86 ;into the second byte of COLCRS
Problem 2.
LDA 84 ;almost, we don't need to comment this...
STA V ;it's just problem 1 in reverse!
LDA 85 ;first byte of COLCRS again
STA H ;into the least significant byte of H
LDA 86 ;and also the second byte
STA H + 1;the high order byte of H
Do you wonder why we didn't try to move both bytes of H at one time, as we did in BASIC A+, above? Simple: the 6502 microprocessor has no way to move two bytes in a single instruction! Honest! (and this is probably its biggest failing as a CPU.)
Of course, if you have a macro assembler, you could write a macro to perform these operations. Here is an example using one macro assembler available for the Atari, though all macro assemblers will operate in at least a similar fashion. First, we define a pair of macros:
.MACRO MOVEWORD
LDA %1
STA %2
LDA %1 + 1
STA %2 + 1
.ENDM
.MACRO MOVEBYTE
LDA %1
STA %2
.ENDM
Both these macros simply move their first "argument" into their second "argument" (and we won't define here just what "arguments" are and how they work -- examine a macro assembler manual for more information). The first macro moves two adjacent bytes (i.e., a "word"), and the second moves a single byte. And now we can write our problem code in a much simpler fashion:
Problem 1.
MOVEBYTE V,84
MOVEWORD H,85
Problem 2.
MOVEBYTE 84,V
MOVEWORD 85,H
And yet another concept before we leave assembly language. One of the most powerful features of an assembler is its ability to handle equated symbols. The real beauty of this, aside from producing more readable code, is that you can change all references to a location or value or whatever simply by changing a single equate in your source code. Thus, if somewhere near the beginning of our source program we had coded the following two lines:
ROWCRS = 84 ;address of ROW CuRSor
COLCRS = 85 ;address of COLumnCuRSor
then we could have "solved" the problems thus:
Problem 1.
MOVEBYTE V,ROWCRS
MOVEWORD H,COLCRS
Problem 2.
MOVEBYTE ROWCRS,V
MOVEWORD COLCRS,H
And I believe that this looks as elegant and readable as any of the higher level languages! In fact, it looks more readable than most of the examples given above. To be fair, though, we should note that all of the examples could have been made more readable by substituting variable names instead of the absolute numbers "84" and "85," but the overhead of declaring and assigning variables is sometimes not worth it for languages such as BASIC and PILOT.
Luckily, the remaining languages (Forth, C, and Pascal) all have a means of declaring constants (akin to the assembly language equate) which has little or no consequential overhead. So go ahead -- be the oddball on your block amd make your code readable and maintainable. It may lose you friends, but it might help you land a job.
For those few locations which do not follow the above patterns (e.g., the system clock, which is a three-byte location in high-middle-low order), you may be able to accomplish your ends by considering each byte individually. Also, we have made no discussion here of the Atai floating point format, which is truly accessible in any reasonable fashion only from assembly language, and which has little pertinence to this memory map in any case.
I think I would like to add only one more comment, which will be in the form of a caution: If you aren't sure what you are doing when changing or examining memory locations, make sure that your program in memory is backed up (on disk or cassette), and then make sure that you have "popped" (unloaded) your disks and/or tapes. It is unlikely that changing memory will cause problems affecting your saved files, but why take chances. (and, if you make a mistake or are in doubt, re-boot the disk; don't just hit RESET, since that won't necessarily clean up all your errors.)
Good luck and happy mapping.