LOCATING MACHINE LANGUAGE PROGRAMS IN MEMORY
When we begin writing subroutines, we must decide where we want to locate them. There are two types of machine language programs, relocatable and fixed. Fixed programs are those which use specific addresses within the program; these addresses cannot change. For instance, suppose our program contained these lines:
30 *= $600
45 LDA ADDR1
50 BNE NOZERO
55 JMP ZERO
60 NOZERO RTS
70 ZERO SBC #1
80 RTS
90 ADDR1 .BYTE 4
45 LDA ADDR1
50 BNE NOZERO
55 JMP ZERO
60 NOZERO RTS
70 ZERO SBC #1
80 RTS
90 ADDR1 .BYTE 4
In this excerpt, we use several references to addresses within the program which are fixed: they cannot change without completely messing up the program. These are more easily seen after we use the assembler to assemble this program, producing output which looks like this:
ADDR
ML LN LABEL OP OPRND
0000 30 *= $600
0600 AD0C06 45 LDA ADDR1
0603 D003 50 BNE NOZERO
0605 4C0906 55 JMP ZERO
0608 60 60 NOZERO RTS
0609 E901 70 ZERO SBC #1
060B 60 80 RTS
060C 04 90 ADDR1 .BYTE 4
0000 30 *= $600
0600 AD0C06 45 LDA ADDR1
0603 D003 50 BNE NOZERO
0605 4C0906 55 JMP ZERO
0608 60 60 NOZERO RTS
0609 E901 70 ZERO SBC #1
060B 60 80 RTS
060C 04 90 ADDR1 .BYTE 4
This output is nicely formatted in columns. The first column lists the hexadecimal addresses at which the machine language instructions translated from the mnemonics are located. The second column lists the machine language code which results from that translation. For instance, the instruction RTS in line 80 generated the machine language code 60, which was located in memory location $060B. The third column lists the line numbers of the assembly language program. The fourth column contains any labels which were present in the original program, and the fifth column contains the mnemonics of the program. The sixth column contains the operand. In this example, there is no seventh column, which would have been present if the original program had contained any comments.
To return to the problem of the fixed addresses discussed above, the first of the problem addresses, ADDR1, can be found in lines 45 and 90. Let's look for a moment at the machine language code which the assembler produced for line 45. Three bytes were produced; AD, 0C, and 06. AD is the machine language code for the absolute addressing mode of the LDA instruction. Since we know that the absolute addressing mode of the LDA instruction requires 3 bytes, we know why 3 bytes were produced by the assembler. The second and third bytes, 0C and 06, make up the address from which to load the accumulator, in the standard 6502 order of least significant byte-most significant byte. Therefore, the address from which to load the accumulator is $060C, which is the address of the line containing the label ADDR1. When we wrote LDA ADDR1, the assembler translated this to mean LDA $060C, since that was the address assigned to ADDR1.
Now we can understand why any attempt to run this program somewhere else in memory is doomed to failure. When line 45 is executed, the microprocessor will look at the original address, $060C, in order to load the accumulator; it expects to find ADDR1 there, since this was the location which was assigned for ADDR1 at the time of assembly. However, the logic of the program was established to perform certain functions based on the value stored in $060C only if $060C was equal to ADDR1. If we move the routine in memory, ADDR1 will be somewhere other than at $060C, and the logic of the program will no longer be valid.
The second fixed address referred to in this program is in line 55. Every JMP instruction has as its destination a fixed address. We can see this by examining the machine language code generated for line 55: 4C, 09, 06. The byte 4C is the machine language code for an absolute JMP instruction, a 3-byte instruction. The next 2 bytes are the address to which to jump, $0609 (remember, least significant byte first). We can now see that when line 55 is executed, the program will jump to $0609, regardless of where in memory we may have moved this program. However, if we do move this program elsewhere in memory, the instruction we intended to have executed at $0609 (the SBC #1 in line 70) will no longer be there. In fact, there will probably be no valid instruction at all at $0609, so the program will crash.
Look for a moment at line 50. Remember that all branch instructions use the relative form of addressing. If we look at the machine language code for this instruction, we'll find D0, 03. D0 is the machine language code meaning to branch on not equal to zero, but that 3 doesn't look like an address. It's not. It simply tells the 6502 to branch forward 3 bytes in memory from the current location of the program counter. When line 50 is executed, the program counter is pointing to the start of the next line. In this case, it points to the 4C of the JMP instruction in line 55. Moving it forward 3 bytes will then point it to 60, the RTS instruction in line 60. That is, when the BNE NOZERO is assembled and executed, this instruction tells the 6502 to branch forward 3 bytes, past the JMP instruction to the address NOZERO ($0608). Since the branch instruction simply says "Branch forward three bytes," rather than "Branch forward to $0608," it can be located anywhere in memory and the branch will still wind up at NOZERO, regardless of where in memory NOZERO is. NOZERO will always be 3 bytes ahead of the branch instruction, so all will be well.
PLEASE
NOTE!!! This teaches us an important lesson:
branches can be included in relocatable code, but JMPs and specific
addresses within the program cannot be.
Why is so much written about relocatable code? For one very simple reason: if code is not relocatable, then we need to find some safe place in the computer's memory to store it. This may not always be easy, since we're sharing the computer with BASIC, and we can't always be sure what locations BASIC will be using. If our code is relocatable, we can put it anywhere. But where?
Let's for a moment review how BASIC handles strings. When you want to use a string in ATARI BASIC, it must be dimensioned. When this is done, the computer reserves space for the string in memory. If for some reason it needs part of that space, it simply moves the string somewhere else, but BASIC is then responsible for remembering where the string is, and is also responsible for protecting its space. Aha! Now we're out of the situation where we have to protect some area of memory from BASIC, and into one in which BASIC does the allocating and protecting for us! We can then store our machine language program as a string in BASIC and access it by using the USR(ADR(ourstring$)) form of command.
To be fair, there will usually be room for a short routine on page 6. Remember that page 6 is guaranteed to always be kept free for the programmer's use. Well, almost always. You should be aware that there is a not infrequent condition under which page 6 may not be safe. As was mentioned in Chapter 3, the space from $580 to $5FF (the top half of page 5) is used as a buffer (a place for temporarily storing information) by your ATARI. If you're entering information from the keyboard, this input buffer may overflow into the bottom of page 6. This overflow will then overwrite anything stored between $600 and $6FF, depending on how much overflow there is. For the purposes of this book, we will assume that such overflow will not occur, and programs which cannot be written to be relocatable will generally have their origin at $600. If they don't work in some specific application you may have, check to be sure that you're not overflowing the buffer from page 5.
Other places to locate non-relocatable programs are up high in memory, or below LOMEM. Both places are generally safe from interference with BASIC if care is taken in their use. Additionally, if you have an application which will never use the tape recorder, you may use the tape buffer, located between $480 and $4FF, for program location. Very small routines may also be placed at the low end of the stack, from $100 to about $160, since only rare applications will ever use the stack to this depth. However, this is extremely risky, and no guarantees about safe performance can be given for programs using this space.
While we're on the subject of tape recorders, one final note about the organization of this book. All programs are written assuming the presence of a disk drive and a resident DOS. If you are using a tape-based system, please refer to your assembler manual for instructions on how to perform certain operations. For instance, loading machine language files from a disk drive may use the L option of DOS, whereas the same operation using a tape recorder will probably use some form of the LOAD or BLOAD commands, depending on which assembler you are using.
A SIMPLE EXAMPLE SUBROUTINE TO CLEAR MEMORY
Let's begin to build our library of subroutines with a very simple example. Remember, if you don't want to type all of the programs, they are available on disk from MMG Micro Software.
In BASIC, we frequently need to clear an area of memory to zeros. This occurs, for instance, when using player-missile graphics or when using memory as a scratchpad or even just when a screen or drawing needs to be cleared. Remember that if we want to store data near the top of memory, the display list and display memory must be relocated below this area of memory, or else this routine will wipe out the picture on our TV screen. This relocation can be accomplished very easily, using the following code in BASIC:
10
ORIG=PEEK(106):REM Save original top of memory
20 POKE 106,ORIG-8:REM Lower the top of memory by 8 pages
30 GRAPHICS 0:REM Reset the display list and screen memory
40 POKE 106,ORIG:REM Restore the top of memory as before
20 POKE 106,ORIG-8:REM Lower the top of memory by 8 pages
30 GRAPHICS 0:REM Reset the display list and screen memory
40 POKE 106,ORIG:REM Restore the top of memory as before
We can, of course, perform the memory clearing operation in BASIC. If we need to clear the top 8 pages of memory to all zeros, we can do so with the following program:
10
TOP=PEEK(106):REM Find the top of memory
20 START=(TOP-8)*256:REM Calculate where to start the clear
30 FOR I=START TO START+2048:REM Area to be cleared
40 POKE I,0:REM Clear each location
50 NEXT I:REM All finished
20 START=(TOP-8)*256:REM Calculate where to start the clear
30 FOR I=START TO START+2048:REM Area to be cleared
40 POKE I,0:REM Clear each location
50 NEXT I:REM All finished
This program works just the way we want it to, but takes approximately 13.5 seconds to execute. If we need to perform this operation several times during the course of a program, or if we have a program which cannot afford the 13.5 seconds required to do this in BASIC, then we have a good candidate for a machine language subroutine.
First we'll need to think about where we'll locate our subroutine. Page 6 is as good a place as any. Then we'll need to know how to find the top of memory, so we'll know what part of memory we want to clear. There is a memory location which always keeps track of where the top of memory, in pages, is in an ATARI, location 106, so that part is easy. Finally, this type of program is usually a good candidate for indirect, Y addressing, so we'll need two page zero locations to hold our indirect address. Let's write what we have so far, in assembly language:
100
; ***************************
110 ; set up initial conditions
120 ; ***************************
130 *= $600 ; we have to assemble it somewhere
140 TOP = 106 ; here's where we find the top stored
150 CURPAG = $CD ; where we store current page being cleared
110 ; set up initial conditions
120 ; ***************************
130 *= $600 ; we have to assemble it somewhere
140 TOP = 106 ; here's where we find the top stored
150 CURPAG = $CD ; where we store current page being cleared
Note that there are only four page zero memory locations, $CC to $CF, which are secure from being changed by both BASIC and the Assembler/Editor cartridge: we'll use two of them, $CC and $CD, for this program. It is possible to find other page zero locations safe from the cartridge, but these are guaranteed by ATARI always to be safe, so we'll use these.
Now let's think about what we'd like the routine to do. First we must remember the PLA required to pull the number of parameters, passed by BASIC in the USR call we'll write, off the stack. After we've found the present top of memory, we'll need to start 8 pages below that, clearing all of memory to zero. The code to find where in memory to start clearing is fairly straighforward, as shown below:
160
; ***************************
170 ; begin with calculations
180 ; ***************************
190 PLA ; remove # of parameters from stack
200 LDA TOP ; find the top
210 SEC ; get ready for subtraction
220 SBC #8 ; find first page to clear
230 STA CURPAG ; we'll need it
240 LDA #0 ; to insert it in memory later
250 STA CURPAG-1 ; the low byte of a page # is always zero
170 ; begin with calculations
180 ; ***************************
190 PLA ; remove # of parameters from stack
200 LDA TOP ; find the top
210 SEC ; get ready for subtraction
220 SBC #8 ; find first page to clear
230 STA CURPAG ; we'll need it
240 LDA #0 ; to insert it in memory later
250 STA CURPAG-1 ; the low byte of a page # is always zero
Now we've set up the indirect address of the first place in memory to clear, and we've stored it in CURPAG-1 (low byte) and CURPAG (high byte). We've also loaded the accumulator with zero, so we're all set to store a zero in each memory location we need to clear.
Next, we need a counter to keep track of how many memory locations have been cleared on each page. If we use the Y register for this, it can act both as a counter and as an offset for the addressing system we're using. All we have to do is set the counter, store the zero in the accumulator into the first location, and decrement the Y register by 1, looping back to perform the store again. Since we began with the counter at zero and we are decrementing by 1 as we clear each location, as long as the Y register has not yet reached zero again, we still have more to clear on this page, since there are 256 locations per page of memory. Let's see what the code will look like:
260
LDY #0 ; for use as a counter
270 ; ***************************
280 ; now we'll enter the clearing loop
290 ; ***************************
300 LOOP STA (CURPAG-1),Y ; the first byte is cleared
310 DEY ; lower the counter
320 BNE LOOP ; if >zero, page is not yet finished
270 ; ***************************
280 ; now we'll enter the clearing loop
290 ; ***************************
300 LOOP STA (CURPAG-1),Y ; the first byte is cleared
310 DEY ; lower the counter
320 BNE LOOP ; if >zero, page is not yet finished
Now that was pretty simple. We stored the zero that was in the accumulator into the address pointed at by the indirect address CURPAG-1, CURPAG (low byte, high byte) offset by Y, which was zero the first time through the loop. Then we decreased the Y register by 1, and looped back to store a zero in the memory location pointed to by the same indirect address, but this time offset by 255, so we've cleared the top byte of the page. Next time through, Y equals 254, so we clear the next-lower byte of memory, and so on, until when Y equals zero the whole page is cleared and our counter is back to zero, ready for the next page.
All right, now we've cleared 1 page. How do we get it to clear all of the other 7 pages? Remember that the indirect address we set up on page zero has 1 byte for the low byte of the indirect address pointing to the page to be cleared, and one byte for the high byte of the address. If we simply increase the high byte by 1, this indirect address will point at the next-higher page, like this:
330
INC CURPAG ; to move on to next page
It really couldn't be much easier than that, could it? Now all we need to do is find out when we're done.
In this case, we know that we're done when the page we're clearing is higher than the top of memory. It is fairly easy to determine if this condition is true, as follows:
340
LDA CURPAG ; need to see if we're done
350 CMP TOP ; is CURPAG > TOP?
360 BEQ LOOP ; no, last page coming up!
370 BCC LOOP ; no, keep clearing
380 RTS ; go back to BASIC
350 CMP TOP ; is CURPAG > TOP?
360 BEQ LOOP ; no, last page coming up!
370 BCC LOOP ; no, keep clearing
380 RTS ; go back to BASIC
If CURPAG is equal to TOP, remember that we've still got that last page to clear. Only if CURPAG is greater than TOP have we finished.
Now that we've written our program, we need to convert it from assembly language to machine language. To do this, we use the assembler part of the cartridge, which can be accessed simply by typing ASM followed by a RETURN. This will start the assembly process, and after a short pause, the following information will appear on your screen:
Listing 7-1
ADDR ML LN LABEL OP OPRND COMMENT
0100 ; ***************************
0110 ; Set up initial conditions
0120 ; ***************************
0000 0130 *= $600 ; Place to assemble it
006A 0140 TOP = 106 ; Where the top is stored
00CD 0150 CURPAG = $CD ; To store page being cleared
0160 ; ***************************
0170 ; Begin with calculations
0180 ; ***************************
0600 68 0190 PLA ; # of parameters off stack
0601 A56A 0200 LDA TOP ; Find the top
0603 38 0210 SEC ; Get ready for subtraction
0604 E908 0220 SBC #8 ; Find first page to clear
0606 85CD 0230 STA CURPAG ; We'll need it
0608 A900 0240 LDA #0 ; To insert it in memory later
060A 85CC 0250 STA CURPAG-1 ; Low byte of page # is zero
060C A000 0260 LDY #0 ; For use as a counter
0270 ; **********************************
0280 ; Now we'll enter the clearing loop
0290 ; **********************************
060E 91CC 0300 LOOP STA (CURPAG-1),Y ; The first byte is cleared
0610 88 0310 DEY ; Lower the counter
0611 D0FB 0320 BNE LOOP ; If >zero, page not done yet
0613 E6CD 0330 INC CURPAG ; Let's move on to the next page
0615 A5CD 0340 LDA CURPAG ; Need to see if we're done
0617 C56A 0350 CMP TOP ; Is CURPAG >TOP?
0619 F0F3 0360 BEQ LOOP ; No, last page coming up!
061B 90F1 0370 BCC LOOP ; No, keep clearing
061D 60 0380 RTS ; Go back to BASIC
ADDR ML LN LABEL OP OPRND COMMENT
0100 ; ***************************
0110 ; Set up initial conditions
0120 ; ***************************
0000 0130 *= $600 ; Place to assemble it
006A 0140 TOP = 106 ; Where the top is stored
00CD 0150 CURPAG = $CD ; To store page being cleared
0160 ; ***************************
0170 ; Begin with calculations
0180 ; ***************************
0600 68 0190 PLA ; # of parameters off stack
0601 A56A 0200 LDA TOP ; Find the top
0603 38 0210 SEC ; Get ready for subtraction
0604 E908 0220 SBC #8 ; Find first page to clear
0606 85CD 0230 STA CURPAG ; We'll need it
0608 A900 0240 LDA #0 ; To insert it in memory later
060A 85CC 0250 STA CURPAG-1 ; Low byte of page # is zero
060C A000 0260 LDY #0 ; For use as a counter
0270 ; **********************************
0280 ; Now we'll enter the clearing loop
0290 ; **********************************
060E 91CC 0300 LOOP STA (CURPAG-1),Y ; The first byte is cleared
0610 88 0310 DEY ; Lower the counter
0611 D0FB 0320 BNE LOOP ; If >zero, page not done yet
0613 E6CD 0330 INC CURPAG ; Let's move on to the next page
0615 A5CD 0340 LDA CURPAG ; Need to see if we're done
0617 C56A 0350 CMP TOP ; Is CURPAG >TOP?
0619 F0F3 0360 BEQ LOOP ; No, last page coming up!
061B 90F1 0370 BCC LOOP ; No, keep clearing
061D 60 0380 RTS ; Go back to BASIC
Now we have assembled the program and stored it in memory. The next task is to store it onto our disk so we can use it in our BASIC program. This can be done in either of two ways. The first is directly from the cartridge, using the SAVE command as follows:
SAVE #D:PROGRAM<0600,061F
This command creates a file on disk called PROGRAM, and stores all of the contents of memory from $0600 to $061F in that file. Note that we've stored a few extra bytes – generally a good idea.
The second way to store this information is to go to DOS and save memory using the K option. This can be done as follows:
PROGRAM,0600,061F
Either method of storing the results of the assembly will be satisfactory.
Now you can switch cartridges, and replace the Assembler/Editor with the BASIC cartridge. After booting up the computer, type DOS, and when the DOS menu appears, use the L option to load the file called PROGRAM that we just created. Then type B to go back to BASIC.
Our program now resides on page 6, and we can access it if we like. However, the next step should be to put it into a form that doesn't require the use of DOS for loading. We could simply write one line of BASIC code in the direct mode to pull this information from page 6; for example,
FOR
I=1 TO 30:?PEEK(1535+I);" ";:NEXT I
However, since we're using a computer, why not write a general-purpose program that will pull the data out of memory and set it up in a form which we can convert easily to DATA statements in a BASIC program? Such a program is given below:
10
FOR J=1 TO 30 STEP l0:REM Length of data in memory
20 FOR I=J TO J+9:REM We'll get DATA statements 10 bytes long
30 PRINT PEEK(I+1535);",";:REM Print the data to the screen
40 NEXT I:REM Finish the line
50 PRINT:PRINT:REM Leave blank lines for easy working
60 NEXT J:REM All done!
20 FOR I=J TO J+9:REM We'll get DATA statements 10 bytes long
30 PRINT PEEK(I+1535);",";:REM Print the data to the screen
40 NEXT I:REM Finish the line
50 PRINT:PRINT:REM Leave blank lines for easy working
60 NEXT J:REM All done!
If we now type this program in and RUN it, our screen will show the following:
104,165,106,56,233,8,133,205,169,0,
133,204,160,0,145,204,136,208,251,230,
205,165,205,197,106,240,243,144,241,96,
133,204,160,0,145,204,136,208,251,230,
205,165,205,197,106,240,243,144,241,96,
It's now a simple matter to move the cursor up to these lines, remove the trailing commas, and convert them to the following:
10000
DATA 104,165,106,56,233,8,133,205,169,0
10010 DATA 133,204,160,0,145,204,136,208,251,230
10020 DATA 205,165,205,197,106,240,243,144,241,96
10010 DATA 133,204,160,0,145,204,136,208,251,230
10020 DATA 205,165,205,197,106,240,243,144,241,96
Now we can erase lines 10 to 60, so that the program in memory consists of just lines 10000 to 10020. At this point, we should save the program to disk, so we don't have to go through this whole procedure again if the power fails. We can incorporate this routine into a short BASIC program to test it, as follows:
10
FOR I=1 TO 30:REM Number of bytes
20 READ A:REM Get each byte
30 POKE 1535+I,A:REM POKE byte in correct location
40 NEXT I:REM Finish POKEing data
50 ORIG=PEEK(106):REM Now relocate display list, as above
60 POKE 106,ORIG-8
70 GRAPHICS 0
80 POKE 106,ORIG:REM Restore top of memory
90 POKE 20,0:REM Set timer to zero
100 X=USR(1536):REM Call our machine language routine
110 ? PEEK(20)/60:REM How many seconds did it take?
120 END:REM Separate DATA from program
10000 DATA 104,165,106,56,233,8,133,205,169,0
10010 DATA 133,204,160,0,145,205,136,208,251,230
10020 DATA 205,165,205,197,106,240,243,144,241,96
20 READ A:REM Get each byte
30 POKE 1535+I,A:REM POKE byte in correct location
40 NEXT I:REM Finish POKEing data
50 ORIG=PEEK(106):REM Now relocate display list, as above
60 POKE 106,ORIG-8
70 GRAPHICS 0
80 POKE 106,ORIG:REM Restore top of memory
90 POKE 20,0:REM Set timer to zero
100 X=USR(1536):REM Call our machine language routine
110 ? PEEK(20)/60:REM How many seconds did it take?
120 END:REM Separate DATA from program
10000 DATA 104,165,106,56,233,8,133,205,169,0
10010 DATA 133,204,160,0,145,205,136,208,251,230
10020 DATA 205,165,205,197,106,240,243,144,241,96
Line 90 first sets the internal real-time clock to zero, and then line 110 reads the time in jiffies (sixtieths of a second). This will measure the elapsed time the USR call, our machine language routine, took to clear 8 pages of memory. It takes 0.0333 seconds, so this machine language routine, which seems so long and time-consuming, is over 400 times faster than the BASIC program that did the same job. Worth the effort, wasn't it?
Of course, all that time spent programming this routine was not wasted, since we've now got a routine which we can use whenever we need to clear the top of memory, such as for player-missile graphics.
The program we wrote has one drawback: the code resides on page 6 and therefore cannot be used in a program which needs page 6 for its own use. Now here's where the relocatable nature of the code comes in. Let's look again at the assembly language program we wrote. Note that we didn't use any jumps, nor did we make reference to any address within the program, except in branch instructions. This program is not tied to any specific memory locations; it can reside anywhere in memory and still work. Let's take advantage of that and turn the program into a string. This process is fairly simple. All we need to do is add a line 5 and change lines 30 and 100, as follows:
5
DIM CLEAR$(30):REM Set up the string
30 CLEAR$(I,I)=CHR$(A):REM Insert byte into string
100 X=USR(ADR(CLEAR$)):REM New location of the clearing routine
30 CLEAR$(I,I)=CHR$(A):REM Insert byte into string
100 X=USR(ADR(CLEAR$)):REM New location of the clearing routine
So now we have a relocatable routine to clear memory, which will be far more versatile than the one tied to specific locations. In fact, if the string which we create contains no control characters, we can simply produce a 1-line subroutine which will contain all of the information we need. We can do this by running the program and then printing CLEAR$ to the screen. We can then move the cursor up to the string of machine language and convert it to a single line of BASIC, as follows:
It can't be any simpler than this! Now whenever we need to clear the top of memory, we can just include this line of code, and access the subroutine to clear the memory.
Other, more sophisticated routines exist for producing BASIC programs once you have created your machine language routine, or you can write such programs yourself. One very nice routine for producing such subroutines as strings was published in the September, 1983, issue of ANTIC magazine, by Jerry White. This routine reads the machine language data directly from the disk and writes the BASIC code back to disk. This avoids one problem with printing such strings to the screen; if the string contains a non-printing character, a fair amount of work is required to be sure the string is correct. One way to check your routines quite easily for such problems is to simply count the number of characters printed to the screen when you print your string, and compare that number to the number of bytes contained in your machine language routine. If the numbers differ, you'd better find out which character has been omitted and insert it in the appropriate place using this key sequence:
ESC
CTRL-key
which will allow you to print normally nonprinting characters. Let's take a simple example of this. Suppose you have written a machine language routine for some purpose, and when you print the string containing this routine to the screen, it is 1 byte shorter than it should be. Furthermore, you hear a bell sound every time you print this string. In reviewing your DATA statements, you find that the 15th byte of your machine language routine is 253. When you attempt to print the character corresponding to ATASCII 253, the bell will sound, since this is the code for the keyboard buzzer, but the character will not be printed to the screen. To solve this problem, print the string to the screen and then position the cursor over the 15th byte of the string. Press the CTRL key and the INSERT key simultaneously, and from the 15th character on the string will move 1 position to the right, leaving space for the missing character. Now press the ESC key, and next simultaneously depress the CTRL key and the 2 key, and the correct character will be inserted in the 15th byte of your string. A line number and the other information required, as shown above, may then be added, and you'll have your routine on a single line.
For short, single routines, the easiest way around this problem is not to use strings in single lines, but rather to insert the characters in a string using DATA statements, as already demonstrated. However, where single-line strings are desirable – for example, where space is at a premium – the more cautious the programmer, the better the results will be.
SUBROUTINE TO RELOCATE THE CHARACTER SET
One of the very nice features of the ATARI computers is the ease with which the standard character set (normally the uppercase, lowercase, and inverse letters, numbers, and symbols we use every day) can be altered for any purpose we desire. For instance, one of ATARI's most popular games, SPACE INVADERS, was programmed using redefined characters for the attacking invaders. These are then simply printed to the screen in the appropriate place. By printing them all 1 position further right each loop of the game, they appear to march across the screen, in their ominous fashion.
As we know, however, the normal ATARI character set resides in ROM, beginning at location 57344 ($E000 hexadecimal). In order to alter any of the standard characters, we need to move the character set to RAM, where we can get at it. This can, of course, be done in BASIC. A very simple BASIC program to accomplish this is given below:
10
ORIGINAL=57344:REM Where character set is in ROM
20 ORIG=PEEK(106):REM Where top of RAM is located
30 CHSET=(ORIG-4)*256:REM Where relocated set will be
40 POKE 106,ORIG-8:REM We'll make room for it
50 GRAPHICS 0:REM Set up new display list
60 FOR I=0 TO 1023:REM Now we'll transfer the whole set
70 POKE CHSET+I,PEEK(ORIGINAL+I)
80 NEXT I
90 END :REM That's it
20 ORIG=PEEK(106):REM Where top of RAM is located
30 CHSET=(ORIG-4)*256:REM Where relocated set will be
40 POKE 106,ORIG-8:REM We'll make room for it
50 GRAPHICS 0:REM Set up new display list
60 FOR I=0 TO 1023:REM Now we'll transfer the whole set
70 POKE CHSET+I,PEEK(ORIGINAL+I)
80 NEXT I
90 END :REM That's it
This program reserves 8 pages of memory near the top of RAM, much like the previous example did. There is no need to clear this area to all zeros in this case, however, since we fill up 4 of the pages with the character set from ROM. The loop from lines 60 to 80 actually accomplishes the transfer of the character set, which is 1024 bytes long (8 bytes per character times 128 characters). The program works fine, and if we don't mind spending 14.7 seconds to accomplish this transfer, we don't need assembly language at all.
If we'd like to go faster, however, we'll need a machine language subroutine to accomplish the transfer. The subroutine we wrote to clear an area of memory to all zeros contains the techniques we will use in such a program. However, we'll need to add two new features. The first will allow BASIC to pass to our subroutine the location at which we would like our character set to reside in RAM, by using the parameter passing discussed in Appendix 1. The second will store different values in each memory location, rather than storing the same character in each location. To do this, we'll need two indirect addresses set up on page zero. In addition, in this routine we will employ the more usual nomenclature, defining our label as being the lower of the two zero page locations and referring to the higher location as label + 1, rather than defining the higher and referring to the lower location as label - 1. Both methods are presented, to demonstrate the flexibility of programming in assembly language. Now, let's begin with the setup:
0100 ; ***************************
0110 ; Set up initial conditions
0120 ; ***************************
0000 0130 *= $600
00CC 0140 FROM = $CC
00CE 0150 TO = $CE
0110 ; Set up initial conditions
0120 ; ***************************
0000 0130 *= $600
00CC 0140 FROM = $CC
00CE 0150 TO = $CE
From this point on, we'll use the output from the assembler for all of the programs shown. To type these programs for yourself, just type the line number, label (if present), mnemonic, operand, and comments, and assemble it for yourself. When displayed on your screen, the output of your assembler should look like the output given here. By presenting the programs this way, we can refer to the machine language code generated by the assembler as well as to the assembly language code we write.
As you can see, we have now reserved two different areas of page zero for our indirect addresses. We have defined the lower of each pair of bytes, $CC and $CE, so that the indirect address for the place from which we will get the character set will be stored in $CC and $CD, and the indirect address for the place to which we will move the character set will be stored in $CE and $CF. We have cleverly named these locations FROM and TO. For both sets of locations, the low byte of the indirect address will be stored in the lower of the two locations, and the high byte will be stored in the higher, using typical 6502 convention.
Now that we've reserved space for the indirect addresses, the next task is to correctly fill them with the addresses we need. Remember that we're going to pass the TO address from BASIC, but the FROM address is fixed at $E000 by the operating system. Let's see how this part of the program looks.
0160 ; *******************************
0170 ; Initialize and set up indirect addresses
0180 ; *******************************
0600 68 0190 PLA ; Remove # of parameters from stack
0601 68 0200 PLA ; Get high byte of destination
0602 85CF 0210 STA TO+1 ; Store it in high byte of TO
0604 68 0220 PLA ; Get low byte of destination
0605 85CE 0230 STA TO ; Store it in low byte of TO
0607 A900 0240 LDA #0 ; Even page boundary LSB=0
0609 85CC 0250 STA FROM ; Low byte of indirect address
060B A9E0 0260 LDA #$E0 ; Page of character set in ROM
060D 85CD 0270 STA FROM+1 ; Completes indirect addresses
0170 ; Initialize and set up indirect addresses
0180 ; *******************************
0600 68 0190 PLA ; Remove # of parameters from stack
0601 68 0200 PLA ; Get high byte of destination
0602 85CF 0210 STA TO+1 ; Store it in high byte of TO
0604 68 0220 PLA ; Get low byte of destination
0605 85CE 0230 STA TO ; Store it in low byte of TO
0607 A900 0240 LDA #0 ; Even page boundary LSB=0
0609 85CC 0250 STA FROM ; Low byte of indirect address
060B A9E0 0260 LDA #$E0 ; Page of character set in ROM
060D 85CD 0270 STA FROM+1 ; Completes indirect addresses
Let's discuss lines 190 to 230 for a moment. Line 190 is our old friend, used for pulling the number of parameters passed by BASIC off the stack, to keep the stack in order. Note that both lines 200 and 220 are PLA instructions. This is the method used when passing parameters from BASIC. The number to be passed is broken up by BASIC into high and low bytes and is placed on the stack low byte first, then high byte. Therefore, the first number we pull off the stack is the one on the top, the high byte. We store that appropriately in TO+1, the high byte of the indirect address we have set up on page zero. Similarly, we store the low byte passed from BASIC in TO, and we have completed setting up the first of the two indirect addresses we will need.
Now we just have to do the easy part. We know that any page boundary has an address with the low byte equal to 0, so we can store a zero in FROM with no difficulty. We know the high byte is $E0, and don't forget the # sign, to let the assembler know that we want to store the number $E0 into FROM+1, and not whatever number is in memory location $E0.
Now all we need to do is write the loop which will accomplish the transfer for us. We know that we need to transfer 1024 bytes, 4 pages of information, so we'll need a counter to keep track of how far we've progressed. For this purpose, we'll use the X register. We'll also need a counter to keep track of where we are on each page we're tranferring, and for this, we'll use the Y register. Let's see the rest of the program to accomplish this transfer:
0280 ; ******************************
0290 ; Now let's transfer the whole set
0300 ; ******************************
060F A204 0310 LDX #4 ; 4 pages in the character set
0611 A000 0320 LDY #0 ; Initialize counter
0613 B1CC 0330 LOOP LDA (FROM),Y ; Get a byte
0615 91CE 0340 STA (TO),Y ; And relocate it
0617 88 0350 DEY ; Is page finished?
0618 D0F9 0360 BNE LOOP ; No - keep relocating
061A E6CD 0370 INC FROM+1 ; Yes - high byte
061C E6CF 0380 INC TO+1 ; High byte - for next page
061E CA 0390 DEX ; Have we done all 4 pages?
061F D0F2 0400 BNE LOOP ; No - keep going
0621 60 0410 RTS ; Yes, so return to BASIC
0290 ; Now let's transfer the whole set
0300 ; ******************************
060F A204 0310 LDX #4 ; 4 pages in the character set
0611 A000 0320 LDY #0 ; Initialize counter
0613 B1CC 0330 LOOP LDA (FROM),Y ; Get a byte
0615 91CE 0340 STA (TO),Y ; And relocate it
0617 88 0350 DEY ; Is page finished?
0618 D0F9 0360 BNE LOOP ; No - keep relocating
061A E6CD 0370 INC FROM+1 ; Yes - high byte
061C E6CF 0380 INC TO+1 ; High byte - for next page
061E CA 0390 DEX ; Have we done all 4 pages?
061F D0F2 0400 BNE LOOP ; No - keep going
0621 60 0410 RTS ; Yes, so return to BASIC
There are only two differences between this part of the program and the corresponding part of the previous program we wrote. The first is the use of the X register to determine when we are done. Line 310 sets the X register for the number of pages to be transferred. Lines 390 and 400 determine if we have finished, by decrementing the X register and looping back to continue the transfer if the value of the X register has not yet reached zero.
The second difference, of course, is that we're not going to store the same value in every location, so we need to load the accumulator using the same technique we use to store it, indexing the zero page indirect location with the Y register. Note that when Y equals 1, we'll load from the second location of the ROM character set in line 330 and store it in the second location of the RAM set in line 340, and so on. Remember that lines 370 and 380 raise both indirect addresses by 1 page, since at that point in the program, we will have finished a page, and we'll be ready to begin another.
All that remains is to convert this program into a machine language subroutine for BASIC. With the same technique we used for the first program discussed, we save the machine language code, put BASIC in, load our code back again, and produce DATA statements by PEEKing the values stored from 1536 to 1569. These DATA statements can then be used in a program such as the one given below:
10
GOSUB 20000:REM Set up machine language routine
20 ORIG=PEEK(106):REM Top of RAM
30 CHSET=(ORIG-4)*256:REM Place for relocated character set
40 POKE 106,ORIG-8:REM Make room for it
50 GRAPHICS 0:REM Set up new display list
60 POKE 20,0:REM Set timer
70 X=USR(ADR(RELOCATE$),CHSET):REM Relocate the whole set
80 ? PEEK(20)/60:REM How long did it take?
90 END :REM It took 0.03 seconds
20000 DIM RELOCATE$(34):REM Set it up as a string
20010 FOR I=1 TO 34:REM Set up the string
20020 READ A:REM Get a byte
20030 RELOCATE$(I,I)=CHR$(A):REM Stuff it into the string
20040 NEXT I:REM Repeat until string is done
20050 RETURN :REM All done, go back
20060 DATA 104,104,133,207,104,133,206,169,0,133
20070 DATA 204,169,224,133,205,162,4,160,0,177
20080 DATA 204,145,206,136,208,249,230,205,230,207
20090 DATA 202,208,242,96
20 ORIG=PEEK(106):REM Top of RAM
30 CHSET=(ORIG-4)*256:REM Place for relocated character set
40 POKE 106,ORIG-8:REM Make room for it
50 GRAPHICS 0:REM Set up new display list
60 POKE 20,0:REM Set timer
70 X=USR(ADR(RELOCATE$),CHSET):REM Relocate the whole set
80 ? PEEK(20)/60:REM How long did it take?
90 END :REM It took 0.03 seconds
20000 DIM RELOCATE$(34):REM Set it up as a string
20010 FOR I=1 TO 34:REM Set up the string
20020 READ A:REM Get a byte
20030 RELOCATE$(I,I)=CHR$(A):REM Stuff it into the string
20040 NEXT I:REM Repeat until string is done
20050 RETURN :REM All done, go back
20060 DATA 104,104,133,207,104,133,206,169,0,133
20070 DATA 204,169,224,133,205,162,4,160,0,177
20080 DATA 204,145,206,136,208,249,230,205,230,207
20090 DATA 202,208,242,96
The subroutine from line 20000 to line 20070 puts each byte of the machine language routine into its appropriate place in a string which we have called RELOCATE$. To access this routine, we use line 70, which passes the parameter CHSET to our machine language routine. Remember that CHSET, defined in line 30, is the address at which we would like to locate the character set in RAM. This program executes almost 500 times faster than the all-BASIC program described above, again demonstrating the speed of machine language routines.
SUBROUTINE TO TRANSFER ANY AREA OF MEMORY
With a few minor modifications to the program we just wrote, we can make it much more versatile. Let's write it in such a way as to allow the transfer of any area of memory to any other area. Looking at the program above, we see that only two parts of the code need to change. The first is the absolute address of FROM, which is set at 57344, and the second is the number of pages stored in the X register, which is set at 4. If we could use variables here instead of constants, our routine would be far more versatile. It's easy to convert the routine in this way; let's just pass the FROM address and the number of pages to transfer as parameters from BASIC. Here is the complete assembly language program for this subroutine:
Listing 7.3
0100 ; *******************************
0110 ; Set up initial conditions
0120 ; *******************************
0000 0130 *= $600
00CC 0140 FROM = $CC
00CE 0150 TO = $CE
0160 ;*******************************
0170 ; Initialize and set up indirect addresses
0180 ; *******************************
0600 68 0190 PLA ; Pull # of parameters off stack
0601 68 0200 PLA ; Get high byte of source
0602 85CD 0210 STA FROM+1 ; Store it in high byte of FROM
0604 68 0220 PLA ; Get low byte of source
0605 85CC 0230 STA FROM ; Store it in low byte of FROM
0607 68 0240 PLA ; Get high byte of destination
0608 85CF 0250 STA TO+1 ; Store it in high byte of TO
060A 68 0260 PLA ; Get low byte of destination
060B 85CE 0270 STA TO ; Store it in low byte of TO
060D 68 0280 PLA ; No high byte exists (= 0)
060E 68 0290 PLA ; Get low byte - number of pages
060F AA 0300 TAX ; Put # of pages in X register
0310 ; *******************************
0320 ; Now let's transfer everything
0330 ; *******************************
0610 A000 0340 LDY #0 ; Initialize counter
0612 BlCC 0350 LOOP LDA (FROM),Y ; Get a byte
0614 91CE 0360 STA (TO),Y ; And relocate it
0616 88 0370 DEY ; Is page finished?
0617 D0F9 0380 BNE LOOP ; No - keep relocating
0619 E6CD 0390 INC FROM+1 ; Yes - high byte
061B E6CF 0400 INC TO+1 ; High byte - now for next page
061D CA 0410 DEX ; Have we done all pages?
061E D0F2 0420 BNE LOOP ; No - keep going
0620 60 0430 RTS ; Yes, so return to BASIC
0100 ; *******************************
0110 ; Set up initial conditions
0120 ; *******************************
0000 0130 *= $600
00CC 0140 FROM = $CC
00CE 0150 TO = $CE
0160 ;*******************************
0170 ; Initialize and set up indirect addresses
0180 ; *******************************
0600 68 0190 PLA ; Pull # of parameters off stack
0601 68 0200 PLA ; Get high byte of source
0602 85CD 0210 STA FROM+1 ; Store it in high byte of FROM
0604 68 0220 PLA ; Get low byte of source
0605 85CC 0230 STA FROM ; Store it in low byte of FROM
0607 68 0240 PLA ; Get high byte of destination
0608 85CF 0250 STA TO+1 ; Store it in high byte of TO
060A 68 0260 PLA ; Get low byte of destination
060B 85CE 0270 STA TO ; Store it in low byte of TO
060D 68 0280 PLA ; No high byte exists (= 0)
060E 68 0290 PLA ; Get low byte - number of pages
060F AA 0300 TAX ; Put # of pages in X register
0310 ; *******************************
0320 ; Now let's transfer everything
0330 ; *******************************
0610 A000 0340 LDY #0 ; Initialize counter
0612 BlCC 0350 LOOP LDA (FROM),Y ; Get a byte
0614 91CE 0360 STA (TO),Y ; And relocate it
0616 88 0370 DEY ; Is page finished?
0617 D0F9 0380 BNE LOOP ; No - keep relocating
0619 E6CD 0390 INC FROM+1 ; Yes - high byte
061B E6CF 0400 INC TO+1 ; High byte - now for next page
061D CA 0410 DEX ; Have we done all pages?
061E D0F2 0420 BNE LOOP ; No - keep going
0620 60 0430 RTS ; Yes, so return to BASIC
We have now set up the routine to obtain first the FROM address in two bytes from the stack, and then the TO address in the same way. Finally, we remove from the stack the number of pages to be transferred. Note that there are only 256 pages of memory in an ATARI, so there can never be a high byte to the number of pages parameter. The low byte is pulled from the stack and transferred to the X register to set up the counter for the number of pages to be transferred.
With the exception of these few changes, the program is identical to our program for transferring the character set from ROM to RAM. In fact, this new routine will accomplish the same goal if we so desire. A BASIC program using this new routine to transfer the character set is given below:
10
GOSUB 20000:REM Set up machine language routine
20 ORIG=PEEK(106):REM Top of RAM
30 CHSET=(ORIG-4)*256:REM Place for relocated character set
40 POKE 106,ORIG-8:REM Make room for it
50 GRAPHICS 0:REM Set up new display list
60 X=USR(ADR(TRANSFER$),57344,CHSET,4):REM Transfer the whole set
70 END
20000 DIM TRANSFER$(33):REM Set it up as a string
20010 FOR I=1 TO 33:REM Set up the string
20020 READ A:REM Get a byte
20030 TRANSFER$(I,I)=CHR$(A):REM Stuff it into the string
20040 NEXT I:REM Repeat until string is done
20050 RETURN :REM All done, go back
20060 DATA 104,104,133,205,104,133,204,104,133,207
20070 DATA 104,133,206,104,104,170,160,0,177,204
20080 DATA 145,206,136,208,249,230,205,230,207,202
20090 DATA 208,242,96
20 ORIG=PEEK(106):REM Top of RAM
30 CHSET=(ORIG-4)*256:REM Place for relocated character set
40 POKE 106,ORIG-8:REM Make room for it
50 GRAPHICS 0:REM Set up new display list
60 X=USR(ADR(TRANSFER$),57344,CHSET,4):REM Transfer the whole set
70 END
20000 DIM TRANSFER$(33):REM Set it up as a string
20010 FOR I=1 TO 33:REM Set up the string
20020 READ A:REM Get a byte
20030 TRANSFER$(I,I)=CHR$(A):REM Stuff it into the string
20040 NEXT I:REM Repeat until string is done
20050 RETURN :REM All done, go back
20060 DATA 104,104,133,205,104,133,204,104,133,207
20070 DATA 104,133,206,104,104,170,160,0,177,204
20080 DATA 145,206,136,208,249,230,205,230,207,202
20090 DATA 208,242,96
AN EXERCISE FOR THE READER
Using these techniques, it should be fairly simple to write your own routine to fill a given number of pages of memory with a character other than zero. To obtain the maximum benefit from this exercise, don't look back at the examples in this chapter, but rather start from scratch, and see how you do.
READING THE JOYSTICK
We are all familiar with the complex code required in BASIC to read the joysticks. Although there are some sophisticated ways of speeding up this process in BASIC, the most common approach used to determine the position of the joystick and change the X and Y coordinates of a player (for example) is as follows in this subroutine for a BASIC program:
10000
IF STICK(0)=15 THEN 10050:REM straight up
10010 IF STICK(0)=10 OR STICK(0)=14 OR STICK(0)=6 THEN
Y=Y-1:REM 11,12 or 1 o'clock position-move player up
10020 IF STICK(0)=9 OR STICK(0)=13 OR STICK(0)=5 THEN Y=Y+1:REM
7,6 or 5 o'clock position-move player down
10030 IF STICK(0)=10 OR STICK(0)=11 OR STICK(0)=9 THEN
X=X-1:REM 10,9 or 8 o'clock position-move player left
10040 IF STICK(0)=6 OR STICK(0)=7 OR STICK(0)=5 THEN X=X+1:REM
2,3 or 4 o'clock position-move player right
10050 RETURN:REM no other possibilities
10010 IF STICK(0)=10 OR STICK(0)=14 OR STICK(0)=6 THEN
Y=Y-1:REM 11,12 or 1 o'clock position-move player up
10020 IF STICK(0)=9 OR STICK(0)=13 OR STICK(0)=5 THEN Y=Y+1:REM
7,6 or 5 o'clock position-move player down
10030 IF STICK(0)=10 OR STICK(0)=11 OR STICK(0)=9 THEN
X=X-1:REM 10,9 or 8 o'clock position-move player left
10040 IF STICK(0)=6 OR STICK(0)=7 OR STICK(0)=5 THEN X=X+1:REM
2,3 or 4 o'clock position-move player right
10050 RETURN:REM no other possibilities
There are several ways of improving the speed of such a routine by improved programming, as you already know. This routine is included here for simplicity; it is easy to follow its logic. In any case, even excellent programming will not make this type of routine the winner in a speed contest. Let's see if we can speed it up significantly by using assembly language.
We'll assume for the purpose of this example that the joystick routine we shall write will be the only way the player can move, and further, that we will be moving only one player. Since the player can move in only two dimensions, we need only remember two coordinates, the X and Y positions of the player. Because of the way we will be moving the player, we'll need only 1 byte of storage for the X position, but we'll need 2 bytes, for an indirect address, for the Y location. The routine needed for reading the joystick is very straightforward and is given below:
0100
; *******************************
0110 ; Initialize locations
0120 ; *******************************
0130 *= $600 ; Safe place for routine
0140 YLOC = $CC ; Indirect address for Y
0150 XLOC = $CE ; To remember X position
0160 STICK = $D300 ; Hardware STICK(0) location
0180 ; *******************************
0190 ; Now read the joystick #1
0200 ; *******************************
0210 PLA ; Keep the stack neat
0220 LDA STICK ; Get joystick value
0230 AND #1 ; Is bit 0 = 1?
0240 BEQ UP ; No - 11, 12 or 1 o'clock
0250 LDA STICK ; Get it again
0260 AND #2 ; Is bit 1 = 1?
0270 BEQ DOWN ; No - 5, 6 or 7 o'clock
0280 SIDE LDA STICK ; Get it again
0290 AND #4 ; Is bit 3 = 1?
0300 BEQ LEFT ; No - 8, 9 or 10 o'clock
0310 LDA STICK ; Get it again
0320 AND #8 ; Is bit 4 = 1?
0330 BEQ RIGHT ; No - 2, 3 or 4 o'clock
0340 RTS ; Joystick straight up
0110 ; Initialize locations
0120 ; *******************************
0130 *= $600 ; Safe place for routine
0140 YLOC = $CC ; Indirect address for Y
0150 XLOC = $CE ; To remember X position
0160 STICK = $D300 ; Hardware STICK(0) location
0180 ; *******************************
0190 ; Now read the joystick #1
0200 ; *******************************
0210 PLA ; Keep the stack neat
0220 LDA STICK ; Get joystick value
0230 AND #1 ; Is bit 0 = 1?
0240 BEQ UP ; No - 11, 12 or 1 o'clock
0250 LDA STICK ; Get it again
0260 AND #2 ; Is bit 1 = 1?
0270 BEQ DOWN ; No - 5, 6 or 7 o'clock
0280 SIDE LDA STICK ; Get it again
0290 AND #4 ; Is bit 3 = 1?
0300 BEQ LEFT ; No - 8, 9 or 10 o'clock
0310 LDA STICK ; Get it again
0320 AND #8 ; Is bit 4 = 1?
0330 BEQ RIGHT ; No - 2, 3 or 4 o'clock
0340 RTS ; Joystick straight up
As you can be see, after the mandatory PLA to keep BASIC happy, reading the joystick is just a matter of loading the accumulator from the hardware location STICK ($D000) and then ANDing it with 1, 2, 4, or 8. The ATARI joysticks set one or more of the lower four bits in location $D000 to zero if the stick is pressed in that direction: bit zero for up, bit 1 for down, bit 2 for left, and bit 3 for right. If none of the 4 bits is set to zero, the joystick is in the straight-up position. Note that the joystick may not be simultaneously pressed right and left or up and down, but it may be right and down simultaneously, or left and up.
This program won't work, as you've probably already noticed, since there are 4 undefined labels, UP, DOWN, LEFT, and RIGHT We will add these routines shortly to produce a machine language subroutine which will not only read the joystick, but also move a player around the screen in response to the joystick direction.
First, note that each of the references to a label for the direction of the joystick uses the BEQ instruction. This says, in effect, that if the result of ANDing a bit forced to 1 with the value found in STICK is a zero, the joystick is pressed in that direction. Think about that. We know that for the result to be zero, in one or both of the numbers each bit must be equal to zero. In the numbers 1, 2, 4, and 8, every bit but one is equal to zero; so in the number stored in STICK, that particular bit must be equal to zero if the result of the AND operation equals zero. For a pictorial example, let's look at the AND operation with 4, with the joystick pressed in different directions:
Joystick
STICK AND with 76543210
right 248 - 11110111
- 4 00000100
Result = 00000100
right 248 - 11110111
- 4 00000100
Result = 00000100
which is not equal to zero. Since the stick was pressed right and ANDing with 4 tests for pressing the joystick left, this is a correct result. Now, another example:
Joystick
STICK AND with 76543210
left 244 - 11111011
- 4 00000100
Result = 00000000
left 244 - 11111011
- 4 00000100
Result = 00000000
which is equal to zero, showing that the test works correctly. It should be emphasized that any of the three left positions of the joystick would have worked, because they all have a zero as bit 2, so all will AND with 4 to produce a result of zero. In fact, the three joystick positions to the left have the following bit patterns:
8
o'clock 11111001
9 o'clock 11111011
10 o'clock 11111010
9 o'clock 11111011
10 o'clock 11111010
It's worth mentioning here that the upper 4 bits of this location reflect the position of a joystick plugged into the second port on your ATARI, in exactly the same way as the lower 4 bits reflect the position of joystick 0.
There are, of course, several other ways of writing the above code. Perhaps one which has occurred to you is to use a subroutine for each direction, as in this excerpt:
0210
PLA
0220 LDA STICK
0230 AND #1
0240 BNE D1
0250 JSR UP
0260 D1 LDA STICK
0270 AND #2
0280 BNE D2
0290 JSR DOWN
0220 LDA STICK
0230 AND #1
0240 BNE D1
0250 JSR UP
0260 D1 LDA STICK
0270 AND #2
0280 BNE D2
0290 JSR DOWN
This type of construction would work fine but for one problem: the code is fixed. The locations UP, DOWN, LEFT, and RIGHT have to be within the program, and if we use JSRs to access these-routines, we will end up with nonrelocatable code. If that creates no problem for you, then write the routine using JSRs. However, since one of our goals in this book is to make as many of the routines as we can relocatable, we will use the demonstrated construction.
How do we move players around the screen using player-missile graphics? Horizontal movement is easy. All we have to do is POKE the desired horizontal position into the horizontal position register for that particular player, who will appear there instantly. In the case of the first player, player zero, the horizontal position register is located at $D000. We'll call it HPOSP0, and we'll need to add line 170 to the above code:
0170
HPOSP0 = $D000
which will enable us to refer to this location using the label name.
What about vertical motion? To move a player vertically, we actually have to move each byte of the player to a new location, which is why we needed to set up the indirect address for the Y position. We'll use a technique we've already we used to move the character set. But in this case we can get by with only one indirect address, since we're moving the player only 1 byte away from its current address. We'll assume that the player is 8 bytes high and appears as a hollow square. If we want to move the player up the screen 1 byte, we'll need to begin by moving the top byte first, and so on down the player. If we try to move the lower byte first, it will overwrite the next higher byte and we'll lose that higher byte. Pictorally, it will look like:
before move
after move
........... ...........
........... .xxxxxxxx..
xxxxxxxx... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .xxxxxxxx..
xxxxxxxx... ...........
........... ...........
........... ...........
........... .xxxxxxxx..
xxxxxxxx... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .x......X..
x......x... .xxxxxxxx..
xxxxxxxx... ...........
........... ...........
Conversely, to move the player down the screen 1 byte, we'll need to begin by moving the bottom byte first, and we'll work our way up the player.
There's one final problem. In the picture above, the bottom of the player would really be 2 bytes high after being moved, since although we have placed a copy of the bottom byte in the correct position 1 byte higher, we have not moved anything into the space originally occupied by this bottom byte. If we don't correct for this problem, the new figure will look like this:
........
........
XXXXXXXX
X......X
X......X
X......X
X......X
X......X
X......X
XXXXXXXX
XXXXXXXX
........
........
XXXXXXXX
X......X
X......X
X......X
X......X
X......X
X......X
XXXXXXXX
XXXXXXXX
........
In fact, if we don't correct for this, as we move the figure up the screen, we'll leave a tail dangling behind the figure, a clever effect, but not what we intended at all!
Fortunately, there is an easy way to solve this problem; just move 1 byte more than is in the player. Note that since the player is 8 bytes high, if we move 9 bytes, we'll be moving a zero byte into the space formerly occupied by the bottom of the player; so the new player will still have a single line at the bottom, instead of the double line pictured above. Obviously, when we are moving the player down the screen, we can also move 9 bytes instead of 8, solving the problem there, as well. Now that we know how to move the players both horizontally and vertically, let's look at the whole routine, and then we'll describe it in detail.
Listing 7.4
0100 ; *******************************
0110 ; Initialize locations
0120 ; *******************************
0000 0130 *= $600 ; Safe place for routine
00CC 0140 YLOC = $CC ; Indirect address for Y
00CE 0150 XLOC = $CE ; To remember X position
D300 0160 STICK = $D300 ; Hardware STICK(0) location
D000 0170 HPOSP0 = $D000 ; Horizontal position player 0
0180 ; *******************************
0190 ; Now read the joystick #1
0200 ; *******************************
0600 68 0210 PLA ; Keep the stack neat
0601 AD00D3 0220 LDA STICK ; Get joystick value
0604 2901 0230 AND #1 ; Is bit 0 = 1?
0606 F016 0240 BEQ UP ; No - 11, 12 or 1 o'clock
0608 AD00D3 0250 LDA STICK ; Get it again
060B 2902 0260 AND #2 ; Is bit 1 = 1?
060D F020 0270 BEQ DOWN ; No - 5, 6 or 7 o'clock
060F AD00D3 0280 SIDE LDA STICK ; Get it again
0612 2904 0290 AND #4 ; Is bit 3 = 1?
0614 F02E 0300 BEQ LEFT ; No - 8, 9 or 10 o'clock
0616 AD00D3 0310 LDA STICK ; Get it again
0619 2908 0320 AND #8 ; Is bit 4 = 1?
061B F02F 0330 BEQ RIGHT ; No - 2, 3 or 4 o'clock
061D 60 0340 RTS ; Joystick straight up - exit to BASIC
0350 ; *******************************
0360 ; Now move player appropriately
0370 ; Starting with upward movement
0380 ; *******************************
061E A001 0390 UP LDY #1 ; Setup for moving byte 1
0620 C6CC 0400 DEC YLOC ; Now 1 less than YLOC
0622 BlCC 0410 UP1 LDA (YLOC),Y ; Get 1st byte
0624 88 0420 DEY ; To move it up one position
0625 91CC 0430 STA (YLOC),Y ; Move it
0627 C8 0440 INY ; Now original value
0628 C8 0450 INY ; Now set for next byte
0629 C00A 0460 CPY #10 ; Are we done?
062B 90F5 0470 BCC UP1 ; No
062D B0E0 0480 BCS SIDE ; Forced branch!!!
0490 ; *******************************
0500 ; Now move player down
0510 ; *******************************
062F A007 0520 DOWN LDY #7 ; Move top byte first
0631 B1CC 0530 DOWN1 LDA (YLOC),Y ; Get top byte
0633 C8 0540 INY ; To move it down screen
0634 91CC 0550 STA (YLOC),Y ; Move it
0636 88 0560 DEY ; Now back to starting value
0637 88 0570 DEY ; Set for next lower byte
0638 10F7 0580 BPL DOWN1 ; If Y >= 0 keep going
063A C8 0590 INY ; Set to zero
063B A900 0600 LDA #0 ; To clear top byte
063D 91CC 0610 STA (YLOC),Y ; Clear it
063F E6CC 0620 INC YLOC ; Now is 1 higher
0641 18 0630 CLC ; Setup for forced branch
0642 90CB 0640 BCC SIDE ; Forced branch again
0650 ; *******************************
0660 ; Now side-to-side - left first
0670 ; *******************************
0644 C6CE 0680 LEFT DEC XLOC ; To move it left
0646 A5CE 0690 LDA XLOC ; Get it
0648 8D00D0 0700 STA HPOSP0 ; Move it
064B 60 0710 RTS ; Back to BASIC - we're done
0720 ; *******************************
0730 ; Now right movement
0740 ; *******************************
064C E6CE 0750 RIGHT INC XLOC ; To move it right
064E A5CE 0760 LDA XLOC ; Get it
0650 8D00D0 0770 STA HPOSP0 ; Move it
0653 60 0780 RTS ; Back to BASIC - we're done
0100 ; *******************************
0110 ; Initialize locations
0120 ; *******************************
0000 0130 *= $600 ; Safe place for routine
00CC 0140 YLOC = $CC ; Indirect address for Y
00CE 0150 XLOC = $CE ; To remember X position
D300 0160 STICK = $D300 ; Hardware STICK(0) location
D000 0170 HPOSP0 = $D000 ; Horizontal position player 0
0180 ; *******************************
0190 ; Now read the joystick #1
0200 ; *******************************
0600 68 0210 PLA ; Keep the stack neat
0601 AD00D3 0220 LDA STICK ; Get joystick value
0604 2901 0230 AND #1 ; Is bit 0 = 1?
0606 F016 0240 BEQ UP ; No - 11, 12 or 1 o'clock
0608 AD00D3 0250 LDA STICK ; Get it again
060B 2902 0260 AND #2 ; Is bit 1 = 1?
060D F020 0270 BEQ DOWN ; No - 5, 6 or 7 o'clock
060F AD00D3 0280 SIDE LDA STICK ; Get it again
0612 2904 0290 AND #4 ; Is bit 3 = 1?
0614 F02E 0300 BEQ LEFT ; No - 8, 9 or 10 o'clock
0616 AD00D3 0310 LDA STICK ; Get it again
0619 2908 0320 AND #8 ; Is bit 4 = 1?
061B F02F 0330 BEQ RIGHT ; No - 2, 3 or 4 o'clock
061D 60 0340 RTS ; Joystick straight up - exit to BASIC
0350 ; *******************************
0360 ; Now move player appropriately
0370 ; Starting with upward movement
0380 ; *******************************
061E A001 0390 UP LDY #1 ; Setup for moving byte 1
0620 C6CC 0400 DEC YLOC ; Now 1 less than YLOC
0622 BlCC 0410 UP1 LDA (YLOC),Y ; Get 1st byte
0624 88 0420 DEY ; To move it up one position
0625 91CC 0430 STA (YLOC),Y ; Move it
0627 C8 0440 INY ; Now original value
0628 C8 0450 INY ; Now set for next byte
0629 C00A 0460 CPY #10 ; Are we done?
062B 90F5 0470 BCC UP1 ; No
062D B0E0 0480 BCS SIDE ; Forced branch!!!
0490 ; *******************************
0500 ; Now move player down
0510 ; *******************************
062F A007 0520 DOWN LDY #7 ; Move top byte first
0631 B1CC 0530 DOWN1 LDA (YLOC),Y ; Get top byte
0633 C8 0540 INY ; To move it down screen
0634 91CC 0550 STA (YLOC),Y ; Move it
0636 88 0560 DEY ; Now back to starting value
0637 88 0570 DEY ; Set for next lower byte
0638 10F7 0580 BPL DOWN1 ; If Y >= 0 keep going
063A C8 0590 INY ; Set to zero
063B A900 0600 LDA #0 ; To clear top byte
063D 91CC 0610 STA (YLOC),Y ; Clear it
063F E6CC 0620 INC YLOC ; Now is 1 higher
0641 18 0630 CLC ; Setup for forced branch
0642 90CB 0640 BCC SIDE ; Forced branch again
0650 ; *******************************
0660 ; Now side-to-side - left first
0670 ; *******************************
0644 C6CE 0680 LEFT DEC XLOC ; To move it left
0646 A5CE 0690 LDA XLOC ; Get it
0648 8D00D0 0700 STA HPOSP0 ; Move it
064B 60 0710 RTS ; Back to BASIC - we're done
0720 ; *******************************
0730 ; Now right movement
0740 ; *******************************
064C E6CE 0750 RIGHT INC XLOC ; To move it right
064E A5CE 0760 LDA XLOC ; Get it
0650 8D00D0 0770 STA HPOSP0 ; Move it
0653 60 0780 RTS ; Back to BASIC - we're done
Let's look at the construction of the program as a whole – the program flow. We first test to see if the joystick is pressed up. If it is up, we branch to UP If not, we test for down, and if it's down, we branch to DOWN. In either of these cases, after moving the player, we need to go back to test for side-to-side movement, since it is possible to move both horizontally and vertically simultaneously. This branch back to test for horizontal movement is accomplished by forced branches in lines 480 and 640. In line 480, the carry bit must be set, since if it were not, line 470 would have branched away from line 480. In line 640, the forced branch is even more obvious, since in line 470, we clear the carry bit and then branch if the carry bit is clear, as we know it must be! Why not just jump back to SIDE? Again, because we want the routine to be relocatable, and if we use any JMP commands, it will not be. This technique of the forced branch is common in relocatable code, and is fairly easy to accomplish, now that you know how.
Once we've tested for both horizontal and vertical movement, we're done and can return to BASIC. Note that this routine contains three RTS instructions. There's no rule about a routine having only one RTS; whatever works, do! In this case, we can return if the stick is vertical (line 340) or if we've moved the player left (line 710) or right (line 780), since in any of these three cases, we've exhausted the possibilities, testing for every combination of movements.
The specific code for moving right or left reads the current X coordinate from its storage location, incrementing or decrementing it as appropriate, and stores it in both its storage location again and the horizontal position register for player zero, HPOSP0.
Now we'll discuss vertical motion. Since moving the player up the screen results in a Y position 1 unit less than its initial value (the lower the Y coordinate, the higher the player appears on the screen), we will need to eventually decrement the YLOC value. We can take advantage of this decrementing if we do it near the beginning of the routine. When YLOC is decremented, it points to the destination of the top byte of the player. Setting Y to 1 allows the command labeled UP1 to point initially to the top byte of the player, in its original location. We then decrement Y, and the next STA instruction puts that byte in its new, higher location on the screen. We must then increment Y twice, once for the decrement we went through and once to get the next byte. We're going to move 9 bytes, and we started with Y = 1 , so when Y = 10, we're done. If we are not done, we'll go back up to get the next byte, and if we are done, we'll take the forced branch back up to check for horizontal motion. The technique here is to use indirect addressing for both the LDA and the STA, but changing the offset (Y register) by 1 between the LDA and the STA. That allows us to load from one location and store into another, without a lot of fuss.
We'll use a slighly different algorithm to move a player down the screen. As mentioned above, we begin with the bottom byte, so we set Y equal to 7 (the bytes are 0 to 7 in this case). We LDA indirect, then increment the Y register, and then STA indirect, like we did above, but in this case, we store into a higher location than we load from. We then decrement twice, once for the increment and once to get the next byte, and if Y is still greater than or equal to zero, we keep going. If not, we'll store a zero into the original lowest byte, by incrementing Y to set it back to zero (it had reached -1, or $FF in hexidecimal) and storing a zero into YLOC, indirect. Then we increment YLOC, since we've moved the player down 1 position on the screen, and force a branch back to check for horizontal movement. Note that when we moved up the screen, we actually moved 9 bytes, but when we moved down the screen, we moved 8 bytes, and then stuffed a zero to eliminate the tail of the player. We used two methods in order to show that either works.
By the way, one concern you may have about this routine is that it reads the joystick four separate times. "What happens," you may ask, "if the position of the joystick changes between reads?" If we calculate the time over which all four reads of the joystick occur, we can see that all reading takes place in less than 25 microseconds. Little chance of a change in that time span!
Now that we have our machine language routine, all we need to do is incorporate it into a BASIC program which can use it appropriately. Such a program is given below:
10
TOP=PEEK(106)-8:REM Save 8 pages
20 POKE 106,TOP:REM Make room for PMG
30 GRAPHICS 0:REM Reset display list
40 PMBASE=TOP*256:REM Set up PM area
50 POKE 54279,TOP:REM Tell ATARI where PMBASE is
60 INITX=120:REM Initial X position
70 INITY=50:REM Initial Y position
80 POKE 559,46:REM Double line resolution
90 POKE 53277,3:REM Enable PM
100 GOSUB 20000:REM Set up our routine
110 FOR I=PMBASE+512 TO PMBASE+640:REM PM Memory
120 POKE I,0:REM Clear it out
130 NEXT I:REM Could use ERASE$ here!
140 RESTORE 25000:REM Player data is stored here
150 Q=PMBASE+512+INITY:REM Where player will be in memory
160 FOR I=Q TO Q+7:REM Player is 8 bytes high
170 READ A:REM Get player data
180 POKE I,A:REM Put it in proper place
190 NEXT I:REM And so on
200 POKE 53248,INITX:REM Setup X position
210 YHI=INT(Q/256):REM High byte of initial Y position
220 YLO=(PMBASE+512+INITY)-YHI*256:REM Low byte
230 POKE 204,YLO:REM Tell ML routine where Y is
240 POKE 205,YHI:REM Tell ML routine where Y is
250 POKE 206,INITX:REM Tell ML routine where X is
260 POKE 704,68:REM Make player red
270 Q=USR(ADR(JOYSTICK$)):REM Let's try it!
280 GOTO 270:REM Just loop
20000 DIM JOYSTICK$(87):REM Where to put routine
20010 FOR I=1 TO 87:REM Length of routine
20020 READ A:REM Get a byte
20030 JOYSTICK$(I,I)=CHR$(A):REM Put it into string
20040 NEXT I:RETURN :REM All done
20050 DATA 104,173,0,211,41,1,240,22,173,0
20060 DATA 211,41,2,240,32,173,0,211,41,4
20070 DATA 240,46,173,0,211,41,8,240,47,96
20080 DATA 160,1,198,204,177,204,136,145,204,200
20090 DATA 200,192,10,144,245,176,224,160,7,177
20100 DATA 204,200,145,204,136,136,16,247,200,169
20110 DATA 0,145,204,230,204,24,144,203,198,206
20120 DATA 165,206,141,0,208,96,230,206,165,206
20130 DATA 141,0,208,96,0,208,96
25000 DATA 255,129,129,129,129,129,129,255
20 POKE 106,TOP:REM Make room for PMG
30 GRAPHICS 0:REM Reset display list
40 PMBASE=TOP*256:REM Set up PM area
50 POKE 54279,TOP:REM Tell ATARI where PMBASE is
60 INITX=120:REM Initial X position
70 INITY=50:REM Initial Y position
80 POKE 559,46:REM Double line resolution
90 POKE 53277,3:REM Enable PM
100 GOSUB 20000:REM Set up our routine
110 FOR I=PMBASE+512 TO PMBASE+640:REM PM Memory
120 POKE I,0:REM Clear it out
130 NEXT I:REM Could use ERASE$ here!
140 RESTORE 25000:REM Player data is stored here
150 Q=PMBASE+512+INITY:REM Where player will be in memory
160 FOR I=Q TO Q+7:REM Player is 8 bytes high
170 READ A:REM Get player data
180 POKE I,A:REM Put it in proper place
190 NEXT I:REM And so on
200 POKE 53248,INITX:REM Setup X position
210 YHI=INT(Q/256):REM High byte of initial Y position
220 YLO=(PMBASE+512+INITY)-YHI*256:REM Low byte
230 POKE 204,YLO:REM Tell ML routine where Y is
240 POKE 205,YHI:REM Tell ML routine where Y is
250 POKE 206,INITX:REM Tell ML routine where X is
260 POKE 704,68:REM Make player red
270 Q=USR(ADR(JOYSTICK$)):REM Let's try it!
280 GOTO 270:REM Just loop
20000 DIM JOYSTICK$(87):REM Where to put routine
20010 FOR I=1 TO 87:REM Length of routine
20020 READ A:REM Get a byte
20030 JOYSTICK$(I,I)=CHR$(A):REM Put it into string
20040 NEXT I:RETURN :REM All done
20050 DATA 104,173,0,211,41,1,240,22,173,0
20060 DATA 211,41,2,240,32,173,0,211,41,4
20070 DATA 240,46,173,0,211,41,8,240,47,96
20080 DATA 160,1,198,204,177,204,136,145,204,200
20090 DATA 200,192,10,144,245,176,224,160,7,177
20100 DATA 204,200,145,204,136,136,16,247,200,169
20110 DATA 0,145,204,230,204,24,144,203,198,206
20120 DATA 165,206,141,0,208,96,230,206,165,206
20130 DATA 141,0,208,96,0,208,96
25000 DATA 255,129,129,129,129,129,129,255
Line 100, which sets up the subroutine we just wrote, prepares us to call the subroutine in line 270. Note that line 280 just loops back to this subroutine call, so all that this program will do is move the red, hollow square player around the screen. The program could be expanded considerably by adding code from line 280 on, as long as line 270 remains in the main loop of the game. Each time line 270 is accessed, the joystick is read and the player is moved appropriately. Try it! Notice how smooth and even the motion of the player is. Then try a similar program all in BASIC, and watch how the vertical movement turns the player into an inch-worm, slowly crawling up or down the screen.
The bulk of this program sets up player-missile graphics in BASIC. Since virtually all parameters, from the color of the player to its shape and size, are controlled from this BASIC program and not from the machine language subroutine, this routine should merge nicely with almost any program requiring joystick movement of player zero. With simple modifications that you can now try, it will handle other players, other joysticks, or even multiple players and joysticks. You can even try adding missiles, perhaps when the joystick button (monitored by location $D010) is pressed! The only way to really learn assembly language programming is through programming, and what better time to start than now?
Return to Table of Contents | Previous Chapter | Next Chapter