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 did
Assembly 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