INTRODUCTION
Bill Wilkinson
When I was asked by the editors at COMPUTE! to write this introduction, I was at first a little hesitant. How does one introduce what is essentially a map of the significant locations on the Atari other than by saying "This is a map of..."? 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.A Common Problem
What? Still with me? Does that mean that you are not comfortable doing memory mapped access in three or four languages? Well, to tell the truth, neither am I. And so the one thing I decided would be of most value in this introduction would be a summary of how to do memory access from no less than seven different languages. (Or is it eight? Well....) 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: Atari BASIC BASIC A + Atari Microsoft BASIC Forth C Pascal PILOT LISP Assembler/Machine Language Does it match yours? You don't get credit for more than one assembler or more than one Forth. And, actually, you shouldn't get credit for Microsoft BASIC, since it uses exactly the same method as Atari BASIC. And I will tell you right now that I will not attempt this task in LISP. If you are a LISP fanatic, more power to you; but I don't have any idea of how to approach the problem with Datasoft's LISP (the only LISP currently available on the Atari). Anyway, let's tackle these languages one at a time.Atari BASIC And Microsoft BASIC
Well, how about two at a time this one time? The implementation really is the same for these two languages. 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. Voilą: 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 bit curious, stick with us. It gets better.BASIC A +
There might be a little bit of prejudice on my part here, but I do feel that this is the easiest language to explain to beginners. In fact, rather than start with text, let's show the solutions: 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.Forth
I think the ease of performing the required problems in Forth will show how tightly and neatly Forth is tied to the machine level of the computer. In fact, we don't really have to "invent" a way to solve these problems; the solutions are within the normal specifications, expectations, and capabilities of virtually all Forth implementations. 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." The second line, then, would read essentially the same except that the "!" used (instead of "c!") implies a full word (double byte) store, as does DPOKE in BASIC A +. The similarity and symmetry of the solutions of Problems 1 and 2 are striking. Let us "read" the first line of the second problem: 84 means use the number 84 (in this case, as a location) c@ means fetch the byte (character) at that location V means fetch the location (variable) called "V" ! means store the data fetched first into the location fetched second 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 it is appropriate 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
No, that does not mean "Section C." Believe it or not, "C" is the name of a computer language. In fact, it is one of the more popular computer languages among systems programmers. It is "the" language used on and by the UNIX operating system, which appears to have the inside track on being the replacement for CP/M on the largest microcomputers (e.g., those based on 68000 and other more advanced processors). 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 that 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 parentheses 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. But "pi" will always point to a word-sized (double byte) 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.Pascal
Actually, standard Pascal has no methods whatsoever available to solve these problems. Remember, Pascal is a "school" language, and access to the machine level was definitely not a desirable feature in such an environment. In fact, most of the Pascal compilers in use today have invented some way to circumvent the restrictions of "standard" Pascal, and it is largely because of such "inventions" that the various versions of the language are incompatible. 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, both 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 reasonably 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).PILOT
And here we are at last at the simplest of the Atari languages. Again, standard PILOT has no defined way of accessing individual memory cells. And, again, the reason for this is that PILOT was (and is) a language designed for use in schools, where the last thing you want is poking around in memory and crashing the 100 megabyte disk with next year's budget on it. 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 didAssembly And Machine Language
I almost didn't include this section, since anyone working with assembly language (and especially those trying to debug at the machine language level) would presumably know how to manipulate bytes and words. And yet, it might prove interesting to those who do not know assembler to see just how the 6502 processor really does perform its feats. 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 lines 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 COLORS LDA H + 1 ; what's this? the second byte of H! STA 86 ; into the second byte of COLORS 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 COLORS 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 by simply 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 COLumn CuRSor 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 and make your code readable and maintainable. It may lose you friends, but it might help you land a job.Happy Mapping
Well, we made it. I hope you now at least have an idea of what to do to modify and examine various memory locations in all of the languages shown. Virtually all of the many locations mapped in this book will fall into one of the two categories examined: they will involve changing or examining either a single byte or a double byte (word, integer, address, etc.). Follow the models shown here, and you should have little trouble effecting your desires. 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 Atari 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.
Return to Table of Contents | Previous Chapter | Next Chapter