Chapter Seven


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

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

    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

    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

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

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

    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

    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

    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

    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!

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,

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

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

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

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:

Atari ATASCII line


    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

    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

    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

    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

    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

    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

    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


    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

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

    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

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

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

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

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...    ...........
...........    ...........

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
........

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

    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

    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