A.N.A.L.O.G. ISSUE 36 / NOVEMBER 1985 / PAGE 97
From time to time, the ANALOG Computing technical staff has their hands full with various tasks around the office, preventing them from writing their monthly magazine columns. This is one of those times. Rather than miss an installment of Boot Camp, we decided to have Matt Ratcliff, an accomplished assembly language programmer and frequent contributor to ANALOG Computing, sit in for Tom Hudson. Matt’s topic this month is the use of the 6502 compare instruction and how to get the most out of it.
Every time I sit down to write an assembly language program, I have to get out a reference manual when it comes to coding IF statements. It’s very simple in BASIC, as you can see below.
However, in assembly language on the Atari computer, there are no IF statements. These “conditional” instructions must be coded with compare and branch command sequences. Instead of having greater than and less than conditional branches (which are self explanatory), the 6502 microprocessor of the Atari requires that you interpret the state of the three flags: Carry, Zero and Sign.
It’s not always clear how to determine an IF THEN sequence in assembly by testing these flags. After studying some assembly manuals, I’ve compiled the following guide for creating IF THEN statements at the assembly level.
Compare instructions are most often used in conditional branch functions. We use them all the time in BASIC, like this:
100 IF A<=35 THEN GOTO 200 110 X=X+1 120 Y=Y-1 130 GOTO 300 200 X=X+2 210 Y=Y+1 300 REM CONTINUE HERE
The code above is simple enough to follow. If the value of A is less than or equal to 35, then continue execution at Line 200. If A is greater than 35, then continue execution at the next statement.
In assembly language, it isn’t quite so simple…not on the 6502, anyway. Some microprocessors, such as the 6809, have instructions like BLE—Branch on Less than or Equal (just like the BASIC instruction above). But the 6502 has no “less than or equal” instructions. It has three flags that you may test and branch on:
BCC Branch on Carry Clear BCS Branch on Carry Set BEQ Branch if EQual BNE Branch if Not Equal BPL Branch if PLus BMI Branch if Minus
Now, the BEQ and BNE are easy enough to follow. But BNE only tells you if the results were not equal; it gives you no indication of which is the greater.
The 6502 gives you three compare instructions:
CMP MEM…Compare A register to a memory location or immediate value. All the usual indexed addressing modes are supported.
CPX MEM…Compare the X-index register to a memory location or an immediate value. Absolute and immediate addressing are allowed for index registers. This will be used most often to test loop counters (what index registers are ideally suited for).
CPY MEM…Compare the Y-index register to a memory location or an immediate value.
The compare instructions perform an “imaginary subtraction.” The value you’re comparing TO is subtracted FROM the specified register. The 6502 microprocessor doesn’t keep the result, but does set the flags Carry, Sign and Zero.
With these flags set, a conditional branch, like BNE, may be used to decide what the results mean. Sometimes it may require two branches in combination to test for a certain condition. To summarize:
Compare instruction results. FLAGS N Z C A, X or Y < Memory 1* 0 0 A, X or Y = Memory 0 1 1 A, X or Y > Memory 0* 0 1 * Valid only for “two’s complement” compare.
A small segment of assembly code for conditional branching might appear as follows:
CMP $600 ;IS A < VALUE AT $600? BCC TRUE ;YES IF THE CARRY FLAG . ;IS CLEAR. IF A < VALUE . ;IN $600 THEN GOTO 'TRUE' FALSE. ;OTHERWISE, GOTO 'FALSE' . TRUE . ;EXECUTE THIS IF . ;CONDITION MET .
The table below shows how to test for common conditions. If the conditions are met, the program will branch to TRUE. Otherwise, execution will continue at FALSE.
Use of branch instructions with compare. To BRANCH if Follow compare instruction with: For Unsigned #s For Signed #s Register is LESS THAN data BCC TRUE BMI TRUE Register is EQUAL TO data BEQ TRUE BEQ TRUE Register is GREATER THAN data BEQ FALSE
Register is LESS THAN or EQUAL TO data BCC TRUE
Register is NOT EQUAL to data BNE TRUE BNE TRUE
I suggest that you keep the above table handy any time you’re doing assembly programming. It will prove to be a very useful reference. The BASIC example above might be coded into assembly as follows:
L100 CMP #35 ;Is A-REG <= 35? BCC L200 ;...LESS THAN 35? BEQ L200 ;...or EQUAL TO 35? L110 INX ;No, then A > 35. L120 DEY L130 JMP L300 L200 INX ;Acc was <= 35 INX L210 INY L300 ... ;continue program here.
A few examples are given below for unsigned numbers. The comments and labels should be self explanatory.
CMP DATA ;Acc < value in 'DATA'? BCC LT ... LT ... CPX DATA ;X = value in 'DATA'? BEQ EQ ... EQ ... CPY #43 ;Is Y > the number 43? BEQ NGT ;Not if they're equal! BCS GT NGT ... ... GT ... CMP DATA ;A <= Value in 'DATA'? BCC LE ;Less than? BEQ LE ;or EQual to? ... LE ... CPX #$9A ;X >= hex nunber $9A? BCS GE ... GE ... CPY #32 ;Y <> number 32? BNE NE ... NE ...
To summarize the above, it might be best to explain the meaning of each of the flags after a compare instruction has been completed.
If the Zero flag is set, then the register had the same value as the data (either immediate or in a memory location). If the Zero flag is clear, then the two were not equal.
The Carry flag indicates a borrow (remember grade school subtraction). If the register had to borrow a one to complete the subtraction, the Carry flag will be clear. This means that the register was less than the data it was being compared to. If the Carry is set, it means that the register didn’t have to borrow for the “imaginary subtraction.” This indicates that the register was either equal to or greater than the data (the Zero flag must be used to differentiate).
The Sign flag is used with signed numbers. This flag will be equal to the most significant bit of the result of the subtraction. A byte may represent an unsigned number from 0 to 255, or a signed number from -128 to +127. When the most significant bit is set, the number is negative. When clear, it is positive.
Note that I did not cover BVC (Branch if overflow Clear) or BVS (Branch if overflow Set). These instructions are seldom used, except in multi-precision (multi-byte) signed math routines.
Armed with this information, you should be ready to tackle the toughest 6502 assembly language comparison situation. Once you master the use of comparison operations, you’ll find 6502 assembly language isn’t that hard, after all.
Matthew J.W. Ratcliff is an Electrical Engineer in St. Louis, Missouri. He has been programming in BASIC and assembly language on the Atari since 1982. He’s also active in telecommunications and is a remote Sysop on the Gateway BBS, (314) 647-3290.
A.N.A.L.O.G. ISSUE 41 / APRIL 1986 / PAGE 125
The “graphics” statement in Atari BASIC opens the door to a world of visual delights. This simple command actually does far more than is apparent, preparing the 8-bit Atari to display images in one of dozens of ways. To the novice assembly language programmer, it isn’t obvious how to perform in assembly the tasks that the GRAPHICS command does in BASIC. Nor is it clear how to display text and graphics in the various Atari graphics modes.
In this and next month’s Boot Camp, we’ll see how to use the Atari operating system (OS) in assembly language to employ the desired graphics modes. Along the way, we’ll see how the BASIC commands GRAPHICS, POSITION, PRINT, SETCOLOR, COLOR, PLOT and DRAWTO can be mimicked in assembly language. This month’s Boot Camp discusses text displays, while plotting points and drawing lines are the subject of the next column.
To begin, let’s see just what happens when a graphics statement is executed in Atari BASIC. Here’s what that simple statement causes:
And that’s just a simple graphics statement! Allowable options include adding 16 to the desired graphics mode number to suppress the text window, adding 32 to suppress the automatic screen clear, and adding 48 to suppress both.
At first glance, it might appear that we have to do a lot of work to mimic the graphics statement in assembly language. Fortunately though, all of these procedures are built right into the OS, so we just have to learn how to control the OS’s input/output functions.
A thorough and understandable presentation of the Atari’s Central Input/Output (CIO) system appeared in earlier Boot Camp columns. See issues 33, 34 and 37, in particular.
The Atari OS was designed with a unified input/output subsystem known as CIO. The elegant simplicity of CIO is that all I/O operations are device-independent. Once some necessary parameters have been set for the specific operation desired, CIO is called by transferring control to a specific memory location. The OS does the rest.
CIO has eight independent communications channels available, allowing up to eight files or peripheral devices to be active at once. You’ve already encountered them in using BASIC to open channels for disk files, printers and the like. These channels are properly termed “Input/Output Control Blocks,” or IOCBs.
By default, IOCB #0 is used by the screen editor (device E:), which handles text displays in graphics and all text windows. It doesn’t need to be explicitly opened for use. You may recall that, in BASIC, the graphics display screen (device S:) always uses channel #6, which is really IOCB #6. To print in graphics 1 or 2, the command PRINT #6; is needed, since a simple PRINT implies the use of IOCB #0. The commands PLOT and DRAWTO also use IOCB #6, although this isn’t explicitly stated anywhere in a BASIC program. These are the two IOCBs that may be opened automatically when a graphics statement is executed—and they’re all concerned with in this article.
Each IOCB uses a block of 16 bytes to specify the desired operations and necessary parameters. Our first task in an assembly language program is to indicate which IOCB we wish to use, by loading the 6502 microprocessor’s X-register with the IOCB number times 16. This provides a pointer into the correct RAM block allocated for the desired IOCB.
Then we tell the OS what function to perform, by setting various bytes in the 16-byte group for that IOCB. Finally, we execute a JSR to the CIO entry point at location 58454 decimal ($E456), known by the label CIOV. Table 1 indicates the bytes in each IOCB that we can modify for our own use; the rest are set by the OS.
Table 1.—User-modified IOCB bytes. Location in IOCB #0 Equate Name Function $342 ICCOM Code for command requested by user. $344 ICBAL Low byte of buffer address for device name, text to print, etc. $345 ICBAH High byte of buffer address. $348 ICBLL Low byte of buffer length; specifies number of bytes to be transferred in input or output operation. $349 ICBLH High byte for buffer length; if less than 256 bytes are involved. $34A ICAX1 Auxiliary byte 1; used to specify kind of file access needed in open operation; controls screen clear and text window in graphics screen. $34B ICAX2 Auxiliary byte 2; specifies graphics mode in screen open operation.
Now let’s explore some examples of using CIOV for our own fiendish purposes. We’ll go through a series of five simple sample programs. Each builds on the previous one, so be sure to enter each block of lines with the lines numbers shown. If you’re using an assembler that doesn’t require line numbers, just figure out from the numbers given where to insert each new block of code. You may want to save each example separately.
Example 1.—Print to screen editor.
Let’s start by using CIO to print a single line in a graphics screen. Type in Listing 1 using the Atari Assembler/Editor cartridge or another assembler editor. The .OPT OBJ statement just insures that object code is generated and loaded beginning at location $3000 hex whenever you assemble a program. This eliminates saving object files on tape or disk each time you modify and assemble the program.
Lines 100–120 define constants for the various IOCB operations we’ll be performing: OPEN an IOCB for some device; PLOTREC (output a record) to the opened device (like a PRINT); and CLOSE the IOCB.
The EOL (Line 130) is the ATASCII end-of-line (carriage return) symbol, which tells CIO that it’s reached the end of an interesting string of text to process at the moment.
Lines 390–450 give standard Atari OS equates (labels) for the bytes in IOCB # we’ll manipulate. For any other IOCB, we’ll use the block of bytes offset from these locations bay 16 times the desired IOCB number (96 decimal or $60 hex for IOCB #6).
Finally, Line 460 establishes an equate for the CIO entry point.
The block of lines under the heading PRINTWINDOW (1150–1270) illustrates the put record operation of CIO, which prints to the graphics screen or text window. Note that the X-register is loaded with 0 because we’re using IOCB #0, which is already open for us.
These bytes must be set for a PUTREC: (1) command byte ICCOM is set to $09; (2) ICBAL receives the low byte of the address of text to be displayed, and ICBAH receives the corresponding high byte value; and (3) ICBLL and ICBLH are set to the number of characters in the string of text to be printed (low byte and high byte, respectively). Finally, initiate the put record with a JSR CIOV statement.
It’s a smart practice to set the output buffer length (ICBLL and ICBLH) to a conveniently large number (such as 80) and make sure that each output text record is terminated by an ATASCII EOL character (155 decimal, $9B hex). This approach makes it easy to change text strings in an existing program, without having to worry about setting the output buffer bytes to exactly the correct length. Just don’t forget the EOL character (Line 1450)!
Assemble this program and run it by entering the debugger and typing 63000. The statement at Line 1390 causes the program to loop until you press the BREAK key or SYSTEM RESET, at which point you’ll re-enter the debugger. This isn’t a very exciting display, but I think you’ll begin to understand how to use CIO from this simple example. You can make the screen clear before printing by inserting a “clear screen” symbol (ESC, SHIFT-<; decimal value 125) just before the S in Line 1450.
Example 2.—Other graphics modes.
Now let’s learn how to simulate the command GRAPHICS using CIO. Merge the lines from Listing 2 into Listing 1 and assemble the resulting program. These lines perform the IOCB functions OPEN and CLOSE.
We use IOCB #6 for the graphics screen, so set the X-register to $60 (Line 520). The command byte for an open operation is $03 (Lines 530–540). The name of the device being opened is placed in a data string, labeled SCREEN here (Line 1430).
Notice that we want to open the graphics screen, known as device S: (the colon is optional). This same procedure is used to open other devices, such as disk files and the keyboard, but we won’t get into that this time.
Again, the high and low bytes of the location containing the device name must be placed into the bytes offset from ICBAL and ICBAH, respectively (Lines 550–580). The IOCB bytes labeled ICAX1 and ICAX2 are set to various values, depending on the graphics mode involved (Lines 590–620); more about this later. Finally, JSR to CIOV to make it all happen (Line 630).
Closing an IOCB is much simpler than opening it. Just store the command value of $0C for a close operation in the byte labeled ICCOM (offset to the appropriate IOCB, of course), and JSR to CIOV (Lines 1310–1350).
The actual graphics mode opened is determined by the contents of the 2 auxiliary bytes, ICAX1 and ICAX2. ICAX2 should be loaded with the BASIC graphics mode number desired, 1 in this example (Lines 610–620).
The contents of ICAX1 (Lines 590–600) dictate whether the mode will be set with no text window (decimal value in ICAX1 is 12); with the usual four-line text window (ICAX1 is 28); with text window but no automatic screen clear (ICAX1 is 60); or no text window and no screen clear (ICAX1 is 44). We’ll ex periment with some of these options in the next example.
When you run this program consisting of Listings 1 and 2, you’ll see the familiar black and blue split screen, with the message from Line 1450 present in the corner of the text window. Press BREAK to terminate the program, but the split screen will remain. You’ll have to press SYSTEM RESET to get back to the standard text display, then re-enter the editor to continue with the examples.
By now you may have detected a similarity between the use of CIO in these illustrations and the format of the Atari BASIC command XIO. For example, the XIO form of our open operation looks like this: XIO 3,#6,28,1,"S:". In general terms, the XIO format is: command, #IOCB, aux1, aux2, device. You can doubtless see the connection between these terms and those used in the open screen segment of this example. Clearly, the XIO statement provides a way to interact directly with CIO from BASIC.
Example 3.—Print to graphics screen.
Moving right along, let’s expand on what we’ve already done and print something in both the graphics screen and the text window. Not surprisingly, we’ll use PUTREC again, to display text in the graphics 1 screen.
However, we’ll use IOCB #6 rather than IOCB #0, which is needed for the text window. Insert the following statements into your evolving program:
0860 ;print a line of text in Graphics 0870 ;1 to screen using IOCB #6 0880 ; 0890 PRINTSCREEN 0900 LDX #$60 0910 LDA #PUTREC ;PUT RECord 0920 STA ICCOM,X 0930 LDA #CHANNEL6&255 ;location of 0940 STA ICBAL,X ;text to be 0950 LDA #CHAHMEL6/256 ;printed 0960 STA ICBAH,X 0970 LDA #80 ;print up to 80 0980 STA ICBLL,X ;characters or to 0990 LDA #0 ;encountering an 1000 STA ICBLH,X ;EOL, whichever 1010 JSR CIOV ;comes first 1020 ; 1440 CHANNEL6 .BYTE "GRAPHICS SCREEN,"
The details of the PRINTSCREEN section of code should be clear from the earlier explanation of PUTREC. Now we can explore the affects of the open operation in more detail.
You can probably guess that changing the 1 in Line 0610 to a 2 will switch the screen to graphics 2. There’s no need to reassemble the whole program, though.
Simply change the contents of location $3017 using the debugger (C3017<2), and rerun with a G3000 command. You can even try nontext modes 3–5 to get a line of colored pixels. (Higher graphics modes are ignored by the OS, unless special tricks are used.)
Try changing the contents of ICAX1 to 60, to suppress the screen clear. Since the debugger uses hexadecimal numbers, execute this command in the debugger: C3012 <3C. What happens if we turn off the text window by setting ICAX1 to 12 (C3012 <C)? You’ll see only a flash of black and orange before the blue graphics display reappears.
As soon as the OS encounters a PUTREC operation to the screen editor (IOCB #0) when no graphics text area is present, it converts the entire screen to graphics 0. So you must delete or jump around the print window segment in our program to see a full screen of graphics 1 or a higher-numbered mode.
As in BASIC, the string of text being printed to either graphics screen or text window can contain upper- or lowercase letters, normal or inverse characters, or graphics symbols, including those with printing significance ESC-left cursor, for example).
As usual in graphics 1 and 2, characters other than normal uppercase letters produce printing in different colors, by selecting one of the four foreground color registers. Try it; you’ll like it.
Example 4.—Simulating the position statement.
So far we’ve been content to let the OS print out text strings wherever it likes, which is always the upper left corner of the designated screen region. But BASIC gives us complete control of text placement through the position statement. No problem; that’s a piece of cake in assembly language, too. Please add the following statements to the program from Example 3:
0150 ;equates for row, colunn cursor 0160 ;position on graphics screen 0170 ; 0180 ROWCRS = $54 0190 COLCRS = $55 0200 ; 0270 ;equates for row, column cursor 0280 ;position in text window 0290 ; 0300 TXTROW = $290 0310 TXTCOL = $291 0320 ; 0330 ;location to Make cursor visible 0335 ;or not 0340 ; 0350 CRSINH = $2F0 0650 ;set cursor at position 1,4 for 0660 ;printing on graphics screen 0670 ; 0680 POSGRAPHICS 0690 LDA #4 0700 STA ROWCRS 0710 LDA #1 0720 STA COLCRS 0730 ; 1030 ;set cursor to position 10,2 in 1040 ;text window and turn off cursor 1050 ; 1060 POSWINDOW 1070 LDA #2 1080 STA TXTROW 1090 LDA #10 1100 STA TXTCOL 1110 STA CRSINH
Locations 84 and 85 ($54 and $55, ROWCRS and COLCRS) contain the row (y) and column (x) locations, respectively, for PUTREC (or PRINT, in BASIC) operations to the display screen. If a text window is present, then locations 656 and 657 ($290 and $291, TXTROW and TXTCOL) contain the row and column positions for the text window.
Lines 690–720, therefore, simulate a BASIC position 1, 4 statement for printing to the graphics screen. Line 1110 simply places a nonzero value into location 752 ($2F0, CRSINH), which turns off the cursor that normally appears in all graphics displays, including the text window. A zero value in CRSINH renders the cursor visible. This happens automatically when an open operation is executed by CIO.
Example 5.—Color your world.
We polish off our text display illustrations by imitating the BASIC setcolor command in assembly language. This command has the form: SETCOLOR register, hue, luminance. The five color registers correspond to locations 708–712 ($2C4–$2C8, COLOR0–COLOR4). The hue and luminance values are combined, to represent the desired color with a single 1-byte number using this formula:
COLOR = 16 * HUE + LUMINANCE
Thus, the following statements can be incorporated into your assembly program, to change the graphics 1 text to pink (color value 88 decimal), the text window background to green (color 198) and the graphics text to black (color 0):
0210 ; equates for color registers used 0220 ; 0230 COLOR0 = $2C4 0240 COLOR1 = $2C5 0250 COLOR2 = $2C6 0260 ; 0740 ;change color registers to pink 0750 ;for register 0, black for reg- 0760 ;ister 1, and light green for 0765 ;register 2 0770 ; 0780 COLORS 0790 LDA #88 0800 STA COLOR0 0810 LDA #0 0820 STA COLOR1 0830 LDA #198 0840 STA COLOR2 0850 ;
Experiment with other color values in these registers to get the look you like. The completed assembly language program for all five examples combined is found in Listing 3.
Now that you’ve gone to all this trouble to create some simple text displays, using assembly language to harness the power of the Atari OS, let’s see how those unfortunate souls with just Atari BASIC available would do the same thing:
10 GRAPHICS 1 20 SETCOLOR 0,5,8 30 SETCOLOR 1,0,0 40 SETCOLOR 2,12,6 50 POKE 752,1 60 POSITION 1,4 70 PRINT #6; "GRAPHICS SCREEN, S:" 80 PRINT : PRINT 90 PRINT " SCREEN EDITOR, E:" 100 GOTO 100
Yes, I have to agree that this is a lot shorter, faster and easier to type than the program in Listing 3. After all, that’s why home computers are sold with BASIC, not with assemblers.
Still, there are many applications for which programming directly in assembly language permits speed and visual effects that are difficult or impossible in BASIC. By studying the use of the Atari OS’s central I/O capabilities as illustrated here, you’ll see that it’s really not difficult to change graphics modes, change colors, and position text in graphics displays and text windows.
Next month, we’ll use CIO to plot points and draw lines in some of the available color graphics modes.
Many of the same IOCB operations will be used in both text and graphics displays, so keep this issue handy for next time. H
Karl E. Wiegers provides computer support for photographic researchers at Eastman Kodak Company. This means he’s wasting his Ph.D. in organic chemistry, but he has a lot of fun. He also writes commercial educational chemistry software for the Apple II.
Listing 1. Assembly listing. 10 ;Listing 1 for 15 ;Assemble Some Graphics 20 ;by Karl E. Wiegers 30 ; 40 .OPT OBJ 50 *=$3000 60 ; 70 ;command byte values for various 80 ;CIOV operations 90 ; 0100 OPEN = $03 ;open IOCB 0110 PUTREC = $09 ;output record 0120 CLOSE = $0C ;close IOCB 0130 EOL = $9B ;ATASCII EOL 0360 ; 0370 ;equates for IOCB locations 0375 ;(given for IOCB #0) 0380 ; 0390 ICCOM = $342 ;command byte 0400 ICBAL = $344 ;buffer addr (lo) 0410 ICBAH = $345 ;buffer addr (hi) 0420 ICBLL = $348 :buffer leng (lo) 0430 ICBLH = $349 ;buffer leng (hi) 0440 ICAX1 = $34A ;auxiliary byte 1 0450 ICAX2 = $34B ;auxiliary byte 2 0460 CIOV = $E456 ;CIO entry point 1120 ; 1130 ;print a line of text using 1135 ;IOCB no (text window) 1140 ; 1150 PRINTWINDOW 1160 LDX #0 ;IOCB #0 1170 LDA #PUTREC ;command is 1180 STA ICCOM,X ;PUT a RECord 1190 LDA #TEXT&255 ;location of 1200 STA ICBAL,X ;text to print 1210 LDA #TEXT/256 1220 STA ICBAH, X 1230 LDA #80 ;print up to 80 1240 STA ICBLL, X ;characters or to 1250 LDA #0 ;an encounter of 1260 STA ICBLH, X ;an EOL. 1270 JSR CIOV 1360 ; 1370 ;loop until SYSTEM RESET pressed. 1380 ; 1390 END JMP END 1480 ; 1410 ;data for necessary text strings, 1420 ; 1450 TEXT .BYTE "SCREEN EDITOR, E:" 1460 .BYTE EOL
Listing 2. Assembly listing. 10 ;Listing 2 for 15 ;Assemble Some Graphics 20 ;by Karl E. Wiegers 0470 ; 0480 ;routine to open IOCB #6 as 0490 ;device "S:" (graphics screen) 0500 ; 0510 OPENSCREEN 0520 LDX #$60 0530 LDA #OPEN ;command is OPEN 0540 STA ICCOM,X 0550 LDA #SCREEN&255 ;name of device 0560 STA ICBAL,X ;to open 0570 LDA #SCREEN/256 0580 STA ICBAH,X 0590 LDA #28 ;text window is 0600 STA ICAX1,X ;present 0610 LDA #1 ;graphics Mode 0620 STA ICAX2,X ;number 0630 JSR CIOV ;go do it 0640 ; 1280 ; 1290 ;close IOCB #6 1300 ; 1310 CLOSESCREEN 1320 LDX #560 1330 LDA #CLOSE ;CLOSE connand 1340 STA ICCOM,X 1350 JSR CIOV 1430 SCREEN .BYTE "S"
A.N.A.L.O.G. ISSUE 42 / MAY 1986 / PAGE 99
Last month, we explored how to use the built-in power of Atari’s Central Input/Output system (CIO) in an assembly language program to display text in graphics modes 0, 1 and 2. First we used CIO to open the graphics screen (device S:, IOCB #6). Then we saw how to output a record to either the graphics screen or the default screen editor (device E:, IOCB #0).
Finally, we used CIO to close IOCB #6, the graphics screen. We also saw how to set the auxiliary bytes ICAX1 and ICAX2 to open the screen in different graphics modes, with or without a text window, with or without clearing the screen.
In this issue, we’ll learn how to plot points and draw lines in the lower resolution graphics modes. Methods for setting the contents, then selecting color registers for plotting will be shown, too. As with the first part of this project, the screen displays are not breathtakingly beautiful, but you can take care of that on your own.
Example 3 in last issue’s Boot Camp explained how to print a string of text in graphics 1 or 2. If you experimented with the suggestion of changing the graphics mode number to 3, 4 or 5 in that example, you found that your text string was miraculously converted into a line of colored pixels.
In Atari BASIC, the PLOT and DRAWTO statements regulate line drawing operations in a graphics screen. Fortunately, one of the CIO commands available for IOCB #6 is DRAW. So let’s explore the fundamentals of plotting to a graphics screen using assembly language. Our examples are restricted to graphics modes 3 through 5, since special tricks are needed for the higher resolution modes.
Example 6.—A straight line.
We’ll start simply, by just drawing a straight line in a default color (green), in a graphics 5 screen. The program in Listing 4 does the trick. Enter this listing using the Atari Assembler Editor or another assembler.
Be sure to type this listing in using the line numbers shown, since we’ll be adding more statements for the next example. The .OPT OBJ,NO LIST statement in Line 40 just makes sure you generate object code starting at location $3000 in RAM. This saves time by not listing the entire program each time you assemble it.
Building on the previous text examples, we have now added the DRAW command (which has a value of 17 decimal, $11 hex) to the equates list (Line 130). The operation to open IOCB #6 for the graphics screen, (device S:) is the same as in Example 2 last time (Lines 450–570), except that this example uses a full screen (ICAX1 is 12) of graphics 5 (ICAX2 is 5).
A line segment is defined by the coordinates of its two endpoints. Just as in BASIC, we need to first plot a pixel at the coordinates of one endpoint, then draw to the coordinates of the other endpoint.
A graphics 5 screen can have X-coordinates ranging from 0 through 79 and Y-coordinates from 0 through 47. Be sure you don’t try to draw outside the allowable coordinate range, or you’ll generate an error. This example draws a line from location 12,10 to 75,40.
The plotting of a single point can be achieved using the PUTREC operation of CIO. This illuminates a single pixel at the coordinates specified in bytes $54 (ROWCRS) for Y and $55 (COLORS) for X. Lines 620–660 in Listing 4 set these coordinates.
This is equivalent to printing a single character on the graphics screen in a text mode. Consequently, the length of the string to be plotted (i.e., the length of the PUTREC buffer in bytes ICBLL and ICBLH) should be set to 1 (Lines 740–780).
In Atari BASIC, the color of points and lines is controlled by selecting a specific color register using the “color” command. You should be able to find a table in your BASIC manual which informs you that, in graphics 5, COLOR 1 selects color register 0, COLOR 2 selects register 1, and COLOR 3 selects register 2.
Don’t ask me why. It’s an interesting quirk that “printing” certain letters in a graphics mode greater than 2 also selects a particular color register. Printing a capital A uses color register 0, B calls for register 1, and C picks register 2.
This feature makes our lives simpler. Lines 1390–1410 declare some data values, to establish this letter-to-color register relationship. Lines 700–730 then choose one of those text strings to be printed, thereby selecting the register of our choice. Change the REG1 in Lines 700 and 720 to REG0 or REG2, to see the effect. Finally, the JSR CIOV in Line 780 plots our point where we want it.
Now, to simulate the BASIC “drawto” command. First, set the coordinates of the line’s endpoint, just as we did for the starting point (Lines 830–870). Then, set up IOCB #6 for a draw operation in Lines 910–940. Lines 950–960 again show the selection of color register 1 (COLOR 2 statement in BASIC).
The number that would appear in the color statement must be stored in location 763 ($2FB, ATACHR). This will ensure that our line is drawn using the default color in color register 1, a light green. You can change the value stored in ATACHR to draw a line in a color different from the one you used to plot the first point. In this case, we did both operations using the same color register, 1.
Finally, let CIOV take over in Line 970, and your line magically appears. Of course, IOCB #6 needs to be closed before you can run the program again. The routine in Lines 1210–1250 should be familiar from the previous examples as a close screen operation. This display will remain on the screen until you press BREAK or SYSTEM RESET, thanks to the infinite loop in Line 1270.
Example 7.—Drawing multiple lines.
No one’s going to get excited about drawing a single straight line in a medium-resolution display. However, a straightforward extension of the program in Listing 4 will plot a series of points and connect them with lines.
You can modify this routine to draw as many line segments as you like, all at top machine speed. Now merge the statements in Listing 5 with your program from Listing 4. Many of the lines in Listing 4 are replaced in Listing 5, so you may wish to save the former intact before proceeding.
The approach here is to plot the initial point of our figure as in the previous example, then look up additional pairs of coordinates from data tables. These points will be plotted successively and will serve as the endpoints for a series of line segments to complete the figure. If all goes well, we’ll end up with a five-pointed star in the middle of the screen.
The first point is plotted as before in the PLOTPOINT routine (Lines 620–780). This time, we select color register as our plotting color, by doing a PUTREC operation with a letter A (Lines 700–730 again).
The contents of the color registers themselves are changed in Lines 830–860, to give a yellow drawing on a purple background. This is equivalent to using the “setcolor” command in BASIC. Color register is selected for the drawing operation in Lines 870–880.
Two tables of X- and Y-coordinates are defined at the end of the program (Lines 1330–1340). The routine in Lines 930–1070 extracts points from these tables and calls a DRAWLINE subroutine, which executes the DRAW operation of CIO (Lines 1120–1170). Except for the fact that this segment of code is a subroutine and thus terminates with an RTS (return from subroutine) instruction, it’s equivalent to the drawing step from Listing 4. Notice that we can have as many DRAWs as we like in a row; only the very first point must be plotted with the PUTREC operation.
The Y-register of the 6502 microprocessor is used as an offset pointer to the next entry in the data tables. It begins with a value of (Line 930). We use this offset to get the next X-coordinate and store it in the appropriate location (Lines 950–960), then do the same for the Y-coordinate (Lines 970–980).
Since CIO operations tend to change the X- and Y-registers, we will save the contents of the Y-register on the stack before executing the DRAW (Lines 990–1000), then retrieve it afterward (Lines 1020–1030).
The pointer is incremented after the drawing procedure (Line 1040). When the value of this pointer reaches 5 (Lines 1050–1070), we’ve plotted all five pairs of points in this example. The program branches to the CLOSESCREEN routine at Lines 1210–1250.
It should be easy to see how to use this approach in your own programs, any time you want to draw a series of connected lines.
Let’s see how we would write the program from Example 7 in Atari BASIC:
10 GRAPHICS 5+16 20 SETCOLOR 8,1,12 30 SETCOLOR 4,6,8 40 COLOR 1 50 PLOT 20,15 60 DRAWTO 60,15 70 DRAWTO 28,35 80 DRAWTO 40,5 90 DRAWTO 52,35 100 DRAWTO 20,15 110 GOTO 110
As with the text examples, BASIC is obviously much less work than doing the same thing in assembly language. However, you’ll find times when assembly is really the tool of choice, for many graphics applications.
These program examples provide an introduction to the use of the 8-bit Atari’s built-in input/output capabilities for controlling screen displays. While some additional refinements are needed to use the higher resolution graphics modes, these examples should get you started on more serious graphics programming in assembly language.
By combining these concepts with other powerful Atari graphics features, such as the ability to use custom character sets, modified display lists for mixing graphics modes in a single screen, and display list interrupts, you can write sophisticated assembly language programs to achieve effects and speed that BASIC simply cannot provide. Have fun!
Listing 4. Assembly listing. 10 ;Listing 4-Assemble Some Graphics 20 ;by Karl E. Wiegers 30 ; 40 .OPT OBJ,NO LIST 50 *= $3000 60 ; 70 ;command byte values for various 80 ;CIOV operations 90 ; 0100 OPEN = $03 ;open IOCB 0110 PUTREC = $09 ;output a record 0120 CLOSE = $0C ;close IOCB 0130 DRAW = $11 ;draw a line seg 0140 ; 0150 ;equates for row and column cursor 0160 ;position on graphics screen 0170 ; 0180 ROWCRS = $54 0190 COLCRS = $55 0200 ; 0260 ;location to use to select color 0270 ;register-plot & draw operations 0280 ; 0290 ATACHR = $02FB 0300 ; 0310 ;equates for IOCB locations 0315 ; (given for IOCB #0) 0320 ; 0330 ICCOM = $0342 ;command byte 0340 ICBAL = $0344 ;buf addr-lo byte 0350 ICBAH = $0345 ;buf addr-hi byte 0360 ICBLL = $0348 ;buf len-lo byte 0370 ICBLH = $0349 ;buf len-hi byte 0380 ICAX1 = $034A ;auxiliary byte 1 0390 ICAX2 = $034B ;auxiliary byte 2 0400 CIOV = $E456 ;entry point to 0405 ;central I/O subsystem 0410 ; 0420 ;routine to open IOCB #6 as 0430 ;device "S:" (graphics screen) 0440 ; 0450 OPENSCREEN 0460 LDX #$60 ;offset to IOCB tt6 0470 LDA HOPEH ;command is OPEN 0480 STA ICCOM,X 0490 LDA #SCREEN&255 ;device to open 0500 STA ICBAL,X 0510 LDA #SCREEN/256 0520 STA ICBAH,X 0530 LDA #12 ;no text window 0540 STA ICAX1,X 0550 LDA #5 ;graphics mode 5 0560 STA ICAX2,X 0570 JSR CIOV ;go do it 0580 ; 0590 ;begin by plotting one endpoint of 0600 ;the line segment, at 12,10 0610 ; 0620 PLOTPOINT 0630 LDA #12 ;X-coordinate is 12 0640 STA COLCRS 0650 LDA #10 ;Y-coordinate is 10 0660 STA ROHCRS 0670 LDX #$60 ;Offset to IOCB #6 0680 LDA #PUTREC ;command is 0685 ; PUT a RECord 0690 STA ICCOM,X 0700 LDA #REG1&255 ;select color 0705 ; register 1 0710 STA ICBAL,X ;by "printing" a 0715 ; letter "B" 0720 LDA #REG1/256 ;this is like 0725 ; a COLOR 2 0730 STA ICBAH,X ;statement in BASIC 0740 LDA #1 ;only print 1 char 0750 STA ICBLL,X 0760 LDA #0 0770 STA ICBLH,X 0780 JSR CIOV ;go do it 0790 ; 0800 ;now set the other endpoint of the 0810 ;line segment, at 75,40 0820 ; 0830 ENDPOINT 0840 LDA #75 ;X-coordinate is 75 0850 STA COLCRS 0860 LDA #40 ;Y-coordinate is 40 0870 STA ROWCRS 0880 ; 0890 ;now use CIOV to connect the dots 0900 ; 0910 DRAWLINE 0920 LDX #$60 ;offset from IOCB#6 0930 LDA #DRAW ;command is DRAW 0940 STA ICCOM,X 0950 LDA 82 ;select color 0955 ;register 1 0960 STA ATACHR 0970 JSR CIOV ;go do it 1180 ; 1190 ; close the graphics screen 1200 ; 1210 CLOSESCREEN 1220 LDX #$60 ;offset to IOCB #6 1230 LDA #CLOSE ;command is CLOSE 1240 STA ICCOM,X 1250 JSR CIOV ;go do it 1260 ; 1270 STOP JMP STOP 1280 ; 1290 ;data values for text strings 1340 ; 1350 ; 1360 ;data values for text strings 1370 ; 1380 SCREEN .BYTE "S" 1390 REG0 .BYTE "A" 1400 REG1 .BYTE "B" 1410 REG2 .BYTE "C"
Listing 5. Assembly listing. 10 ;Listing 5-Assemble Some Graphics 20 ;by Karl E. Wiegers 30 ; 0210 ;equates for color registers used 0220 ; 0230 COLOR0 = $02C4 0240 COLOR4 = $02C8 0250 ; 0590 ;begin by plotting the first point 0600 ;of the figure, at 20,15 0610 ; 0630 LDA #20 ;X-coordinate is 20 0650 LDA #15 ;Y-coordinate is 15 0700 LDA #REG0&255 ;select color 0705 ; register 0 0715 ; a letter "A" 0720 LDA #REG0/256 ;this is like 0725 ; a COLOR 1 0800 ;change colors to purple background 0810 ;and yellow lines 0830 LDA #104 ;purple into 0840 STA COLOR4 ;color register 4 0850 LDA #28 ;yellow into 0860 STA COLOR0 ;color register O 0870 LDA #1 ;selects color 0875 ; register 0 0880 STA ATACHR 0890 ; 0900 ;routine to connect X,Y points 0910 ;from tables of coordinate data 0920 ; 0930 LDY #0 ;use Y as table 0935 ; offset pointer 0940 POINT 0950 LDA XDATA,Y ;no, get next 0952 ; x-coordinate 0955 ; 0960 STA COLCRS ;goes in column reg 0970 LDA YDATA,Y ;get next 0975 ; y-coordinate 0980 STA ROWCRS ;in row register 0990 TYA ;save Y-register 1000 PHA 1010 JSR DRAWLINE ;draw from last 1015 ; point to this one 1020 PLA ;restore Y-register 1030 TAY 1040 INY ;increment table 1045 ; offset pointer 1050 CPY #5 ;equal to 5 yet? 1060 BNE POINT ;no, go for next 1065 ; point 1070 BEQ CLOSESCREEN ;yes, close 1075 ; screen and quit 1080 ; 1090 ;subroutine to draw from last 1100 ;plotted point to the current one 1110 ; 1120 DRAWLINE 1130 LDX #$60 1140 LDA #DRAW 1150 STA ICCOM,X 1160 JSR CIOV 1170 RTS 1280 ; 1290 ;data for plotting points, in two 1300 ;tables, one for X-coordinates and 1310 ;one for Y-coordinates 1320 ; 1330 XDATA .BYTE 60,28,40,52,20 1340 YDATA .BYTE 15,35,5,35,15
A.N.A.L.O.G. ISSUE 43 / JUNE 1986 / PAGE 111
Several recent Boot Camp installments have explored the many useful functions performed by the 8-bit Atari’s Central Input/Output (CIO) system. Most recently, we saw how to use CIO to create text and graphics displays on-screen.
In future issues, our discussion of graphics will continue in earnest. Look forward to explanations of how to: create mixed-mode graphics displays; use display list interrupts to get many colors on-screen at once; use player/missile graphics; move your players around the screen with a joystick; create scrolling displays; and perform other wondrous feats in assembly language programs.
This month’s topic is a bit different. A little-known section of the Atari operating system (OS) is the floating point arithmetic package. It may be terra incognita to you, but you’ve taken advantage of its presence every time you added two numbers together in a BASIC program.
The 6502 microprocessor in the Atari can perform numerical operations only on integers. For example, the instruction set for the 6502 contains op codes for integer addition (ADC) and subtraction (SBC). However, many numbers used in computing—and the rest of the world—contain decimal points. They’re “floating point” (FP) numbers, rather than integers. Since 1 byte of RAM can only contain values from 0 to 255, we clearly need some special storage format to represent floating point numbers. Also, we require some special subroutines to execute mathematical operations with our FP numbers. These operations include both simple arithmetic (addition, subtraction, multiplication and division) and more complex functions like logarithms, exponentials and trigonometric functions.
Let’s think about BASIC for a minute. Atari BASIC has only two kinds of variables: numeric and character. Many other versions of BASIC possess different types of numeric variables. One is integer; another is FP. The integer variables typically occupy 2 bytes of memory. This permits a range of unsigned values from 0 through 65535 or signed integers from -32767 through +32767
There is no integer data type in Atari BASIC. All numbers used by Atari BASIC are stored in a floating point representation requiring 6 bytes of storage per number. This means that even a simple constant like 1 takes up a full 6 bytes every time it appears in a program.
Not only does this eat up memory quickly, but the increased complexity of FP arithmetic takes much more execution time than do calculations with integers. This is one reason why the Atari has a reputation as a v-e-r-y s-l-o-w computer, when it comes to number crunching. However, we’re stuck with the design, so let’s see how to work with it in assembly language programs.
Now to refresh your memory a bit…Imagine that we wish to add two 2-byte integers in an assembly program. The numbers are stored in typical low-byte, high-byte fashion. Suppose that one number is in locations $0600 and $0601, the second in $0602 and $0603. We wish to add these and place the result in locations $0604 and $0605. The following code does the job:
CLC LDA $0600 ADC $0602 STA $0604 LDA $0601 ADC $0603 STA $0605
Now let’s see how floating point numbers are stored—and how to add a couple of them together. The details of FP representation aren’t required knowledge to do calculations, so feel free to skip the next section if it looks too unappetizing.
I mentioned earlier that all numbers in Atari BASIC occupy 6 bytes of storage, in floating point form. We need to use this same format for all FP numbers we handle in assembly language.
As an example, consider how the number 63298.47 is stored in Atari FP representation. First, recall that any FP number can be depicted in “scientific notation.” This involves writing the number with just one digit to the left of the decimal point and multiplying the result by 10 raised to a power equal to the number of places we had to shift the decimal point. Our candidate would thus be shown as 6.329847 * 10**4.
The Atari uses a slight twist on the scientific notation principle. You need to write your number with either one or two digits to the left of the decimal point, and multiply it by 100 (not 10) raised to the appropriate power. In our case, we’re already set: 6.329847 * 100**2.
Notice that our FP number now consists of two parts. The number itself, with the decimal point properly positioned, is called the “mantissa.” The power to which 100 is raised is called the “exponent.” The 6-byte FP representation uses the most significant byte for the exponent and the remaining 5 bytes for the mantissa.
The exponent byte is equal to $40 plus the value of the exponent. In our case, $40+$02=$42. (It gets trickier for negative numbers, but I’ll skip that aspect.) The 5 mantissa bytes contain the digits of the number stored as binary-coded decimal, two digits per byte. Think of the 6 before the decimal as an 06, and we’re all set. Here’s the full Atari floating point representation, in hexadecimal, of our decimal number, 63298.47.
42 86 32 98 47 88
This storage format can handle numbers ranging in magnitude from 10** -98 to 10** + 98, with nine or ten digits of accuracy. The computer will keep track of all this stuff for you, but perhaps you can use this information in a trivia game sometime.
Let’s think about the various functions our floating point package must be able to carry out. Table 1 lists most of the ones that exist. These are all subroutines, called by a JSR instruction. The table shows the symbolic name given to each routine and the hexadecimal address of the entry point to the routine. This is the address to which the JSR instruction must go.
All these routines reside in the OS ROM, except for the functions SIN, COS, ATAN and SQR. These are located in Atari BASIC. BASIC must be available any time you want to access these functions; plug in your BASIC cartridge on the old 400 and 800 models.
FLOATING POINT ROUTINES IN THE ATARI OS
NAME ADDRESS FUNCTION PERFORMED AFP $0800 Convert ATASCII string in LBUFF to FP number in FR0 FASC $D8E6 Convert FP number In FR0 to ATASCII string in LBUFF IFP $D9AA Convert integer number in FR0 to FP number in FR0 FPI $D9D2 Convert FP number in FR0 to integer number in FR0 FSUB $DA60 FR0 = FR0 - FR1 FADD $DA66 FR0 = FR0 + FR1 FMUL $DADB FR0 = FR0 * FR1 FDIV $DB28 FR0 = FR0 / FR1 EXP $DDC0 FR0 = e ** FR0 EXP10 $DDCC FR0 = 10 ** FR0 LOG $DECD FR0 = natural (base e) logarithm of FR0 LOG10 $DED1 FR0 = common (base 10) logarithm of FR0 SIN $BDA7 FR0 = SIN(FR0) (in BASIC) COS $BDB1 FR0 = COS(FR0) (in BASIC) ATAN $BE77 FR0 = ATAN(FR0) (in BASIC) SQR $BEE5 FR0 = SQR(FR0) (in BASIC) FLD0R $DD89 Load FR0 from memory using X and Y registers FLD1R $DD98 Load FR1 from memory using X and Y registers FST0R $DDA7 Store value in FR0 from memory using X and Y registers FMOVE $DDB6 Move FP number from FR0 to FR1 ZFR0 $DA46 Set all 6 bytes of FR0 equal to 0
Besides these addresses for the FP subroutines themselves, two other 6-byte blocks of page RAM are used as working locations (or registers) for FP computations. These are called FR0 ($D4) and FR1 ($E0). Now, what exactly should an FP package do for us?
First, consider that any data entered using CIO is just a string of characters. The number 63298.47 is just an 8-byte string. We need a routine to convert such a string of numeric ATASCII characters into an FP number, so we can do arithmetic with it.
The first entry in Table 1, AFP, does the trick. Conversely, we must be able to transform an FP number (stored in the bizarre format I described earlier) into an ATASCII representation suitable for output. Hence, routine FASC. Procedures are also required to convert 2-byte integer numbers into FP numbers (IFF) and vice versa (FPI).
Oh yes, arithmetic. How about some routines to perform your basic floating point subtraction (FSUB), addition (FADD), multiplication (FMUL) and division (FDIV)? Notice from the table that each of these operations requires the two FP numbers being processed be present in FP work registers FR0 and FR1. The answer always winds up in FR0, but FR1 gets altered during the process, so don’t ever try to use it again after an operation.
Some less common kinds of mathematical operations also have counterparts in the FP package. These include the aforementioned trigonometric procedures SIN, COS and ATAN (sine, cosine and arctangent), plus the square root function SQR. In addition, exponentials and logarithms can be taken, in either natural (base e) or common (base 10) form. These routines are called EXP, EXP10, LOG, and LOG10.
Naturally, any good software package needs some utility-type capabilities. The FP package has routines to load FR0 and FR1 with an FP number stored somewhere else in memory (FLD0R and FLD1R). Of course, we often need to move an FP number from register FR0 out to another 6-byte storage location in RAM, and FST0R does the trick.
FMOVE copies the number in FR0 into FR1. Finally, ZFR0 zeroes all 6 bytes of FR0 in one quick step. This is a good practice whenever you aren’t quite sure what kind of junk is left over in FR0.
Example 1.—FP addition.
Think back to the example of adding a pair of 2-byte integers we saw a little earlier. It only took seven lines of assembly code to do the job. Listing 1 shows how we do the same sort of thing in floating point. It’s a lot more work to set up, but the actual calculations fall into place pretty readily.
We begin with two integer numbers that we must convert into their FP representations, add, then store the result someplace convenient in RAM, where we can examine it using the debugger.
The numbers we wish to add together are called NUM1 and NUM2. Lines 100–110 set these to decimal 372 and 145, but you can try other values if you like. These numbers are stored in 2-byte locations labeled INT1 and INT2. Lines 280–350 split our numbers into high- and low-byte portions, and store them safely away.
Step 2 is to zero the FP work register FR0 with a call to subroutine ZFR0 in Line 360. Next, move one of our integers into the first 2 bytes of FR0, accomplished in Lines 370–400. The subroutine call in Line 410 to routine IFP converts this into an FP number, still in FR0.
Now move the result into the second FP work register, FR1, with the help of routine FMOVE (Line 420). After zeroing FR0 again, move the second integer into the first 2 bytes of FR0 (Lines 440–470).
Another call to IFP sets us up with FP numbers in both FR0 and FR1. A peek at Table 1 shows that we can add these two together and find the result in FR0 with a JSR FADD instruction (Line 490).
We really don’t want the result to stay in FR0. I defined a place for it in Line 160, where FPANS is the first byte of a 6-byte block that can hold any FP number. To execute the transfer, load the 6502’s X-register with the low byte of the destination address and load the Y-register with the high byte. Lines 500 and 510 accomplish this. Finally, a call to routine FST0R moves all 6 bytes at once (Line 520), and we’re done.
Enter Listing 1, assemble it, enter the debugger, and run the program with a G5000 command. To examine the results, display memory from $0600060F. With the Atari Assembler-Editor cartridge, a D600,60F command puts the following display on the screen:
0600 74 01 91 00 08 80 00 00 0608 41 05 17 00 08 88 00 00
Bytes $0600–$0601 contain the hexadecimal value of NUM1 in low-byte, high-byte form. That is, decimal 372 equals hex $0174. Similarly, decimal 145 (hex $0091) is stored in bytes $0602–$0603. As it happens, 372 + 145 = 517.
The floating point representation of decimal 517 is located in bytes $0608–$060D. Can you predict the contents of these bytes from the discussion of FP storage format above?
Example 2.—Loan payment calculations.
Once upon a time, I wrote a BASIC program to calculate the monthly payment required to amortize a loan with a given initial principal, annual percentage rate of interest and term in months. It then occurred to me that the steps involved in this calculation provided an excellent illustration of many of the floating point routines listed in Table 1.
The BASIC statements you would use to calculate the monthly payment for a loan are:
10 IAPR=APR/1200 20 PAV=(PRIN*IAPR)/(1-(IAPR+1)**(-TERM))
where APR is the annual percentage rate of interest (for eleven percent, enter 11, not 0.11), PRIN is the principal, TERM is the duration of the loan in months, and PAY is the monthly payment.
Listing 2 performs these computations in assembly language. It will prompt you to enter the initial principal of the loan, the APR, and the term. Make all entries as just numeric digits with a decimal point, if needed. The monthly payment is calculated and displayed in dollars and cents (two decimal places). Then, you can press RETURN to try another calculation. To stop execution of the program, press the BREAK key several times in rapid succession, or press RESET.
The equates list in Listing 2 contains: entries for CIO command operations (Lines 130–140); some constants used by the program (Lines 150–170); some addresses needed by the FP routines (Lines 180–290); four 6-byte storage locations for the FP representations of numbers used in the calculations (Lines 300–330); CIO addresses for screen I/O (Lines 370–420); and entry points for the FP routines used by the program (Lines 460–590).
The biggest nuisance about assembly language programming is that you have to keep track of everything yourself. In BASIC, we could just create variables like PRIN, IAPR, DENOM and PAY out of thin air and think about them no further.
When doing FP calculations in assembly, we need to consciously set aside a 6-byte block of RAM for every number we wish to store. I’ve put mine in the ever-popular page 6, but you can put them in any safe place you like.
BASIC has another talent we take for granted: the ability to decipher complex mathematical expressions and calculate the result. Since our available FP routines only handle simple operations like division, we must first dissect a complex equation into a series of elementary steps.
It helps your plarming to remember that the result of an operation winds up in register FR0, where it’s ready for the next step immediately. I kept this in mind as I broke apart the equations needed to calculate a monthly payment. Now, let’s walk through Listing 2 and see how it works.
Lines 660–710 set up to print a prompt message for the user to enter the initial principal. The subroutine called WRITE is found in Lines 2150–2240. It completes the CIO PUTREC operation and displays the line on-screen.
Line 720 calls subroutine INPUT, which resides in Lines 2460–2630. It uses the CIO GETREC command to read a string of ATASCII characters from the keyboard and place them into a buffer called LBUFF.
Line 730, since we can’t do arithmetic with these characters, calls yet another subroutine, MAKEFF (Lines 2650–2800). MAKEFP converts the characters in LBUFF into a floating point number in FR0, using the AFP function in the FP package.
If an error occurs during the conversion to FP, the carry flag in the 6502 status register is set. This would happen if, for example, you had a letter as the first character in LBUFF. (If the first character is numeric, then the AFP routine works until it encounters a character that cannot be part of an FP number, then it quits.) Hence, the BCS instruction in Line 2790 transfers control to the routine labelled AFPERR (Lines 2840–2920) to tell the user about the problem, accept another input line and try again.
Now that we have the loan principal stored in FP form in FR0, we need to stash it safely in the 6-byte storage location labelled PRIN. Lines 740–760 do the trick, using routine FST0R to execute the move (really, it’s a copy; FR0 does not change).
Lines 770–940 are equivalent to the simple BASIC expression shown in statement 10 a few paragraphs back. First, load the decimal integer 1200 into the first 2 bytes of FR0, then call IFP to transform it to FP and, finally, call FMOVE to move the result into FR1.
Next, input the value for APR just as we did for PRIN (Lines 840–890). We now have FR0 = APR and FR1 = 1200, so a call to FDIV (Line 900) will give us the result we seek in FR0. Lines 920–940 store this result in IAPR.
Some of the FP subroutines set the carry flag if a mathematical error occurs, such as division by zero.
Line 910 illustrates the trap I set after many of the numerical FP operations for such errors. Because of the length of this program, I had to use a two-stage branch to get to the actual error-handling routine. The first branch is always to MTHERR (Line 1320), which forwards control on to routine BADMTH at Line 1730. An error message is printed, and you have to start the entire calculation over again.
I won’t explain every single statement in Listing 2. By now, you see what I’m up to: input the necessary data and store in FP form; break the complex expression into bite-sized (no pun intended) pieces that can be processed by the available FP routines; transfer numbers among FR0, FR1 and memory as necessary with the utility subroutines FMOVE, FST0R, FLD0R and FLD1R, until the entire calculation is complete.
There are a couple of tricks I should explain. First, the denominator of the equation shown in BASIC statement 20 above is fairly complex. I decided to store the result of this calculation in an intermediate FP variable called DENOM, rather than trying to keep track of everything in FR0 and FR1.
Second, there’s no FP routine to directly perform the (IAPR+1)**(-TERM) operation, an exponentiation. I used a mathematical trick. We take the logarithm of (IAPR + 1), multiply it times (-TERM) and take the antilogarithm of the result, using the EXP10 routine. Most of the calculations you’re likely to do won’t require getting this creative.
By the time we reach Line 1570, the monthly payment has been calculated and stored in PAY. To show it on the screen, we need to do a little more work. Line 1620 calls subroutine OUTPUT, which wraps it all up in Lines 1820–2240.
OUTPUT first converts the FP number in FR0 (the payment) into an ATASCII string in LBUFF, using routine FASC. But LBUFF is 128 characters long, and we only want to show the characters up to the decimal point, plus two to the right of the decimal. The final interesting character in LBUFF from the FASC conversion has the most significant bit (bit 7) set, so at least we can deduce where the resulting string terminates.
Subroutine ADDEOL (Lines 2260–2440) scans through the characters in LBUFF until it finds one that’s negative (bit 7 set). This is our final byte. The routine then clears bit 7 of that final byte, advances 1 more byte, and adds an ATASCII EOL (carriage return) character, so that printing with the CIO PUTREC command will stop at that point.
Going back to Line 1890, the next few lines scan through LBUFF until a decimal point is located. Then it skips over the next 2 bytes, since we want to keep two places past the decimal (the cents part of the monthly payment). Any remaining characters in LBUFF up to the EOL character are set to ATASCII blanks (decimal 32, hex $20), so they aren’t printed. Finally, the answer is displayed on-screen in Lines 2100–2240.
This has been a pretty heavy session, but now you know a lot about a part of the Atari OS you may never have heard of before. Next time, we’ll get back to something a little less somber—the famous Atari graphics.
Listing 1. Assembly listing. 10 ;Listing 1 for floating point 20 ;operations in assembly language 30 ; 40 ;convert two integers to FP 50 ;and add then together 60 ; 70 .OPT NO LIST 80 ; 90 ; 0180 NUM1 = 372 ;add these two 0110 NUM2 = 145 ;numbers together 0120 FR0 = $04 ;2 floating point 0130 FR1 = $E0 ;work registers 0140 INT1 = $0600 ;store one int. 0150 INT2 = $0602 ;and the other 0160 FPANS = $0608 ;store FP answer 0170 IFP = $D9AA ;integer to FP 0180 FADD = $DA66 ;add 2 FP nunbers 0190 FST0R = $DDA7 ;move FR0 0280 FM8VE = $DDB6 ;move FR0 to FR1 0210 ZFR0 = $DA44 ;zero FR0 0220 ; 0230 ;set starting address 0240 ; 0250 *= $5000 0260 ; 0270 CLD ;binary mode! 0280 LDA #NUM1&255 ;put the two 0290 STA INT1 ;nums we want to 0280 LDA #NUM1/256 ;add into their 0310 STA INT1+1 ;integer storage 0320 LDA #NUM2&255 ;locations 0330 STA INT2 0340 LDA #NUM2/256 0350 STA INT2+1 0360 JSR ZFR0 ;set FR0=0 0370 LDA INT1 ;move 1st number 0380 STA FR0 ;into first two 0390 LDA INT1+1 ;bytes Of FR0 0480 STA FR0+1 0410 JSR IFP ;convert to FP 0420 JSR FMOVE ;move it to FR1 0430 JSR ZFR0 ;set FR0=0 0440 LDA INT2 ;Move second 0450 STA FR0 ;number into FR0 0460 LDA INT2+1 0470 STA FR0+1 0480 JSR IFP ;convert to FP 0490 JSR FADD ;+ to one in FR1 0500 LDX #FPANS&255 ;store result, 0510 LDY #FPANS/256 ;FR0 0520 JSR FST0R ;in FPANS 0530 BRK ;all done
Listing 2. Assembly listing. 10 ;Listing 2 for floating point 20 ;operations in assembly language 30 ; 40 ;This program allows you to input 50 ;an initial principal, annual 60 ;percentage rate, and term in 70 ;months for a loan. It then 80 ;calculates the monthly payment 90 ;needed to amortize the loan. 0100 ; 0110 .OPT NO LIST 0120 ; 0130 GETREC = $05 ;equates for CIO 0140 PUTREC = $09 ;operations 0150 BLANK = $20 ;ATASCII blank 0160 DECIMAL = $2E ;ATASCII period 0170 EOL = $9B ;carriage return 0180 LENGTH = $CB ;temp. variable 0190 FR0 = $D4 ;the two FP work 0200 FR1 = $E0 ;registers 0210 CIX = $F2 ;offset pntr to 0220 ; character of LBUFF 0230 ; for AFP routine 0240 INBUFF = $F3 ;2-byte pntr to 0250 ; text buffer used by 0260 ; FASC and AFP subs. 0270 LBUFF = $0580 ;128-byte buffer 0280 ; for FASC (output) 0290 ; and AFP (input) 0380 PRIN = $0600 ;RAM loc. for 0310 IAPR = $0606 ;FP variables, 0320 DENOM = $060C ;six bytes each 0330 PAY = $0612 0340 ; 0350 ;CIO addresses for IOCB #0 0360 ; 0370 ICCOM = $0342 ;command byte 0380 ICBAL = $8344 ;buffer address, 0390 ICBAH = $8345 ;lo and hi bytes 0480 ICBLL = $8340 ;buffer length, 0410 ICBLH = $8349 ;lo and hi bytes 0420 CIOV = $E456 ;CIO entry point 0430 ; 0440 ;equates for FP routines 0450 ; 0460 AFP = $D800 ;ATASCII to FP 0470 FASC = $D8E6 ;FP to ATASCII 0480 IFP = $D9AA ;integer to FP 0490 FSUB = $DA60 ;subtraction 0500 FADD = $DA66 ;addition 0510 FMUL = $DADB ;multiplication 0520 FDIV = $DB28 ;division 0530 FLD0R = $DD89 ;load FR0 0540 FLD1R = $DD98 ;load FR1 0550 FST0R = $DDA7 ;store FR0 0560 FMOVE = $DDB6 ;move FR0 to FR1 0570 EXP10 = $DDCC ;FR0 = 10**FR0 0580 L0G10 = $DED1 ;common log, FR0 0590 ZFR0 = $DA44 ;zero FR0 0600 ; 0610 ;Set starting address 0620 ; 0630 *= $5000 0640 ; 0650 CLD ;binary mode! 0660 START 0670 LDX #0 ;use IOCB #0 0680 LDA #LINE1&255 ;prompt 0690 STA ICBAL,X ;using WRITE sub 0700 LDA #LINE1/256 ;for CIO 0710 JSR WRITE ;output 0720 JSR INPUT ;input PRIN, 0730 JSR MAKEFP ;convert to FP 0740 LDX #PRIN&255 ;lo byte in X 0750 LDY #PRIN/256 ;hi byte in Y 0760 JSR FST0R ;store FR0 in RAM 0770 LDA #1200&255 ;store decimal 0780 STA FR0 ;integer 1200 0790 LDA #1200/256 ;in FR0 0800 STA FR0+1 ;and FR0+1 0810 JSR IFP ;convert to FP 0820 JSR FMOVE ;transfer to FR1 0830 LDX #0 ;use IOCB #0 0840 LDA #LINE2&255 ;prompt, APR 0850 STA ICBAL,X 0860 LDA #LINE2/256 0870 JSR WRITE 0880 JSR INPUT ;input APR into 0890 JSR MAKEFP ;FR0 0900 JSR FDIU ;FR0=APR/1200 0910 BCS MTHERR ;math error? 0920 LDX HIAPR&255 ;move result to 0930 LDY #IAPR/256 ;IAPR(6-byte FP 0940 JSR FST0R ;storage in RAM) 0950 JSR FMOVE ;transfer to FR1 0960 JSR ZFR0 ;set FR0=0 0970 LDA #1 ;store 1 in FR0 0980 STA FR0 0990 JSR IFP ;convert to FP 1000 JSR FADD ;FR0=IAPR+1 1010 BCS MTHERR ;math error? 1020 JSR L0G10 ;FR0=LOG(IAPR+1) 1030 BCS MTHERR ;math error? 1040 JSR FMOVE ;transfer to FR1 1050 LDX #0 ;use IOCB no 1060 LDA #LINE3&255 ;prompt, TERM 1070 STA ICBAL,X 1080 LDA #LINE3/256 1090 JSR WRITE 1100 JSR INPUT ;input TERM 1110 JSR MAKEFP ;convert to FP 1120 JSR FMUL ;FR0=TERM* 1130 ; LOG (IAPR+1) 1140 BCS MTHERR ;math error? 1150 JSR FMOVE ;transfer to FR1 1160 JSR ZFR0 ;set FR0=0 1170 JSR FSUB ;FR0=-TERM* 1180 ; LOG(IAPR+1) 1190 BCS MTHERR ;math error? 1200 JSR EXP10 ;FR0=(IAPR+1)** 1210 ; (-TERM) 1220 BCS MTHERR ;math error? 1230 JSR FMOVE ;transfer to FR1 1240 JSR ZFR0 ;set FR0=0 1250 LDA #1 ;store 1 in FR0 1260 STA FR0 1270 JSR IFP ;convert to FP 1280 JSR FSUB ;FR0=1-(IAPR+1)** 1290 ; (-TERM) 1300 BCS MTHERR ;math error? 1310 BCC GOON ;no, keep going 1320 MTHERR 1330 BCS BADMTH ;2-step branch 1340 GOON 1350 LDX UDEN0M&255 ;move result 1360 LDY #DENOM/256 ;to DENOM for 1370 JSR FST0R ;moment or two 1380 JSR ZFR0 ;set FR0=0 1390 LDX #IAPR&255 ;load IAPR into 1400 LDY #IAPR/256 ;FR0 using X- 1410 JSR FLD0R ;and Y-registers 1420 LDX #PRIN&255 ;load PRIN into 1430 LDY #PRIN/256 ;FR1 using X- 1440 JSR FLD1R ;and Y-registers 1450 JSR FMUL ;FR0=IAPR*PRIN 1460 BCS MTHERR ;math error? 1470 LDX #DENOM&255 ;DENOM into 1480 LDY #DENOM/256 ;FR1 as usual 1490 JSR FLD1R ;FR1=DENOM 1500 JSR FDIV ;FR0=IAPR*PRIN/ 1510 ; (1-(IAPR+1)** 1520 ; (-TERM)) 1530 BCS MTHERR ;math error? 1540 LDX #PAY&255 ;store answer in 1550 LDY #PAY/256 ;in PAY 1560 JSR FST0R 1570 LDX #0 ;use IOCB no 1580 LDA #LINE4&255 ;"MONTHLY 1590 STA ICBAL,X ;"PAYMENT IS:" 1600 LDA HLINE4/256 1610 JSR WRITE 1620 JSR OUTPUT ;show payment 1630 RESTRT 1640 LDA #LINE5&255 ;show prompt 1650 STA ICBAL,X ;to go on 1660 LDA #LINE5/256 1670 JSR WRITE 1680 JSR INPUT ;get any input 1690 JMP START ;do it all again 1700 ; 1710 ;routine to handle FP Math errors 1720 ; 1730 BADMTH 1740 LDX #0 ;use IOCB #0 1750 LDA #ERMSG1&255 ;show Message 1760 STA ICBAL,X ;on screen 1770 LDA #ERMSG1/256 1780 JSR WRITE 1790 CLC 1800 BCC RESTRT ;start all over 1810 ; 1820 ;subroutine to output the answer 1830 ;first convert FP number in FR0 1840 ;to ATASCII string in LBUFF 1850 ; 1860 OUTPUT 1870 JSR FASC 1880 JSR ADDEOL ;add EOL to end 1890 STY LENGTH ;Y=bytes in LBUFF 1900 LDY #255 ;scan LBUFF to 1910 ; find a decimal 1920 LOOP1 1930 CPY LENGTH ;at end of LBUFF? 1940 BEQ SHOWIT ;yes,print answer 1950 INY ;no,keep going 1960 LDA (INBUFF),Y ;get next byte 1970 CMP #DECIMAL ;decimal point? 1999 BNE LOOP1 ;no, go on 1999 INY ;yes, skip next 2 2909 INY ;bytes to keep 2 2919 ; decimal places 2929 LOOP2 2030 INY ;keep going 2040 CPY #LENGTH ;at end Of LBUFF? 2050 BEQ SHOHIT ;yes,print answer 2060 LDA #BLANK ;no, replace byte 2070 STA (INBUFF),Y ;in LBUFF With 2080 ; a blank 2090 BNE LOOP2 ;go on 2100 SHOHIT 2110 LDX #0 ;use IOCB #0 2120 LDA INBUFF ;tell CIO where 2130 STA ICBAL,X ;to find the 2140 LDA INBUFF+1 ;answer string 2150 WRITE 2160 STA ICBAH,X ;output routine 2170 LDA #PUTREC ;using CIO 2180 STA ICCOM,X ;PUTREC function 2190 LDA #40 ;max output line 2200 STA ICBLL,X ;length = 40 2210 LDA #0 2220 STA ICBLH,X 2230 JSR CIOU ;go do it 2240 RTS 2250 ; 2260 ;subroutine to find last 2270 ;character in LBUFF (has bit 7 2280 ;set), clear bit 7, and add a 2290 ;carriage return to the 2300 ;following byte so we can print 2310 ;the line using CIO 2320 ; 2330 ADDEOL 2340 LDY #255 ;start at byte 0 2350 LOOP3 2360 INY ;point to next 2370 LDA (INBUFF),Y ;byte, get it 2380 BPL LOOP3 ;if >0, go on 2390 AND #127 ;if <0, keep the 2400 STA (INBUFF),Y ;lowest 7 bits 2410 INY ;go one more byte 2420 LDA #EOL ;put a carriage 2430 STA (INBUFF),Y ;return there 2440 RTS ;all done 2450 ; 2460 ;subroutine to input ATASCII 2470 ;string into LBUFF and convert 2480 ;it to floating point in FR0 2490 ; 2500 INPUT 2510 LDX #0 ;use IOCB no 2520 LDA #GETREC ;input a record 2530 STA ICCOM,X 2540 LDA #LBUFF&255 ;rec will go 2550 STA ICBAL,X ;into buffer at 2560 LDA #LBUFF/256 ;address LBUFF 2570 STA ICBAH,X 2580 LDA #40 ;max length of 2590 STA ICBLL,X ;input buffer is 2600 LDA #0 ;40 bytes 2610 STA ICBLH,X 2620 JSR CIOV ;go do it 2630 RTS ;all done 2640 ; 2650 ;subroutine to convert data in 2660 ;input buffer LBUFF to FP number 2670 ;in FR0 2680 ; 2690 MAKEFP 2700 LDA #LBUFF&255 ;load addr Of 2710 STA INBUFF ;LBUFF into 2720 LDA #LBUFF/256 ;INBUFF (10) 2730 STA INBUFF+1 ;and INBUFF+1 2740 LDA #0 ;offset into this 2750 STA CIX ;buffer is zero 2760 JSR AFP ;convert number 2770 ; in LBUFF to FP 2780 ; number in FR0 2790 BCS AFPERR ;conversion err? 2800 RTS ;all done 2810 ; 2820 ;AFP conversion error 2830 ; 2840 AFPERR 2850 LDX #0 ;use IOCB #0 2860 LDA #ERMSG2&255 ;show message 2870 STA ICBAL,X ;on screen 2880 LDA #ERMSG2/256 2890 JSR WRITE 2900 JSR INPUT ;get another 2910 CLC ;input record 2920 BCC MAKEFP ;and try again 2930 ; 2940 ;Text lines to be printed 2950 ; 2960 LINE1 2970 .BYTE 125,"Enter the " 2975 .BYTE "initial " 2980 .BYTE "principal:",EOL 2990 LINE2 3000 .BYTE "Enter the APR:",EOL 3010 LINE3 3020 .BYTE "Enter the term:",EOL 3030 LINE4 3040 .BYTE "Monthly Payment ",EOL 3045 .BYTE "is:",EOL 3050 LINE5 3060 .BYTE "Press " 3070 .BYTE 210,197,212,213,210,206 3080 .BYTE " to go on",EOL 3090 ERMSG1 3100 .BYTE "A MATHEMATICAL ERROR " 3110 .BYTE "HAS OCCURRED.",EOL 3120 ERMSG2 3130 .BYTE "CAN'T CONVERT TO FP; " 3140 .BYTE "PLEASE TRY AGAIN",EOL
A.N.A.L.O.G. ISSUE 44 / JULY 1986 / PAGE 101
I must be out of my mind. I spent all of last month’s column talking about number crunching, when we all know home computers are really bought for their graphics (good thing, in the case of the Atari). Let’s get back to graphics programming in assembly lanuage.
The Boot Camp columns in issues 41 and 42 laid a foundation for us, explaining how to place text and graphics displays on-screen. In the next few months, I’ll open the doors to more sophisticated graphics techniques. We’ll do this in the context of a game-type project. This time, we’ll build the title screen for “Attack of the Suicidal Road-Racing Aliens.” Next month, we’ll spice up that title screen with some display list interrupts.
In issue 41’s Boot Camp, you learned how to open the graphics screen (device S:) to get a particular graphics mode. You can make much more interesting screens by combining several different modes, to form a “mixed-mode display.”
To refresh your memory. Table 1 summarizes the available graphics modes. Note that each has both a BASIC mode number and an ANTIC mode number (except ANTIC mode 3). The table shows the number of horizontal TV “scan lines” that make up a single “mode line” in each mode. There are 192 scan lines in a standard Atari display.
*GTIA modes also use ANTIC mode 15.
ATARI GRAPHICS MODES
ANTIC Mode BASIC Mode Scan Lines/Mode Line Mode Lines/Screen Bytes/Mode Line 2 0 8 24 40 3 NONE 10 about 19 40 4 12 (XL) 8 24 40 5 13 (XL) 16 12 40 6 1 8 24 20 7 2 16 12 20 8 3 8 24 10 9 4 4 48 10 10 5 4 48 20 11 6 2 96 20 12 14 (XL) 1 192 20 13 7 2 96 40 14 15 (XL) 1 192 40 15* 8 1 192 40
You probably know ANTIC is a chip in the Atari that controls the screen display. ANTIC has its own tiny programming language, and a program for ANTIC is called a “display list.” The display list tells ANTIC which graphics mode each on-screen line uses and where to find the section(s) of RAM allocated to the screen display.
When you open the screen device S: using CIO, the operating system sets aside a block of RAM for the screen display and creates an appropriate display list, based on the graphics mode requested. By default, the screen RAM is located at the very top of available memory, and the display list is stored immediately beneath screen RAM.
To use mixed-mode displays, we need to create our own customized display list, decide where we want screen RAM to be located, then set some pointers to tell ANTIC what we’re up to. Other display list modifications are needed to use display list interrupts and fine scrolling—topics for upcoming columns.
To create a mixed-mode display, first sketch out your screen. It will contain several horizontal bands or “segments,” each having a different graphics mode from the ones above and below it. Next, decide how many mode lines of the appropriate graphics mode each segment will contain. Remember, you want the total number of scan lines to be about 192 (you can use a few more without causing major problems). The next thing we must decide is where the display list and screen RAM will reside. If you aren’t making drastic changes from the default display list, you can leave this up to the computer. I prefer to control the situation myself.
There are a couple of rules about memory allocation. First, the display list can’t cross a 1K boundary (multiple of $400). Since the list will never be over 256 bytes long, I always start it at a page boundary, like $3F00.
Second, the screen RAM display can’t cross a 4K boundary (multiple of $1000), at least not without using a trick. Of course, in modes like ANTIC 15, where 8K of screen RAM is needed, a 4K boundary is sure to be encountered. I always start screen RAM at a 4K boundary (like $4000), to minimize these problems.
There are four sections to the display list:
Enough preliminaries. Figure 1 is a sketch of the title screen for our alien game. There are four segments: two lines of ANTIC 6; six lines of ANTIC 7; twenty-four hues of ANTIC 13; and four lines of ANTIC 2. (Keep Table 1 handy, since we need both the ANTIC and the BASIC graphics mode numbers at various times). The program to create this enticing display is found in Listing 1.
Figure 1. ATTACK OF THE SUICIDAL ROAD-RACING ALIENS .... (space ship) *** Press START to Begin *** Analog Productions
As usual, Listing 1 begins with equates. These should be familiar to you from our earlier graphic excursions. The new locations of interest are in Lines 240–270.
DINDEX contains the BASIC mode number of the segment in which we’re currently printing or plotting. SAVMSC is the 2-byte address for the beginning of screen RAM. SDMCTL is a byte that lets you turn the screen off (store 0) or on (store decimal 34). It’s also used in controlling player/missile graphics (another future topic). SDLSTL is the 2-byte address for the display list itself.
A few other locations we need appear in Lines 420–440. CONSOL tells us which of the console buttons (START, OPTION, SELECT) are being pressed. STRGNO is my own variable, for storing the number of the text string we printed most recently. And SCRRAM is the beginning of screen RAM, which I’ve set at $4000.
Our display list is described in Lines 490–560. Can you find the four sections of the display list mentioned above? The long list of 13s is for the 24 lines in segment 3. The display list for a graphics 8 screen has 191 15s in a row (plus the LMS instruction I promised not to mention)!
The program itself begins at address $5000 in Line 600. I want to digress here and describe how I lay out my assembly programs.
First, of course, come the equates. After that, I put things appearing in memory below the main program—in this case, the display list. The third section is the program code itself, followed by all the subroutines called.
I might place small, separate routines (like display list interrupts and vertical blank interrupts) after the subroutines. Finally, text strings to be printed and data tables appear at the very end of the program. This may not be the best way to organize things, but it works for me.
One problem with choosing your own screen RAM region is that you don’t know what’s initially stored there. So let’s begin by zeroing the screen RAM area. But wait! We know where it starts, but where does it stop? Multiply the number of mode lines (of each kind) times the bytes needed for each such line, and add them up to find out how much memory our custom display consumes:
2*20 + 6*20 + 24*40 + 4*40 = 1280
This is exactly five pages (5*256 bytes) of RAM, so the routine in Lines 690–760 zeroes five pages of RAM.
Now we can open the screen device. I’ve written the code for that (and some other common operations) as subroutines which you can include in your own programs. This one is called OPENSCREEN. It’s called in Line 800, and the subroutine itself resides in Lines 2220–2350. Issue 38’s Boot Camp talks about using subroutines in assembly language.
I’m not going to explain all the screen I/O procedures in Listing 1. Please refer to issues 41 and 42 for details.
OPENSCREEN sets up a default display list and screen RAM area. Lines 2300–2330 show that I opened the screen in graphics mode (ANTIC 2) without a text window. It really doesn’t matter much which mode you use to open the screen, when you’re handling the display list and screen RAM yourself. This display will actually look as if it has a text window, because of the four lines of blue ANTIC 2 at the bottom of the screen—but that’s a coincidence.
Our first task is to override the default display list with our own. Lines 830–860 tell ANTIC to use our own screen RAM area, not the one it just selected. I like to turn the screen off momentarily when switching display lists, just to avoid unsightly flashes (Lines 870–880).
Lines 890–920 store the address of our display list in SDLSTL, so ANTIC forgets all about the one it just created. Now we can turn the screen back on (Lines 930–940).
Now, to write on the screen. The idea is to think of each segment as a separate little screen. We need to tell ANTIC the graphics mode of the current segment and where it begins in memory. Then we can print and plot in it as usual.
To move on to the next segment, add the number of bytes occupied by the current segment to the value stored in SAVMSC. This makes ANTIC think the screen RAM starts at the first byte of the next segment.
This trick is required because mode lines in different graphics modes demand different amounts of memory. It does make it awkward to write into previous segments, since you must first subtract the right number of bytes from SAVMSC to back up in memory.
Here’s how it works, line by line:
Lines 980–990: The first segment is in graphics 1 (ANTIC 6).
Line 1000: Calls the subroutine to position the cursor for the first text string. This is where the STRGNO equate is used. The data tables in Lines 3150–3160 contain the coordinates for all text strings to be printed. Notice that these coordinates are all relative to the upper left corner of the segment, not of the entire screen.
Lines 1010–1060: Print the text string, using the subroutine PRINTLINE at Lines 2600–2680.
Lines 1070–1080: Add 40 (two mode lines of graphics 1 at 20 bytes per line) to the value in SAVMSC, using another subroutine (ADDMEM, Lines 2740–2800).
Lines 1090–1170: Now we’re in segment 2, six lines of graphics 2 (ANTIC 7) . Notice that the text string in Lines 3220–3280 covers several lines on the screen. This is a nice way to reduce program code, at the expense of a few bytes of memory in the blank text strings. Make sure each text line is the right length (in this case, twenty characters).
Lines 1180–1210: Now, in the third segment, we have graphics 7 (ANTIC 13).
Lines 1250–1340: Plot one point in the stylized rocket ship at coordinates 60,8 within the segment, using color register 1. The PLOTPOINT subroutine at Lines 2850–2930 will be used again later.
Lines 1380–1530: Draw the rest of the rocket, using color register 1. The loop plots thirteen points, whose coordinates are pulled from tables called XDATA and YDATA (Lines 3390–3440). If you trace this out, you’ll see that some line segments are drawn twice. This is a tiny sacrifice in execution time, to gain the valuable simplification in program code afforded by using this loop with data tables and the DRAWLINE subroutine (Lines 2980–3030).
Lines 1570–1750: Use a similar loop and data tables to plot four points for the rocket exhaust, selecting color register 0.
Lines 1800–1850: Segment 3 requires 960 bytes. I couldn’t add this to SAVMSC in one step, so I built a little loop to do it.
Lines 1890–2040: Complete the display by printing two lines in the fourth segment, a graphics (ANTIC 2) block. It looks like a text window, but really isn’t. (Incidentally, by using a custom display list you can put a text window anywhere you want on the screen.)
Okay, if you assemble this beast and run it at address $5000, you should see our title screen. This screen prompts you to press START, to begin the (nonexistent) game. This is where CONSOL comes in.
CONSOL will contain values from 0 to 7, depending on which combination of the START, SELECT and OPTION keys have been pressed. Table 2 gives the lowdown. It’s a good practice to clear this register by loading an 8 into it before checking for a button press, as we do in Lines 2090–2100.
Table 2 reveals that pressing the START button alone sets CONSOL to a value of 6. The loop in Lines 2110–2140 just checks endlessly for this situation. If it happens, the graphics screen is closed, then re-opened under default conditions, just to clear it. In real life, you’d probably have another custom display list set up to use for the next screen of the game. Press RESET when you’re tired of looking at the blue screen.
Value Buttons Pressed 0 OPTION, SELECT, START 1 OPTION, SELECT 2 OPTION, START 3 OPTION 4 SELECT, START 5 SELECT 6 START 7 none
Notice the subroutine called CLOSEANY (Lines 2390–2430). If you load the right multiple of hex $10 into the X-register before calling this subroutine, it will close the corresponding IOCB. See Lines 2150–2160 for an example using IOCB #6.
This wraps up our session. You can apply these ideas to create mixed-mode displays of limitless complexity, just by building the right display list and setting aside enough memory for the screen requirements. Next month, we’ll see how to spice up this screen, with lots of colors and perhaps some character set changes, using the powerful and simple display list interrupts.
0100 ;Mixed-Mode graphics displays 0110 ;in Atari assembly language 0120 ; 0130 ;by Karl E. Wiegers 0140 ; 0150 .OPT NO LIST 0160 ; 0170 OPEN = $03 ;equates for CIO 0180 PUTREC = $09 ;operations 0190 CLOSE = $0C 0200 DRAW = $11 0210 EOL = $9B ;carriage return 0220 ROWCRS = $54 ;cursor row 0230 COLCRS = $55 ;cursor column 0240 DINDEX = $57 ;graphics mode 0250 SAVMSC = $58 ;screen RAM area 0260 SDMCTL = $022F ;screen on/off 0270 SDLSTL = $0230 ;starting address 0280 ; of display list 0290 CRSINH = $02F0 ;disable cursor 0300 ATACHR = $02FB ;select color reg 0310 ; 0320 ;equates for IOCB #0 0330 ; 0340 ICCOM = $0342 ;command byte 0350 ICBAL = $0344 ;buffer address, 0360 ICBAH = $0345 ;low and high 0370 ICBLL = $0348 ;buffer length, 0380 ICBLH = $0349 ;low and high 0390 ICAX1 = $034A ;auxiliary byte 1 0400 ICAX2 = $034B ;auxiliary byte 2 0410 CIOV = $E456 ;CIO entry point 0420 CONSOL = $D01F ;console buttons 0430 STRGNO = $CB ;work byte I need 0440 SCRRAM = $4000 ;screen RAM start 0450 ; 0460 ;Display list starts at address 0470 ;$3F00, screen RAM at $4000 0480 ; 0490 *= $3F00 0500 DLIST 0510 .BYTE 112,112,112,70,0,$40 0520 .BYTE 6,7,7,7,7,7,7,13,13 0530 .BYTE 13,13,13,13,13,13,13 0540 .BYTE 13,13,13,13,13,13,13 0550 .BYTE 13,13,13,13,13,13,13 0560 .BYTE 13,2,2,2,2,65,0,$3F 0570 ; 0580 ;program begins here 0590 ; 0600 *= $5000 0610 ; 0620 CLD ;binary mode! 0630 LDA #0 ;zero current 0640 STA STRGNO ;text line counter 0650 TAX 0660 ; 0670 ;zero out screen RAM area 0680 ; 0690 ZERO 0700 STA SCRRAM,X ;loop goes 0710 STA SCRRAM+$0100,X ;256 times 0720 STA SCRRAM+$0200,X ;each line 0730 STA SCRRAM+$0300,X ;does one 0740 STA SCRRAM+$0400,X ;page of RAM 0750 INX 0760 BNE ZERO 0770 ; 0780 ;open screen device, "S:" 0790 ; 0800 JSR OPENSCREEN 0810 LDA #1 ;turn off cursor 0820 STA CRSINH 0830 LDA DLIST+4 ;tell ANTIC 0840 STA SAVMSC ;where to find 0850 LDA DLIST+5 ;display memory 0860 STA SAVMSC+1 0870 LDA #0 ;turn off the 0880 STA SDMCTL ;screen briefly, 0890 LDA #DLIST&255 ;tell ANTIC 0900 STA SDLSTL ;where to 0910 LDA #DLIST/256 ;find the 0920 STA SDLSTL+1 ;display list, 0930 LDA #34 ;turn screen 0940 STA SDMCTL ;back on 0950 ; 0960 ;start printing text lines 0970 ; 0980 LDA #1 ;graphics mode 1 0990 STA DINDEX 1000 JSR POSITION ;position cursor 1010 LDX #$60 ;use IOCB #6 1020 LDA #LINE1&255 ;print first 1030 STA ICBAL,X ;line of text, 1040 LDA #LINE1/256 ;in Graphics 1 1050 STA ICBAH,X ;segment 1060 JSR PRINTLINE 1070 LDA #40 ;skip ahead 40 1080 JSR ADDMEM ;bytes in RAM 1090 LDA #2 ;Graphics mode 2 1100 STA DINDEX 1110 JSR POSITION ;position cursor 1120 LDX #$60 ;use IOCB #6 1130 LDA #LINE2&255 ;print all 1140 STA ICBAL,X ;text lines 1150 LDA #LINE2/256 ;in Graphics 2 1160 STA ICBAH,X ;segment 1170 JSR PRINTLINE 1180 LDA #120 ;go up 120 bytes 1190 JSR ADDMEM ;in screen RAM to 1200 LDA #7 ;Graphics 7 1210 STA DINDEX ;segment 1220 ; 1230 ;plot 1st point of rocket ship 1240 ; 1250 LDA #60 ;set coordinates 1260 STA COLCRS ;of first point 1270 LDA #8 ;to plot for 1280 STA ROWCRS ;rocket 1290 LDX #$60 ;use IOCB #6 1300 LDA #REG1&255 ;color register 1 1310 STA ICBAL,X 1320 LDA #REG1/256 1330 STA ICBAH,X 1340 JSR PLOTPOINT 1350 ; 1360 ;routine to draw the rocket 1370 ; 1380 LDA #2 ;color register 1 1390 STA ATACHR 1400 LDY #0 1410 POINT ;loop to plot 1420 LDA XDATA,Y ;points and 1430 STA COLCRS ;connect them 1440 LDA YDATA,Y ;with lines 1450 STA ROWCRS 1460 TYA 1470 PHA 1480 JSR DRAWLINE ;drawing sub. 1490 PLA 1500 TAY 1510 INY 1520 CPY #13 ;done all 13 pts? 1530 BNE POINT ;no, loop 1540 ; 1550 ;plot points for rocket exhaust 1560 ; 1570 LDX #$60 1580 LDA #REG0&255 ;color reg. 0 1590 STA ICBAL,X 1600 LDA #REG0/256 1610 STA ICBAH,X 1620 LDY #0 1630 POINT2 ;loop to get 1640 LDA EXHAUSTX,Y ;coordinates 1650 STA COLCRS ;for points 1660 LDA EXHAUSTY,Y ;from table, 1670 STA ROWCRS 1680 TYA 1690 PHA 1700 JSR PLOTPOINT ;plotting sub. 1710 PLA 1720 TAY 1730 INY 1740 CPY #4 ;done 4 pts? 1750 BNE POINT2 ;no, loop 1760 ; 1770 ;add 960 bytes to current screen 1780 ;RAM starting point, 10*96 1790 ; 1800 LDX #10 1810 ADDEMUP 1820 LDA #96 1830 JSR ADDMEM 1840 DEX 1850 BNE ADDEMUP 1860 ; 1870 ;now in bottom segment, Gr. 0 1880 ; 1890 LDA #0 ;Graphics 0 1900 STA DINDEX 1910 JSR POSITION ;set cursor 1920 LDX #$60 ;use IOCB #6 1930 LDA #LINE3&255 ;print first 1940 STA ICBAL,X ;text line 1950 LDA #LINE3/256 ;in Graphics 0 1960 STA ICBAH,X ;segment 1970 JSR PRINTLINE 1980 JSR POSITION ;print last 1990 LDX #$60 ;text line 2000 LDA #LINE4&255 2010 STA ICBAL,X 2020 LDA #LINE4/256 2030 STA ICBAH,X 2040 JSR PRINTLINE 2050 ; 2060 ;loop until START pressed, then 2070 ;close screen & reopen so blank 2080 ; 2090 LDA #8 ;initialize 2100 STA CONSOL ;buttons 2110 EXIT 2120 LDA CONSOL ;value of 6 here 2130 CMP #6 ;means START 2140 BNE EXIT ;no? try again 2150 LDX #$60 ;close screen 2160 JSR CLOSEANY 2170 JSR OPENSCREEN ;and reopen 2180 END JMP END ;wait for reset 2190 ; 2200 ;subroutine to open the screen 2210 ; 2220 OPENSCREEN 2230 LDX #$60 ;use IOCB #6 2240 LDA #OPEN ;command is OPEN 2250 STA ICCOM,X 2260 LDA #SCREEN&255 ;device to open 2270 STA ICBAL,X 2280 LDA #SCREEN/256 2290 STA ICBAH,X 2300 LDA #12 ;no text window 2310 STA ICAX1,X 2320 LDA #0 ;graphics mode 0 2330 STA ICAX2,X 2340 JSR CIOV ;go do it 2350 RTS 2360 ; 2370 ;subroutine to close any IOCB 2380 ; 2390 CLOSEANY 2400 LDA #CLOSE ;close screen 2410 STA ICCOM,X 2420 JSR CIOV 2430 RTS 2440 ; 2450 ;subroutine to position cursor 2460 ;for next text string to write 2470 ; 2480 POSITION 2490 LDX STRGNO ;get point number 2500 LDA XPOS,X ;get x-coordinate 2510 STA COLCRS ;and store 2520 LDA YPOS,X ;get y-coordinate 2530 STA ROWCRS ;and store 2540 INC STRGNO ;ready for next 2550 RTS ;point, and exit 2560 ; 2570 ;subroutine to print line up to 2580 ;120 chars long at cursor 2590 ; 2600 PRINTLINE 2610 LDA #120 ;maximum length 2620 STA ICBLL,X ;of text string 2630 LDA #0 ;is 120 chars. 2640 STA ICBLH,X 2650 LDA #PUTREC ;operation is to 2660 STA ICCOM,X ;PUT a RECord 2670 JSR CIOV ;go do it 2680 RTS 2690 ; 2700 ;subroutine to add a constant 2710 ;(in accumulator) to current 2720 ;address for start of screen RAM 2730 ; 2740 ADDMEM 2750 CLC 2760 ADC SAVMSC ;add constant to 2770 STA SAVMSC ;low byte & save 2780 BCC NOINC ;if carry set, 2790 INC SAVMSC+1 ;increment high 2800 NOINC RTS ;byte,then exit 2810 ; 2820 ;subroutine to plot a point 2830 ;using current color register 2840 ; 2850 PLOTPOINT 2860 LDA #PUTREC 2870 STA ICCOM,X 2880 LDA #1 2890 STA ICBLL,X 2900 LDA #0 2910 STA ICBLH,X 2920 JSR CIOV 2930 RTS 2940 ; 2950 ;subroutine to draw from last 2960 ;plotted point to current one 2970 ; 2980 DRAWLINE 2990 LDX #$60 3000 LDA #DRAW 3010 STA ICCOM,X 3020 JSR CIOV 3030 RTS 3040 ; 3050 ;data values needed for opening 3060 ;screen and picking color regs. 3070 ; 3080 SCREEN .BYTE "S" 3090 REG0 .BYTE "A" 3100 REG1 .BYTE "B" 3110 ; 3120 ;tables of X- and Y-positions 3130 ;for lines to be printed 3140 ; 3150 XPOS .BYTE 3,0,5,11 3160 YPOS .BYTE 0,0,1,3 3170 ; 3180 ;text strings to print 3190 ; 3200 LINE1 3210 .BYTE "attack of the",EOL 3220 LINE2 3230 .BYTE " SUICIDAL " 3240 .BYTE " " 3250 .BYTE " ROAD-RACING " 3260 .BYTE " " 3270 .BYTE " ALIENS " 3280 .BYTE EOL 3290 LINE3 3300 .BYTE "*** Press START to" 3310 .BYTE " begin ***",EOL 3320 LINE4 3330 .BYTE "Analog" 3340 .BYTE " Productions",EOL 3350 ; 3360 ;tables of coordinates for 3370 ;drawing silly-looking rocket 3380 ; 3390 XDATA 3400 .BYTE 100,130,100,60,60,95 3410 .BYTE 68,81,60,60,95,68,81 3420 YDATA 3430 .BYTE 8,11,14,14,8,8,0,8 3440 .BYTE 8,14,14,22,14 3450 EXHAUSTX .BYTE 40,46,52,58 3460 EXHAUSTY .BYTE 11,11,11,11
A.N.A.L.O.G. ISSUE 46 / SEPTEMBER 1986 / PAGE 29
Last time out, we ended up with the title screen to what promises to be a very silly program, “Attack of the Suicidal Road-Racing Aliens.”
This is a mixed graphics mode screen, containing segments of graphics modes 1, 2, 7 and 0. We created the screen by setting up our own “display list.” This tells the computer which graphics mode to use for each horizontal line on the TV, and where in RAM to find the information to be displayed.
Our title screen is nice, but it just doesn’t have the colorful pizzazz of real games. We need some way to get around the usual five-colors-at-a-time limit. Fortunately, just such an mechanism exists: the display list interrupt or DLI Today we’ll explore the DLI—and spice up our title screen in the process.
An introduction to DLIs always begins with a review of the TV display process. The TV’s electron beam begins writing on the screen at the upper left corner. It draws one horizontal line, then is turned off just long enough for the electron beam to move back across the screen and down one line.
During this brief period, while the gun’s doing a horizontal retrace, we have just enough time to run a tiny machine language program. Such a program, my friends, is a DLI, sometimes called a “horizontal blank interrupt.” The execution time may be short, but the possibilities are many.
Depending on the graphics mode, the horizontal blanking period is between 15 and 60 machine cycles long. This gives us time to run a program containing five to ten load-and-store operations. It’s plenty of time to change the contents of color registers, point to a redefined character set, change the positions of players or missiles, alter the contents of sound registers, and so on. These changes will be in effect as soon as the electron gun begins to draw the next scan line on the TV screen. Many amazing special effects can be produced using DLIs. We’ll concentrate mostly on color changes this month.
There are just a few things to keep in mind as you write a DLI. The most important is that it must be short. If the DLI is still executing when the electron gun comes back on, there may be distortions in the screen display. Let’s examine the DLI routine that appears in Lines 620–670 of Listing 1, reproduced here:
0620 DLI PHA 0630 LDA #68 0640 STA WSYNC 0650 STA COLPF2 0660 PLA 0670 RTI
The first instructions in your DLI must save the contents of any registers (accumulator, X, Y) used in the DLI. In 6502 lingo, the PHA instruction is used to push the contents of the accumulator onto the program stack. This is a good place to briefly stash the accumulator. Our load-and-store operations use only the accumulator, so that’s the only register saved on the stack (Line 500).
To save the X- and Y-registers requires a two-step procedure. First, transfer the contents to the accumulator with a TXA or TYA instruction. Then store the accumulator on the stack with the PHA again. Now our registers are ready for DLI use.
The next portion of the DLI code contains the load-and-store operations. The decimal 68 is a color value for red. It’s stored first into location $D40A (WSYNC). Storing something in WSYNC synchronizes the DLI with the TV display. Omit this line and you’ll see a flickering, jagged edge, because the color change takes place on-screen, rather than tidily behind the scenes. (Incidentally, keyboard input can also interfere with DLI timing, so you may see flickers and jumps in your display as keys are pressed.)
Next, the accumulator (still 68) is stored in the “hardware color register” responsible for the background color of a graphics screen (COLPF2, $D018). A discussion of hardware registers is coming up.
Naturally, we need to restore the contents of the registers before exiting from the DLI. The converse of saving them is to move a byte from the stack to the accumulator with a PLA (pull) instruction. This byte can be transferred to the X- or Y-registers using the TAX or TAY instruction. Be sure to restore registers in the reverse order in which you saved them.
The final instruction of the DLI routine must be to “return from interrupt” (RTI). This is similar to the return from subroutine (RTS) instruction, but don’t get them confused!
I’m sure you remember the five color registers at addresses $2C4–$2C8 (COLOR0–COLOR4) that we’ve manipulated in previous columns. These are “shadow registers” for a corresponding group of “hardware registers” at addresses $D016–$D01A (COLPF0–COLPF4).
The real computer action takes place in the hardware registers. However, the hardware registers are “write-only”; we can’t read them and find out what they contain at any time. Hence, the corresponding read/write shadow registers were created.
Most programs make color register changes in the shadow registers, as we have. Every sixtieth of a second, during the vertical blanking period, contents of the shadow registers are written into the corresponding hardware registers, thus implementing any color changes in the next TV frame drawn.
DLI routines are executed in between vertical blank intervals. Thus, copying to a shadow register does us no good. The solution is to write directly into the hardware registers in our DLI routines. The playfield color registers are not unique in this regard. Table 1 lists some other hardware/shadow combinations useful for DLI routines.
Table 1. — Hardware and Shadow Registers. Hardware Shadow Purpose $D016–$D01A $2C4–$2C8 Playfield colors $D012–$D015 $2C0–$2C3 Player/missile colors $D409 $2F4 Character set base address $D01B $26F Player priorities
Besides writing the DLI routine itself, we have to tell the Atari what to do with it. There are three steps:
Example 1—Just a color change.
This short example simply opens the screen and changes the background color from the default blue to a bright red, starting with the ninth mode line on the screen.
We begin with the familiar process of opening the screen device in graphics (Lines 320–430). The DLI code itself appears at the end of the listing, in Lines 620–670. Lines 440–470 store the starting address of the DLI routine into locations $200 and $201 (VDSLST, low/high format).
Since the color change is to start with the ninth line of graphics 0, we must set bit 7 of the display list byte for the eighth mode line. But where’s the display list?
The open screen procedure lets the Atari create a display list wherever it likes. Fortunately, it stores the address of the first byte of the display list in locations $230 and $231, referred to as SDLSTL. I’m going to copy these values into a couple of spare bytes in page 0, called TEMP in this example (Lines 480–510).
Now, why did I do that? I want to access a byte in the display list, and an easy way is to use the 6502’s “indirect indexed” addressing mode. An indirect indexed instruction for loading the accumulator looks like this:
This procedure begins with an address in a 2-byte page location ($CD and $CE, low/high). It then points to that address, offsets by the value in the Y-register, and loads the contents of the resulting location into the accumulator. Going back to Listing 1, we have the address of the display list in locations TEMP and TEMP+1. Now think about what the display list looks like, based on last month’s discussion.
It begins something like this (in decimal form): 112, 112, 112, 66, xx, xx, 2, 2, 2, 2, 2, 2, 2, 2… (The xs refer to some unknown location for the start of screen memory; it’s not important now.) This portion of the display list goes down through the first nine mode lines of graphics (ANTIC 2) on the screen. The DLI is to be executed after the eighth mode line. Count down the display list, starting at 0, and the magic number is 12 bytes from the start. So load the Y-register with a decimal 12 and load the accumulator using indirect addressing mode, as in Lines 520–530 of Listing 1.
To set bit 7 of whatever’s in the accumulator, we can use the ORA instruction. Bit 7 corresponds to decimal 128 ($80 hex), so Line 540 does the trick. Store the result right back where we found it initially (Line 550), and our display list has been properly activated for one DLI. Lines 560–570 actually cause the DLI to begin being executed. Run this program from address $5000 and you’ll see the two-tone screen until you press RESET.
Example 2. Back to the aliens.
The statements in Listing 2 are numbered, so they can be merged with the code from last month’s title screen program. A new block of equates is inserted in Lines 441–448. These cover the color registers and some other registers useful in DLIs. The DLI routines themselves are at the very end of the listing.
The only other change in this program from last time is that we’re using color register 2, rather than 1, to draw the rocket ship; Lines 1300–1380 contain some alterations.
Our goal is to enliven the title screen to “Aliens” by using four DLIs to create several regions of different colors on the screen. For kicks, we’ll also throw in a little character set manipulation.
Please examine the custom display list in Lines 510–560. Notice that several bytes are larger than they were last month—by the quantity of 128. These are the mode lines after which our four DLIs will be executed.
Lines 621–624 place the address of the first DLI (DLI1, naturally) into VDSLST. But wait! There’s only the one DLI address pointer, yet we have four DLIs. Whatever shall we do?
I suggest that DLI1 load the address of DLI2 into VDSLST, DLI2 load the address of DLI3, and so on, with DLI4 pointing back to DLI1 for the next time the screen’s drawn. This is one cumbersome feature of using multiple DLIs in a single display. Alternatives that sometimes work are “table-driven” or algorithmic DLIs, which we may encounter in future columns.
The code at Lines 921–931 enables DLIs and sets some initial values in the color registers. Notice that we’re using the playfield color registers here, not the hardware registers.
Look now at the DLI routines, starting at Line 3480. DLI1 simply changes the background of the top part of the screen to gray (color 4). Then it loads the address of DLI2 into VDSLST (Lines 3530–3560).
DLI2 uses both the accumulator and the X-register, so you can see how to save both on the stack and restore them later. The X-register is used for color change, but I got more creative with the accumulator. It actually selects a different character set to be used.
You’ll recall that the normal character set used for graphics 1 and 2 shows only uppercase letters in four different colors. In effect, only half of the standard Atari characters can be displayed in these modes. The hardware register called CHBASE ($D409) can be loaded with the decimal value 226, to show lowercase and control characters from the other half of the standard cheiracter set (the default value in CHBASE is 224). Unfortunately, this half of the character set contains no blank character; a heart is printed instead. This explains the funny-looking display you see from DLI2.
My point is to illustrate how a DLI can be used to change character sets in the middle of the screen. Many character set editor programs use this feature to show both the normal characters and your redefined characters at the same time.
DLI3 and DLI4 simply cause some additional color changes, in both foreground and background registers. DLI4 also sets the contents of VDSLST back to the address of the first DLI on this display, DLI1.
Notice that there’s no relationship between DLIs and the “segments” in our custom display list. The DLIs can be placed anywhere on-screen. This feature permits niceties such as the two-tone graphics segment at the bottom of the display.
I encourage you to experiment with different values in the color registers of these four DLIs, to make sure you understand the effects each one is causing. Play with them until you get the look you like. Don’t fiddle with the character set address in Line 3630, or you could get some very bizarre displays. Handling redefined character sets in assembly language will be one of our future topics.
I think our title screen is spiffy enough now. Every Atari game has to have plenty of things moving around the screen. These “things” are usually the famous players and their sidekicks, the missiles. Next month, we’ll talk about how to define the shapes of some players and have them move around the screen under their own power. Moving them around under your control with a joystick will come after that. Do check back…
Karl E. Wiegers provides computer support for photographic researchers at the Eastman Kodak Company. This means he’s wasting his Ph.D. in organic chemistry, but he has a lot of fun. He also writes commercial educational chemistry software for the Apple II.
Listing 1. Assembly listing. 0100 ;DLI Listing 1 0110 ; 0120 ;by Karl E. Wiegers 0130 ; 0140 .OPT NO LIST, OBJ 0150 ; 0160 TEMP = $CB 0170 VDSLST = $0200 0180 SDLSTL = $0230 0190 ICCOM = $0342 0200 ICBAL = $0344 0210 ICBAH = $0345 0220 ICBLL = $0348 0230 ICBLH = $0349 0240 ICAX1 = $034A 0250 ICAX2 = $034B 0260 CIOV = $E456 0270 COLPF2 = $D018 0280 WSYNC = $D40A 0290 NMIEN = $D40E 0300 ; 0310 *= $5000 0320 LDX #$60 ;open screen 0330 LDA #3 0340 STA ICCOM,X 0350 LDA #SCREEN&255 0360 STA ICBAL,X 0370 LDA #SCREEN/256 0380 STA ICBAH,X 0390 LDA #12 0400 STA ICAX1,X 0410 LDA #8 0420 STA ICAX2,X 0430 JSR CIOV 0440 LDA #DLI&255 ; point to DLI 0450 STA VDSLST 0460 LDA #DLI/256 0470 STA VDSLST+1 0480 LDA SDLSTL ;copy DL address 0490 STA TEMP ;to page zero 0500 LDA SDLSTL+1 0510 STA TEMP+1 0520 LDY #12 ;set DLI bit on 0530 LDA (TEMP),Y ;line 8 0540 ORA #128 0550 STA (TEMP),Y 0560 LDA #192 ;enable DLI 0570 STA NMIEN 0580 END JMP END ;wait for reset 0590 ; 0600 SCREEN .BYTE "S" 0610 ; 0620 DLI PHA ;save A on stack 0630 LDA #68 ;color is red 0640 STA HSYNC ;synchronize 0650 STA COLPF2 ;background reg. 0660 PLA ;restore A 0670 RTI ;all done
Listing 2. Assembly listing. 0100 ;DLI Listing 0110 ; 0120 by Karl E. Wiegers 0130 ; 0225 VDSLST = $0200 0441 ; 0442 ;color & charset registers,etc. 0443 ; 0444 COLOR0 = $02C4 ;shadow register 0445 COLPF0 = $D016 ;hardware reg. 0446 CHBASE = $D409 ;hardware charset 0447 WSYNC = $D40A ;synchronize 0448 NMIEN = $D40E ;enable DLI 0470 ; 190 *= $3F00 0580 DLIST 0510 .BYTE 112,112,112,70,0,$40 0520 .BYTE 134,7,7,135,7,7,7,13,13 0530 .BYTE 13,13,13,13,13,13,13 0540 .BYTE 13,13,13,13,13,13,13 0550 .BYTE 13,13,13,13,13,13,13 0560 .BYTE 141,2,130,2,2,65,0,$3F 0561 ; 0621 LDA #DLI1&255 ;set address 0622 STA VDSLST ;of first DLI 0623 LDA #DLI1/256 0624 STA VDSLST+1 0921 LDA #191 ;enable DLI 0922 STA NMIEN 0923 LDA #152 ;set initial 0924 STA COLOR0 ;colors 0925 LDA #86 0926 STA COLOR0+1 0927 LDA #14 0928 STA COLOR0+2 0929 LDA #26 0930 STA COLOR0+4 0931 LDA #34 ;turn screen on 1300 LDA #REG2&255 ;color reg.2 1310 STA ICBA1,X L320 LDA #REG2/256 1330 STA ICBAL+1,X 1340 JSR PLOTPOINT 1380 LDA #3 3105 REG2 .BYTE "C" 3470 ; 3480 DLI1 3490 PHA ;save accum. 3500 LDA #4 ;color gray 3510 STA WSYNC ;synchronize 3520 STA COLPF0+4 3530 LDA #DLI2&255 ;point to 3540 STA VDSLST ;next DLI 3550 LDA #DLI2/256 3560 STA VDSLST+1 3570 PLA ;restore accum. 3580 RTI ;all done 3590 DLI2 3600 PHA ;save registers 3610 TXA 3620 PHA 3630 LDA #226 ;change charset 3640 STX #70 ;color is red 3650 STX COLPF0 ;for foreground 3660 STA WSYNC 3670 STA CHBASE ;change charset 3680 LDA #DLI3&255 ;point to 3690 STA VDSLST ;next DLI 3700 LDA #DLI3/256 3710 STA VDSLST+1 3720 PLA ;restore registers 3730 TAX 3740 PLA 3750 RTI ;all done 3760 ; 3770 DLI3 3780 PHA 3790 TXA 3800 PHA 3810 LDA #14 ;more colors... 3820 LDH #18 ;you've seen 3830 STA WSYNC ;it all before 3840 STA COLPF0+1 3850 STX COLPF0+2 3860 LDA #DLI4&255 3870 STA VDSLST 3880 LDA #DLI4/256 3890 STA VDSLST+1 3900 PLA 3910 TAX 3920 PLA 3930 RTI 3940 DLI4 3950 PHA 3960 THA 3970 PHA 3980 LDA #0 3990 LDX #198 4000 STA WSYNC 4010 ITA COLPF0+1 4020 STX COLPF0+2 4030 LDA #DLI1&255 4040 STA VDSLST 4050 LDA #DLI1/256 4060 STA VDSLST+1 4070 PLA 4080 TAX 4090 PLA 4100 RTI
A.N.A.L.O.G. ISSUE 48 / NOVEMBER 1986 / PAGE 90
When the Atari 400s and 800s first hit the market, the magic word was graphics, more specifically, player/missile graphics. You probably learned how to create and animate players in BASIC programs—and have observed that it’s a bit cumbersome. Now, we’ll see how to do the same thing in assembly language. It really isn’t hard at all.
In the last two issues, we created the title screen for “Attack of the Suicidal Road-Racing Aliens,” then jazzed it up with some display list interrupts. This time we move on to an actual playing screen, with an alien and a car dashing about. Unfortunately, they’re all alone on a blank screen, and their movement is monotonous.
That’s the way the tutorial crumbles for this month. In the next two issues, we’ll build on this simple player action and get considerably more sophisticated. Be sure to enter this month’s listing with the line numbers shown, since we’ll be merging additional code with this program in the next two issues.
We really aren’t going to create a complete, playable game with this stepwise process. Instead, I’ll present the elements of a game, to illustrate the programming techniques involved. If you do want to put the pieces together, just keep adding code at the end of last month’s program, rather than starting today’s program at the $5000 address. Also, you’ll have to fix the routine for detecting the START button from last time, so that execution continues with the first line of today’s program.
First, a quick refresher on player/missile graphics (PMG). The background screen display is referred to as the “playfield.” A “player” is simply an 8-pixel-wide stripe of dots that runs the full vertical height of the screen. We can selectively turn on whichever dots we want, and superimpose the resulting figure on the playfield. A control register in the operating system lets us select whether a player or a playfield object appears to be “in front” whenever they occupy the same space. Other registers allow us to check for collisions between players and playfield objects.
A “missile” is a 2-pixel-wide analog to a player. The Atari can have up to four players (and their missiles) present on-screen at once. Each player/missile combination has its own color register, but a missile is always the same color as the player with which it’s associated. We can double or quadruple the width of each player independently, making them 16 or 32 pixels wide. Other PMG control registers set the horizontal positions of the players and missiles on the screen.
Players can be defined as having either “single-line” or “double-line” vertical resolution. In single-line resolution, the player stripe is 256 pixels (scan lines) high. A double-resolution player is 128 pixels high, where each pixel occupies two scan lines. All players must have the same resolution. Naturally, single-line resolution permits finer detail in the player dot pattern. The price you pay is in memory: single-line resolution requires twice as many bytes of storage as double-line.
The first task when setting up a PMG display is to decide what you want your players to look like. I’ll assume you’re familiar with the technique of defining a player shape by deciding which of the eight dots in each line are to be illuminated, and computing a numeric byte value from the binary pattern that results. Many good articles on PMG fundamentals can be found in Atari literature. There are also many good programs available (commercially, and in magazines or books) to help you design your players.
As with most assembly programs, the next step is to allocate memory. Double-line resolution requires 1024 bytes of RAM, and single-line resolution requires 2048 bytes. The starting byte of the RAM allocated to PMG (called PMBASE) must be on a 1K boundary for double-line resolution, and on a 2K boundary for single-line. I usually use single-line resolution starting at address $3000. The PMG region will thus extend up to address $37FF.
Usage of the RAM block allocated to player/missile graphics, in bytes offset from PMBASE.
Unused 0 - 383 0 - 767 Missiles 384 - 511 768 - 1023 Player 0 512 - 839 1024 - 1279 Player 1 640 - 767 1280 - 1535 Player 2 768 - 895 1536 - 1791 Player 3 896 - 1023 1792 - 2047
Table 1 shows how the PMG RAM is used. In single-line resolution, one page (256 bytes) is used for each of the four players and a fifth page is for the four missiles (64 bytes each). This leaves the block from byte 0 of the PMG RAM through byte 767 empty and available for your use. The location of the player within its allocated RAM section determines its vertical screen position. I alluded to the various registers used to control PMG functions. Table 2 lists the important ones for today. Notice in Table 2 that there are registers for the horizontal positions of players, but not for their vertical positions. We’ll discuss the significance of this fact later on. Now, turn your attention to the assembly listing and we’ll see how it’s done.
Important PMG registers.
Name Hex Address Function SDMCTL $022F 62 for single-line, 46 for double-line resolution PCOLR0–3 $02C0–$02C3 color of players 0–3 HPOSP0–3 $D000–$D003 horizontal positions of players 0–3 SIZEP0–3 $D008–$D00B player size: 0=normal, 2=double, 3=quadruple GRACTL $D01D store 3 to enable PMG, to disable PMBASE $D407 store high byte of PMBASE here
This month’s sample program illustrates how to implement player/missile graphics and how to move players vertically and horizontally under program (as opposed to user) control. The program employs two players: an alien who moves up and down, and a car that moves from left to right.
The listing begins with the familiar CIO equates. All we’ll do with CIO is open the screen in graphics mode 3 and leave it blank. Lines 350–430 contain the equates for the PMG registers in Table 2. Some other variables I use in the program are defined in Lines 470–560. SHAPE and PLYRSTRT are 2-byte variables placed in some of the few free 0-page bytes. I’ll use these for some loads and stores, with the 6502’s indirect indexed addressing mode. I set aside a block of 4 bytes to keep track of the Y-positions of the four players (YPOSP0), and another block for the X-positions (XPOSP0). NBYTES is the number of bytes of shape data in the player currently being manipulated.
Sometimes you may want to confine the movement of your players to a certain area on-screen. In this example, I want the alien to hang around between specified vertical limits. Hence, the variables TOP and BOTTOM, which will be loaded with the scan lines of my upper and lower movement limits. Finally, DIRECT is used to keep track of which way the alien is moving, up (1) or down (0). I tend to use page 6 for this sort of variable storage, but you can put your variables anyplace where they’ll remain intact.
I’m using single-line resolution, so I must locate PMBASE on a 2K boundary (hex addresses of $X000 or $X800). Line 620 shows that the PMG region begins at address $3000. The program itself begins at address $5000, as is my habit; execute this program at address $5000.
Recall that only pages 4–8 of the PMG RAM block contain information that’ll be shown on the screen. Lines 1910–2000 of the listing zero all the bytes in those five pages, thus preventing any extraneous junk in that section of RAM from affecting the screen display.
The high byte of the first byte of the PMG block must be stored into the register called PMBASE ($D407), as in Lines 2010–2020. Lines 2030–2100 initialize the player sizes and horizontal positions to 0.
The player shapes themselves are found in Lines 4920–4950 and 5210–5240. Player 0 is the alien, and player 1 is a car. I decided to store these data tables in the unused portion of the PMG block. The alien shape is at the beginning of the PMG block ($3000), and the car immediately thereafter.
The first byte in each table is the number of bytes of actual player shape data. The alien shape contains 14 bytes, the car 15. This means the alien is 14 scan lines high and the car is 15 lines. The remaining bytes are the decimal values of the bit patterns for each line of the player shape. The last byte in each player is a 0, which doesn’t contribute to the appearance of the player, but helps with vertical movement (as we’ll see shortly).
Now that memory’s been allocated, we need to copy the player shapes into the correct page of the PMG RAM block. Of course, we could have put them in the right place initially. But sometimes you want to have multiple shapes for a single player, (having an alien face to the left, to the right, forward, or backward). In such cases, the various player shapes all have to be stored somewhere, and you must copy them to the right part of the PMG block whenever you want to change the shape. I’ve primed your mind for this technique by storing the shapes in the free PMG area. (This trick resurfaces in a couple of months).
The code in Lines 2140–2250 copies the data for the alien into a specified portion of the RAM block for player 0. Line 2190 indicates that this block is four pages higher in RAM than the start of the PMG block. Line 2220 sets the initial vertical position of the alien at scan line 180, by storing the player data at an offset of 180 bytes from the beginning of the allocated RAM page. I also store this vertical position in variable YPOSP0, to keep track of where the little guy is. Feel free to play with this number and see what happens.
The subroutine called COPYPLAYER (Lines 3900–4020) actually performs the copy of the player data (how about that). By writing this segment as a subroutine, I can use it to copy any player data into any player RAM area. The car shape data is copied the same way in Lines 2300–2410, with an initial vertical position of 122. Finally, Line 2420 opens the screen using a familiar subroutine (OPENSCREEN, Lines 3350–3480) in graphics mode 3.
Lines 2460–2490 set the alien’s upper and lower limits of movement. The upper vertical position where a player’s still visible at the top of the screen is about 32 for single-line resolution, 16 for double. At the other extreme, vertical positions larger than about 224 (single) or 112 (double) will make the player fall off the bottom of the TV. Lines 2540–2570 make the alien yellow and the car purple. Notice that player colors are controlled with special registers PCOLR0–3, separate from the playfield color registers.
Now for the lowdown on the other critical PMG registers. Last month, we learned that location SDMCTL ($22F) controls whether the screen display is off (store a 0) or on (store a decimal 34). This register also controls whether the players are double-line resolution (decimal 46) or single-line (decimal 62). Lines 2580–2590 request single-line resolution players.
The horizontal size of players is controlled by the registers called SIZEP0–3 ($D008-D00B). Storing a 0 in one of these registers gives the default (normal) width of 8 pixels. Store 1 for a double-wide and 3 for a quadruple-wide player. Lines 2600–2610 make the car double width. The register called GRACTL ($D01D) enables player/missile graphics. Store a 1 to turn on the missiles, a 2 to turn on players and a 3 for both, as we do in Lines 2620–2630.
The horizontal position of players is dictated by the contents of registers HPOSP0–3 ($D000–D003). Allowable values are 0 to 255, but positions below about 48 (left) and above about 208 (right) will be outside the boundaries of the TV screen. In Lines 2640–2650, we place the alien near the center of the screen. Since the HPOSP0–3 registers are write-only, I’ve created a second block of storage locations called XPOSP0–3, in case I need to find out where a player is at any given moment. Last but not least in this section, Lines 2670–2680 set the initial direction of alien movement as upward.
Now for some animation. Recall that we want the alien to move up and down, and the car to move horizontally across the screen. Occasionally, they may have an encounter session. In a future column, we’ll see how to detect collisions among players and take some appropriate action. For now, though, let’s just get the rudiments of player motion down pat.
The routine labeled ACTION, beginning at Line 2740, starts with a yawn by calling the subroutine DELAY (Lines 4080–4150). This subroutine does…absolutely nothing. Nonetheless, it’s useful. If we let the players move around at top computer speed, things would be far too fast to follow. The delay subroutine simply loops for a number of cycles controlled by the contents of the X-register when DELAY is called. Line 2750 uses a value of 15 in X, but try different numbers and see what happens.
After that ever-so-brief pause, the car (player 1) is moved 1 pixel to the right, thanks to the instructions in Lines 2770–2790. Since this process goes on forever, the car will simply zip from left to right over and over again. Lines 2800–2850 prime the system for vertical movement of the alien (player 0).
Unfortunately, moving a player up and down isn’t as simple as moving him left and right. Recall that the vertical orientation of a player reflects a pattern of bytes stored in a particular section of RAM. To move a player up one line, we must shift each byte of the player shape data 1 byte lower in RAM. Conversely, moving data bytes higher in RAM causes the player to move down the screen. There are several ways to do this. One is to simply copy the player shape from its original storage location into the new desired position in the PMG block, making certain to zero out the old position so no extraneous player parts are visible. A second method is to actually shift the player shape 1 byte at a time within the RAM block. That’s the approach I used in this example.
A certain amount of program logic is required to move the alien, as found in Lines 2900–3120. In short, we move the alien up if he’s already going up and isn’t yet at the top. If he’s at the top, we start moving him down, until he reaches the bottom. Back and forth he goes, where he stops only the pusher of the RESET Key knows.
Vertical movement is accomplished with subroutines MOVEDOWN (Line 4200) and MOVEUP (Line 4350). I think you can see how these routines simply shift the player data 1 byte in the appropriate direction. What would happen if we didn’t have limits on the vertical movement area? Eventually the alien, player 0, would be moved into the section of RAM reserved for player 1, or into the block set aside for the missiles. Either way it’s bad news, so I like to set the limits and worry about it no further.
As the alien mimics a yo-yo and the car flashes by, you’ll see an occasional flicker or jump in the animation. This jerkiness is particularly noticeable at slow speeds (long delay set in Line 2750).
The fix to this is simple: move all your players while the TV gun is turned off, every sixtieth of a second. To accomplish this feat, we must create a “vertical blank interrupt” (VBI) routine, which gives us flicker-free motion. Next time, we’ll write a VBI that lets you move the alien around the screen via a joystick. I’ll throw in a bonus: a procedure for using the VBI in a BASIC program.
Listing 1. Assembly listing. 0100 ;Player/Missile graphics exanple 0110 ; 0120 ;by Karl E. Wiegers 0130 ; 0140 .OPT NO LIST 0150 ; 0160 OPEN = $03 0240 ICCOM = $0342 0250 ICBAL = $0344 0260 ICBLL = $0348 0270 ICAX1 = $034A 0280 ICAX2 = $034B 0290 CIOV = $E456 0320 ; 0330 ;PMG-related equates 0340 ; 0350 PCOLR0 = $02C0 0360 SDMCTL = $022F 0370 HPOSP0 = $D000 0390 SIZEP0 = $D008 0410 GRACTL = $D01D 0430 PMBASE = $D407 0440 ; 0450 ;some variables I need to use 0460 ; 0470 SHAPE = $CB 0480 PLYRSTRT = $CD 0490 YPOSP0 = $0630 0500 XPOSP0 = $0634 0510 NBYTES = $0638 0520 TOP = $0639 0530 BOTTOM = $063A 0560 DIRECT = $063D 0570 ; 0580 ;PMG area of 2K begins at $3000; 0590 ;player images are stored in 0680 ;unused part of PMG area 0610 ; 0620 PMG = $3000 0630 ; 1350 ;******************************* 1860 ; MAIN PROGRAM STARTS HERE 1870 ;******************************* 1880 ; 1890 *= $5000 1900 CLD ;binary mode 1910 LDX #0 1920 TXA 1930 INIT1 1940 STA PMG+$0300,X ;zero out 1950 STA PMG+$0400,X ;player and 1960 STA PMG+$0500,X ;missile 1970 STA PMG+$0600,X ;parts of 1980 STA PMG+$0700,X ;PMG area 1990 INX 2000 BNE INIT1 2010 LDA #PMG/256 ;store address 2020 STA PMBASE ;of PMG area 2030 LDA #0 2040 LDX #3 2050 INIT2 2060 STA SIZEP0,X ;zero sizes, 2070 STA HPOSP0,X ;horizontal 2080 STA XPOSP0,X ;positions 2090 DEX ;for all 2100 BNE INIT2 ;players 2110 ; 2120 ;load alien shape into player 2130 ; 2140 LDA #ALIEN&255 ;store address 2150 STA SHAPE ;of shape in 2160 LDA #ALIEN/256 ;page 8 bytes 2170 STA SHAPE+1 2180 CLC 2190 LDA #$04 ;store address 2200 ADC #PMG/256 ;where player 2210 STA PLYRSTRT+1 ;is to be 2220 LDA #180 ;stored into 2230 STA PLYRSTRT ;page 0 bytes 2240 STA YPOSP0 ;and variable 2250 JSR COPYPLAYER ;store image 2260 ; 2270 ;load car shape into player 1 2280 ;the same way as the alien 2290 ; 2300 LDA #CAR&255 2310 STA SHAPE 2320 LDA #CAR/256 2330 STA SHAPE+1 2340 CLC 2350 LDA #$05 2360 ADC #PMG/256 2370 STA PLYRSTRT+1 2380 LDA #122 2390 STA PLYRSTRT 2480 STA YPOSP0+1 2410 JSR COPYPLAYER 2420 JSR OPENSCREEN ;open screen 2430 ; 2440 ;set up PMG environment 2450 ; 2460 LDA #30 ;top of alien 2470 STA TOP ;movement area. 2480 LDA #200 ;bottom of alien 2490 STA BOTTOM ;movement area. 2540 LDA #28 ;alien is yellow 2550 STA PCOLR0 2560 LDA #86 ;car is purple 2570 STA PCOLR0+1 2580 LDA #62 ;single-line PMG 2590 STA SDMCTL ;resolution 2600 LDA #1 ;car is double 2610 STA SIZEP0+1 ;wide 2620 LDA #3 ;enable PMG 2630 STA GRACTL 2640 LDA #120 ;alien starts in 2650 STA HPOSP0 ;middle of scree 2660 STA XPOSP0 2670 LDA #1 ;initial direc- 2680 STA DIRECT ;tion is up 2690 ; 2700 ;commence player movement: 2710 ;alien moves only vertically, 2720 ;car moves only horizontally 2730 2740 ACTION 2750 LDX #15 ;do nothing 2760 JSR DELAY ;for a bit 2770 INC XPOSP0+1 ;move car 1 2780 LDA XPOSP0+1 ;position to 2790 STA HPOSP0+1 ;the right 2880 CLC 2810 LDA #$04 ;store initial 2820 ADC #PMG/256 ;RAM position 2830 STA PLYRSTRT+1 ;of alien in 2840 LDA YPOSP0 ;page bytes 2850 STA PLYRSTRT 2860 ; 2870 ;logic to figure out if alien is 2880 ;be moved up or down 2890 ; 2900 LDA DIRECT ;current dir 2910 BNE CHKTOP ;up, check top 2920 CHKBOT 2930 LDA YPOSP0 ;is he at the 2940 CMP BOTTOM ;bottom? 2950 BEQ UP ;yes. move up 2960 BNE DOMN ;no. move down 2970 CHKTOP 2980 LDA YPOSP0 ;is he at the 2990 CMP TOP ;top? 3880 BNE UP ;no, move up 3010 DOWN 3020 JSR NOUEDOMN ;move him down 3830 LDA #0 ;current direc- 3840 STA DIRECT ;tion is down 3850 CLC ;keep going 3860 BCC ACTION 3070 UP 3880 JSR MOVEUP ;move him up 3890 LDA #1 ;current direc- 3180 STA DIRECT ;tion is up 3110 CLC ;keep going 3120 BCC ACTION 3240 ;******************************** 3250 ; END OF MAIN PROGRAM 3260 ;******************************** 3270 ; 3280 ; 3290 ;******************************** 3300 ;SUBROUTINES START HERE 3310 ;******************************** 3320 ; 3330 ;open screen in Graphics 3 3340 ; 3350 OPENSCREEN 3360 LDX #$60 3370 LDA #OPEN 3380 STA ICCOM,K 3390 LDA #SCREEN&255 3400 STA ICBAL,X 3410 LDA #SCREEN/256 3420 STA ICBAL+1,X 3430 LDA #12 3440 STA ICAX1,X 3450 LDA #3 3460 STA ICAX2,X 3470 JSR CIOV 3480 RTS 3490 ; 3870 ;copy player from data region 3880 ;to desired PMG location 3890 ; 3900 COPYPLAYER 3910 LDY #0 ;get no. of 3920 LDA (SHAPE),Y ;bytes of 3930 STA NBYTES ;player data 3940 INC KBYTES ;to be moved 3950 LDY #1 3960 PLOOP 3970 LDA (SHAPE),Y ;copy to PMG 3980 STA (PLYRSTRT),Y ;area 3990 INY ;data area 4000 CPY MBYTES ;all bytes yet? 4010 BNE PLOOP ;no, keep going 4020 RTS ;yes, stop 4030 ; 4040 ;do-nothing delay subroutine 4050 ;number in X-register deternines 4060 ;length of delay 4070 ; 4080 DELAY 4090 LDY #0 4100 DELAY2 4110 DEY 4120 BNE DELAY2 4130 DEX 4140 BNE DELAY 4150 RTS 4160 4170 ;sub. to Move alien shape down 4180 ;one line (up one byte in RAM) 4190 4200 MOVEDOWN 4210 LDY ALIEN ;get # bytes 4220 LOOPDOMN 4230 LDA (PLYRSTRT),Y ; get a byte 4240 INY ;store one 4250 STA (PLYRSTRT),Y ;byte higher 4260 DEY ;point to 4270 DEY ;lower byte 4280 BPL LOOPDOWN ;go until 4290 INC YPOSP0 ;new Y position 4300 RTS 4310 4320 ;sub. to Move alien shape up 4330 ;one line (down one byte in RAM) 4340 4350 MOVEUP 4360 LDA ALIEN ;# bytes to move 4370 STA NBYTES ;is 1 more than 4380 INC NBYTES ;# player bytes 4390 LDY #1 4400 LOOPUP 4410 LDA (PLYRSTRT),Y ;get a byte 4420 DEY ;store 1 4430 STA (PLYRSTRT),Y ;byte lower 4440 INY ;point to 4450 INY ;next one 4460 CPY NBYTES ;done all? 4470 BNE LOOPUP ;no, go on 4480 DEC YPOSP0 ;new Y position 4490 RTS 4580 ; 4590 ;data values needed 4600 ; 4610 SCREEN .BYTE "S" 4620 ; 4840 ;data for player shapes are 4850 ;stored in unused portion of 4860 ;PMG area 4870 ; 4880 *= PMG 4890 ; 4900 ;normal alien 4910 ; 4920 ALIEN 4930 .BYTE 14,60,24,126,189,189 4940 .BYTE 189,189,60,50,36 4950 .BYTE 36,36,102,0 5180 ; 5190 ;car shape 5200 ; 5210 CAR 5220 .BYTE 15,126,195,219,219 5230 .BYTE 91,219,219,219,219 5240 .BYTE 91,219,219,195,126,0
A.N.A.L.O.G. ISSUE 49 / DECEMBER 1986 / PAGE 113
In part 1 of our discussion of using player/missile graphics (PMG) in assembly language (issue 48, last month), we saw how to allocate memory for PMG, design player shapes and make them move around under program control. One limitation of our program: the player movement was somewhat jerky and erratic. Another was that we didn’t yet have any way for a user to move a player around the screen via a joystick or other input device.
This lesson provides a solution to both problems. We’ll write a vertical blank interrupt (VBI) routine to read the status of the joystick and move a player accordingly. Further, by making all player movement take place in the vertical blanking period, the unsightly flickers and jerks will disappear. And our evolving “Attack of the Suicidal Road-Racing Aliens” moves forward yet another notch in sophistication.
The assembly statements in the accompanying listing are designed to be merged with the listing from our last month’s column, to make a complete example. Follow this procedure. First, boot up your assembler and retrieve last month’s program. Then, delete Lines 2670–3120 and Lines 4160–4490 (with the now-defunct routines for moving the alien and car under program control). Finally, enter the statements from this month’s listing with the line numbers shown, and store the whole thing as today’s example. Run this composite program at address $5000. We’ll conclude the PMG discussion next month, by adding further to this composite listing.
I’m including a bonus BASIC program that incorporates a VBI routine to move a player via joystick, in case you ever write another BASIC program. First, though, the requisite background material.
Video display devices like TV sets generate images on a screen with one or more electron guns. These guns paint a picture by scanning a large number of horizontal lines, one after the other. The normal Atari video display contains a vertical stack of 192 of these scan lines.
The scanning process begins in the upper left corner of the screen and concludes when the guns are aimed at the lower right corner. Then there’s a short delay, while the guns point back at the upper left corner—to begin the entire scanning process again. This sequence of electron gun scanning to create a full screen display takes place sixty times per second.
During every 1/60 second, while the electron beams are returning to the top of the screen, they’re turned off. This very short period is called the “vertical blank interval.” We don’t notice the vertical blanking process, because our eyes and brain can’t react to something that happens so quickly.
But a few milliseconds is a long time to the computer. Your Atari uses that slack time to perform some housekeeping chores, like updating the hardware registers from their shadow registers, as we discussed a couple months back.
The Atari designers thoughtfully provided the option for us to execute our own machine language program during the vertical blank interval. The act of getting the computer’s attention is called a “vertical blank interrupt,” or VBI, and the machine language routine we execute is called a “VBI routine.”
Lots of tasks lend themselves to processing in a VBI. Anything you’d like done at a regular interval is suitable. If sixty times per second is too frequent, use a counter so the task in question is only executed every so many vertical blank intervals. (If sixty times per second is too slow, you’re out of luck.)
Consider our present goal: to read the deflection of a joystick and adjust the position of a player based on that deflection. If we move the player 1 pixel per vertical blank interval, it will take him about 3 seconds to move across the screen. You can get faster movement by shifting the player more than 1 pixel each time.
Not only that, but by performing all this movement while the TV is essentially off, there’s no flicker while the player moves. This kind of “behind the scenes” movement is similar to the curtain’s dropping between acts for a change of scenery in a stage play. When the curtain is raised again, the set might be totally different, yet we saw no activity associated with the change.
A VBI routine can use up to 3000 (or even more) machine cycles to execute—with no adverse effects. This is more than enough to solve our present problem. Let’s summarize what our VBI is to accomplish:
Move the car to the right 1 or more pixels (depending on the speed desired).
Read a joystick plugged into port 1 and check for deflection to the right or left.
Shift the alien (let’s call him Bonzo) 1 pixel in the direction the joystick is deflected.
Keep track of Bonzo’s horizontal position and make sure he doesn’t move outside our specified boundaries.
Check the joystick for deflection up or down.
Shift Bonzo 1 pixel (scan line) in the direction the joystick is deflected.
Keep track of his vertical position and make sure he doesn’t move out of bounds.
Of course, the joystick could be at a diagonal, in which case, movement in both horizontal and vertical directions is required. If the joystick is centered, Bonzo stays put.
You’ve probably used the STICK function in Atari BASIC to determine which way a joystick is pushed. The usual procedure is to follow the joystick read operation with a series of IF statements, to perform some function based on the value returned by the STICK function. The decimal numbers returned by STICK certainly don’t reveal any pattern, making it difficult to discern the relationship between deflection direction and the numeric decimal equivalent. However, if the numeric value of each joystick deflection direction is expressed in binary terms, a pattern emerges.
Figure 1 labels the nine stick deflection directions with their binary values. Notice that the center position, corresponding to no stick movement, returns a binary value of 1111. The “up” position has a value of 1110. The in the least significant bit (bit 0) thus indicates that the stick is pressed upward. Upper-left and upper-right diagonal deflections also have a 0 in bit 0, as well as in other positions.
1110 | 1010 | 0110 \ | / \ | / \ | / 1011 ----- 1111 ----- 0111 / | \ / | \ / | \ 1001 | 0101 | | 1101 Figure 1.—Binary values of joystick.
Turning to downward stick movement, notice that all three such positions contain a 0 in bit 1. The three possible stick positions of left deflection each have a 0 in bit 2, while bit 3 is the magic 1, for right deflection. Let’s see what to do with this precious information.
Consider how we can tell if the joystick is pushed to the right. First, we need to read joystick by retrieving the value stored at address $0278 (STICK0; joysticks 1–3 are read in $0279–$027B). Figure 1 says that right deflection results in bit 3 of this byte being set to 0. So we just need to see if the contents of STICK0 have bit 3 set (meaning the stick is not pushed to the right) or unset (meaning that it is).
The logical operator AND provides a convenient way to test for a specific bit pattern. The AND operation simply compares the bit patterns of 2 bytes. If the bits in corresponding positions of each byte are both set to 1, then the result will have a 1 in that same bit position. Otherwise, the result has a 0 in that bit position.
If we compare the value from STICK0 with a byte that has only bit 3 set, then the decimal value of the resulting byte will either be 0 (if bit 3 of STICK0 is 0) or 8 (if bit 3 of STICK0 is 1). In other words, if the result from the AND operation is 0, then the joystick is indeed pushed to the right. If the result is 8, then it isn’t. Here are two examples:
Example 1. Stick bit pattern = 00000110 (decimal 6) AND bit pattern = 00001000 (decimal 8) Result bit pattern = 00000000 (decimal 0)
The stick is pushed to the upper right, and the result of the AND operation is 0.
Example 2. Stick bit pattern = 00001101 (decimal 13) AND bit pattern = 00001000 (decimal 8) Result bit pattern = 00001000 (decimal 8)
The result of decimal 8 tells us that the stick was not pushed to the right. In fact, it was pushed down.
So our assembly code to examine the joystick must test sequentially for deflection in the four primary directions, by appropriate AND operations.
Please turn your attention to the assembly listing. We have a few new equates defined in Lines 190–550. One is ATRACT ($4D). Storing any number in this location prevents the Atari’s attract mode of screen color shifting from taking place, as it normally does nine minutes after the last key was pressed. This is a good idea in programs that don’t involve keyboard input. I described the STICK0 location earlier. SETVBV and XITVBV are vectors to jump through (with a JSR instruction) when turning on a VBI (SETVBV) or exiting the VBI routine itself (XITVBV). Also, we’re now placing some horizontal restrictions on Bonzo’s movement, with two of my own variables, LEFT and RIGHT.
You can store the VBI routine anywhere in memory you like. Since we have player/missile graphics taking up the block from $3000–$37FF and the main program starts at $5000, I stashed the VBI starting at $4000. This is obviously not the most efficient use of RAM, but this program won’t be large enough to cause a problem. If you really get tight for memory, remember the unused portion of the dedicated PMG region. A VBI routine might well fit in that block.
The VBI code begins at Line 700. First, turn off attract mode (Lines 710–720). Lines 730–760 move the car 2 pixels to the right. Change the number of INC instructions, to make it move slower or faster. Lines 820–870 set up a page address (PLYRSTRT), so we can move Bonzo vertically using indirect indexed addressing.
Lines 940–960 begin the joystick checking, by looking for deflection to the right. If not, as indicated by a nonzero result from the AND operation, go look for deflection to the left (Line 970). If the stick is pushed to the right, see if Bonzo is at our designated right boundary (Lines 980–990). If so, don’t move him, but go check for upward deflection (Line 1000). If Bonzo’s not at the outer limits, move him 1 pixel to the right (Lines 1010–1030) and head for the STICKUP routine (unconditional branch in Lines 1170–1180).
The procedure to handle stick deflection to the left (Lines 1190–1280) is equivalent to the STICKRIGHT routine. Notice that the AND instruction now has an operand of 4, to examine bit 2.
The STICKUP routine beginning at Line 1420 checks for stick deflection upward. If no, then look for downward deflection. If yes, check for Bonzo trying to escape off the top of our playing area. Leave the VBI if this is found to be the case, by going to the label VBIEXIT and jumping through the XITVBV vector (Line 1830).
Bonzo’s vertical movement is handled by a procedure identical to the approach we used last month in the yo-yo example, so I won’t elaborate on it here. Finally, the procedure for handling the case where the joystick is pressed in the downward direction (Lines 1640–1810) is just the converse of the up direction situation. Remember, moving a player image to a location higher in memory makes it move down on the TV screen.
Lines 2500–2530 set the horizontal boundaries for Bonzo’s movement. Feel free to change these, if you like. Lines 2670–2680 set the stage for the vertical player movement we’ll encounter when the VBI routine is executed.
Having all this VBI code is fine, but how do we activate it? Please see Lines 2980–3010. Load the 6502’s Y-register with the low byte of the VBI starting address; load the X-register with the high byte; and load the accumulator with the decimal value 7. JSR through the SETVBV vector, and the VBI routine begins executing immediately. Line 3020 simply loops forever, or until you press RESET, whichever comes first.
There are actually two flavors of VBI routine, “deferred” and “immediate.” The 7 in the accumulator in Line 3000 tells the operating system that we wish to use a deferred VBI. This makes sure our routine doesn’t interfere with the system vertical blank procedures that must be performed. I always use the deferred VBI, and haven’t had any problems yet.
Now you can move Bonzo around the screen with the joystick, at least until he bumps into one of our boundaries. The car continues to zip by at a regular pace. Notice that the animation of both car and Bonzo is perfectly smooth and flicker-free, thanks to the VBI.
It would be nice if something happened when Bonzo and the car shared the same space, wouldn’t it? And, hey, maybe the game would be more fun if there were some walls for Bonzo to either hide behind or have to maneuver around. If you come back a mere thirty days from now, we’ll add those features.
The crux of the matter is to check for collisions among players, and between players and playfield objects—then come up with something interesting to do about it. I’ll also show you how you can change Bonzo’s shape and color, depending on whether he’s facing to the right, to the left, or dead ahead.
As advertised, the BASIC listing in this column is a sample program that contains a VBI to read joystick 1 and move player accordingly. The VBI routine itself is found in the DATA statements in Lines 110–310. You can include this directly in your own programs, if you pay attention to the other POKEs needed to make it all work correctly.
This program creates a player who resembles an orange hot-air balloon, using double-line resolution (hence the POKE 559,46 in Line 440). It might be interesting to study the comments in this BASIC listing, comparing its player/missile functions with those we’ve been doing in assembly language. Any time you see this many POKE and DATA statements in a BASIC program, assembly is likely to be a viable alternative.
There’s a small chance that your next BASIC program won’t require an animated orange balloon. Here are what some of the values in the DATA statements mean, so you can change them for your own purposes.
Line 140—the 200 is the right edge of the player area.
Line 180—the 50 is the left edge.
Line 210—the 14 is the top edge.
Line 230—the 18 is the number of bytes in the player image plus 3.
Line 260—the 100 is the bottom edge of the player area (remember, this is double-line resolution).
Line 270—the 17 is the number of bytes in the player image plus 2.
See you next time, when we wrap up this three-volume player/missile graphics extravaganza.
Listing 1. Assembly listing. 0100 ;Vertical Blank Interrupt Example 0110 ; 0120 ;by Karl E. Wieges 0130 ; 0190 ATRACT = $4D 0220 STICK0 = $0278 0300 SETVBV = $E45C 0310 XITVBV = $E462 0540 LEFT = $063B 0550 RIGHT = $063C 0630 ; 0640 ;******************************** 0650 ;VERTICAL BLANK INTERRUPT ROUTINE 0660 ;******************************** 0670 ; 0680 *= $4000 0690 ; 0700 VBI 0710 LDA #0 ;turn off 0720 STA ATRACT ;attract mode 0730 INC XPOSP0+1 ;move car 2 0740 INC XPOSP0+1 ;pixels to the 0750 LDA XPOSP0+1 ;right 0760 STA HPOSP0+1 0820 CLC 0830 LDA #$04 0840 ADC #PMG/256 0850 STA PLYRSTRT+1 0860 LDA YPOSP0 0870 STA PLYRSTRT 0940 STICKRIGHT 0950 LDA STICK0 ;stick reading 0960 AND #8 ;pointing right? 0970 BNE STICKLEFT ;no,check left 0980 LDA XPOSP0 ;yes,get X-pos 0990 CMP RIGHT ;at right edge? 1000 BEQ STICKUP ;yes,check for up 1010 INC XPOSP0 ;no, 1020 LDA XPOSP0 ;move one pixel 1030 STA HPOSP0 ;to the right 1170 CLC ;go check for 1180 BCC STICKUP ;stick up 1190 STICKLEFT 1200 LDA STICK0 ;stick reading 1210 AND #4 ;pointing left? 1220 BNE STICKUP ;no,check for up 1230 LDA XPOSP0 ;yes 1240 CMP LEFT ;at left edge? 1250 BEQ STICKUP ;yes,check for up 1260 DEC XPOSP0 ;no, move him 1270 LDA XPOSP0 ;one pixel to 1280 STA HPOSP0 ;the left 1420 STICKUP 1430 LDA STICK0 ;stick reading 1440 AND #1 ;pointing up? 1450 BNE STICKDOWN ;no,check down 1460 LDA YPOSP0 ;yes 1470 CMP TOP ;at top edge? 1480 BEQ VBIEXIT ;yes, leave VBI 1490 STA PLYRSTRT ;no, use Y as 1500 INC NBYTES ;offset pointer 1510 LDY #1 ;to move alien 1520 LOOPUP 1530 LDA (PLYRSTRT),Y ;take a byte 1540 DEY ;shift to 1 1550 STA (PLYRSTRT),Y ;byte lower 1560 INY ;in RAM 1570 INY ;point to next 1580 CPY NBYTES ;done yet? 1590 BNE LOOPUP ;no, go again 1600 DEC YPOSP0 ;yes, set new 1610 DEC NBYTES ;Y-position 1620 CLC 1630 BCC VBIEXIT ;leave VBI 1640 STICKDOWN 1650 LDA STICK0 ;stick reading 1660 AND #2 ;pointing down? 1670 BNE VBIEXIT ;no, leave VBI 1680 LDA YPOSP0 ;yes 1690 CMP BOTTOM ;at bottom edge? 1700 BEQ VBIEXIT ;yes, leave VBI 1710 STA PLYRSTRT ;no, move alien 1720 LDY NBYTES ;down just like 1730 DEY ;we moved him 1740 LOOPDOWN ; up, a byte 1750 LDA (PLYRSTRT),Y ;at a time 1760 INY 1770 STA (PLYRSTRT),Y 1780 DEY 1790 DEY 1800 BPL LOOPDOWN 1810 INC YPOSP0 ;set new Y-pos 1820 VBIEXIT 1830 JMP XITVBV ;leave VBI 1840 ; 2500 LDA #191 ;right edge of 2510 STA RIGHT ;alien area. 2520 LDA #56 ;left edge of 2530 STA LEFT ;alien area. 2670 LDA ALIEN ;set up for 2680 STA NBYTES ;vertical motion 2950 ; 2960 ;turn on VBI routine 2970 ; 2980 LDY #VBI&255 2990 LDX #VBI/256 3000 LDA #7 3010 JSR SETVBV 3020 END JMP END ;loop forever
Listing 2. BASIC listing. 10 REM ..... VBI Routine for reading 20 REM ..... joystick 0 and moving 30 REM ..... player 0 60 REM Reserve 3K of RAM for PMG 70 RAMTOP=PEEK(106)-12 80 PMBASE=RAMTOP*256 90 REM Read VBI data values and POKE into safe place in RAM 100 FOR I=0 TO 124:READ A:POKE PMBASE+128+I,A:NEXT I 110 DATA 104,160,10,162,6,169 120 DATA 7,76,92,228,173,120 130 DATA 2,41,8,208,16,165 140 DATA 203,201,200,240,30,230 150 DATA 203,165,203,141,0,208 160 DATA 24,144,20,173,120,2 170 DATA 41,4,208,13,165,203 180 DATA 201,50,240,7,198,203 190 DATA 165,203,141,0,208,173 200 DATA 120,2,41,1,208,24 210 DATA 165,204,201,14,240,46 220 DATA 160,1,177,204,136,145 230 DATA 204,200,200,192,18,208 240 DATA 245,198,204,24,144,28 250 DATA 173,120,2,41,2,208 260 DATA 21,165,204,201,100,240 270 DATA 15,160,17,177,204,200 280 DATA 145,204,136,136,192,255 290 DATA 208,245,230,204,76,98 300 DATA 228,104,160,98,162,228 310 DATA 169,7,76,92,228 320 REM Change 3rd number in VBI to low byte of 330 REM starting location for VBI routine in RAM 340 POKE PMBASE+130,10+128 350 REM Change 5th number to 360 REM corresponding high byte 370 POKE PMBASE+132,RAMTOP 380 REM Protect PMG area from accidental destruction 390 POKE 106,RAMTOP 400 REM Tell computer where to find player data 410 POKE 54279,RAMTOP 420 GRAPHICS 3:REM Set up a graphics screen 430 POKE 704,56:REM Set player 0 color to orange 440 POKE 559,46:REM Enable PMG 450 POKE 53277,3:REM Turn on players and missiles 460 REM Clear out player 0 area 470 FOR I=PMBASE+512 TO PMBASE+640:POKE I,0:NEXT I 480 REM Initialize player 0 X-position 490 X=100:POKE 203,X:POKE 53248,X 500 REM Initialize player 0 Y-position 510 Y=50:POKE 204,Y:POKE 205,RAMTOP+2 520 REM Read in player 0 bit image data 530 FOR I=1 TO 15:READ A:POKE PMBASE+512+I+Y,A:NEXT I 540 DATA 60,126,255,255,255 550 DATA 255,189,129,66,66 560 DATA 36,36,36,24,24 570 REM Turn on VBI routine by machine language call 580 A=USR(PMBASE+128) 590 REM Turn off VBI routine with this 600 REM machine language call 610 REM A=USR(PMBASE+243) 620 END
A.N.A.L.O.G. ISSUE 50 / JANUARY 1987 / PAGE 55
Over the past two months, we’ve learned something about how to create and animate shapes in assembly language, using the Atari’s player/missile graphics (PMG) capability. This was all in the context of my silly game concept, “Attack of the Suicidal Road-Racing Aliens.” I warned you this would never evolve into a complete (and worthwhile) game, but I hope you discovered how to apply some of these ideas to your own projects.
This month, I conclude the PMG trilogy. I’ll spice up the vertical blank interrupt (VBI) routine we wrote last time, so the alien (dubbed Bonzo in the last installment) is facing the direction he’s moving. Also, the featureless graphics 3 playfield will be embellished with a few blue lines, from whence we’ll see how to handle collisions between Bonzo and the terrain. Finally, I’ll show you how to process a collision between Bonzo and the car he so desperately wishes would run him over (nobody ever claimed Bonzo was very bright).
A few notes on programming tactics wrap up this month’s adventure. As before, begin by booting your assembler and loading the source code you built from last month’s column (which itself included many lines from the previous month). Then enter the lines in the assembly listing in these pages, and save the whole thing. Assemble the result, and Bonzo responds to the whim of your joystick. The starting address to execute the composite program is, as usual, $5000.
Let’s look first at the VBI enhancements. We already have a complete routine for moving Bonzo every sixtieth of a second, based on which direction the joystick is pressed. Now I’d like to have a different incarnation of Bonzo appear, depending on the direction he’s moving. I designed left- and right-facing Bonzos to supplement our standard forward-facing fellow. Bonzo’s chameleon-like character will result in his assuming a different color in each direction, too.
The player byte patterns for the left and right versions of Bonzo are found in Lines 4990–5090 of this month’s listing. As with the original alien shape, these are stored in the unused portion of RAM set aside for player/missile graphics, beginning at address $3000. Now, we just need to modify the VBI routine, to copy the desired player shape into the RAM block for player 0 (Bonzo) and set the player color each time the VBI is executed. The default shape (if he isn’t moving at all) will be the standard, forward-facing form.
Lines 770–900 commence each pass through the VBI routine, by loading the default shape for Bonzo and making him yellow (color 28). This way, the forward form is there—unless an appropriate joystick deflection is detected.
Lines 1040–1160 are inserted into the VBI code, for the routine to move Bonzo 1 pixel to the right. These lines copy the shape labeled ALIENR (R is for “right”) into the RAM block for player 0, and also set the player color to blue. Lines 1290–1410 perform the same function with the ALIENL (guess what the L stands for) shape, although he comes out green in this incarnation. All three player-copying routines use a couple of page addresses (SHAPE and PLYSTRT) to copy the byte pattern into PMG RAM, using the very versatile indirect indexed addressing mode.
When certain conditions are met, this simple technique of copying player byte shapes into the correct RAM region is very useful. You can have many different player shapes stored, since they only require a few bytes each. You can perform some pretty elaborate animation by sequentially replacing the present player with a new, slightly different one. The VBI is a nice way to do this, thanks to both the regular timing it provides and the elimination of flicker from the animation.
In player/missile parlance, “collision” refers to the overlap of a pixel from a player (or missile) with a pixel from another player or missile, or with some nonbackground pixel on the playfield. Simply being adjacent doesn’t constitute a collision; the objects must actually overlap.
Let’s see…there are four players, four missiles and four foreground color registers. This gives us a lot of possible kinds of collisions. The Atari operating system has but 16 bytes set aside to keep track of collisions. The collisions monitored with these registers include player/player, player/playfield, player/missile and missile/playfield interactions. No registers are provided for the missile/missile or playfield/playfield collisions.
Tables 1 and 2 identify the sixteen collision registers and their contents following a collision. The four least significant bits of each byte are the interesting ones. Look at the first half of Table 1 for an example. The four registers shown handle missile/playfield collisions. If missile overlaps with a playfield object drawn using color register 0, then bit 0 of register $D000, M0PF, is set, resulting in a decimal value of 1. Subsequent collisions of the same variety have no additional effect. A collision between missile 0 and a playfield object using color register 1 sets bit 1, for a decimal value of 2.
Missile/Playfield and Player/Playfield Collision Registers
Address Color Reg. 0 Color Reg. 1 Color Reg. 2 Color Reg. 3 Missiles $D000, M0PF 1 2 4 8 $D001, M1PF 1 2 4 8 $D002, M2PF 1 2 4 8 $D003, M3PF 1 2 4 8 Players $D004, P0PF 1 2 4 8 $D005, P1PF 1 2 4 8 $D006, P2PF 1 2 4 8 $D007, P3PF 1 2 4 8
More than 1 bit can be set if the missile collided with objects drawn using different playfield color registers. The decimal content of the byte in such a case is the sum of all such hits. Clearly, testing for the various collisions is best handled using our old friends, the AND and CMP assembly instructions, as we discussed last month when reading the joystick. The other entries in the tables show similar behavior for the other collision registers. An anomaly is seen in Table 2, where the logical impossibility of, for example, player 0 colliding with itself means that address P0PL will never have a decimal value of 1.
Player/Playfield and Player/Missile Collision Registers.
Address Player 0 Player 1 Player 2 Player 3 Missiles $D008, M0PL 1 2 4 8 $D009, M1PL 1 2 4 $D00A, M2PL 1 2 4 8 $D00B, M3PL 1 2 4 8 Players $D00C, P0PL 0 2 4 8 $D00D, P1PL 1 0 4 8 $D00E, P2PL 1 2 0 8 $D00F, P3PL 1 2 4 0
One minor problem arises: the collision registers are not automatically zeroed from time to time, so you can end up with junk left over from earlier hits. The solution is to store any number into a register called HITCLR ($D01E), after doing all your testing of the collision registers. This process resets all collision registers to 0.
One collision we’re obviously interested in is the intersection of Bonzo and the car. After all, that’s the entire point of this ridiculous game. To make things more interesting, let’s draw some lines on the playfield, so that collisions between players and playfield objects become possible.
Lines 2720–2940 draw some lines on the screen, using color register 0. This code gets some help from a subroutine called PLOTPOINT in Lines 3540–3710, and another subroutine called DRAWLINE in Lines 3770–3850. These are slight modifications of the drawing routines introduced in issue 42’s Boot Camp.
The basic approach is to plot a point, draw a series of line segments connecting several additional points, then start the process again to draw a separate figure elsewhere on the screen. I built tables of the X- and Y-coordinates of the points to be plotted. These tables are labeled XVALUE and YVALUE, and occupy Lines 4700–4820 of the listing. Each table has a series of coordinates, ending with a -1. The plotting routine plots the first point (Lines 2740–2810), fetches the next one (Lines 2820–2840) and checks to see if it is negative (Line 2850). If not, a line is drawn from the last point to the present one and the next point is acquired (Lines 2860–2910).
If the current point is indeed -1, then the counter (Y-register value) is compared to a preset number of data values to be processed (Line 4660). This LIMIT value is one less than the total number of coordinate pairs, including all the -1 entries. If the contents of the Y-register equals LIMIT, then the plotting is complete. Otherwise, the next group of points is begun (Line 2940).
Now that the scene is set, let’s start checking for some action. The routine in Lines 3060–3120 examines the contents of register P0PL to see if player (Bonzo) has run into anything lately. A value in P0PL of 2 means that bit 1 is set, telling us Bonzo and the car have had a close encounter of the worst kind. Subroutine P0P1COL (player 0/player 1 collision) is called to take some action.
In the event that Bonzo and the car are not sharing the same space. Lines 3170–3220 perform an analogous check of register P0PF. This looks to see if Bonzo has stumbled into a blue wall. A value of 1 means that Bonzo is sitting on a playfield object drawn with color 1, a 2 means color 2, and a 4 means color 3. In this program, I don’t really care what color register was used for the playfield object, so I just test for a nonzero value in P0PF. You could certainly use the different values in P0PF and its analogous registers for the other players to perform different actions, based on what kind of object (color register) the player encountered.
The collision processing itself takes place in subroutines P0P1COL and P0PFCOL (Lines 4200–4330). All I do in this example is copy a new shape for Bonzo to indicate that he’s now deceased, and change the player color to show which kind of collision took place.
These subroutines call yet another subroutine, KILLED (Lines 4400–4570). KILLED copies the squashed alien shape into PMG RAM, using the familiar player-copying routine. The shape in question is called DEADALIEN (Lines 5130–5170). Also, KILLED clears the collision registers (Line 4520) and turns off the VBI routine (Lines 4530–4560). The subroutine that called KILLED sets the color of Bonzo’s remains to white for a car collision (Lines 4220–4230), or orange for a playfield collision (Lines 4310–4320).
The result of all this collision jazz is that, whenever Bonzo encounters any other nonblack item on the screen, the squashed Bonzo shape appears in a new color, and the car movement and joystick control of Bonzo’s position hah. To rerun the program, start over at address $5000. Obviously, in a real game, you’d want to take some other action when Bonzo dies—like change the score, subtract 1 from his remaining lives, play a sad song and pick up the action after a suitable delay. I’ll let you work out those details.
This completes our introductory venture into the world of player/missile graphics. We have seen how to design and use player shape byte patterns, and how to allocate a block of memory for the players to live in. The various registers in the operating system controlling PMG features like size and color were explained. We used a VBI to read the joystick and move Bonzo around his two-dimensional world. (The VBI will return in future issues, since lots of other neat things can be stuffed into a VBI.) Finally, we saw how to use the collision registers to figure out if Bonzo had gotten into any trouble.
One of the PMG topics we didn’t cover is use of the priority register, to control what happens when a player overlaps with another player or a playfield display. You can specify which object will appear to be in front of the other. You can also obtain a different color where two players overlap, permitting multicolored players. We’ve also neglected the entire topic of missiles, including the technique for combining all four missiles to form a fifth player. Perhaps another time…
I now turn you loose, to apply all these ideas to the program plans brewing in your brain. But first, a little advice.
You may have noticed that I use quite a few subroutines in my programs. Many of these aren’t really “subroutines” in the classic sense. Traditionally, the subroutine is a block of code that could be written once and called repeatedly, thereby reducing the total amount of program code. Another view of a subroutine is to regard it as a little module of code which performs a specific task, even if it’s only used once during program execution. The advantage is that the code needed for the function is a nice, tidy package, kept separate from the rest of the program.
Professional programmers practice a method known as “structured programming.” One facet of structured programming is called “top-down design.” This refers to the practice of designing a program as a relatively short and quite abstract main program, which calls a number of less abstract subroutines to perform a series of functions—which, themselves, may call still other subroutines, until finally the work gets done at a very detailed level.
There are great advantages to using a top-down design. You can decide what functions your program needs to perform, without having to define every single itty-bitty detail at the outset. You don’t even need to know what language you’ll be using. Programs are vastly easier to read, debug and modify (not that any of us ever have to make changes) when the different functions are separated in a logical way. And, the clearer a picture you have of what you’re trying to accomplish, even at a fairly abstract level, the more efficient your time spent at the keyboard will be.
I started using a top-down design approach after reading an excellent series of articles on structured programming in BASIC, published in the now-defunct Creative Computing in June-September 1984. I estimate that it has shortened the time I need to complete a program to about one-third what it would be otherwise. And the resulting program is much cleaner and easier to understand than my old ones.
Modern languages are designed to be highly structured, with this modular approach built right in. BASIC is usually less structured, and Atari BASIC is particularly weak. It lacks even a simple IF-THEN-ELSE type of control structure. The result is the ubiquitous “spaghetti code” of indecipherable GOTOs.
Assembly language is no better. However, with a little thought you can clean up your assembly programming quite a bit, by using a top-down approach. The trick is to write many short, task-oriented subroutines instead of one huge linear block of code. The first FORTRAN program I wrote using top-down design involved a very short main menu and about forty-five subroutines at four different levels of detail (main program calls A, which calls B, which calls C, which calls D). And I knew just what each subroutine was to accomplish before I wrote a single line of code! (I wound up with over 2500 lines of code.)
“Attack of the Suicidal Road-Racing Aliens” evolved over several months, preventing a complete structured approach—mainly because three months ago I didn’t know where we would end up. However, you can see some illustrations of what I’m talking about in this month’s example. Consider the routine in Lines 3060–3220 that loops forever, looking for collisions. If one takes place, an appropriate subroutine is called, either P0P1COL or P0PFCOL. Each of those routines in turn calls the subroutine KILLED. You can also envision them calling a sound-effects subroutine, then a scoring subroutine, then…I think you get the picture.
I cannot recommend the top-down approach strongly enough. It’s been a tremendous asset in my own programming, both professional and recreational. I think you’ll be pleased with the results.
Listing 1. Assembly listing. 0100 ;multiple players & collisions 0110 ; 0120 ;by Karl E. Wiegers 0130 ; 0170 PUTREC = $09 0180 DRAW = $11 0200 ROWCRS = $54 0210 COLCRS = $55 0230 ATACHR = $02FB 0380 P0PF = $D004 0400 P0PL = $D00C 0420 HITCLR = $D01E 0690 ; 0770 FORWARD 0780 LDA #ALIEN&255 ;make alien 0790 STA SHAPE ;face forward 0800 LDA #ALIEN/256 ;initially 0810 STA SHAPE+1 0820 CLC 0830 LDA #$04 0840 ADC #PMG/256 0850 STA PLYRSTRT+1 0860 LDA YPOSP0 0870 STA PLYRSTRT 0880 JSR COPYPLAYER 0890 LDA #28 ;make alien 0900 STA PCOLR0 ;yellow 0910 ; 0920 ;joystick checking begins here 0930 ; 1040 LDA #ALIENR&255 ;load alien 1050 STA SHAPE ;shape going 1060 LDA #ALIENR/256 ;to the 1070 STA SHAPE+1 ;right 1080 CLC 1090 LDA #$04 1100 ADC #PMG/256 1110 STA PLYRSTRT+1 1120 LDA YPOSP0 1130 STA PLYRSTRT 1140 JSR COPYPLAYER 1150 LDA #138 ;color him 1160 STA PCOLR0 ;blue 1290 LDA #ALIENL&255 ;load alien 1300 STA SHAPE ;shape for 1310 LDA #ALIENL/256 ;going left 1320 STA SHAPE+1 1330 CLC 1340 LDA #$04 1350 ADC #PMG/256 1360 STA PLYRSTRT+1 1370 LDA YPOSP0 1380 STA PLYRSTRT 1390 JSR COPYPLAYER 1400 LDA #200 ;color him 1410 STA PCOLR0 ;green 2690 ; 2700 ;draw some lines on the playfield 2710 ; 2720 LDA #1 ;color register 0 2730 STA ATACHR 2740 LDY #255 2750 FIRSTPOINT ; plot the first 2760 INY ;point in the 2770 LDA XVALUE,Y ;data table 2780 STA COLCRS 2790 LDA YVALUE,Y 2800 STA ROWCRS 2810 JSR PLOTPOINT 2820 NEXTPOINT 2830 INY ;aim at the 2840 LDA XVALUE,Y ;next point 2850 BMI CHKDONE ;if -1,then quit, 2860 STA COLCRS ;else draw 2870 LDA YVALUE,Y ;from last 2880 STA ROWCRS ;point to 2890 JSR DRAWLINE ;this one and 2900 CLC ;get next 2910 BCC NEXTPOINT ;point 2920 CHKDONE 2930 CPY LIMIT ;all points done? 2940 BNE FIRSTPOINT ;no, continue 3020 ; 3030 ;check for collisions between 3040 ;players 0 and 1 3050 ; 3060 CHECKCOL 3070 LDA P0PL ;if collision, 3080 CMP #2 ;then call 3090 BNE CHECKPF ;subroutine 3100 JSR P0P1COL ;to handle it 3110 CLC 3120 BCC CHECKCOL 3130 ; 3140 ;check for collisions between 3150 ;player 0 and playfield 3160 ; 3170 CHECKPF 3180 LDA P0PF ;if collision, 3190 BEQ CHECKCOL ;call another 3200 JSR P0PFCOL ;subroutine to 3210 CLC ;process it 3220 BCC CHECKCOL 3230 ; 3500 ;plot a point at current cursor 3510 ;position; save Y-register on 3520 ;the stack 3530 ; 3540 PLOTPOINT 3550 TYA 3560 PHA 3570 LDX #$60 3580 LDA #PUTREC 3590 STA ICCOM,X 3600 LDA #REG2&255 3610 STA ICBAL,X 3620 LDA #REG2/256 3630 STA ICBAL+1,X 3640 LDA #1 3650 STA ICBLL,X 3660 LDA #0 3670 STA ICBLL+1,X 3680 JSR CIOV 3690 PLA 3700 TAY 3710 RTS 3720 ; 3730 ;draw a line from last point to 3740 ;current cursor position - save 3750 ;Y-register on the stack 3760 ; 3770 DRAWLINE 3780 TYA 3790 PHA 3800 LDA #DRAW 3810 STA ICCOM,X 3820 JSR CIOV 3830 PLA 3840 TAY 3850 RTS 3860 ; 4170 ;subroutine to handle collisions 4180 ;between alien and car 4190 ; 4200 P0P1COL 4210 JSR KILLED ;change to dead, 4220 LDA #14 ;white alien 4230 STA PCOLR0 4240 RTS 4250 ; 4260 ;subroutine to handle collisions 4270 ;between alien and walls 4280 ; 4290 P0PFCOL 4300 JSR KILLED ;change to dead, 4310 LDA #40 ;orange alien 4320 STA PCOLR0 4330 RTS 4340 ; 4350 ;subroutine to copy dead alien 4360 ;shape when he hits something, 4370 ;clear collision registers, and 4380 ;turn off VBI routine 4390 ; 4400 KILLED 4410 LDA #DEADALIEN&255 4420 STA SHAPE 4430 LDA #DEADALIEN/256 4440 STA SHAPE+1 4450 CLC 4460 LDA #$04 4470 ADC #PMG/256 4480 STA PLYRSTRT+1 4490 LDA YPOSP0 4500 STA PLYRSTRT 4510 JSR COPYPLAYER 4520 STA HITCLR ;clear coll. 4530 LDY #XITVBV&255 ;turn off 4540 LDX #XITVBV/256 ;VBI routine 4550 LDA #7 4560 JSR SETVBV 4570 RTS 4580 ; 4620 REG2 .BYTE "C" 4630 ; 4640 ;counter for drawing on playfield 4650 ; 4660 LIMIT .BYTE 16 4670 ; 4680 ;tables of X-coordinates 4690 ; 4700 XVALUE 4710 .BYTE 3,9,9,13,-1 4720 .BYTE 10,21,-1 4730 .BYTE 24,24,35,-1 4740 .BYTE 25,34,34,22,-1 4750 ; 4760 ;tables of Y-coordinates 4770 ; 4780 YVALUE 4790 .BYTE 2,2,9,9,-1 4800 .BYTE 14,14,-1 4810 .BYTE 8,3,3,-1 4820 .BYTE 14,14,19,19,-1 4830 ; 4960 ; 4970 ;alien facing to left 4980 ; 4990 ALIENL 5000 .BYTE 14,60,24,62,61,61 5010 .BYTE 61,61,60,60,36 5020 .BYTE 36,36,108,0 5030 ; 5040 ;alien facing to right 5050 ; 5060 ALIENR 5070 .BYTE 14,60,24,124,188,188 5080 .BYTE 188,188,60,60,36 5090 .BYTE 36,36,54,0 5100 ; 5110 ;poor, squashed alien 5120 ; 5130 DEADALIEN 5140 .BYTE 17,33,34,150,84,57 5150 .BYTE 30,60,123,159,30 5160 .BYTE 52,86,151,36 5170 .BYTE 194,193,0
A.N.A.L.O.G. ISSUE 51 / FEBRUARY 1987 / PAGE 77
We’ve spent three months on a sojourn into the realm of player/missile graphics, the heart of Atari animation. The crux of the matter was to overlay bit patterns in some section of memory (the players) on top of a background display of the usual screen graphics in some other part of memory (the playfield). By manipulating the player RAM independently of the playfield RAM, we could make objects move around the screen.
So far, Boot Camp has limited its playfield displays to the sort of things you can make by using the POSITION/PRINT and PLOT/DRAWTO commands in BASIC, or their assembly language equivalents. But the Atari has another graphics trick lurking up its silicon sleeve, the ability to redesign characters printed on the screen. You say you don’t need a capital Q in your program, but you sure would like a little happy face? No problem. You may be familiar with the method in BASIC, and this month you’ll learn how to do it in assembly language.
So, get ready to explore the powerful techniques of character set manipulations. Along the way, we’ll add yet another item to your input/output toolbox. This is the ability to read data files from disk into whatever part of memory you want, using the sequential (as opposed to random, or direct) access method. Now, let’s get started with our usual tutorial…
You don’t need a lot of computer expertise to recognize a character on the monitor screen when you see one. A character is basically anything you can put on-screen by pressing one or more keys on the keyboard. The Atari, like most computers, lets you create 256 unique characters. Since you spotted the magic computer number of 256, you won’t be surprised when I tell you that these characters are numbered from 0 through 255.
The folks who made computers in the first place realized that it would be nice if there were some standards for representing characters in all computers and computer peripherals, such as printers. Well, they didn’t quite cover all computers (IBM mainframes are a notable exception), but all of the personal computers I know of adhere to the ASCII (American Standard Code for Information Interchange) standard.
There are really only 128 standard ASCII characters, numbered 0 through 127. Characters 0 through 31 are the control characters you get by holding down the CONTROL key while pressing a letter. Control characters usually don’t print anything recognizable, but the Atari designers chose to make these the block graphics symbols. ASCII characters 32 through 127 are the usual upper- and lowercase letters, numbers, punctuation marks and the other special symbols.
Characters 128 through 255 can be anything the computer designer chooses. On the Atari, these are the inverse video representations of characters 0 through 127. The complete ASCII character set with the Atari modifications is called the ATASCII set.
So, what is a character, really? Nothing more than a pattern of dots in an 8×8 grid. That’s 8 pixels horizontally by 8 television scan lines vertically. Recalling the Atari pixel resolution of 320 by 192 leads us to the inescapable conclusion that the Atari screen display can show 24 lines of 40 characters each. I imagine these numbers are familiar to you.
You may have read somewhere that graphics 0 (40×24 characters) is really the same mode as graphics 8 (320×192 pixels). The difference is that graphics 0 is a “character-mapped” mode, while graphics 8 is a “bit-mapped” mode. This has important implications for memory usage. A full screen of graphics requires 40×24=960 bytes, since any one of the 256 available characters is referenced by a single byte value. However, a full screen of graphics 8 consumes eight times as much memory—almost 8K of RAM. If you could change the shapes of the characters in graphics 0, you could achieve all the same displays you can get in graphics 8. You gain both the advantage of much lower RAM consumption and much quicker displays (printing is faster than drawing). Besides, you could use these new characters in the color text modes, graphics 1 and 2, and even in the multicolor character modes known as ANTIC 4 and 5 (graphics 12 and 13 on the XL machines).
The dot pattern in each character forms the shape you see on the screen in graphics 0. To define the shape of a character you need 8 bytes of RAM, 1 for each scan line. The first byte corresponds to the top scan line in the character and the eighth byte to the bottom scan line. Within a byte, the bit pattern translates into the horizontal 8-pixel dot pattern that appears on the screen in a single scan line. Bits which are set (1) appear as dots, while bits which are cleared (0) show the background color.
Even though there are 256 ATASCII characters, there are only 128 unique dot patterns. This is because characters 128 through 255 are the inverse video incarnations of the characters through 127. So we need to store dot patterns for 128 characters at 8 bytes per character…128×8=1024, which just happens to equal 1K, which just happens to equal four pages (1 page=256 bytes) of memory. Pretty convenient, eh? (You probably never thought you’d reach the point in your life where something like 1024 qualified as a “round number.”)
The standard ATASCII character set resides in ROM, beginning at address $E000 and continuing through address $E3FF. Unfortunately, the 128 characters are not stored in contiguous ATASCII sequence. Table 1 shows the actual order of storage. The reason for this oddity: in graphics 1 and 2, you only have access to half of the character set at a time (64 characters). The peculiar grouping Atari engineers selected lets you use either the uppercase letters, numbers and punctuation marks, or the lowercase letters and control characters. (Not both. Unless we get tricky. Display list interrupts. Remember?)
Table 1.—ROM character set storage sequence. ROM Address ATASCII Values Principal Contents $E000 32–63 numbers, punctuation marks $E100 64–95 capital letters $E200 0–31 control characters $E300 96–127 lowercase letters
But wait! I told you the standard character set was in ROM. That’s Read-Only Memory, and we can’t change anything in it. Whatever shall we do? No problem. We’ll simply copy the standard character set from ROM into RAM, and then do anything we want to it.
We must set aside four pages of RAM for our new character set, starting on a page boundary that’s evenly divisible by 2. Addresses like $4000 are nice. Oh, and let’s not forget to tell the computer where to find our new characters. Location $2F4 (756 decimal, known as CHBAS) must be loaded with the high byte of the starting location of our RAM character set, then all characters are taken from the byte patterns in the reserved block of RAM. You can have as many different character sets in RAM as you like, at 1K apiece. Just load CHBAS with the right value to use whichever set you want.
Incidentally, CHBAS is the shadow register for a hardware character set pointer at address $D409, CHBASE. You can store your RAM character set address directly into CHBASE in a display list interrupt (or DLI) routine, to use multiple character sets on the screen at once. We’ll discuss this more, later on.
In graphics 1 and 2, the contents of CHBAS tell the computer which half of the character set to use. Loading CHBAS with the high byte of the actual starting address of the whole set causes the computer to use the first half of the character set (uppercase letters, etc.) This is the default condition, with CHBAS containing $E0 for the ROM set. To use the upper half of the set, store the high byte plus 2 into CHBAS ($E2 for the ROM set).
Please turn now to the example program in Listing 1. In keeping with the philosophy of structured program design discussed last time, I’ve written several subroutines you can use in your own programs to perform specific tasks. Listing 1 includes a subroutine called MOVECHAR, which copies the entire ATASCII character set from ROM into RAM. The desired address for the new character set is specified in the equates list as RAMSET ($4000 in Listing 1), while the original location is termed ROMSET (always $E000).
Another subroutine in Listing 1 is called PRINT. This begins in Lines 1120–1130, by pointing to the RAM character set. Following this are the familiar instructions for performing a put record operation to IOCB #0, a procedure also referred to as printing a line of text on the screen. As written, you can print a line up to 512 characters long with subroutine PRINT. (Do you see why?) The line to be printed is called TEXT (Line 1300), and just shows five characters (ABCDE) we’ll use to illustrate how a redefined character set works. Now, how about that third subroutine tantalizingly named REDEFINE?
You can create a custom character set in two ways. One is to copy the ROM set into RAM, then modify selected characters by actually changing the contents of RAM, byte by byte. This is the approach we’re employing in Listing 1. The second method is to generate a complete new character set, with up to all of the 128 dot patterns changed. The latter technique is easiest when you use one of the many character set editor programs available commercially, and in magazines and books.
These editors usually let you change dot patterns using a joystick, and allow you to view characters in different graphics modes (sometimes in groups, handy when you’re building large images that are mosaics of several redefined characters). Character set editors usually let you save the complete character set, with whatever changes you made, in a disk file. These disk files are nine single-density sectors long—1024 bytes again. Once you have such a disk file (let’s call it a “font” file), you can simply load it from disk into the block of RAM you reserved for your character set, and off you go. The simplest way to do this is to read the disk file sequentially, using our old friend CIO, the Central Input/Output system. The example in Listing 2 uses this method, and we’ll get there shortly.
In Listing 1, we’re simply modifying the alphabet’s first five capital letters (A through E, in case you’re fuzzy tonight) to be a little different from the standard Atari representation. I drew out some dot patterns I found pleasing on graph paper, using the 8×8 grid. Figure 1 shows what the A looks like. Going across each scan line, I drew the dot pattern as a binary number, with 1 representing a dot, and 0 a blank. The top line of the A came out as 00111100 in binary, which corresponds to $3C hex or 60 decimal. Thus, the byte of my RAM character set that corresponds to the first byte of the capital letter A should be loaded with the value of decimal 60. I repeated this procedure for all eight scan lines of my five characters to get a group of forty numbers that must be stored in RAM in place of the standard byte contents for these characters. The values appear in decimal form in Listing 1, in the table named NEWCHAR (Lines 950–1040).
Figure 1. 0 0 1 1 1 1 0 0 0 1 1 0 0 1 1 0 1 1 1 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 0 0 1 1 1 0 1 1 0 0 0 0 0 0 0 0
So, where do I stuff these numbers? Your Atari BASIC manual tells you that the letter A is stored as character number 33 in ROM, even though its ATASCII number is 65. This means there are 264 bytes of character data between the start of the character set and the first byte of A. Remember that we start counting at 0, and you can compute that the 8 bytes defining the shape of an A are found at address RAMSET+264 through RAMSET+271. The data for the other four characters immediately follows the data for the A.
The purpose of subroutine REDEFINE in Listing 1 is now clear. It loads those forty data values into RAM, starting at address RAMSET+264. This substitutes my own dot patterns for the original letters A through E. (Yes, I know that Lines 830–880 really load the data beginning at RAMSET+264+39 and going backwards, but it’s shorter to code that way.) Now, whenever we print one of those letters (after telling the Atari to use our RAM character set, rather than its own ROM set), we’ll see my concepts of the letters. Assemble Listing 1 and run it, and you’ll see what I mean. Notice that all capital letters A through K on the screen are modified, not just the ones printed after we created the new character set. Press RESET when you wish to return to the normal display.
You can use this method to redefine any number of characters. It’s most convenient when you want to use the majority of the hardware character set intact, but with a few alterations. It works fine, for example, if you just wish to generate some terrain shapes for a game’s playing field. However, if you want to create a lot of new characters, it’s easier to use a character set editor that creates a disk file, and read it into RAM. This is appropriate if you want a whole new font, like a computer-style alphabet, or a fancy Gothic font for a medieval adventure game. Now let’s learn how to read disk files.
Think back to issue 43, if you were with us then. The theme for that month was performing floating-point arithmetic operations in assembly language. One of the operations discussed involved accepting keyboard input in the form of a string of characters typed by the user. The GETREC (GET a RECord) operation of CIO came to our aid.
Sequential disk I/O involves the same kind of PUTREC (for writing) and GETREC (for reading) operations as does screen I/O. In fact, that’s the whole point of having the CIO subsystem available: one sequential device behaves the same as another, be it a screen editor, keyboard, printer, or a cassette or disk file. (Of course, it’s hard to read from the printer or write to the keyboard.)
Today we want to read an existing disk file of character font information, placing the 1024 bytes right into our reserved block of RAM for the redefined character set. I’m assuming that you have access to a character set editor and have a redefined font file. My example refers to a disk file called FANCY.FNT (as in “font”). However, you can actually read any disk file this way, substituting the name of the file where I have FANCY.FNT. Of course, anything besides genuine font data will give you relatively bizarre-looking characters.
The program in Listing 2 shows the necessary techniques. Again, notice the subroutine-oriented structure of the program. The “main” program is merely a few instructions in Lines 430–550. Listing 2 is identical to Listing 1 in Lines 10–360 and, also. Lines 1110–1260 (the PRINT subroutine again), so you need not type everything in all over again.
Listing 2 has three new subroutines to perform, in a general way: the three operations involved in reading a disk file. These may be familiar to you from BASIC. First, the subroutine OPENIOCB opens the IOCB (Input/Output Control Block) of your choice. Next, subroutine READFILE actually reads the file and transfers the contents to our desired location in RAM. Finally, subroutine CLOSEIOCB closes the IOCB we opened in the first place. Each of these subroutines is written in such a way that you must load the 6502’s X-register with a number which equals 16 times the IOCB number you wish to use before the JSR instruction is executed. Notice that Lines 430, 500 and 520 all load X with 16, for IOCB #1.
The command bytes for the OPEN, GETREC and CLOSE operations are in the equates list (Lines 110–140). OPENIOCB needs to specify the OPEN command (Lines 660–670), and point to the device name. The device name (at label FNAME, Line 1470) includes both a disk drive number and the complete filename, D:FANCY.FNT, followed by the end-of-line (EOL) character. Finally, Lines 720–730 specify that we wish to open the file in read access mode by storing a 4 in auxiliary byte 1, ICAX1. A JSR to the CIOV entry point actually carries out our instructions (Line 740).
Other disk file opening options are 6 (read the disk directory), 8 (write a new file over the existing file), 9 (write by appending to the existing data in the file) and 12 (read and write, starting with the first byte in the file). Notice, please, that opening in access mode 8 destroys the existing file!
You might accidentally try to open a disk file that doesn’t exist. This, and other illegal operations, would trigger an error condition from CIO. Upon returning from the JSR CIOV instruction, the error number is in the Y-register. Since error numbers are all negative numbers (bit 7 is set), this also sets the negative flag in the processor status register. To check for an error, then, we just need to test the negative flag.
Lines 450–480 in Listing 2 check for the negative flag being set. If not, control branches to label NOERR1, and execution continues. Otherwise, subroutine OPENERROR is called. This routine, in Lines 1510–1660, simply prints an appropriate error message on-screen and returns. Then Lines 470–480 perform an unconditional branch to the infinite loop at label END, terminating execution of the program. In real life, you’d want to trap for all possible errors and take appropriate action. For now, this simple trap will hold us. We’ll talk more about disk I/O and error messages in a future issue.
Now to fish some stuff out of the file. The READFILE subroutine issues a GETREC command. We need to tell the OS where to put data from the disk. Again, we want to store it at address RAMSET ($4000), so that’s our buffer address (Lines 850–880). Next, we tell CIO how many bytes of data to get. We know the file contains 1024 bytes, so we set the low byte of the buffer length to 0 and the high byte to 4 (Lines 890–920). The JSR through CIOV transfers the character set data from disk to RAM. Unless you anticipate doing more I/O operations from the disk file, it is a good practice to close it. This frees up the IOCB for another use and also prevents you from inadvertently accessing the wrong file. Subroutine CLOSEIOCB (Lines 1000–1040) is a concise way to tidy up after ourselves.
And there you have it! The only other thing Listing 2 does is display most of the standard characters on-screen, using the long strings at the label TEXT (Lines 1340–1420). This is so you can examine what you’ve loaded from the disk. You can actually type anything you want in Lines 1350–1410, but don’t omit the EOL character. I hope you were able to find a real font file. Again, press RESET to kill this display.
Are you up for a challenge? Try modifying Listing 2 to include a DLI that changes the character set pointer (CHBASE) from the ROM set to your own RAM set partway down the screen. Print the same text lines above and below the DLI mode line. You may wish to refer to the Boot Camp columns in previous issues for help. Issue 41 showed how to print text at any position on the screen. Listing 1 in issue 46 described how to enable a DLI at a particular mode line in a graphics screen. The issue 46 routine called DLI2 in Listing 2 illustrated a character set change using a DLI.
This DLI technique is used in many character set editors, so you can simultaneously see the original ROM set, along with the RAM set you’re editing. If you can write a program to these specifications, you’ve really tied together a lot of concepts. Treat yourself to an ice cream cone with sprinkles.
You may have heard about “color artifacting” in graphics 8. This refers to the phenomenon of seeing colored vertical lines in what is, nominally, a monochrome graphics mode. The colors are different, depending on whether the line is drawn on an odd- or even-numbered point.
Since graphics 0 is really the same as graphics 8, you can also see artifacting. You don’t see it in the ROM character set, because all those characters have at least two adjacent horizontal pixels lit in any vertical segment. This gives the pure foreground color you expect. However, if you redefine some characters to have vertical lines (as in an 1) only 1 pixel wide, you’ll see different colors. You can use this technique to produce colored characters in graphics 0, but at a loss in resolution. Try it.
You probably thought I had forgotten about “Attack of the Suicidal Road-Racing Aliens,” but you’re wrong. Another nice application of redefined character sets is in our graphics programs. We can transform a letter A into a tree shape, and print it in graphics 2 as dark green on a light green background. Or we can make some little figures, using the two multicolored text modes, and supplement our limited player supply for animation, or create highly detailed scenery. Virtually all the arcade-style games you’ve seen make heavy use of custom character sets, and there’s no limit to the visual effects you can produce.
I suggest you get your hands on a character set editor and hack away. Just imagine the vivid screen displays you can create with a custom display list, a creatively modified character set, some animated players and some DLIs for additional seasoning. For extra flair, consider adding a vertical blank interrupt routine to create some pulsing color effects. You might try your hand at designing a playing screen for “…Aliens”—I haven’t come up with a good one yet.
Your toolbox of Atari graphics techniques is getting pretty crowded, but there’s more to come. We haven’t even touched on horizontal and vertical scrolling techniques yet. And, sometime soon, I’ll take a break from graphics and talk about the macro part of macro assemblers. If you have any preferences for topics or techniques covered in future issues, please drop me a line.
Listing 1. Assembly listing. 0010 ;Listing 1 for Character Set 0020 ;Redefinition Examples 0030 ; 0040 ;by Karl E. Wiegers 0050 ; 0060 .OPT OBJ,NO LIST 0070 ; 0080 ; 0090 ;Equates for IOCB operations 0100 ; 0110 OPEN = $03 0120 GETREC = $05 0130 PUTREC = $09 0140 CLOSE = $0C 0150 EOL = $9B 0160 ; 0170 ;pointer to character set 0180 ; 0190 CHBAS = $02F4 0200 ; 0210 ;equates for IOCB addresses 0220 ; 0230 ICCOM = $0342 0240 ICBAL = $0344 0250 ICBAH = $0345 0260 ICBLL = $0348 0270 ICBLH = $0349 0280 ICAX1 = $034A 0290 ICAX2 = $034B 0300 CIOV = $E456 0310 ; 0320 ;redefined character set will 0330 ;start at $4000 and go to $43FF 0340 ; 0350 RAMSET = $4000 0360 ; 0370 ;ROM character set starts at 0380 ;$E000 and goes to $E3FF 0390 ; 0400 ROMSET = $E000 0410 ; 0420 ;****************************** 0430 ; PROGRAM BEGINS HERE 0440 ;****************************** 0450 ; 0460 *= $5000 0470 ; 0480 JSR MOVECHAR 0490 JSR REDEFINE 0500 JSR PRINT 0510 END JMP END 0520 ; 0530 ;****************************** 0540 ; SUBROUTINES BEGIN HERE 0550 ;****************************** 0560 ; 0570 ;subroutine to copy entire ROM 0580 ;character set into RAM at your 0590 ;designated address (RAMSET) 0600 ; 0610 MOVECHAR 0620 LDX #0 0630 CHLP1 0640 LDA ROMSET,X 0650 STA RAMSET,X 0660 LDA ROMSET+$0100,X 0670 STA RAMSET+$0100,X 0680 LDA ROMSET+$0200,X 0690 STA RAMSET+$0200,X 0700 LDA ROMSET+$0300,X 0710 STA RAMSET+$0300,X 0720 INX 0730 BNE CHLP1 0740 RTS 0750 ; 0760 ;subroutine to redefine selected 0770 ;group of characters in the RAM 0780 ;set; in this case, 5 characters 0790 ;totalling 40 bytes; replaces 0800 ;characters 33-37 in RAM set 0810 ; 0820 REDEFINE 0830 LDX #39 0840 RDLP1 0850 LDA NEWCHAR,X 0860 STA RAMSET+264,X 0870 DEX 0880 BPL RDLP1 0890 RTS 0900 ; 0910 ;table of byte data for the new 0920 ;characters, 8 bytes per char. 0930 ; 0940 NEWCHAR 0950 .BYTE 60,102,198,198 0960 .BYTE 198,102,59,0 0970 .BYTE 62,99,99,99 0980 .BYTE 126,99,126,0 0990 .BYTE 60,99,96,96 1000 .BYTE 96,99,62,0 1010 .BYTE 60,99,99,99 1020 .BYTE 99,102,124,0 1030 .BYTE 60,98,96,124 1040 .BYTE 96,127,60,0 1050 ; 1060 ;subroutine to print a string of 1070 ;text on the screen (up to 512 1080 ;characters long), using the RAM 1090 ;character set 1100 ; 1110 PRINT 1120 LDA #RAMSET/256 ;point to 1130 STA CHBAS ;RAM charset 1140 LDX #0 ;IOCB #1 1150 LDA #PUTREC ;operation is 1160 STA ICCOM,X ;PUT a RECord 1170 LDA #TEXT&255 ;point to the 1180 STA ICBAL,X ;text string 1190 LDA #TEXT/256 1200 STA ICBAH,X 1210 LDA #0 ;print up to 1220 STA ICBLL,X ;2*256=512 1230 LDA #2 ;bytes 1240 STA ICBLH,X 1250 JSR CIOV ;go do it! 1260 RTS 1270 ; 1280 ;string being printed on screen 1290 ; 1300 TEXT .BYTE "ABCDE",EOL
Listing 2. Assembly listing. 0010 ;Listing 2 for Character Set 0020 ;Redefinition Examples 0030 ; 0040 ;by Karl E. Wiegers 0050 ; 0060 .OPT OBJ,NO LIST 0070 ; 0080 ; 0090 ;equates for IOCB operatios 0100 ; 0110 OPEN = $03 0120 GETREC = $05 0130 PUTREC = $09 0140 CLOSE = $0C 0150 EOL = $9B 0160 ; 0170 ;pointer to character set 0180 ; 0190 CHBAS = $02F4 0200 ; 0210 ;equates for IOCB addresses 0220 ; 0230 ICCOM = $0342 0240 ICBAL = $0344 0250 ICBAH = $0345 0260 ICBLL = $0348 0270 ICBLH = $0349 0280 ICAX1 = $034A 0290 ICAX2 = $034B 0300 CIOV = $E456 0310 ; 0320 ;redefined character set will 0330 ;start at $4000 and go to $43FF 0340 ; 0350 RAMSET = $4000 0360 ; 0370 ;******************************* 0380 ; PROGRAM BEGINS HERE 0390 ;******************************* 0400 ; 0410 *= $5000 0420 ; 0430 LDX #16 0440 JSR OPENIOCB 0450 BPL NOERR1 0460 JSR OPENERROR 0470 CLC 0480 BCC END 0490 NOERR1 0500 LDX #16 0510 JSR READFILE 0520 LDX #16 0530 JSR CLOSEIOCB 0540 JSR PRINT 0550 END JMP END 0560 ; 0570 ;******************************* 0580 ; SUBROUTINES BEGIN HERE 0590 ;******************************* 0600 ; 0610 ;subroutine to open an IOCB for 0620 ;read access - need to load 0630 ;with IOCB# * 16 first 0640 ; 0650 OPENIOCB 0660 LDA #OPEN ;operation is to 0670 STA ICCOM,X ;OPEN an IOCB 0680 LDA #FNAME&255 ;point to 0690 STA ICBAL,X ;name of disk 0700 LDA #FNAME/256 ;file to open 0710 STA ICBAH,X 0720 LDA #4 ;open for 0730 STA ICAX1,X ;read access 0740 JSR CIOV ;go do it! 0750 RTS 0760 ; 0770 ;subroutine to read 1024 bytes 0780 ;from open IOCB and put in RAM 0790 ;at address RAMSET. IOCB# * 16 0800 ;must be in X-register first. 0810 ; 0820 READFILE 0830 LDA #GETREC ;operation is 0840 STA ICCOM,X ;GET a RECord 0850 LDA #RAMSET&255 ;will store 0860 STA ICBAL,X ;at address 0870 LDA #RAMSET/256 ;RAMSET 0880 STA ICBAH,X 0890 LDA #0 ;want to get 0900 STA ICBLL,X ;4*256=1024 0910 LDA #4 ;bytes 0920 STA ICBLH,X 0930 JSR CIOV ;go do it! 0940 RTS 0950 ; 0960 ;subroutine to close an IOCB. 0970 ;X-register must contain the 0980 ;IOCB# * 16 first. 0990 ; 1000 CLOSEIOCB 1010 LDA #CLOSE 1020 STA ICCOM,X 1030 JSR CIOV 1040 RTS 1050 ; 1060 ; 1070 ; 1080 ;same PRINT subroutine that we 1090 ;used in Listing 1 1100 ; 1110 PRINT 1120 LDA #RAMSET/256 1130 STA CHBAS 1140 LDX #0 1150 LDA #PUTREC 1160 STA ICCOM,X 1170 LDA #TEXT&255 1180 STA ICBAL,X 1190 LDA #TEXT/256 1200 STA ICBAH,X 1210 LDA #0 1220 STA ICBLL,X 1230 LDA #2 1240 STA ICBLH,X 1250 JSR CIOV 1260 RTS 1270 ; 1280 ;print this long text string so 1290 ;you can see what the characters 1300 ;you loaded from the disk file 1310 ;look like (actually, type any 1320 ;thing you want in 1350-1410) 1330 ; 1340 TEXT 1350 .BYTE "ABCDEFGHIJKLM" 1360 .BYTE "NOPQRSTUVWXYZ" 1370 .BYTE "abcdefghijklm" 1380 .BYTE "nopqrstuvwxyz" 1390 .BYTE "1234567890" 1400 .BYTE "!#$%&'@()<>" 1410 .BYTE "-=+*_|\^,./?" 1420 .BYTE EOL 1430 ; 1440 ;complete name of the file with 1450 ;your character set data 1460 ; 1470 FNAME .BYTE "D1:FANCY.FNT",EOL 1480 ; 1490 ;subroutine to handle open error 1500 ; 1510 OPENERROR 1520 LDX #0 ;print message on 1530 LDA #PUTREC ;screen 1540 STA ICCOM,X 1550 LDA #OETEXT&255 ;error text 1560 STA ICBAL,X ;to print 1570 LDA #OETEXT/256 1580 STA ICBAH,X 1590 LDA #80 ;80 chars, or 1600 STA ICBLL,X ;EOL, whichever 1610 LDA #0 ;comes first 1620 STA ICBLH,X 1630 JSR CIOV 1640 RTS 1650 OETEXT 1660 .BYTE "Error on open",EOL
A.N.A.L.O.G. ISSUE 52 / MARCH 1987 / PAGE 74
Not long ago, I attended my first meeting of an Atari users’ group, ACORN (Atari Computer Owners of Rochester, New York). ACORN’s 8-bit disk librarian, Nick Cup, demonstrated his clever BASIC program illustrating some nifty aspects of character set animation. The program is an electronic birthday card Nick wrote for his girlfriend, Judy. It shows a man and woman holding hands, bending toward each other and kissing (repeat ad infinitum, or ad System Resetum). Nick was kind enough to share his BASIC source code with me, and today I present the same program in assembly language.
Nick’s program extends last month’s introduction to redefined character sets in assembly programs. Before, we talked about creating a modified character set for use in graphics 0; Nick’s program uses the five-color text mode called ANTIC 5. Last time, we copied the character set from ROM into RAM, then changed just a few characters. We also discussed loading a complete character set from disk. In today’s program, the entire custom character set resides within the assembly program as a bunch of .BYTE statements. The bad news is that you have to type them all in. We’ll also cover random numbers, animation methods, timing loops and some clever tricks Nick played on the Atari operating system. Onward…
Have you ever discovered that four (okay, five) players just can’t handle the animation needs of a program? Character sets come to the rescue! You can create a set of characters with just the shapes you want and move them around the screen along with the players. In BASIC, you’d PRINT these characters; in assembly, we’ll rely on the PUTREC command of CIO—the same thing, really.
There are several steps in designing a character animation process. Animation consists of the rapid replacement of one image with another that is slightly different. If the changes are subtle and the substitution rapid, your eye and brain will blur the process into a smooth movement. In practice, we’re limited by the complexity of the images and the speed of the overall process.
In today’s example, we begin with a man (Nick) and a woman (Judy) facing each other and holding hands. That’s image 1 (call it STANDING). The final image shows them kissing (okay, call it KISSING). To smooth things out, we need a couple of intermediate images that show the happy couple approaching each other (BENDING) and nearly touching (ALMOST). If we print these images successively at the same place on the screen, we’ll get the animation we seek. Our complete animation sequence consists of: STANDING, BENDING, ALMOST, KISSING, ALMOST, BENDING and STANDING again. Repeating this pattern over and over gives a more or less continuous motion. To avoid the dreadful boredom of rhythmic kissing, we’ll pause for random times at the STANDING and KISSING stages.
But how are we going to create the four images we need? And what graphics mode should we use? In this case, Nick chose ANTIC mode 5, also known as graphics 13 on the XL/XE machines. This mode has pixels the same size as graphics 2 (ANTIC 7), 16 by 16. ANTIC 5 produces characters in four colors, depending on the bit pattern (00, 01, 10, 11) of each pair of bits in the byte defining one scan line of the character. (Actually, each byte defines two scan lines, just as in graphics 2; ANTIC 4 is the analogous mode with one scan line per data byte.) Each bit pattern selects a different color register. The 01 selects color register COLOR0, at address $2C4; 10 chooses register COLOR1 ($2C5); 11 calls on register COLOR2 at $2C6; and 00 displays the background color from $2C8. An ANTIC 5 character displayed in reverse video uses color register 3 ($2C7) for the 11-bit patterns, and the other patterns are unchanged.
Now, how do we redefine the characters to look the way we want? By using a good character set editor program, of course. You need one that handles ANTIC 5, and preferably one that lets you design blocks of several characters at a time. A good choice is “Antic Aerobics” by Charles Brannon, which appeared in the October 1983 issue of COMPUTE! magazine.
Nick spent some time with “Antic Aerobics.” He decided to use a block of characters 8 wide by 4 deep for the movement part of the animation. Two pairs of legs (Nick’s and Judy’s) are stationary during the process, and that took an 8×2 block of characters. Let’s see—8 times 4 is 32, times 4 images is 128, plus 16 for the legs equals 144 characters needed. But we only have 128 characters in a set. Whatever shall we do?
We have two choices. We could use some display list interrupts to employ several character sets in one screen. (Really, we do this anyway, so the normal letters saying Happy Birthday appear above the animated display.) Fortunately, though, a number of the characters in the 8×4 boxes are blanks, and a few others can be used in more than one place. So we can get away with only about 120 or so unique characters. There are some morals here:
(1) Always leave a blank character in your redefined set. You really only have 127 to play with.
(2) Plan ahead. Some careful work with graph paper, or a good eye for shapes (which I don’t have) might keep you from running out of available character slots prematurely. Now, let’s dive into the program and see some other tricks of the trade.
As usual, we must figure out how to allocate memory for our tasks. We need room for the character set, 1024 bytes; a custom display list of about 20 bytes; a display list interrupt of about a dozen bytes; screen display RAM for a mixed ANTIC 5 and 7 display rounding up to 512 bytes; the text strings that will be printed as our four images plus legs need about 150 bytes; and, lest we forget, a program of some kind will need space.
Since we all now have plenty of memory in our Ataris, I like to be generous with RAM. I put the character set from address $4000 to $43FF (Line 580). And there they all are, from Lines 600–2700, at 8 bytes apiece. The ones with eight Os are blanks, or else unused in this redefined set.
Following the character set is the display list, at $4400 (Lines 2760–2820). This display consists of six mode lines of ANTIC 7 and six of ANTIC 5. The standard character set is used in the ANTIC 7 segment, so we need a display list interrupt (DLI) in the last mode line of that segment (the -135 in Line 2800).
Note the use of the .WORD operative. This places the data following the operative into 2 consecutive bytes, in low-byte/high-byte order. In Line 2790, I load the address of the beginning of screen RAM into the display list. Line 520 of the listing defined SCRRAM as beginning at location $4800. Similarly, Line 2820 loads the address of the display list itself into the final 2 bytes of the display list, as is proper. I imagine you can comprehend this display list from our earlier discussions.
The DLI is at address $4500. It simply switches to our custom character set halfway down the screen, as we have seen done before. Address CHSET is defined in Line 620.
Finally, the program code begins at $5000. First, Lines 3090–3130 zero out two pages of screen RAM, which is enough for this display list (6*20+6*40=360 bytes needed). Lines 3140–3170 set the pointer to the DLL Lines 3180–3190 make the cursor invisible.
Line 3200 performs an important, yet obscure function. You’ll recall that a number of the characters in the standard set are used for cursor movement, screen clearing and the like. We can use these in our modified set, but we need to tell the operating system to disregard any standard function they have and just display the dot patterns we assign to that character number. Storing any nonzero value in location DSPFLG ($2FE, decimal 766) takes care of this problem. If you omit this step, funny things happen.
Lines 3210–3240 point toward the beginning of screen RAM. Lines 3250–3260 turn off the display screen while we set things up. The first big trick takes place in Line 3270.
You may recall from earlier issues that we opened the display screen device, S:, for a particular graphics mode, then printed using IOCB #6. Nick did something sneakier. He told the computer to use this mixed ANTIC 6 and 7 display list, but he let the computer continue to think it was in graphics mode 0. This has a couple of ramifications. First, we can use IOCB #0, the default for printing to device E:, and have no need to open the screen. Second, the settings of the left and right margins, which only have meaning in graphics 0, are respected.
Skip down to Lines 3560–3590, and you’ll see that we closed the margins down so the printable area is only eight characters wide. By printing a string of all thirty-two characters for one Nick-and-Judy image now, you’ll get a stack of four lines of eight characters each. This keeps us from having to build 20-byte long strings of characters to fill an ANTIC 5 line, with all blanks except the image part in the middle of the screen. Clever, eh?
Lines 3280–3370 load the color registers with the desired values. Lines 3380–3410 activate our custom display list, and Lines 3420–3430 activate the DLL The screen comes back to life in Lines 3440–3450. The Happy Birthday greeting is printed in Lines 3480–3550. (Hint: change the name in Line 4800 to impress your own special friend if he or she isn’t named Judy)
The rest of the program performs the animation, between MAINLOOP in Line 3620 and the JMP MAINLOOP instruction in Line 4090. First, print the STANDING image, in Lines 3630–3670. We use subroutines to help us save code, as with the POSITION routine (Lines 4320–4370), since we’re printing each image in the same place on the screen. The PRINTLINE subroutine should be familiar from earlier columns. Another subroutine, DOLEGS, performs multiple functions. What’s the first one? It prints the constant shapes of the legs, of course, in Lines 4440–4530. We have to print these each time, because the changing upper body image overwrites the top row of legs, due to the carriage return (EOL) character.
Next, DOLEGS pauses for a fixed period of time, by using the DELAY subroutine. DELAY (Lines 4650–4720) relies on the internal real-time clock in the Atari operating system. This clock uses the 3 bytes at addresses $12–$14. The least significant byte, $14, is incremented during each vertical blank interval, or sixty times per second (1/60 of a second is called a “jiffy”—honest). DELAY begins by setting address $14 to 0, then looping until the address reaches some value in the data byte I called TIMER, before returning. In Lines 4540–4550, I stored a 3 in TIMER, indicating that I want each image to hang around for three jiffies before the program continues. Increasing this value slows down the action; decreasing it to 2 or 1 shows how enthusiastic Nick and Judy can be.
To make things less repetitive, we’re going to show the STANDING image a bit longer before printing the next image. Location $D20A (RANDOM) acts as a random number generator, producing a value between and 255 each time you look in it. Line 3690 fetches such a random number. After trying this for a while, I felt the action was slowed down too much, so I decided to take the random number thus retrieved, and divide it by 2 before using it.
The LSR A instruction in Line 3700 divides the contents of the accumulator by 2. Really, it performs a Logical Shift Right operation. Each bit in the accumulator is moved to the adjacent less significant position. The value of bit 0 is moved into the carry flag of the processor status register, and a 0 is placed in bit 7, like this:
┌───┬───┬───┬───┬───┬───┬───┬───┐ 0 ──► │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │ ──► C └───┴───┴───┴───┴───┴───┴───┴───┘ │ ▲ │ ▲ │ ▲ │ ▲ │ ▲ │ ▲ │ ▲ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
If you think about this operation, you should realize that the net result is to divide the contents of the accumulator by two. Anyway, I store the resulting value into TIMER and do another delay in Lines 3710–3720.
Continuing with the main loop, we print the BENDING position with legs and pause (paws?) in Lines 3730–3790. Following closely is the ALMOST position in Lines 3800–3850. Finally, we reach that special moment when lips touch (Lines 3860–3910). Another random delay seemed in order here (Lines 3920–3950). Finally, run the whole operation in reverse, with ALMOST and BENDING, and loop back around to repeat the STANDING position. As usual, it takes a RESET to break out of the program.
You may notice that the characters look a bit blocky. You can double the vertical resolution by using ANTIC 4 rather than ANTIC 5. The bad news is that if you want the same size images, you’ll need twice as many individual characters (sixty-four per image rather than thirty-two). That means two RAM character sets and twice as many hours with a character set editor. You make the choice. Personally, all my girlfriend noticed was that the name was Judy and not Chris.
I’ll recap what we covered concisely. ANTIC 5. Character set animation. Typing .BYTE statements. The .WORD directive. DSPFLG. Random numbers. Real-time clock delay loop. LSR instruction. Outwitting the operating system. Kissing. Need we say more?
Listing 1. Assembly listing. 10 ;Character Graphics Anination 20 ;in Antic Mode 5 30 ; 40 ; by Karl E. Wiegers 50 ; 60 .OPT NO LIST 70 ; 80 ;CIO command equates 90 ; 0100 OPEN = $03 0110 PUTREC = $09 0120 EOL = $9B 0130 ; 0140 ;equates for timing delay loop 0150 ; 0160 RTCLOK = $14 0170 TIMER = $CB 0180 ; 0190 ;screen control equates 0200 ; 0210 LMARGN = $52 0220 RMARGN = $53 0230 ROWCRS = $54 0240 COLCRS = $55 0250 DINDEX = $57 0260 SAVMSC = $58 0270 VDSLST = $0200 0280 SDMCTL = $022F 0290 COLOR0 = $02C4 0300 SDLSTL = $0230 0310 CRSINH = $02F0 0320 DSPFLG = $02FE 0330 ; 0340 ;CIOV address equates 0350 ; 0360 ICCOM = $0342 0370 ICBAL = $0344 0380 ICBLL = $0348 0390 ICAX1 = $034A 0400 ICAX2 = $034B 0410 CIOV = $E456 0420 ; 0430 ;hardware registers used 0440 ; 0450 RANDOM = $D20A 0460 CHBASE = $D409 0470 WSYNC = $D40A 0480 NMIEN = $D40E 0490 ; 0500 ;screen RAM starts at $4800 0510 ; 0520 SCRRAM = $4800 0530 ; 0540 ;******************************* 0550 ;redefined Antic 5 character set 0560 ;******************************* 0570 ; 0580 *= $4000 0590 ; 0600 ;chars 0-7 0610 ; 0620 CHSET 0630 .BYTE 0,0,0,0,0,0,0,0 0640 .BYTE 0,0,0,2,10,42,42,170 0650 .BYTE 0,0,0,0,0,0,0,0 0660 .BYTE 0,0,0,170,170,170 0670 .BYTE 169,169 0680 .BYTE 0,0,0,0,128,160,80,20 0690 .BYTE 0,0,0,0,0,3,5,20 0700 .BYTE 0,0,0,63,255,255,127,95 0710 .BYTE 0,0,0,240,252,255 0720 .BYTE 255,255 0730 ;chars 8-15 0740 .BYTE 170,170,170,170,170 0750 .BYTE 170,42,42 0760 .BYTE 165,165,165,149,149 0770 .BYTE 149,149,84 0780 .BYTE 85,85,87,85,85,80,64,0 0790 .BYTE 85,85,85,213,85,21,0,0 0800 .BYTE 87,86,86,89,85,85,85,85 0810 .BYTE 255,255,127,95,92,84 0820 .BYTE 80,80 0830 .BYTE 0,0,0,3,3,15,15,15 0840 .BYTE 235,251,255,207,207 0850 .BYTE 243,252,255 0860 ;chars 16 - 23 0870 .BYTE 255,255,207,207,243 0880 .BYTE 252,255,63 0890 .BYTE 0,0,192,192,192,0 0900 .BYTE 245,244 0910 .BYTE 3,3,3,0,3,15,31,92 0920 .BYTE 255,207,63,252,243 0930 .BYTE 207,63,255 0940 .BYTE 255,207,63,255,255 0950 .BYTE 255,255,255 0960 .BYTE 192,192,192,240,240 0970 .BYTE 252,252,255 0980 .BYTE 63,63,63,63,255,255 0990 .BYTE 63,15 1000 .BYTE 207,240,240,240,252 1010 .BYTE 252,252,255 1020 ;chars 24 - 31 1030 .BYTE 245,0,0,0,0,0,0,0 1040 .BYTE 0,0,0,0,0,0,0,0 1050 .BYTE 1,0,0,0,0,0,0,0 1060 .BYTE 85,85,85,85,85,85,84,84 1070 .BYTE 64,64,0,0,0,0,0,0 1080 .BYTE 2,0,0,0,0,0,0,0 1090 .BYTE 170,170,170,170,42 1100 .BYTE 42,42,10 1110 .BYTE 160,160,160,160,168 1120 .BYTE 160,160,160 1130 ;chars 32 - 39 1140 .BYTE 0,0,0,2,2,2,10,10 1150 .BYTE 0,42,170,170,170 1160 .BYTE 170,170,170 1170 .BYTE 0,128,160,168,84 1180 .BYTE 69,85,85 1190 .BYTE 0,0,0,0,0,0,64,80 1200 .BYTE 0,0,0,0,0,0,1,5 1210 .BYTE 0,0,3,15,21,81,85,85 1220 .BYTE 0,255,255,255,255 1230 .BYTE 127,111,111 1240 .BYTE 0,192,240,252,252 1250 .BYTE 255,255,255 1260 ;chars 40-47 1270 .BYTE 10,10,10,10,2,2,2,0 1280 .BYTE 169,169,169,169,169 1290 .BYTE 169,169,41 1300 .BYTE 87,85,85,85,84,80,80,80 1310 .BYTE 64,192,64,0,0,0,0,0 1320 .BYTE 1,0,1,1,0,0,0,0 1330 .BYTE 21,85,85,85,21,5,1,1 1340 .BYTE 151,85,85,85,85 1350 .BYTE 85,85,85 1360 .BYTE 255,255,124,124,92 1370 .BYTE 64,64,64 1380 ;chars 48-55 1390 .BYTE 3,3,3,3,3,15,15,15 1400 .BYTE 250,255,63,207,243 1410 .BYTE 252,255,255 1420 .BYTE 240,252,63,207,240 1430 .BYTE 255,63,207 1440 .BYTE 0,0,0,0,0,244,245,244 1450 .BYTE 0,0,0,0,31,95,31,64 1460 .BYTE 15,15,12,3,255 1470 .BYTE 255,255,0 1480 .BYTE 255,63,255,255,252 1490 .BYTE 243,15,255 1500 .BYTE 252,60,60,63,255 1510 .BYTE 255,255,255 1520 ;chars 56-63 1530 .BYTE 63,63,63,63,63 1540 .BYTE 255,255,255 1550 .BYTE 255,255,255,255,255 1560 .BYTE 255,255,255 1570 .BYTE 240,252,252,252,252 1580 .BYTE 255,255,255 1590 .BYTE 15,15,15,15,2,2,2,2 1600 .BYTE 255,255,255,255,170 1610 .BYTE 170,170,170 1620 .BYTE 255,255,255,255,160 1630 .BYTE 160,160,160 1640 .BYTE 84,84,84,84,220 1650 .BYTE 245,207,135 1660 .BYTE 0,0,0,0,0,0,192,240 1670 ;chars 64-71 1680 .BYTE 0,0,0,10,42,42,170,170 1630 .BYTE 0,0,0,168,170,170 1700 .BYTE 165,164 1710 .BYTE 0,0,0,0,0,128,64,80 1720 .BYTE 0,0,0,0,0,3,1,5 1730 .BYTE 0,0,0,63,255,255,95,23 1740 .BYTE 0,0,0,252,255,255 1750 .BYTE 255,255 1760 .BYTE 0,0,0,0,0,192,240,240 1770 .BYTE 0,2,2,2,2,2,2,0 1780 ;chars 72-79 1790 .BYTE 149,149,149,149,85 1800 .BYTE 85,85,84 1810 .BYTE 84,85,84,92,84,80,0,0 1820 .BYTE 21,85,21,5,21,5,0,0 1830 .BYTE 87,86,86,89,85,85,85,85 1840 .BYTE 255,255,127,87,87 1850 .BYTE 85,84,84 i860 .BYTE 240,240,240,192,192 1870 .BYTE 192,0,0 1880 .BYTE 3,3,3,3,15,15,15,15 1890 .BYTE 171,239,255,207,243 1900 .BYTE 252,255,255 1910 ;chars 80-87 1920 .BYTE 252,255,207,243,252 1930 .BYTE 255,63,207 1940 .BYTE 0,0,0,192,192,0,244,245 1950 .BYTE 0,0,0,0,0,31,95,31 1960 .BYTE 255,255,243,207,63 1970 .BYTE 255,252,195 1980 .BYTE 255,255,243,243,207 1990 .BYTE 63,255,255 2000 .BYTE 192,192,240,240,252 2010 .BYTE 252,252,255 2020 .BYTE 63,63,63,63,255 2030 .BYTE 255,255,63 2040 .BYTE 243,240,252,252,255 2050 .BYTE 255,255,255 2060 ;chars 88-95 2070 .BYTE 244,0,0,0,0,0,0,192 2080 .BYTE 64,0,0,0,0,0,0,0 2090 .BYTE 255,63,63,63,2,2,2,2 2100 .BYTE 255,255,255,255,170 2110 .BYTE 170,170,170 2120 .BYTE 0,0,0,0,0,0,0,0 2130 .BYTE 0,0,0,0,0,0,0,0 2140 .BYTE 0,0,0,0,0,0,0,0 2150 .BYTE 0,0,0,0,0,0,0,0 2160 ;chars 96-103 2170 .BYTE 0,0,0,0,0,2,2,2 2180 .BYTE 0,0,42,170,170 2190 .BYTE 170,170,170 2200 .BYTE 0,0,128,168,170 2210 .BYTE 149,145,149 2220 .BYTE 0,0,0,0,0,0,64,80 2230 .BYTE 0,0,0,0,0,0,1,5 2240 .BYTE 0,0,3,15,63,87,69,85 2250 .BYTE 0,0,255,255,255 2260 .BYTE 255,255,127 2270 .BYTE 0,0,0,192,192,240 2280 .BYTE 252,252 2290 ;chars 104-111 2300 .BYTE 10,10,10,10,2,2,2,0 2310 .BYTE 170,170,170,170,170 2320 .BYTE 170,170,170 2330 .BYTE 149,85,85,85,85 2340 .BYTE 84,80,80 2350 .BYTE 84,208,112,80,64,0,0,0 2360 .BYTE 21,4,1,5,5,1,0,0 2370 .BYTE 85,85,85,85,85,85,21,5 2380 .BYTE 111,111,151,85,85 2390 .BYTE 85,85,85 2400 .BYTE 252,252,252,240 2410 .BYTE 240,64,0,0 2420 ;chars 112-119 2430 .BYTE 3,15,15,15,15,15,15,63 2440 .BYTE 234,254,63,207,243 2450 .BYTE 252,255,255 2460 .BYTE 176,252,63,207,240 2470 .BYTE 255,63,207 2480 .BYTE 0,0,0,0,0,245,244,245 2490 .BYTE 0,0,0,0,31,31,95,0 2500 .BYTE 63,60,60,3,255 2510 .BYTE 255,252,3 2520 .BYTE 255,252,252,252,252 2530 .BYTE 243,15,255 2540 .BYTE 240,240,240,240,252 2550 .BYTE 252,252,255 2560 ;chars 120-127 2570 .BYTE 63,63,63,255,255 2580 .BYTE 255,255,255 2590 .BYTE 240,252,252,252,252 2600 .BYTE 255,255,255 2610 .BYTE 255,63,63,63,10 2620 .BYTE 10,10,10 2630 .BYTE 255,255,252,252,160 2640 .BYTE 160,160,160 2650 .BYTE 10,10,10,10,10 2660 .BYTE 10,63,252 2670 .BYTE 160,160,160,160,160 2680 .BYTE 160,240,240 2690 .BYTE 0,0,0,0,0,0,0,0 2700 .BYTE 0,0,0,0,0,0,0,0 2710 ; 2720 ;******************************** 2730 ;display list 2740 ;******************************** 2750 ; 2760 *= $4400 2770 ; 2780 DLIST .BYTE 112,112,112,71 2790 .WORD SCRRAM 2800 .BYTE 7,7,7,7,135 2810 .BYTE 5,5,5,5,5,5,65 2820 .WORD DLIST 2830 ; 2840 ;******************************** 2850 ; DLI to change character set 2860 ;******************************** 2870 ; 2880 *= $4500 2890 ; 2900 DLI PHA 2910 LDA #CHSET/256 2920 STA WSYNC 2930 STA CHBASE 2940 PLA 2950 RTI 2960 ; 2970 ;******************************** 2980 ; MAIN PROGRAM STARTS HERE 2990 ;******************************** 3000 ; 3010 *= $5000 3020 ; 3030 CLD ;binary mode 3040 LDA #0 3050 TAX 3060 ; 3070 ;zero out screen raw area 3080 ; 3090 ZERO 3100 STA SCRRAM,X 3110 STA SCRRAM+$0100,X 3120 IHX 3130 BNE ZERO 3140 LDA #DLI&255 ;point to DLI 3150 STA VDSLST 3160 LDA #DLI/256 3170 STA VDSLST+1 3180 LDA #1 ;turn off cursor 3190 STA CRSINH ;and cursor 3200 STA DSPFLG ;control chars. 3210 LDA #SCRRAM&255 ;point to 3220 STA SAVMSC ;screen RAM 3230 LDA #SCRRAM/256 3240 STA SAVMSC+1 3250 LDA #0 ;turn off screen 3260 STA SDMCTL 3270 STA DINDEX ;pretend Gr. 0 3280 LDA #60 ;set color regs. 3290 STA COLOR0 ;pink 3300 LDA #36 ;light brown 3310 STA COLOR0+1 3320 LDA #34 ;dark brown 3330 STA COLOR0+2 3340 LDA #70 ;purple 3350 STA COLOR0+3 3360 LDA #0 ;black 3370 STA COLOR0+4 3380 LDA #DLIST&255 ;point to 3390 STA SDLSTL ;display list 3400 LDA #DLIST/256 3410 STA SDLSTL+1 3420 LDA #192 ;enable DLIs 3430 STA NMIEN 3440 LDA #34 ;turn screen 3450 STA SDMCTL ;back on 3460 LDA #0 ;position cursor 3470 STA ROWCRS ;at 2,0 3480 LDA #2 3490 STA COLCRS 3500 LDX #0 ;use IOCB #0 3510 LDA #HAPPY&255 ;print Happy 3520 STA ICBAL,X ;Birthday 3530 LDA #HAPPY/256 ;line with 3540 STA ICBAL+1,X ;victim's name 3550 JSR PRINTLINE 3560 LDA #14 ;close Graphics o 3570 STA LMARGN ;Margins to 14 3580 LDA #21 ;(left) and 21 3590 STA RMARGN ;(right) 3600 JSR POSITION ;cursor at 14,3 3610 LDX #0 ;IOCB #0 3620 MAINLOOP 3630 LDA #STANDING&255 ;print 1st 3640 STA ICBAL,X ;image of 3650 LDA #STANDING/256 ;the happy 3660 STA ICBAL+1,X ;couple 3670 JSR PRINTLINE 3680 JSR DOLEGS ;add some legs 3690 LDA RANDOM ;get random # 3700 LSR A ;divide by 2 3710 STA TIMER ;wait this Many 3720 JSR DELAY ;jiffies extra 3730 LDX #0 3740 LDA #BENDING&255 ;now print 3750 STA ICBAL,X ;2nd image 3760 LDA #BENDING/256 3770 STA ICBAL+1,X 3780 JSR PRINTLINE 3790 JSR DOLEGS ;legs & paws 3800 LDA #ALMOST&255 ;3rd image 3810 STA ICBAL,X 3820 LDA #ALMOST/256 3830 STA ICBAL+1,X 3840 JSR PRINTLINE 3850 JSR DOLEGS ;(paws=pause) 3860 LDA #KISS&255 ;4th image 3870 STA ICBAL,X ;contact! 3880 LDA #KISS/256 3890 STA ICBAL+1,X 3900 JSR PRINTLINE 3910 JSR DOLEGS ;(ha,ha,ha) 3920 LDA RANDOM ;linger a bit, 3930 LSR A ;savor the Monent 3940 STA TIMER 3950 JSR DELAY 3960 LDX #0 3970 LDA #ALMOST&255 ;3rd image - 3980 STA ICBAL,X ;pulling 3990 LDA #ALMOST/256 ;apart 4000 STA ICBAL+1,X 4010 JSR PRINTLINE 4020 JSR DOLEGS 4030 LDA #BENDING&255 ;2nd image 4040 STA ICBAL,X 4050 LDA #BENDING/256 4060 STA ICBAL+1,X 4070 JSR PRINTLINE 4080 JSR DOLEGS ;this is fun,so 4090 JMP MAINLOOP ;keep going 4100 ; 4110 ;******************************* 4120 ; SUBROUTINES START HERE 4130 ;******************************* 4140 ;------------------------------- 4150 ;sub. to print up to 40 chars 4160 ;of a line; point to address of 4170 ;line before calling PRINTLINE 4180 ;------------------------------- 4190 PRINTLINE 4200 LDA #40 4210 STA ICBLL,X 4220 LDA #0 4230 STA ICBLL+1,X 4240 LDA #PUTREC 4250 STA ICCOM,X 4260 JSR CIOV 4270 RTS 4280 ;------------------------------- 4290 ;sub. to position cursor at 14,3 4300 ;in our fake Gr. 0 screen 4310 ;------------------------------- 4320 POSITION 4330 LDA #14 4340 STA COLCRS 4350 LDA #3 4360 STA ROWCRS 4370 RTS 4380 ;------------------------------- 4390 ; sub. to print the legs each 4400 ;each time; pause 3 jiffies; set 4410 ;up to print next line 4420 ;------------------------------- 4430 DOLEGS 4440 LDA #14 ;position cursor 4450 STA COLCRS ;at 14,7 4460 LDA #7 4470 STA ROWCRS 4480 LDX #0 4490 LDA #LEGS&255 ;print the legs 4500 STA ICBAL,X 4510 LDA #LEGS/256 4520 STA ICBAL+1,X 4530 JSR PRINTLINE 4540 LDA #3 ;want to wait 4550 STA TIMER ;3 jiffies 4560 JSR DELAY ;call delay sub. 4570 JSR POSITION ;cursor for next 4580 LDX #0 ;line & IOCB #0 4590 RTS 4600 ;------------------------------- 4610 ;sub. to do nothing until real- 4620 ;time clock has increnented to 4630 ;desired number of jiffies 4640 ;------------------------------- 4650 DELAY 4660 LDA #0 ;initialize clock 4670 STA RTCLOK 4680 DELAY2 4690 LDA RTCLOK ;compare to value 4700 CMP TIMER ;you put in TIMER 4710 BNE DELAY2 ;until they match 4720 RTS 4730 ; 4740 ;******************************* 4750 ; TEXT LINES TO PRINT ARE HERE 4760 ;******************************* 4770 ; 4780 HAPPY 4790 .BYTE " HAPPY BIRTHDAY " 4800 .BYTE " JUDY",EOL 4810 STANDING 4820 .BYTE "@ABCDEFG" 4830 .BYTE "HIJKLMNO" 4840 .BYTE "PQRSTUVW" 4850 .BYTE "XYZ [\]",EOL 4860 BENDING 4870 .BYTE "♦abcdefg" 4880 .BYTE "hijklmno" 4890 .BYTE "pqrstuvw" 4900 .BYTE "xYy [\♠",EOL 4910 ALMOST 4920 .BYTE " ♥├|┘┤┐/" 4930 .BYTE "\i◢▗◣▝▘" 4940 .BYTE "▂▖♣┌─┼●▄" 4950 .BYTE "▎Y┬┴▌└␛]",EOL 4960 KISS 4970 .BYTE " !#$%&' " 4980 .BYTE " ()*+,- " 4990 .BYTE "./012345" 5000 .BYTE "6Y789.\]",EOL 5010 LEGS 5020 .BYTE ":;< =>?" 5030 .BYTE " ^_ |.",EOL
A.N.A.L.O.G. ISSUE 54 / MAY 1987 / PAGE 55
How many of you out there are using a macro assembler? Let’s see a show of hands. Okay, that’s about what I expected. Now, how many of you are using MAC/65 from Optimized Systems Software? Mm, hmm. How about the venerable Atari Macro Assembler (AMAC)? I see. I’ll bet some of you aren’t using a macro assembler at all. The original Atari Editor/Assembler cartridge (or OSS’s equivalent, called EASMD, where D is for “debugger”) is still hanging around a lot of cartridge slots, I suspect.
Today’s discussion will be of most interest to those of you who own a macro assembler, but haven’t really fiddled around with macros yet. However, this doesn’t mean readers using an assembler without a macro capability should toss this issue out. Once you see what you can do with macros, you just might want to upgrade your system.
This is part one of a two-part discussion on using macros in assembly language programming. We’ll see just what macros are, and talk about how they’re created using both MAC/65 and AMAC. Along the way, we’ll compare some features of the two assemblers, encounter some of the less commonly used directives (such as those for conditional assembly) and discuss some programming techniques. Next time, we’ll continue building a library of macros to let us write assembly language programs that read suspiciously like BASIC. Don’t stop here just because you can’t do macros yet, or because the terminology scares you off. If you already knew all this stuff, I’d be out of a job!
The word or prefix macro refers to large things, as opposed to micro, which refers to small things. In computerese, a macro is one “command” that performs the functions of several individual commands. By writing a macro, you are essentially creating your own extensions to the intrinsic commands available in the language you’re using. Once created, this kind of pseudocommand is a shorthand way of representing several functions in a single statement.
In assembly language, a macro is a block of individual assembly statements. When defining the macro, we indicate where the block of instructions that make up the macro begin, and where they end. We give the macro a name, so we can refer to it when writing a program for some particular application. To use the macro, we simply type the name we gave it as if we were entering a standard assembly mnemonic, like LDA or TAX. During the assembly process, the assembler will replace this macro name with the block of individual statements the name represents. We can use the macro as many times as we want in the program, saving us the labor (and associated chance of errors) of re-entering the same block of statements each time we need them.
So far, this description sounds suspiciously similar to that of a subroutine. However, there are some fundamental differences between an assembly language macro and a subroutine.
A subroutine is a block of assembly statements beginning with a label (so we can call it) and ending with an RTS (ReTurn from Subroutine) instruction. The subroutine instructions appear only once in the source and object code of an assembly program. The object code for the subroutine is separated in memory from the instructions making up the body of the program that calls the subroutine. We can call the subroutine as many times as we want, and, each time we do, the program jumps to the same memory location and begins executing the subroutine.
In contrast, a macro definition exists only in the assembly source code. When the assembler encounters a call to a macro, it inserts into the object code the entire block of code generated by the macro definition. (An exception occurs when the macro contains instructions for conditional assembly, which we’ll get to a little later.) So, if you’ve defined a macro named PRINTIT that assembles to 50 bytes of machine language, those 50 bytes are inserted into the object code produced by the assembler—everywhere the PRINTIT “instruction” appears in the source code. This process is called macro expansion.
If references to PRINTIT occur four times in the source code for a program, this will lead to 200 bytes of object code derived from the macro. However, if you had a subroutine named PRINTIT that was 50 bytes long, you could call PRINTIT a thousand times in the program and still only have those 50 bytes of object code derived from the subroutine. See the difference?
Macros provide another degree of flexibility that assembly subroutines don’t have. It is possible to define a macro with several parameters that can be different each time you invoke (or use) the macro. This way, the object code you get each time the macro is expanded can be different. With a subroutine, you write it once and it stays that way forever.
The bottom line is that macros are great when you’re writing source code. They can save a lot of typing, debugging and file space. You can write macros to do just about any task that pops up repeatedly in a program. However, you create additional object code every time you invoke the macro. So trust me: you’d rather use a subroutine than a macro if you have to perform exactly the same operation many times. Of com’se, we can combine a macro with a subroutine…
Enough theory. Let’s meet your first macro.
Suppose you’re writing some Display List Interrupts (also known as DLIs), and you need to save the contents of the accumulator and X- and Y-registers on the stack in each DLL (This idea should be familiar from issue 46). Such a repetitive operation lends itself nicely to a macro.
Let’s define a macro named SAVEREGS. In MAC/65 syntax, we write a segment of code that looks like this:
0100 .MACRO SAVEREGS 0110 PHA 0120 TYA 0130 PHA 0140 TXA 0150 PHA 0160 .ENDM
In each DLI we write, we can perform the whole process of saving the registers by typing SAVEREGS as an instruction. We might as well write a counterpart macro to restore the registers at the end of the DLI:
0170 .MACRO RESTORE 0180 PLA 0190 TAY 0200 PLA 0210 TAX 0220 PLA 0230 .ENDM
We could use this in a typical DLI, in the following fashion (assume that addresses CHSET. WSYNC, CHBASE and COLOR0 have been declared in an equates list elsewhere in the program):
0240 DLI 0250 SAVEREGS 0260 LDA #CHSET/256 0270 LDX #1 0280 LDY #198 0290 STX WSYNC 0300 STA CHBASE 0310 STY COLOR0+2 0320 RESTORE 0330 RTI
Using the SAVEREGS and RESTORE macros like this saves eight lines of code in the DLI routine—and makes it easier to read. We can do this wherever—in the whole program—we need to save all three registers. Another advantage is that, if we need to change the macro for some reason (not that you ever forget anything the first time through), the change is applied everywhere the macro is used the next time you assemble the program.
The Atari Macro Assembler uses a slightly different syntax than does MAC/65. The SAVEREGS macro would be entered like this in AMAC:
SAVEREGS: MACRO PHA TXA PHA TYA PHA ENDM
Notice that AMAC doesn’t require statement numbers, and the name of the macro appears in the label field of the source line. Also, the MACRO and ENDM delimeters don’t begin with a period in AMAC.
Perhaps you’ve wondered about the neutral habitat of the macro. Macros are fomid in many parts of the assembly language world. Quite a number have been spotted lurking at the very beginning of the source listing. Some have carved out an ecological niche between the equates normally fomid near the top of a listing and the beginning of the executable statements in the program (that is, before the * = $5000 or similar directive). Still other macros are known to inhabit the nether reaches of programs, among the .BYTE statements that so often abomid there. Some varieties share the same environment as subroutines, although the two really are quite different species.
The macro is a hardy creature, able to live practically anywhere in a source program except directly amidst the primary code. Reported sightings of macros have been increasing recently, and their population appears to be growing rapidly. My own collection of captive macros prefers to be nestled just below the equates list. Perhaps it’s warmer there.
Let’s write a macro to simulate the BASIC POKE command. If you recall your Atari BASIC, a statement like POKE 710,84 means you want to store the decimal value 84 into location 710. Thus, the POKE command requires two pieces of data: what to poke and where to poke it. The value to poke will always be from 0 through 255 decimal, and the address to be poked must be from 0 through 65535 decimal. How can we write a macro to accommodate any possible combination of these two pieces of data?
The solution is to pass parameters to our macro. The macro definition is written using symbols to represent some of the data items in the assembly instructions. Each time you invoke the macro, you specify the actual values of the parameters to use at that time. During the macro expansion process, the actual values are substituted in place of the parameters. This way, each use of the macro in a program can result in the generation of a different object code if the parameters are different. Let’s see an example.
Our POKE macro requires two parameters. Both MAC/65 and AMAC use the symbols %1 and %2 to represent the first and second parameters passed to the macro, respectively. Each time you invoke the POKE macro, the value of the first parameter you supply will replace every occurrence of %1 in the macro. Please enter the following program for MAC/65:0100 .OPT OBJ 0110 .MACRO POKE 0120 LDA #%2 0130 STA %1 0140 .ENDM 0150 ;program starts here 0160 *= $5000 0170 POKE 710,84 0180 BRK
The POKE macro says to take the second parameter specified when the macro is invoked (84 in Line 170) and load that value into the accumulator (Line 120). Then, store the accumulator contents into the address specified by the first parameter (Line 130). If you run this program, you should see the background color of the graphics screen change. The POKE 710,84 syntax is exactly the same as the BASIC POKE command.
AMAC does things a little differently. Below is the equivalent progam for AMAC.POKE: MACRO WHERE, WHAT LDA #%2 STA %1 ENDM ;program starts here ORG $5000 POKE 710,84 BRK
Notice that we specified names for the two parameters (WHERE and WHAT) in the AMAC macro declaration statement. These names are dummies, used for documentation simply to remind you that parameter %1 is the address you’re going to poke (WHERE) and %2 is the value you’re going to poke (WHAT). Try changing the second parameter to different values and see what happens. AMAC also differs in that it requires the ORG directive (AMAC calls it a “pseudo-operation” or “pseudo-op”) to specify the starting location for the program.
AMAC lets you use up to nine parameters in a single macro, called %1 through %9. A maximum of sixty-three parameters can be used in a MAC/65 macro. MAC/65 also has a special “parameter” called %0. This always contains the number of parameters actually passed to a particular macro expansion. Why would anyone want to know this, you ask? After all, if I defined a macro containing two parameters, like POKE, I’ll always be sure to pass two parameters when I invoke the macro, like POKE 710,84. Right?
Maybe. Maybe not. I don’t know about you, but I sometimes make mistakes when I’m programming. Or suppose you give your nifty new macro to a friend, and she omits one of the parameters inadvertently? The technical term for the result is a crash. This is an opportune time to take a brief diversion and talk about what I call “defensive programming.”
Every day, I go to work at Eastman Kodak Company in Rochester, New York. There I sit down at a terminal connected to an IBM mainframe and write applications programs for other research scientists to use. (This is an odd career for an ex-organic chemist, but that’s the way it worked out.) The people who use these programs don’t care how they’re written; they just want them to work correctly and be easy to use. Other people frequently want to modify programs I’ve written. Thus, I have to build in lots of error trapping to protect users from themselves, and my programs must be clearly organized and well documented so other programmers can understand them. I call this approach “defensive programming.”
An important programming law when you’re writing for users other than yourself states that, just when you think you’ve covered all the ways a user can do do something silly with your program, someone will come up with something new and creative. Take my POKE macro (please). Can you be positive another person will always remember to use two parameters when invoking the macro? Can you be positive another user will never supply a second parameter (the value to be poked) outside the range of through 255?
The answer to both questions is no. So how can we build some protection into the POKE macro, in order to keep people from fowling up and wasting a lot of time trying to find their problem? It’s pretty easy to cover the two scenarios I mentioned in the POKE macro.
First solution: with MAC/65, examine the number of parameters specified in %0. The .IF directive can test for a specific condition, just as in BASIC (only better), and we can cause an assembler error to be issued with the .ERROR directive. Detecting an error at assembly time is lots nicer than waiting mitil execution time. Second solution: take the low byte of the second parameter to make sure we always have a value from 0 through 255. The modified POKE macro below illustrates these methods.
0100 .MACRO POKE 0110 .IF %0 <> 2 0120 .ERROR "Wrong number of parameters in POKE" 0130 .ELSE 0140 LDA # <%2 0150 STA %1 0160 .ENDIF 0170 .ENDM
Lines 110–160 define an .IF block for conditional assembly. If the condition in Line 110 is true (that is, if the number of parameters is not equal to 2), then the statements in the next lines axe assembled, down to the .ELSE in Line 130. But if the condition in Line 110 is false (meaning that the correct number of parameters was supplied), then assembly continues with Line 140, the first statement after the .ELSE.
The .IF blocks are very powerful. You can actually nest the .IF/.ENDIF blocks within each other, down to fourteen levels. You can have as many lines of code as you like between the .IF/.ELSE, .IF/.ENDIF and .ELSE/.ENDIF delimiters. And .IF blocks can be used in macros, or anywhere else in an assembly program. AMAC has the same sort of IF blocks, except the directives do not begin with a period (IF instead of .IF).
Remember, these directives or pseudo-ops don’t correspond to actual 6502 machine language instructions. They’re just commands that make oirr assembly programs smarter and more flexible.
A word about operators in the two assemblers. In Line 140 of the preceding macro, we used the less-than symbol (<) to indicate that we want to load the accumulator with the low byte of the value in parameter %2 . This is a MAC/65 convention. In AMAC, you accomplish the same thing with the LOW operator: LDA #LOW %2. A technique that works with any assembler is to perform a logical AND operation of the subject value with decimal 255: LDA #%2&255. All of these methods retain only the 8 least significant bits of the operand, which is a fancy way of saying the low byte of %2.
Let’s try to write a macro to emulate another commonly used BASIC command, SETCOLOR. In BASIC, this command has the form: SETCOLOR X,Y,Z. X refers to the color register we wish to change (0–4), Y to the hue number (0–15), and Z to the luminance mmiber (0–15). The SETCOLOR command simply stores the desired color number into a particular color register address. The actual color number equals 16 * hue + luminance. I imagine you know that you can mimic the SETCOLOR command with a POKE, in the form: POKE 708+X,16*Y+Z. We should be able to write a macro to do the same thing.
I’ll begin with a bare-bones macro to do the dirty deed, then we can worry about error trapping and other refinements. If our SETCOLOR macro is to have the same form as the BASIC command, it obviously requires three parameters (%1, %2 and %3). Furthermore, we’ll have to multiply %2 by 16 and add the result to %3, to calculate the color number. Fortunately, assemblers let you use operators like * for multiplication in expressions.
Here’s a first try at the SETCOLOR macro, in MAC/65 format:
0100 COLOR0 = $2C4 0110 .MACRO SETCOLOR 0120 LDX #%1 0130 LDA #%2*16 0140 CLC 0150 ADC #%3 0160 STA COLOR0,X 0170 .ENDM
Line 100 establishes an equate for the address of color register 0. Line 120 loads the value of the first parameter into the X-register, so we can use it as an offset to point to the desired color register in Line 160. Line 130 loads the accumulator with the second parameter multiplied by 16. Lines 140–150 add the value of parameter 3 to whatever is already in the accumulator.
The ADC instruction means “ADd with Carry.” The data following the ADC is added to the value in the accumulator. In this case, we’re using the immediate addressing mode, so the data to be added is just the value of parameter 3. If the result is greater than 255, the accumulator contains the sum minus 255, and the carry flag in the processor status register is set to 1. It’s important to CLear the Carry flag to (CLC instruction) before performing an ADC, so you can reliably test the carry flag if you need to later on.
After all this arithmetic, the accumulator contains the actual color number. Line 160 stuffs that number into the appropriate color register.
The SETCOLOR macro will work fine as written. Now let’s consider some possible problems. There’s the one we discussed before, of supplying the wrong number of parameters when invoking the macro. That one’s easy to fix with MAC/65, by using the %0 test. But what if a parameter isn’t an immediate value like 84, referring instead to the contents of address location 84? The macro has to be able to handle either situation. Since the only valid color register numbers are through 4, let’s regard any %1 values greater than 4 as referring to a memory location. Similarly, hue and luminance values can only go from 0 through 15, so we’ll assume that %2 and %3 values greater than this refer to memory locations. Hmmmm. Sounds like a job for conditional assembly with .IF!
Listing 1 contains a complete form of the SETCOLOR macro, with comments so anyone else could use it and a tiny program to let you try it out. Experiment with different values for the parameters and see what happens. In practice you might not always make your macros this elaborate, but I hope we’ve brought up some points you may not have thought about otherwise.
Lines 410–460 of the listing illustrate another point. Remember how we multiplied parameter %2 by 16 in a single expression a little earlier? Well, that works fine with an immediate operand for the LDA instruction, but not when we’re loading the register with the contents of a memory location, as in Line 420. An easy way to multiply the contents of the accumulator by 16 is to shift each bit in the accumulator 4 bits to the left (the more significant direction) . Each execution of the ASL A (Arithmetic Left Shift of the Accumulator) instruction multiplies the contents of the accumulator by 2. Of course, any bits set in the high nybble (left 4 bits) fall off the edge of the earth after the fourth ASL A, but that’s okay. If you’re trying to use a hue number greater than 15, you deserve that fate.
The Y-register is just used as a temporary holding place for parameter %3 in Lines 570–610. This is sort of a juggling act, but it gets the job done.
Think back to the form of the macros we talked about today, POKE and SETCOLOR. Do they remind you of any other language? (Hint: say Atari BASIC.) If we can write enough macros to perform the familiar BASIC operations, we can begin to write assembly programs almost as easily as BASIC programs. Plus they’ll be nearly as easy to read.
In the next installment, I’ll present a number of macros to get you started writing pseudo-BASIC programs using MAC/65. (Users of AMAC or other assemblers will have to adapt the routines, but that’s not too hard.) I’ll also describe a way to manage this library of macros and incorporate them easily into your future programs, with the .INCLUDE directive. Until then, think about the kinds of tasks you perform in your own programs which might lend themselves to “macrofication.”
Despite having a Ph.D. in organic chemistry, Karl Wiegers earns a living writing applications software for photographic research at Eastman Kodak Company, mostty on an IBM mainframe. He is also interested in educational applications of Atari 8-bit, Atari ST and Apple II computers.
Listing 1. Assembly listing. 0100 ;SETCOLOR macro example 0110 ;by Karl E. Wiegers 0120 ; 0130 ;Usage: SETCOLOR X,Y,Z 0140 ; 0150 .OPT OBJ 0160 ; 0170 ;in a real program, put this 0180 ;equate in your usual equates 0190 ; 0200 COLOR0 = $02C4 0210 ; 0220 .MACRO SETCOLOR 0230 ; 0240 ;check for right # of parameters 0250 ; 0260 .IF %0<>3 0270 .ERROR "SETCOLOR error" 0280 .ELSE 0290 ; 0300 ;set offset for color register 0310 ; 0320 .IF %1>4 0330 LDX %1 0340 .ELSE 0350 LDX #%1 0360 .ENDIF 0370 ; 0380 ;if %2>15 assume it's an address; 0390 ;multiply by 16 with four ASL A 0400 ; 0410 .IF %2>15 0420 LDA %2 0430 ASL A 0440 ASL A 0450 ASL A 0460 ASL A 0470 ; 0480 ;otherwise just multiply by 16 0490 ; 0500 .ELSE 0510 LDA #%2*16 0520 .ENDIF 0530 ; 0540 ;if %3>15 assume it's an address; 0550 ;put it in Y-reg. temporarily 0560 ; 0570 .IF %3>15 0580 LDY %3 0590 .ELSE 0600 LDY #%3 0610 .ENDIF 0620 ; 0630 ;store what's already in the 0640 ;accumulator briefly, even though 0650 ;we haven't added yet 0660 ; 0670 STA COLOR0,X 0680 ; 0690 ;keep just the low nybble of 0700 ;what's in the Y-register 0710 ; 0720 TYA 0730 AND #15 0740 ; 0750 ;add %3 (now in A) to %2*16 (now 0760 ;in COLOR0,X) and store again 0770 ; 0780 CLC 0790 ADC COLOR0,X 0800 STA COLOR0,X 0810 .ENDIF 0820 .ENDM 0830 ; 0840 ;sample program to try SETCOLOR 0850 ; 0860 *= $5000 0870 SETCOLOR 2,6,4 0880 BRK
A.N.A.L.O.G. ISSUE 55 / JUNE 1987 / PAGE 75
Let’s begin today’s discussion with a bit of time travel. Way back in issues 41 and 42 of ANALOG Computing, I showed you how to simulate a number of Atari BASIC graphics commands in assembly language. The culmination of that discussion was a program that drew a yellow five-pointed star on a purple background in graphics 5. Please go dig up issue 42 if you have it; I’ll wait here until you get back. (Don’t panic if you don’t have issue 42 handy—you’ll live.)
Got it? Now, please turn to page 101 of that issue. There you see a BASIC program that did exactly what our final assembly program did. I’ll reproduce it here, in case your back issues are lost somewhere:
10 GRAPHICS 5+15 20 SETCOLOR 6,1,12 30 SETCOLOR 4,6,8 40 COLOR 1 50 PLOT 26,15 60 DRAWTO 68,15 70 DRAWTO 28,35 80 DRAWTO 46,5 90 DRAWTO 52,35 100 DRAWTO 20,15 110 GOTO 110
What would you say if I told you that, after studying today’s Boot Camp, you’ll be able to write a program in assembly language that looks almost identical to the above BASIC program—and does exactly the same thing? I detect some murmurings of skepticism in the crowd. But it’s all true, through the Miracle of Macros!
Today, I present macros to simulate ten common BASIC statements: SETCOLOR, POKE, POSITION, OPEN, CLOSE, GRAPHICS, PRINT, COLOR, PLOT and DRAWTO. Once you have this set in hand, writing assembly programs to perform any of these functions is scarcely more difficult than in BASIC. We’ll begin to build a macro library that covers most of the common BASIC commands, as well as some commands BASIC doesn’t have but should. Future issues will add to this library from time to time.
The (possible) bad news is that you must have a macro assembler to use these programs. My examples are specifically written for MAC/65. With a little effort, you can modify most of them to work with the Atari Macro Assembler (AMAC), although I’ll point out some important differences between the assemblers. If you have a different macro assembler, try to adapt the ones I have here to your own environment. The manual for your assembler should help with specific syntax questions.
Even if you don’t have a macro assembler, I think you’ll get some useful ideas about assembly programming in the words that follow, so plunge onward.
Last month, I introduced the idea of macros and looked at two examples, POKE and SETCOLOR, in detail. A macro is just a named block of assembly language statements. When we invoke (call, refer to, use—all are synonyms) a macro, the instructions in the macro are placed right into the somce (and hence object) code upon assembly. This is called “expansion” of the macro. Two very powerful features of macros are: the ability to pass variable parameters each time the macro is invoked, and the use of conditional (.IF, .ELSE, .ENDIF) assembly statements within the macro. These methods let the macro generate different object code each time it is expanded.
Today, we’ll look at some other ways to use macros. We’ll see macros that invoke other macros, macros that call subroutines, macros that use string (as opposed to numeric) parameters, macros containing labels, and more. I’ll also demonstrate a convenient way to keep track of your macro (and subroutine) libraries as you build them. All these methods will help you write assembly language programs much faster and cleaner than you may be doing now. So let’s get started!
Listing 1 contains the assembly code definitions for the ten macros I mentioned earlier, plus the equates needed by the macros. You could use these macros in a couple of ways as you write programs. In the simplest case, you could just type the code for the macros you need into each program you write. A less time-consuming method is to put each macro into a separate file and use the editor portion of your assembler to pull in the ones you need and piece together a program. But the best way is to use the .INCLUDE directive of your assembler.
The .INCLUDE directive has the form: .INCLUDE #D:MACRO.LIB. Whenever this directive is encountered during assembly of a program, the entire contents of the file named are inserted into the source code being assembled, at the location of the .INCLUDE. This way, you can manage a large program—or programs containing common segments of code—by linking together several smaller files.
I’d like you to create a file called MACRO.LIB that contains everything in Listing 1. Now, anytime you want to use a macro from this library, just put the .INCLUDE #D:MACRO.LIB statement at the beginning of your program. Please note that, with MAC/65, .INCLUDEd files must be saved in the standard MAC/65 tokenized form. LISTed files cannot be .INCLUDEd this way.
There is a bad side to the use of .INCLUDE files. One of MAC/65’s strong points is that it assembles very quickly from the source code in RAM. If we .INCLUDE some files, the assembly process will slow down, as those files must be read from disk. This isn’t such a big deal with AMAC, since AMAC must read source code files from disk to assemble them, anyway. If you have a 130XE with a RAMdisk, you’re in luck. I always begin my MAC/65 sessions by booting DOS, setting up the RAMdisk, then copying all my .INCLUDE files onto the RAMdisk. (If the DOS you use permits batch files, you can copy these files automatically on booting.) All my .INCLUDE statements then refer to files on drive D8:, and assembly is still very fast.
Listing 2 is another file that will be .INCLUDEd in our programs. Please call this one SUBS.LIB. It contains a couple of subroutines that are called by some of the macros. You might consider adding to this file any other subroutines you use often in your own programs. We have encountered a few in previous columns, such as timing delay routines. Now, let’s look at a couple of sample programs and study the macros in more detail.
Listing 3 is a short assembly program that looks a lot like BASIC. It sets up a standard graphics 2 screen, changes a couple of color registers, and prints one line of text on the screen and one in the text window. Sounds simple enough. Note that the MACRO.LIB file is .INCLUDEd at the beginning of the source code, and the subroutines are at the end. Macro definitions can go just about anywhere except in the middle of the program. I like to group them at the top. Conceptually, I like the subroutines at the end of the program, so the assembler doesn’t get confused by hitting source code before it gets to the program origin address in Line 150.
I won’t dissect the SETCOLOR and POKE macros, because we covered them in gory detail last month. The GRAPHICS macro does essentially the same thing that the Atari BASIC GRAPHICS statement does. It opens the graphics screen device S: in a particular graphics mode, with or without a text window. By now, you’re all old hands at opening IOCBs, but to make it even easier, I wrote an OPEN macro. In fact, the GRAPHICS macro actually calls the OPEN macro…but we’re getting ahead of ourselves.
Let’s look at the OPEN macro in Lines 1290–1550 of Listing 1. For each macro, I’ve shown a usage example and described the parameters it uses. Notice that no comments are included in the body of the macro (that is, between the .MACRO and .ENDM directives). Since the whole macro is plugged right into the source code during the assembly process, you waste RAM if you have comments within the macro definition.
The OPEN macro, just like the OPEN command in BASIC, requires four parameters: the IOCB number to open; two auxiliary values; and the name of the device to use. As with all these macros, the first function is to test and make sure the correct number of parameters is supplied (Lines 1300–1310). If not, an error message is printed on the assembly listing.
Incidentally, you may have seen macros similar to mine in other publications. There are different ways to write these macros, although they all do essentially the same thing if they’re trying to simulate their BASIC counterparts. You can make the macros quite a bit more elaborate than I have in many cases, but I’ll let you do some of that on your own.
The first strange thing about the OPEN macro is that device name jazz. Aren’t those usually some kind of character string, like S:, or D1:DATA.FIL? They sure are. Fortunately, MAC/65 permits string parameters to be used in macros, something AMAC can’t do. String parameters are numbered as usual, but a dollar sign ($) after the percent sign (%) identifies the parameter as a string: %$4, for example. Line 1340 reserves a block of data bytes the length of whatever is in %$4 as the device name, followed by an end-of-line (EOL) character. We don’t want to try to “execute” those data bytes as instructions, so Line 1330 jumps over the line labeled @DEVICE.
Since AMAC can’t handle string variables, you have to get more inventive. You could write a version of the OPEN macro that uses an address for the fourth (device) parameter. This address could be a byte string set to something like S: or D1:DATA.FIL. Lines 580–630 of Listing 2 show some common device names you might want to use. If you take this approach, an AMAC OPEN macro call might look like: OPEN 6,12,2,S.
And now a word about labels. You can use labels in macros just like you do elsewhere in your program. However, it’s actually possible to refer to those intra-macro labels from outside the macro, which can cause some strange things to happen. It’s a good practice to use a different convention for naming labels inside and outside macros. I use label names beginning with an at sign (@) within a macro. Fortunately, the label definitions are changed automatically each time the macro is invoked, so @DEVICE will have a unique address each time you call OPEN. You don’t have to worry about this, since the assembler will.
Line 1360 uses the first parameter to determine which IOCB we want to use. This parameter must be an actual IOCB number, not an address containing the desired IOCB number. Lines 1370–1400 set up the IOCB with the pointers to the device name passed in parameter %$4.
I elected to provide the flexibility to handle the auxiliary values as being either actual numbers or addresses containing the numbers to be used. Lines 1410–1460 illustrate the logic used for auxiliary value 1. If parameter %2 is greater than 255, I assume it’s referring to an address, so I snatch the contents of that address in Line 1420. Otherwise, I assume the actual auxiliary number has been supplied, so I use it directly in Line 1440. Whatever value I end up with is stored in ICAX1 in Line 1460. The same method is used for the second auxiliary byte in Lines 1470–1520. Of course, this method will fail if you want to point to an address in page 0, in which case %1 will be 255 or lower (think about it!) You could use this same address/value approach to make the IOCB selection from parameter 1 more intelligent, if you like.
At this point, all necessary registers are set to perform the OPEN, except the command value and the jump to CIO. Since these steps are the same for all OPEN operations, I decided to put them into a subroutine called OPEMOCB (Lines 390–430 in Listing 2). By doing this, we save a few bytes of object code each time the OPEN macro is expanded. Including a subroutine call within a macro definition makes your object code more efficient. In this case, we’re only talking about a few bytes, so if you feel more comfortable including the entire OPEN procedure within the macro, go right ahead. I often prefer that sort of comprehensibility to a small efficiency improvement.
This is all fine, but what does it have to do with the GRAPHICS command? Well, since the heart of the BASIC GRAPHICS command is opening IOCB 6 for the S: device, I thought we’d better cover the OPEN macro first. Now, how do we take the single parameter from the GRAPHICS macro and turn it into the four parameters needed by OPEN?
The GRAPHICS macro lives in Lines 1870–2070. It requires one parameter, a value (not address) that is any valid BASIC graphics mode for your computer. (Remember that the XL/XE machines have access to some ANTIC modes from BASIC that the 400/800 do not.) As with BASIC, you can add 16 to this mode number to suppress the text window.
Line 1910 jumps beyond a couple of bytes reserved for the two auxiliary parameters needed by OPEN. The BASIC graphics mode number must be placed into @AUX2. Line 1980 keeps only the 4 least significant bits of parameter 7ol, thereby making sure that @AUX2 has a valid value from 0 through 15.
Some gyrations are required to set the right value in @AUX1 to control the text window. Back in issue 41, I told you that @AUX1 (ICAX1 in IOCB terminology) should be set to 28 to have a text window present, and to 12 to get rid of the text window. However, since we told the GRAPHICS macro to get rid of the text window by adding 16 to the graphics number (equivalent to setting bit 5), some bit manipulations are needed. Lines 2000–2040 load @AUX2 with a decimal 28 if the graphics mode is less than 16 (window) and a decimal 12 if the mode is greater than 16 (no window). You might find it illuminating to work through the AND, EOR, and ORA for the two cases.
Line 2050 finally does the dirty work. Notice that we are invoking the OPEN macro from within the GRAPHICS macro. This nested macro situation is perfectly legal. (Oh right, when I talked about macro ecology last issue, I forgot to tell you where they nest!) The four parameters passed to OPEN are a 6 (IOCB 6 for the screen, always), the values we synthesized for @AUX1 and @AUX2, and the screen device S:. Note the use of the literal string in parameter 4. This isn’t so bad, is it? The nicest part is: now that you have these macros, you don’t have to worry about such details anymore.
What do you suppose would happen if you had a macro that called itself? This is called “recursion.” You might be able to get away with this, provided you have conditional code to permit an exit under specific circumstances. If a recursive macro with no way out tries to call itself indefinitely, your computer will explode after 5 seconds (just kidding).
Getting back to Listing 3, once the GRAPHICS macro has been called, you can set the color registers to whatever values you like. Line 170 shows how to use the SETCOLOR macro for this, and Line 180 illustrates the use of the POKE macro to set a color register. Now let’s print something on the screen.
POSITION is one of the obvious candidates for a simple macro. Lines 1040–1150 of Listing 1 show that POSITION requires two parameters, the X- and Y-coordinates where the cursor should go. Both parameters are assumed to be values, not addresses. I think you can follow the code in this macro without too much difficulty. The first parameter (X-coordinate) can only be greater than 255 in graphics mode 8, where it can range from 0 to 319.
Here’s another one where you can imagine a pretty sophisticated macro to fully emulate its BASIC counterpart. In BASIC, a PRINT command can be followed by nothing (print a blank line), a literal (print what’s between the quotes), or a variable name (print the current value of the variable). Also, the IOCB number to use may or may not be specified; the absence of an IOCB number means use IOCB for the screen editor, device E:. I haven’t written a full implementation of the PRINT macro; the MAC/65 manual contains a very detailed version you might try to comprehend. The one I give here doesn’t handle the printing of literals, just strings at addresses. Each such string must terminate with an EOL character. Lines 250–260 of Listing 3 are some text strings to print. The PRINT macro is in Lines 2260–2450 of Listing 1. The IOCB number is optional; IOCB is assumed if you supply only one parameter (the string address). This macro calls subroutine PRINTLINE (Lines 230–310 of Listing 2), which issues the PUTREC command to CIO. In Listing 3, text is printed on both the graphics 2 screen and in the graphics text window. You shouldn’t have much trouble adapting this macro to AMAC. How’s about whipping up a method to handle string literals, thereby completing the emulation of the BASIC PRINT command (except for trailing commas and semicolons)?
It’s a good practice to close an IOCB when you’re all done with it. Hence, the CLOSE macro (Lines 1650–1740 of Listing 1). Very simply, it takes the parameter you pass as the IOCB number to be closed, and does the dirty deed forthwith. What could be simpler?
This wraps up our discussion of macros in the first sample program. Play with the graphics mode, colors, position and text strings until you’re convinced this really is almost as easy as BASIC. The big difference becomes apparent when you have to go through the assembly process each time you make a change in Listing 3. The value of an interpreted language like BASIC becomes clearer. However, things execute an awful lot faster in machine language, so you just have to grit your teeth through the assembly step.
Now turn to Listing 4. Do you notice a strong similarity to the short BASIC program you encountered at the beginning of this article? Essentially, we’ve just added some of the assembly directives, but the resemblance is uncanny. This program uses just three new macros, COLOR, PLOT and DRAWTO.
Lines 2550–2620 of Listing 1 are devoted to the COLOR macro. This works exactly like the BASIC COLOR statement. However, we do need to stash the color value you pass until we need it in the PLOT and DRAWTO macros. Address $C8, labeled COLOR, is the temporary holding tank for the color value. Note that there’s no conflict between this label and the identical macro name.
Lines 2730–2870 of Listing 1 define the PLOT macro. As you expect, it takes two parameters, X- and Y-coordinates, to plot a point. It simply passes these along to the POSITION macro in Line 2770, which actually positions the cursor in the right place. Again, we have a case of a macro calling a macro. We’ve now seen examples in which a macro passes parameters to a second macro in the form of numeric values, labels and string values (all from GRAPHICS to OPEN), and as raw parameters (%1 and %2 from PLOT to POSITION).
PLOT assumes you are using IOCB 6 (Line 2780). It uses an unusual form of the CIO PUTREC operation. Normally, PUTREC requires that you point to a buffer address and set a buffer length for the record to be output. An exception to this allows you to output a 1-byte “record.” First, set the buffer length to (Lines 2810–2830). Then load the accumulator with the character to be output. Line 2840 takes the number stored in address COLOR from the COLOR statement. It turns out that “printing” a 1 in this way (COLOR 1) selects color register 0, and so on for the other COLOR values. A simple JSR to CIOV in Line 2850 plots a single point in our graphics 5+16 screen.
We wrap up today’s macros with DRAWTO, in Lines 2980–3130 of Listing 1. Again, two parameters are expected; a line is to be drawn from the current ciursor position to these coordinates. The POSITION macro is again called to place the cursor as desired. The command for drawing a line is (guess what) DRAW. ICAX1 is set to 12, and ICAX2 to 0. To select the color register for the line drawn, we again fish out the result from the most recent COLOR macro call and stuff it into a location called ATACHR ($02FB). Jump to CIO, and your line magically appears.
You should now have a pretty good understanding of how we can get away with the pseudo-BASIC program in Listing 4.1 think you’ll agree that having these macros around makes it easier and faster to write assembly language programs. When you combine these macro and subroutine library files with the use of the RAMdisk on a 130XE and the great speed of the MAC/65 assembler, you have a powerful assembly development environment.
I see a Boot Camp column with macros for still more BASIC commands. I see macros for BASIC commands you never expected from Atari (maybe from OSS). I see a discussion of how to do all sorts of disk things from assembly language. I see a return to graphics programming and scrolling. I see you—next month.
Listing 1. Assembly listing. 0100 ;pseudo-BASIC macros for MAC/65 0110 ;put in file called MACRO.LIB 0120 ; 0130 ;******************************* 0140 ; 0150 ;equates needed by macros 0160 ; 0170 EOL = $9B 0180 OPEN = $03 0190 GETREC = $05 0200 PUTREC = $09 0210 CLOSE = $0C 0220 DRAW = $11 0230 ROWCRS = $54 0240 COLCRS = $55 0250 COLOR = $C8 0260 COLOR0 = $02C4 0270 ATACHR = $02FB 0280 ICCOM = $0342 0290 ICBAL = $0344 0300 ICBAH = $0345 0310 ICBLL = $0348 0320 ICBLH = $0349 0330 ICAX1 = $034A 0340 ICAX2 = $034B 0350 CIOV = $E456 0360 ; 0370 ;******************************* 0380 ; 0390 ;SETCOLOR macro 0400 ; 0410 ;Usage: SETCOLOR X,Y,Z 0420 ; 0430 ;X, Y, and Z can be values or 0440 ;memory addresses 0450 ; 0460 .MACRO SETCOLOR 0470 .IF %0<>3 0480 .ERROR "Error in SETCOLOR" 0490 .ELSE 0500 .IF %1>4 0510 LDX %1 0520 .ELSE 0530 LDX #%1 0540 .ENDIF 0550 .IF %2>15 0560 LDA %2 0570 ASL A 0580 ASL A 0590 ASL A 0600 ASL A 0610 .ELSE 0620 LDA #%2*16 0630 .ENDIF 0640 .IF %3>15 0650 LDY %3 0660 .ELSE 0670 LDY #%3 0680 .ENDIF 0690 STA COLOR0,X 0700 TYA 0710 AND #15 0720 CLC 0730 ADC COLOR0,X 0740 STA COLOR0,X 0750 .ENDIF 0760 .ENDM 0770 ; 0780 ;******************************* 0790 ; 0800 ;POKE macro 0810 ; 0820 ;usage: POKE X,Y 0830 ; 0840 ;X is an address, Y is a value 0850 ; 0860 .MACRO POKE 0870 .IF %0<>2 0880 .ERROR "Error in POKE" 0890 .ELSE 0900 LDA # <%2 0910 STA %1 0920 .ENDIF 0930 .ENDM 0940 ; 0950 ;******************************* 0960 ; 0970 ;POSITION macro 0980 ; 0990 ;usage: POSITION X,Y 1000 ; 1010 ;X and Y are both values; X 1020 ;can go from 0-319, Y from 0-191 1030 ; 1040 .MACRO POSITION 1050 .IF %0<>2 1060 .ERROR "Error in POSITION" 1070 .ELSE 1080 LDA # < %1 1090 STA COLCRS 1100 LDA # >%1 1110 STA COLCRS+1 1120 LDA # <%2 1130 STA ROWCRS 1140 .ENDIF 1150 .ENDM 1160 ; 1170 ;******************************* 1180 ; 1190 ;OPEN macro 1200 ; 1210 ;Usage: OPEN chan,aux1,aux2,dev 1220 ; 1230 ;'chan' is an IOCB number 1240 ;'aux1' is a task number 1250 ;'aux2' is the 2nd auxiliary byte 1260 ;'dev' is the name of the device 1270 ; to open, as a literal 1280 ; 1290 .MACRO OPEN 1300 .IF %0<>4 1310 .ERROR "Error in OPEN" 1320 .ELSE 1330 JMP @SKIPOPEN 1340 @DEVICE .BYTE %$4,EOL 1350 @SKIPOPEN 1360 LDX #%1*16 1370 LDA # <@DEVICE 1380 STA ICBAL,X 1390 LDA # >@DEVICE 1400 STA ICBAH,X 1410 .IF %2>255 1420 LDA %2 1430 .ELSE 1440 LDA #%2 1450 .ENDIF 1460 STA ICAX1,X 1470 .IF %3>255 1480 LDA %3 1490 .ELSE 1500 LDA #%3 1510 .ENDIF 1520 STA ICAX2,X 1530 JSR OPENIOCB 1540 .ENDIF 1550 .ENDM 1560 ; 1570 ;******************************* 1580 ; 1590 ;CLOSE macro 1600 ; 1610 ;Usage: CLOSE chan 1620 ; 1630 ;'chan' is an IOCB number 1640 ; 1650 .MACRO CLOSE 1660 .IF %0<>1 1670 .ERROR "Error in CLOSE" 1680 .ELSE 1690 LDX #%1*16 1700 LDA #CLOSE 1710 STA ICCOM,X 1720 JSR CIOV 1730 .ENDIF 1740 .ENDM 1750 ; 1760 ;******************************* 1770 ; 1780 ;GRAPHICS macro 1790 ; 1800 ;Usage: GRAPHICS X 1810 ; 1820 ;X is the number of the graphics 1830 ;mode desired; add 16 to this 1840 ;number to eliminate the text 1850 ;window 1860 ; 1870 .MACRO GRAPHICS 1880 .IF %0<>1 1890 .ERROR "Error in GRAPHICS" 1900 .ELSE 1910 JMP @SKIPGR 1920 @AUX1 1930 .BYTE 0 1940 @AUX2 1950 .BYTE 0 1960 @SKIPGR 1970 LDA #%1 1980 AND #$0F 1990 STA @AUX2 2000 LDA #%1 2010 AND #$F0 2020 EOR #$10 2030 ORA #$0C 2040 STA @AUX1 2050 OPEN 6,@AUX1,@AUX2,"S:" 2060 .ENDIF 2070 .ENDM 2080 ; 2090 ;******************************* 2100 ; 2110 ;PRINT macro 2120 ; 2130 ;usage: PRINT IOCB,address 2140 ; 2150 ;IOCB is channel number to use; 2160 ;'address' is the label of the 2170 ;text string to be printed; the 2180 ;text string must have an EOL 2190 ;character ($9B) at the end 2200 ; 2210 ;if only one parameter, then 2220 ;IOCB is assumed to be 0 (E:) 2230 ; 2240 ;calls subroutine PRINTLINE 2250 ; 2260 .MACRO PRINT 2270 .IF %0<1.OR%0>2 2280 .ERROR "Error in PRINT" 2290 .ELSE 2300 .IF %0=1 2310 LDX #0 2320 LDA # <%1 2330 STA ICBAL,X 2340 LDA # >%1 2350 STA ICBAH,X 2360 .ELSE 2370 LDX #%1*16 2380 LDA # <%2 2390 STA ICBAL,X 2400 LDA # >%2 2410 STA ICBAH,X 2420 .ENDIF 2430 JSR PRINTLINE 2440 .ENDIF 2450 .ENDM 2460 ; 2470 ;******************************* 2480 ; 2490 ;COLOR macro 2500 ; 2510 ;Usage: COLOR X 2520 ; 2530 ;X must be a value 2540 ; 2550 .MACRO COLOR 2560 .IF %0<>1 2570 .ERROR "Error in COLOR" 2580 .ELSE 2590 LDA #%1 2600 STA COLOR 2610 .ENDIF 2620 .ENDM 2630 ; 2640 ;******************************* 2650 ; 2660 ;PLOT macro 2670 ; 2680 ;Usage: PLOT X,Y 2690 ; 2700 ;X is the x-coordinate 2710 ;Y is the Y-coordinate 2720 ; 2730 .MACRO PLOT 2740 .IF %0<>2 2750 .ERROR "Error in PLOT" 2760 .ELSE 2770 POSITION %1,%2 2780 LDX #$60 2790 LDA #PUTREC 2800 STA ICCOM,X 2810 LDA #0 2820 STA ICBLL,X 2830 STA ICBLH,X 2840 LDA COLOR 2850 JSR CIOV 2860 .ENDIF 2870 .ENDM 2880 ; 2890 ;******************************* 2900 ; 2910 ;DRAWTO macro 2920 ; 2930 ;Usage: DRAWTO X,Y 2940 ; 2950 ;X and Y are the endpoints of 2960 ;the line to draw; must be values 2970 ; 2980 .MACRO DRAWTO 2990 .IF %0<>2 3000 .ERROR "Error in DRAWTO" 3010 .ELSE 3020 POSITION %1,%2 3030 LDX #$60 3040 LDA #DRAW 3050 STA ICCOM,X 3060 LDA #12 3070 STA ICAX1,X 3080 LDA #0 3090 STA ICAX2,X 3100 LDA COLOR 3110 STA ATACHR 3120 JSR CIOV 3130 .ENDM
Listing 2. Assembly listing. 0100 ;SUBS.LIB file to go with macro 0110 ;library file; required equates 0120 ;are in MACRO.LIB file 0130 ; 0140 ;******************************* 0150 ; 0160 ;subroutine PRINTLINE 0170 ;called by PRINT macro 0180 ; 0190 ;prints up to 160 characters on 0200 ;IOCB number that is already 0210 ;in the X-register 0220 ; 0230 PRINTLINE 0240 LDA #160 0250 STA ICBLL,X 0260 LDA #0 0270 STA ICBLH,X 0280 LDA #PUTREC 0290 STA ICCOM,X 0300 JSR CIOV 0310 RTS 0320 ; 0330 ;****************************** 0340 ; 0350 ;subroutine OPENIOCB 0360 ; 0370 ;called by OPEN macro 0380 ; 0390 OPENIOCB 0400 LDA #OPEN 0410 STA ICCOM,X 0420 JSR CIOV 0430 RTS 0440 ; 0450 ;------------------------------ 0460 ; 0470 ;if you use AMAC: 0480 ; 0490 ;some devices you might want 0500 ;to open - add your own if you 0510 ;use other custom handlers; 0520 ;you'll need to define a disk 0530 ;filename in full elsewhere in 0540 ;your program, such as: 0550 ; 0560 ;FILE1 .BYTE "D1:SCORES.DAT" 0570 ; 0580 S .BYTE "S:" 0590 E .BYTE "E:" 0600 C .BYTE "C:" 0610 P .BYTE "P:" 0620 K .BYTE "K:" 0630 R .BYTE "R:"
Listing 3. Assembly listing. 0100 ;Example 1 for macro library 0110 ;by Karl E. Wiegers 0120 ; 0130 .OPT OBJ,NO LIST 0140 .INCLUDE #D:MACRO.LIB 0150 *= $5000 0160 GRAPHICS 2 0170 SETCOLOR 2,6,4 0180 POKE COLOR0+4,6 0190 POSITION 1,1 0200 PRINT 6,TEXT1 0210 PRINT TEXT2 0220 CLOSE 6 0230 END JMP END 0240 .INCLUDE #D:SUBS.LIB 0250 TEXT1 .BYTE "THIS is a test",EOL 0260 TEXT2 .BYTE "Text window!",EOL
Listing 4. Assembly listing. 0100 ;Example 2 for macro library 0110 ;by Karl E. Wiegers 0120 ; 0130 .OPT OBJ,NO LIST 0140 .INCLUDE #D:MACRO.LIB 0150 *= $5000 0160 GRAPHICS 5+16 0170 SETCOLOR 0,1,12 0180 SETCOLOR 4,6,8 0190 COLOR 1 0200 PLOT 20,15 0210 DRAWTO 60,15 0220 DRAWTO 28,35 0230 DRAWTO 40,5 0240 DRAWTO 52,35 0250 DRAWTO 20,15 0260 CLOSE 6 0270 END JMP END 0280 .INCLUDE #D:SUBS.LIB
A.N.A.L.O.G. ISSUE 57 / SEPTEMBER 1987 / PAGE 69
How about a little sing-along? Follow the bouncing cursor: “Old MacDonald had an Atari, X-I, X-I-O…”
Okay, okay; no more singing. But today we’re going to talk about all sorts of things you can do with disk files, using an assembly language analog (I love that word) of the Atari BASIC XIO command. And, lest you fear I’ve forgotten the visual appeal of programming, we’ll also take a look at a special XIO graphics command. Along the way, we’ll add some entries to the MACRO.LIB and SUBS.LIB files we generated last month. Finally, we’ll see how to read a disk directory.
The XIO command is one of the less well known in Atari BASIC. XIO lets you perform a myriad of disk file operations, like locking and deleting, that are impossible to do with other BASIC commands. What XIO really does is provide direct access to the central input-output (CIO) subsystem from BASIC. Since we’ve spent a lot of time here probing into the mysteries of CIO, it seems reasonable to add disk operations to our toolbox.
The format of the BASIC XIO command is:
I’m sure you see the resemblance to the other CIO functions we’ve learned to perform. The command parameter is the numeric code for a particular CIO operation, such as $03 for OPEN and $11 for DRAW. The #IOCB parameter is (guess what) the number of the input-output control block to use for the operation. Aux1 and aux2 are the two auxiliary parameters required for some CIO operations. Filespec is a device (and optional file) name, such as P: or D2:DATA.FIL.
The assembly language code to perform a particular CIO operation also involves choosing an IOCB, setting the command byte and setting the auxiliary values if they’re needed. The filespec function is handled by pointing to a “buffer address” which is the label of a data string like P: or D2:DATA.FIL. About the only IOCB bytes not set explicitly by the XIO command are the high and low buffer length bytes, but these aren’t even needed by most of the new CIO commands we’ll explore today.
You’re already used to CIO commands like OPEN, PUTREC, CLOSE, GETREC and DRAW. The ones I’ll introduce today pertain to disk file operations, including LOCK, UNLOCK, RENAME, DELETE, GET STATUS and FORMAT. One final CIO command is FILL, which quickly fills a polygon shape on a graphics screen with a solid color. Table 1 summarizes a bunch of CIO commands, old and new.
Table 1.—CIO commands. Name Hex Code Function. OPEN $03 Open an IOCB. GET RECORD $05 Input a record up lo EOL or end of buffer. GET CHARACTERS $07 Input a specified number of characters. PUT RECORD $09 Output a record up to EOL or end of buffer. PUT CHARACTERS $0B Output a specified number of characters. CLOSE $0C Close an IOCB. GET STATUS $0D Return the status of a device or file. DRAW $11 Draw a straight line on a graphics screen. FILL $12 Fill a polygonal area on graphics screen. RENAME $20 Rename a disk file. DELETE $21 Erase a disk file. LOCK $23 Lock a disk file. UNLOCK $24 Unlock a disk file. POINT $25 Move file pointer to specific sector and byte. NOTE $26 See where file pointer is now. FORMAT $FE Format a disk
No, this doesn’t refer to the dreaded keyboardus lockupus. We’re just going to lock a disk file. First we need a file to practice on. Create a file called PRACTICE.FIL on whatever drive you like; the contents are irrelevant. I do my experimenting with the RAMdisk, drive D8:. It’s a lot faster than using a physical drive, and reduces wear and tear on the mechanical drives. If you use a drive other than D8:, you’ll need to change the designation in all my examples to match yours (Line 530 in Listing 1).
(Incidentally, there’s a neat way to create a simple ATASCII disk file for this. Use the DOS COPY command to copy from E: to D8:PRACTICE.FIL. Now type a few lines, pressing RETURN at the end of each. When done, press CTRL-3 on a new line to close the file and write it to the disk.)
Please turn to Listing 1 to see how to lock a file. The equates list contains the standard items for CIO operations, except that the LOCK command value of $23 is supplied in Line 180. First we need to choose an available IOCB. In Line 330, I opted to use IOCB #3. The command is LOCK, and all the buffer length and auxiliary bytes can be set to (Lines 360–400). In Lines 410–440, I set the buffer address to point to the name of the disk file we wish to lock, at label FILENAME. The filespec at label FILENAME can contain wildcards, if you wish to lock more than one file at a time. As usual, the JSR CIOV in Line 450 locks the file.
Whenever we’re doing this sort of disk file manipulation, possibilities for error are limitless. What if the file doesn’t exist? What if you accidentally specified a nonexistent device name? If the disk is write protected? We need a mechanism to recover from such errors; CIO helps out.
On exiting from a CIO call, error status is recorded in two places: the 6502’s Y-register and another byte in each IOCB called ICSTA ($0343 in IOCB #0). A successful operation leaves a status value of 1. Any abnormality generates an error number from $80 to $AB. Note: all such error numbers have bit 7 set, and so are regarded as negatives. A CIO error thus sets a negative flag in the 6502’s processor status register, and we can easily test for an error via an instruction like the BPL ALLDONE in Listing 1’s Line 460.
If the negative flag is set, the BPL instruction fails and control drops through to Line 470. At that point, we call subroutine LOCKERR to do the graceful recovering. In this case, I simply print a message on-screen saying that an error was encountered during the locking attempt, and return to the calling program.
In real (as opposed to tutorial) life, you’d probably want to print a more detailed message and let the user decide what sort of action to take next. You might even choose from a collection of possible messages, based on the specific error encountered. After all, the cryptic ERROR-170-type messages aren’t terribly illuminating.
Table 2 lists many of the error numbers you might encounter during a CIO operation. Your error handling procedure might load the contents of the ICSTA byte for the correct IOCB and compare it against several anticipated error numbers to decide which message to print. Don’t forget to leave a catch-all message in case someone generates an error you either didn’t think of or didn’t know about.
Table 2.—Some CIO error (status) numbers. Hex Code Decimal Meaning 1 1 No error—everything’s cool. 80 128 BREAK key pressed during I/O operation. 81 129 IOCB is already in use. 82 130 Device specified does not exist. 83 131 Attempt to read from an IOCB opened only for output. 84 132 CIO command is invalid or syntax is bad. 85 133 Attempt to use an IOCB that isn’t open. 86 134 IOCB number isn’t in the range 1–7. 87 135 Attempt to write on an IOCB opened only for input. 88 136 End of file was reached. 89 137 Data record longer than 256 bytes was truncated. 8A 138 Device does not respond, causing a timeout. 8B 139 Device malfunction. 90 144 Disk is write protected, or directory is garbled. 92 146 Attempted an invalid operation on the device. A0 160 Disk drive number error. A1 161 Too many disk files are open at once. A2 162 The disk is full. A3 163 Unspecified fatal disk error. A4 164 Disk file is garbled, or file pointer is not pointing to part of the open file. A5 165 Filename error. A6 166 POINT error—pointing to nonexistent byte number. A7 167 File is locked. A8 168 Attempt to use invalid or unknown CIO command. A9 169 Disk directory is full (64 tiles maximunn). AA 170 File is not found. AB 171 POINT error—pointing to a sector that is not part of the open file.
Last month, we found lots of shortcuts to writing assembly programs. Why should this month be any different? All this XIO/CIO stuff looks like a job for…
Listing 2 contains some additional equates for more CIO commands, and an XIO macro. Please add this code to your MACRO.LIB file from last month, using the line numbers shown. Now let’s take a look at the XIO macro.
Just like the BASIC version, our XIO macro takes five parameters. The only thing missing is the number sign (#) in front of the IOCB number in parameter 2. Not all of these disk CIO commands require five parameters, so we could probably write a smarter macro to handle variable numbers of parameters (such as missing aux1 and aux2). In fact, the MAC/65 manual does include a very sophisticated XIO example with just such a capability. However, I’ll keep things simple today, remaining consistent with the BASIC XIO format.
As usual, I make sure the right number of parameters were passed in Line 3290. Line 3320 selects the IOCB number, and Lines 3330–3340 set the command value. Lines 3350–3380 stuff the auxiliary parameters passed into the correct bytes. Note that all four of these parameters must be values, not addresses. However, the fifth parameter for the filespec can either be a string literal (like D8:PRACTICE.FIL) or a non-zero-page address (like FILENAME).
The one catch is that, if you use an address, it must have been defined before invoking the macro. When I tried this with the XIO call prior to a FILENAME .BYTE…line in the program, I saw the dreaded PHASE ERROR. Most of the time, a literal will work just fine. All the code in Line 3390-3520 just handles the two flavors of parameter 5, literal (3400–3460) or address (3480–3510). Again, the filespec can contain wildcard symbols (? and *) if the operation you’re performing permits them.
In keeping with my philosophy of dividing the program’s work among a bunch of subroutines. Listing 3 contains some code I’d like you to tack onto the end of your SUBS.LIB file from last time. Please use the line numbers shown. These are all subroutines for handling general errors encountered during the XIO LOCK, UNLOCK, RENAME, FORMAT and DELETE operations.
There’s also a routine for handling the CIO GET STATUS (or just STATUS) operation. This is like the BASIC STATUS command. There are three possible responses from the STATUS command, which again wind up in the Y-register and in ICSTA. A value of 1 means the filespec supplied was found and is unlocked. A value of 167 (hex $A7) means the file is there but it’s locked. If the file isn’t found, a status of 170 ($AA) results. You may be familiar with these numbers from your activities at the DOS menu.
It’s a very good idea to check the status of a file before performing operations that could result in an error if all’s not well. For example, before deleting a file, it must be unlocked. You can either let the delete operation generate an error and handle it there, or make a preemptive strike with the status command. The subroutine STATUSERR (Lines 1290–1630 in Listing 3) handles the found-but-locked and not-found conditions, as well as being a catch-all in case some unanticipated value shows up in the status byte. These aren’t really “errors,” just responses to inquiries. However, an ounce of prevention…
Now let’s have some fun! Please create a file called simply P on a disk—doesn’t matter what’s in it. The example in Listing 4 refers to drive D8:, so please change D8: to Dwhatever: throughout the listing if you aren’t using a RAMdisk. We’re going to .INCLUDE the MACRO.LIB and SUBS.LIB files, and I have those on drive D8: also (Lines 240 and 990).
Here’s the plan: we’re going to lock P, unlock it, rename it to Q.R, lock Q.R, then try to delete Q.R (it won’t work). The nice part: this example is structured so that you can fool around with it a lot. Try substituting the names of files that don’t exist and see how good my error trapping is. Make mistakes in the syntax for the rename command—see if I care. Comment out the lock operation in Lines 590–620, so the subsequent delete operation succeeds. Comment out some of the status checks and let the errors get caught after trying the CIO command. Do whatever you like, but please play with this example enough to feel comfortable with what we’re trying to learn.
I’ve even supplied the format operation, in Lines 880–910. As found in Listing 4, this block of code is commented out. The FORMAT command is dangerous! I’m sure you know this already, but make sure you have a disk that isn’t dear to your heart in the drive before uncommenting those lines and letting ’er rip. As is, the format is for drive D2:, so you may need to change that. Don’t try to format your RAMdisk. And please, folks, don’t drink and format. The disk you save may be your own.
I’ve taken my own advice and done a status check before many of the other operations in this example. However, you really don’t need to do it everywhere. For example, I do a status check first thing, before trying to lock file D8:P. But, once the LOCK command in Line 380 is successful, I’m pretty confident that the UNLOCK immediately thereafter will go okay, so I don’t do another status check. Of course, this won’t catch those of you who manage to flip the disk drive door open between Lines 380 and 390, but I’m not too worried about that.
Note the format of the RENAME command in Line 480. The old and new names follow the drive identifier, separated by a comma, all within the one pair of double quotes. Be careful using RENAME, since it’s pretty easy to wind up with duplicate filenames on the same disk. This intensive error checking results in a leapfrogging structure to the code, as we keep skipping around the JSRs to error routines. Hey, nobody forced you to become an assembly language programmer—if it’s structure you seek, try Pascal. You’ll have to use your judgment as to how extensive the error trapping should be in your own programs, based largely on the intended users. Just keep in mind how you feel when a program you bought or got from the public domain crashes due to sloppy error handling!
Our third example, in Listing 5, illustrates the other graphics XIO command, FILL. This command fills a polygon with a solid color, from the color register of your choice. The very compact listing uses our graphics macros from last month to good advantage. When you write programs like this, don’t forget to .INCLUDE the MACRO.LIB file at the beginning of the code, and .INCLUDE the SUBS.LIB file at the end. I mention this yet again because I repeatedly forget SUBS.LIB, then wonder why my simple program won’t run.
We need an additional equate, which I added to MACRO.LIB in Listing 2. This is for an address called FILDAT ($02FD). Before doing a fill operation, you simply load FILDAT with the number of the color register you want to use. In Listing 5, we open a full screen of graphics 5, select color register 1, and plot the lower right corner of a rectangle at location 40,35. Next, draw a line to the upper right corner of the figure at 40,10 and draw over to the upper left corner at 20,10. Then position the cursor at the lower left corner at coordinates 20,35. Select color register 1 for the fill operation using our POKE macro and, finally, execute the fill with the XIO call in Line 290. The infinite loop in Line 300 just keeps the display on-screen; press RESET when you’re done admiring it.
You can get some interesting effects by using different color registers for the outline (Line 230) and the filler (Line 280). A SETCOLOR or two frees you from the confines of the default color values. To draw a solid triangle, you can just omit the second DRAWTO in Line 260. Experiment with different angles of lines and numbers of vertices in your polygon, and see what this command lets you get away with.
Perhaps the most interesting data on a disk is that in the disk directory. The directory lists all the files on the disk, the number of disk sectors occupied by each, and the number of free sectors remaining on the disk. Many applications need to access the directory, and Listing 6 shows you how to do it in assembly language.
Begin by opening an IOCB with the first auxiliary byte set to 6. Line 250 of Listing 6 uses the OPEN macro from last month to do this. The filespec shown (D8:*.*) will list all files on drive D8:. Change this filespec if you want to list some or all of the files on another drive. A really conscientious programmer would check for a CIO error following the OPEN, but I’ve omitted that step to save space.
Now, we need to read records one at a time from the disk directory The subroutine called INPUT (Lines 470–600) performs that task. INPUT takes a record of eighteen characters and stashes it at label FILENAME (Line 390). The .DS 18 directive at FILENAME simply reserves 18 bytes for holding our filespec. Why 18 bytes? This is the format of a disk directory record:
The final record, however, is 17 bytes long, in the format; XXX FREE SECTORS, followed by an EOL.
Since we don’t know how many entries there are in the directory until we read it, we need some way to know when to stop reading the directory. One way is to see if the fifth byte in FILENAME is an F (for FREE), meaning that we’re done. Alternatively, we can let CIO return an end-of-file error (error number 136, hex $88) and then stop our routine. That’s how I do it in Listing 6,Lines 280–290. If the end of the file wasn’t reached yet, I use the PRINT macro to print the current contents of FILENAME on the screen (Line 300) and go back to get another record (310–320). When the end of the file is reached, the code at label DONE simply closes the IOCB and halts execution.
Notice that, in Lines 310–320 I used an unconditional branch operation to loop back to READONE. Wouldn’t a JMP READONE instruction work just as well? Yes, of course it would.
The difference is that the branch operation is relative (e.g., go back 25 bytes), whereas the jump operation is absolute (go to a specific address). If your program contains only relative branching, then it is “relocatable” in memory; you can load it at any address (not just the one at which you originally assembled it) and it should run just fine. Programs containing absolute jumps to addresses within the program are usually not relocatable. They must be reassembled at a new origin to run at a different address.
It is generally preferable to write relocatable assembly routines, especially if you’re writing a machine language subroutine for a BASIC program. I haven’t been too rigorous about making all my examples relocatable, but that’s something we should all keep in mind. All the internal JMPs in my macros really ought to be changed to unconditional branches.
Once you’ve read the disk directory using this sort of routine, you can twiddle it. Suppose you don’t care about the number of sectors occupied by the files, and just want to see their names. You could add an end-of-line marker in byte 14 of the FILENAME data block before printing it, thereby retaining only the data through the extension. Here’s your homework assignment; write a general INPUT macro, to read a string up to a specified maximum number of characters from a specified IOCB, and store the string at a particular address. Then, modify the directory program in Listing 6 to use this macro instead of the INPUT subroutine. There will be a short quiz at the beginning of the next class.
Despite his Ph.D. in organic chemistry, Karl Wiegers earns a living writing applications software for photographic research at Eastman Kodak Company, mostly on an IBM mainframe. He is also interested in educational applications of Atari 8-bit, Atari ST and Apple II computers.
Listing 1. Assembly listing. 0100 ;Example 1 - Locking a disk file 0110 ;by Karl E. Wiegers 0120 ; 0130 .OPT OBJ,NO LIST 0140 ; 0150 ;equates for CIO operations 0160 ; 0170 PUTREC = $09 0180 LOCK = $23 0190 EOL = $9B 0200 ICCOM = $0342 0210 ICBAL = $0344 0220 ICBLL = $0348 0230 ICAX1 = $034A 0240 ICAX2 = $034B 0250 CIOV = $E456 0260 ; 0270 ;******************************* 0280 ; PROGRAM BEGINS HERE 0290 ;******************************* 0300 ; 0310 *= $5000 0320 ; 0330 LDX #$30 ;use IOCB #3 0340 LDA #LOCK ;command is LOCK 0350 STA ICCOM,X 0360 LDA #0 ;don't need 0370 STA ICBLL,X ;to set buffer 0380 STA ICBLL+1,X ;length or 0390 STA ICAX1,X ;any aux bytes 0400 STA ICAX2,X 0410 LDA # <FILENAME ;point to 0420 STA ICBAL,X ;file to be 0430 LDA # >FILENAME ;locked 0440 STA ICBAL+1,X 0450 JSR CIOV ;do it! 0460 BPL ALLDONE ;error? 0470 JSR LOCKERR ;yes, print msg. 0480 ALLDONE 0490 BRK 0500 ; 0510 ;name of file to be locked 0520 ; 0530 FILENAME .BYTE "D8:PRACTICE.FIL" 0540 ; 0550 ;******************************* 0560 ; 0570 ;error handling subroutine - just 0580 ;prints a message if any error is 0590 ;encountered 0600 ; 0610 LOCKERR 0620 LDX #0 0630 LDA #PUTREC 0640 STA ICCOM,X 0650 LDA # <ERRMSG 0660 STA ICBAL,X 0670 LDA # >ERRMSG 0680 STA ICBAL+1,X 0690 LDA #80 0700 STA ICBLL,X 0710 LDA #0 0720 STA ICBLL+1,X 0730 STA ICAX2,X 0740 JSR CIOV 0750 RTS 0760 ; 0770 ;error message to print 0780 ; 0790 ERRMSG 0800 .BYTE "Error during lock",EOL
Listing 2. Assembly listing. 0211 STATUS = $0D 0221 FILL = $12 0222 RENAME = $20 0223 DELETE = $21 0224 LOCK = $23 0225 UNLOCK = $24 0226 POINT = $26 0227 NOTE = $27 0228 FORMAT = $FE 0272 FILDAT = $02FD 0285 ICSTA = $0343 3140 ; 3150 ;******************************* 3160 ; 3170 ;XIO macro 3180 ; 3190 ;Usage: XIO cmd,IOCB,aux1,aux2,fs 3200 ; 3210 ;'cmd' is a CIO command number 3220 ;'IOCB' is the IOCB number to use 3230 ;'aux1' is the 1st auxiliary byte 3240 ;'aux2' is the 2nd auxiliary byte 3250 ;'fs' is a filespec (literal or 3260 ; previously-defined address) 3270 ; 3280 .MACRO XIO 3290 .IF %0<>5 3300 .ERROR "Error in XIO" 3310 .ELSE 3320 LDX #%2*16 3330 LDA #%1 3340 STA ICCOM,X 3350 LDA #%3 3360 STA ICAX1,X 3370 LDA #%4 3380 STA ICAX2,X 3390 .IF %5<256 3400 JMP @SKIPXIO 3410 @XDEV .BYTE %$5,EOL 3420 @SKIPXIO 3430 LDA # <@XDEV 3440 STA ICBAL,X 3450 LDA # >@XDEV 3460 STA ICBAH,X 3470 .ELSE 3480 LDA # <%5 3490 STA ICBAL,X 3500 LDA # >%5 3510 STA ICBAH,X 3520 .ENDIF 3530 JSR CIOV 3540 .ENDIF 3550 .ENDM
Listing 3. Assembly listing. 0640 ; 0650 ;******************************* 0660 ; 0670 ;LOCK error handler subroutine 0680 ; 0690 LOCKERR 0700 LDX #0 0710 LDA # <LOCKMSG 0720 STA ICBAL,X 0730 LDA # >LOCKMSG 0740 STA ICBAL+1,X 0750 JSR PRTERROR 0760 RTS 0770 LOCKMSG 0780 .BYTE "Locking error",EOL 0790 ; 0800 ;******************************* 0810 ; 0820 ;UNLOCK error handler subroutine 0830 ; 0840 UNLOCKERR 0850 LDX #0 0860 LDA # <UNLOCKMSG 0870 STA ICBAL,X 0880 LDA # >UNLOCKMSG 0890 STA ICBAL+1,X 0900 JSR PRTERROR 0910 RTS 0920 UNLOCKMSG 0930 .BYTE "Unlocking error",EOL 0940 ; 0950 ;******************************* 0960 ; 0970 ;sub. to print error messages 0980 ; 0990 PRTERROR 1000 LDA #PUTREC 1010 STA ICCOM,X 1020 LDA #80 1030 STA ICBLL,X 1040 LDA #0 1050 STA ICBLL+1,X 1060 STA ICAX2,X 1070 JSR CIOV 1080 RTS 1090 ; 1100 ;******************************* 1110 ; 1120 ;RENAME error handler subroutine 1130 ; 1140 RENAMERR 1150 LDX #0 1160 LDA # <RENAMEMSG 1170 STA ICBAL,X 1180 LDA # >RENAMEMSG 1190 STA ICBAL+1,X 1200 JSR PRTERROR 1210 RTS 1220 RENAMEMSG 1230 .BYTE "Renaming error",EOL 1240 ; 1250 ;******************************* 1260 ; 1270 ;STATUS error handler subroutine 1280 ; 1290 STATUSERR 1300 CPY #$AA ;file not found? 1310 BNE CHKLOCK ;no, skip ahead 1320 LDX #0 1330 LDA # <NOTFNDMSG ;yes, say so 1340 STA ICBAL,X 1350 LDA # >NOTFNDMSG 1360 STA ICBAL+1,X 1370 CLC 1380 BCC PRTSTATUS 1390 CHKLOCK 1400 CPY #$A7 ;file locked? 1410 BNE UNKNOWN ;no, skip ahead 1420 LDX #0 1430 LDA # <STLOCKMSG ;yes,say so 1440 STA ICBAL,X 1450 LDA # >STLOCKMSG 1460 STA ICBAL+1,X 1470 CLC 1480 BCC PRTSTATUS 1490 UNKNOWN 1500 LDX #0 1510 LDA # <UNKNOWNMSG ;handle all 1520 STA ICBAL,X ;other status 1530 LDA # >UNKNOWNMSG ;values 1540 STA ICBAL+1,X 1550 PRTSTATUS 1560 JSR PRTERROR 1570 RTS 1580 NOTFNDMSG 1590 .BYTE "File not found",EOL 1600 STLOCKMSG 1610 .BYTE "File is locked",EOL 1620 UNKNOWNMSG 1630 .BYTE "Unknown error",EOL 1640 ; 1650 ;******************************* 1660 ; 1670 ;FORMAT error handler subroutine 1680 ; 1690 FORMATERR 1700 LDX #0 1710 LDA # <FORMATMSG 1720 STA ICBAL,X 1730 LDA # >FORMATMSG 1740 STA ICBAL+1,X 1750 JSR PRTERROR 1760 RTS 1770 FORMATMSG 1780 .BYTE "Formatting error",EOL 1790 ; 1800 ;******************************* 1810 ; 1820 ;DELETE error handler subroutine 1830 ; 1840 DELETERR 1850 LDX #0 1860 LDA # <DELETEMSG 1870 STA ICBAL,X 1880 LDA # >DELETEMSG 1890 STA ICBAL+1,X 1900 JSR PRTERROR 1910 RTS 1920 DELETEMSG 1930 .BYTE "Deleting error",EOL
Listing 4. Assembly listing. 0100 ;Example 2 - using the XIO macro 0110 ;by Karl E. Wiegers 0120 ; 0130 ;examples of using STATUS, LOCK, 0140 ;UNLOCK, RENAME, DELETE, and 0150 ;FORMAT disk operations 0160 ; 0170 ;you need the MACRO.LIB and 0180 ;SUBS.LIB files on the disk in 0190 ;the drive named in lines 0200 ;0240 and 0990 0210 ; 0220 ; 0230 .OPT OBJ,NO LIST 0240 .INCLUDE #D8:MACRO.LIB 0250 ; 0260 ;****************************** 0270 ; PROGRAM STARTS HERE 0280 ;****************************** 0290 ; 0300 *= $5000 0310 ; 0320 XIO STATUS,3,0,0,"D8:P" 0330 CPY #1 0340 BEQ OK1 0350 JSR STATUSERR 0360 BRK 0370 OK1 0380 XIO LOCK,3,0,0,"D8:P" 0390 BPL OK2 0400 JSR LOCKERR 0410 BRK 0420 OK2 0430 XIO UNLOCK,3,0,0,"D8:P" 0440 BPL OK3 0450 JSR UNLOCKERR 0460 BRK 0470 OK3 0480 XIO RENAME,3,0,0,"D8:P,Q.R" 0490 BPL OK4 0500 JSR RENAMERR 0510 BRK 0520 OK4 0530 XIO STATUS,3,0,0,"D8:Q.R" 0540 CPY #1 0550 BEQ OK5 0560 JSR STATUSERR 0570 BRK 0580 OK5 0590 XIO LOCK,3,0,0,"D8:Q.R" 0600 BPL OK6 0610 JSR LOCKERR 0620 BRK 0630 OK6 0640 ; 0650 ;------------------------------- 0660 ;NOTE: program will stop here 0670 ;and tell you that the file is 0680 ;locked. 0690 ;------------------------------- 0700 ; 0710 XIO STATUS,3,0,0,"D8:Q.R" 0720 CPY #1 0730 BEQ OK7 0740 JSR STATUSERR 0750 BRK 0760 OK7 0770 XIO DELETE,3,0,0,"D8:Q.R" 0780 BPL OK8 0790 JSR DELETERR 0800 BRK 0810 OK8 0820 ; 0830 ;------------------------------- 0840 ;BE VERY CAREFUL WHEN USING THE 0850 ;FORMAT COMMAND !!!!!!!! 0860 ;------------------------------- 0870 ; 0880 ; XIO FORMAT,3,0,0,"D2:" 0890 ; BPL OK9 0900 ; JSR FORMATERR 0910 ; BRK 0920 OK9 0930 BRK 0940 ; 0950 ;------------------------------- 0960 ;pull in all the subroutines 0970 ;------------------------------- 0980 ; 0990 .INCLUDE #D8:SUBS.LIB
Listing 5. Assembly listing. 0100 ;Example 3-using the FILL command 0110 ;by Karl E. Wiegers 0120 ; 0130 .OPT OBJ,NO LIST 0140 .INCLUDE #D8:MACRO.LIB 0150 ; 0160 ;******************************* 0170 ; PROGRAM STARTS HERE 0180 ;******************************* 0190 ; 0200 *= $5000 0210 ; 0220 GRAPHICS 5+16 0230 COLOR 1 0240 PLOT 40,35 0250 DRAWTO 40,10 0260 DRAWTO 20,10 0270 POSITION 20,35 0280 POKE FILDAT,1 0290 XIO FILL,6,0,0,"S:" 0300 END JMP END 0310 ; 0320 ;include the subroutine library 0330 ; 0340 .INCLUDE #D8:SUBS.LIB
Listing 6. Assembly listing. 0100 ;Example 4-reading disk directory 0110 ;by Karl E. Wiegers 0120 ; 0130 .OPT OBJ,NO LIST 0140 .INCLUDE #D8:MACRO.LIB 0150 ; 0160 ;******************************* 0170 ; PROGRAM STARTS HERE 0180 ;******************************* 0190 ; 0200 *= $5000 0210 ; 0220 ;open IOCB #1 to read disk 0230 ;directory - use any filespec 0240 ; 0250 OPEN 1,6,0,"D1:*.*" 0260 READONE 0270 JSR INPUT ;get a filename 0280 CPY #136 ;end of file? 0290 BEQ DONE ;yes, quit 0300 PRINT FILENAME ;no,write it 0310 CLC ;get the next one 0320 BCC READONE 0330 DONE 0340 CLOSE 1 ;all done 0350 BRK 0360 ; 0370 ;save 18 bytes for a filename 0380 ; 0390 FILENAME .DS 18 0400 ; 0410 ;------------------------------- 0420 ;subroutine to read a filename 0430 ;from the disk directory and 0440 ;store it at address FILENAME 0450 ;------------------------------- 0460 ; 0470 INPUT 0480 LDX #$10 ;use IOCB #1 0490 LDA #GETREC ;command is 0500 STA ICCOM,X ;GET a RECord 0510 LDA # <FILENAME ;put it at 0520 STA ICBAL,X ;address FILENAME 0530 LDA # >FILENAME 0540 STA ICBAH,X 0550 LDA #18 ;get 18 chars 0560 STA ICBLL,X 0570 LDA #0 0580 STA ICBLH,X 0590 JSR CIOV ;go do it 0600 RTS 0610 ; 0620 ;include the subroutine library 0630 ; 0640 .INCLUDE #D8:SUBS.LIB
A.N.A.L.O.G. ISSUE 58 / OCTOBER 1987 / PAGE 75
Last month’s Boot Camp ended with a homework assignment. I asked you to write an INPUT macro that would read a string up to a specified maximum number of characters from a particular IOCB, and store the string at a particular address in RAM. Don’t bother trying to tell me why you didn’t get around to doing this assignment. I used to teach organic chemistry to pre-med students at the University of Illinois—I’ve heard all the excuses. I’ll just assume you did do it, and we’ll proceed from there.
Today I’ll present my version of the INPUT macro, along with other macros and subroutines that do some pretty neat things. Would you believe a delay subroutine to do nothing for a precise period of time? How about a FOR/NEXT loop implementation in assembly language? Or a MOVE macro to copy a block of data of any length from one address to another? All this, and more, lies in the next few pages.
Perhaps you’re among the readers who don’t own a macro assembler. You flip to Boot Camp each month and groan, “When is this clown going to quit with the macros already and get back to something I can use?” Please don’t feel left out.
Virtually everything we’ve discussed during the foray into macroland is useful to you, anyway. Remember, a macro is just a shorthand way to write assembly programs. You can adapt all the macros I’ve presented just by expanding the source code yourself when writing a program where you would otherwise invoke a macro. In fact, since you’ll know exactly what your parameters are, you can skip all the conditional assembly (.IF/.ELSE/.ENDIF) code I’ve built into the general macro formats. And, of course, you can use the subroutines exactly as I’ve written them.
How about another programming philosophy discourse: efficiency. I think of three kinds of programming efficiency: the time I spend developing the program; size of the resulting object code; and execution speed. Programming in assembly is a great way to come up with fast, compact object code. However, the act of designing and writing the source code is a lot slower than it is if you’re using a high-level language like BASIC.
To make your programming more efficient, use good tools—like a fast assembler, a RAMdisk, macros and common subroutines. But all is still not roses. These shortcuts exact a toll in both size and speed. Consider the way we .INCLUDE the subroutine library file we’ve been constructing, SUBS.LIB. Those subroutines get assembled into object code whether or not the main program calls any of them. This both slows the assembly process as the file is read and results in larger object files than we’d get if only the subroutines actually called were included in the source code.
Similarly, many of our macros are written to be flexible, handling a variety of parameters and situations. This sometimes results in more assembled instructions than does individually coding each instance. And more instructions to be executed mean slightly longer execution times. In essence, we’ve been building our own “high-level” language, with the accompanying benefits and drawbacks.
Are these compromises worth it? Assembly programs generally run fast enough that a few extra instructions don’t do any harm. Our computers have a lot more RAM now than they used to, so a longer object file doesn’t hurt much, either. And these shortcuts save a lot of your time. I usually prefer to program for ease of comprehension and maintainability, rather than for fastest or shortest code.
Just one more thing before we get to the business at hand. Last month, I showed how to format a disk using an XIO macro. The CIO command value I mentioned, $FE, will actually format a disk in an Atari 1050 disk drive in enhanced-density mode. Drives other than the 1050 automatically format in single density with this command. If you’re using a 1050 drive and want to format in single density, use command value $FD instead. This is equivalent to option P at the DOS 2.5 menu, “Format Single.”
Please merge Listing 1 with the MACRO.LIB file you’ve been building over the past few months. Be sure to use the line numbers shown. Listing 1 contains a few more equates for some variables used in the new macros. Notice that I am using four zero-page locations ($CB–$CE); these bytes are free for your own needs, except when using the MOVE macro. Also, bytes $0681–$0687 are used by the FOR/NEXT and MOVE macros. MAC/65 gobbles up locations $0600–$067F, so I might as well keep consuming page 6 for these work variables.
Lines 845–915 of Listing 1 modify our existing POKE macro to accept either an address or a value as parameter 2. If parameter 2 is smaller than 256, it’s assumed to be a value. Otherwise, it’s assumed to be an address, and the contents of that address are copied into the address specified in parameter 1.
The rest of Listing 1 is the code for eight new macros: INPUT, PAUSE, ADD, SUBTRACT, DPOKE, FOR, NEXT and MOVE. Listing 2 contains two new subroutines that should be added to your SUBS.LIB file.
Let’s start very simply. Lines 1970–2020 of Listing 2 define a subroutine called CLS, for clear screen. If you want to completely erase a graphics screen, use a JSR CLS instruction. This simply prints character 125, the ATASCII clear screen character, on IOCB 0. The BASIC equivalent is: PRINT CHR$(125).
Last month we talked about writin’, so this time we’ll do some readin’. My INPUT macro lives in Lines 3590–3950 of Listing 1. It takes three parameters: the IOCB number; the buffer address where the string read is to be stored; and the number of bytes to be read. Parameter 3 is optional. If absent, the buffer length is set to 255 bytes, although an end-of-line character (EOL) will also terminate the input step. The CIO command is GETREC (Lines 3760–3770). The rest of the macro just involves the familiar CIO activities of pointing to the buffer address (Lines 3780–3810) and setting the buffer length (Lines 3820–3920).
The easiest way to use INPUT is to reserve space for the string to be input, with a statement such as: FILENAME .DS 17, which will reserve 17 bytes at address FILENAME. To let the user enter a filename at the keyboard, then, you’d use a statement like: INPUT 0,FILENAME,17. There’s another example in Listing 3, which is a sample program to exercise a bunch of our macros.
Sometimes you feel like giving the computer (or maybe its user) a breather, simply doing nothing for a period of time. In BASIC, the empty FOR/NEXT loop is often employed. Four hundred iterations consume about a second of time, but this is only approximate. The macro called PAUSE (Lines 3990–4290 of Listing 1) lets you suspend execution of your program for a precise period of time.
PAUSE takes one parameter, the number of “jiffies” that you want to wait before continuing execution of the program. A jiffy is 1/60th of a second, and the Atari has an internal real-time clock that increments a particular byte every jiffy. This is part of the system housekeeping performed during every vertical blank period, which you no doubt recall occurs sixty times per second. The PAUSE parameter can be either a value or an address (not zero-page).
The real-time clock is located in bytes $12–$14. The equate for RTCLOK is in the first line in Listing 1. Address $14 (RTCLOK+2) is the one that gets incremented every jiffy. After 255 jiffies (about 4.27 seconds), RTCLOK + 2 is reset to 0 and RTCLOK+1 is incremented. After 65,535 jiffies (18.2 minutes), RTCLOK+1 is reset to 0 and RTCLOK is incremented. The PAUSE macro uses only RTCLOK + 2, so you’re limited to a delay of just over four seconds, precise to 1/60th of a second. To wait longer, you could call PAUSE several times in a row. If you’re more ambitious, modify PAUSE to accept a 2-byte parameter, and you could then set wait times up to 18.2 minutes.
PAUSE simply stores the desired number of jiffies to wait at address @TIMER. Lines 4180–4190 use the contents of an address as the delay time, and Lines 4210–4220 use a value. RTCLOK+2 must be initialized to 0 (Lines 4150–4160). Then a loop simply compares RTCLOCK+2 to @TIMER until they match. Of course, the computer really isn’t doing “nothing” during the pause; computers don’t know how to do nothing. It’s frantically looping as fast as it can through Lines 4240–4270.
Let’s return to the roots of computing: mathematical operations. Lines 4330–4660 of Listing 1 define a macro called ADD. To preserve the symmetry of the universe (can you guess I’m a Libra?), SUBTRACT is found immediately following ADD.
Both macros need two parameters. The first is the address of a 2-byte number; the second, either a value or another address. In ADD, the contents of the first address are added to either the value or contents of the second address; the resulting 2-byte number is stored back at the first address. More concisely, %l = %l + %2. SUBTRACT can be summarized as %l = %l−%2. The algorithms are straightforward 2-byte binary arithmetic, processing first the low bytes and then the high bytes of each pair of operands. Notice that the carry flag is cleared (CLC) before the additions, and it’s set (SEC) before a subtraction.
These macros can be very handy when working with customized mixed graphics mode displays. If you turn back to Boot Camp in issue 44, you’ll recall that, to write in different graphics mode segments of the screen, we treated each segment as a separate little screen. We changed the pointer to the beginning of screen RAM (SAVMSC, $58 and $59) to point at the beginning of the RAM used by each segment. To accomplish this, we figured out how many bytes of screen RAM were consumed by the first segment and added that number to the contents of SAVMSC. SAVMSC then pointed to the second segment, and POSITION and PRINT statements were relative to the upper left corner of that segment. The ADD macro is perfect for such operations.
Conversely, if you write in, say, the fourth segment of a mixed-mode display and then need to write in the second segment, you can use SUBTRACT to reset SAVMSC to where you need it. If this discussion leaves you baffled, please review Lines 980–1210 of the listing in issue 44’s Boot Camp.
A short macro called DPOKE (for double poke) is in Lines 5070–5250 of Listing 1. DPOKE is similar to POKE, except the first parameter is a 2-byte address and the second is a 2-byte value. DPOKE lets you store a value greater than 255 in the usual low/high format.
It would be nice to let parameter 2 take either a value or an address, but how could we tell them apart? In macros like POKE, if the parameter is larger than 255, we can safely conclude that it’s an address. Things are not so simple in DPOKE. Actually, if you find it more useful to have DPOKE expect parameter 2 to be an address, just rewrite it. Or you could have two versions, say, DPOKEA and DPOKEV. Or you could add a third parameter, a “flag” to tell the macro if parameter 2 is a value or an address. The possibilities are endless; the decision is yours.
Ah, I like this one! I was very smug when I figured out how to simulate (at least in a simple way) the useful FOR/ NEXT construct from BASIC. Lines 5290–5590 of Listing 1 are the FOR macro, and Lines 5630–5910 are the NEXT macro. Let’s review how FOR/NEXT works.
Sample FOR/NEXT statements are: FOR I=4 TO 12 STEP 2/NEXT I. The FOR statement defines an “index variable” (I) that will be changed systematically each time we go through the loop. It also states the initial (4) and final (12) values of the index variable, and an optional increment (2) by which the index variable is changed on each iteration. If STEP is omitted, it’s assumed to be 1. In BASIC, the initial, final and increment values can all be variables.
When a NEXT statement is encountered, the index variable is changed by the value of the increment. The result is compared to the final value. If the index variable is greater than the final value (for a loop in which it’s increasing), the loop is terminated. Otherwise, the statement immediately following the FOR statement is executed again, and the process continues.
My FOR/NEXT macros simulate the BASIC situation reasonably well, with some restrictions. First, you can’t have nested FOR/NEXT loops (that is, a loop having index variable I within a loop having index variable I). Also, I’ve reserved only 1 byte for the value of the index variable, limiting it to values from 0 through 255. The equates used by my FOR/NEXT macros are in Lines 351–354 of Listing 1. These are 1-byte integers. A statement like FOR J = 35.4 TO 114.6 STEP 0.2 is perfectly legal in BASIC, but not here. Also, as written, the FOR macro accepts only values for all parameters except the index variable, which is an address.
On the plus side, my FOR/NEXT macros do permit loops with negative increments. You could write more elaborate macros to overcome the limitations I mentioned, but you’ll probably find that even these simple ones are very useful.
Now for the nitty-gritty. The FOR macro is invoked with either three or four parameters. Parameter 1 is the name of the index variable (address). Parameter 2 is the initial index value, and parameter 3 is the final value. The optional parameter 4 is the increment. If absent, an increment of 1 is assumed, just as in BASIC. Lines 5420–5430 load the index variable with the initial value, and Lines 5440–5450 stash the final value at address ENDLOOP ($0682). Lines 5460–5520 store the increment value at address INCLOOP ($0683), based on the number of parameters supphed.
It would be nice to remember where the first executable statement in the loop is, so we can go back to the right place when the NEXT macro is executed. The 2 bytes at LOOPADD ($0684) are used to store the address where code in the loop actually begins. If you think about it, you’ll realize that this address is 1 byte past the end of the object code generated by the FOR macro. I called this @LOOPSTART (following my practice of beginning label names inside macros with an “at” sign).
The NOP instruction at address @LOOPSTART (Line 5580) means “no operation”; don’t do anything, just continue on with the next instruction. I’m simply using this as a placeholder, and it’s actually the first instruction inside the loop. Lines 5540–5570 store the actual address of @LOOPSTART in LOOPADD, which will be used by the NEXT macro. Naturally, the address of @LOOPSTART will be different every time the macro is invoked within a particular program,
The NEXT macro is responsible for changing the index variable, seeing if we’re finished yet, and going back to @LOOPSTART to run through the loop again if it’s not time to quit.
NEXT only needs one parameter, the name of the index variable. Lines 5740–5750 check to see if the increment is positive or negative. If positive. Line 5760 adds the increment value to the current value of the index variable, and Line 5770 compares the sum to the final value in ENDLOOP The result of this comparison sets one or more flags in the 6502’s processor status register; more about that later. Based on the results of the comparison, we either save the new value of the index variable and go back to the beginning of the loop (Lines 5810–5820), or terminate the whole process by branching to the last line of the macro, @LOOPDONE.
Lines 5830–5880 handle the case in which the increment is negative. We can still add the increment to the current value, because adding a negative number is the same as subtracting.
The JMP instructions in Lines 5820 and 5880 look a little funny. Rather than using the normal absolute addressing, this is an “indirect absolute addressing” mode. JMP is the only 6502 operation that uses this mode. The JMP (LOOPADD) syntax means to jump to the address stored at address LOOPADD and continue execution. Contrast this with the usual JMP SOMEWHERE format, which means to jump to address SOMEWHERE and continue execution. Now you see why FOR stored the address of the first instruction in the loop in LOOPADD. NEXT refers to the contents of LOOPADD when deciding where to jump if it wants to go through the loop again.
Let’s examine the comparison operation. Remember the processor status register in the 6502? That’s the register with 7 bits that indicate, among other things, whether the result of the last operation was or negative, and whether it caused a carry operation. The compare instructions, such as CMP 45, affect the zero, negative and carry flags.
Table 1 shows how these flags appear, based on whether (for a CMP operation) the contents of the accumulator are smaller than the operand (45 in the example above), equal to it, or greater than the operand. The results are the same for CPY and CPX operations.
Flags set by compare operations: CMP operand.
Situation Negative Zero Carry A < operand 1 0 0 A = operand 0 1 1 A > operand 0 0 1
You can use the branch instructions to control the flow of program execution, based on the results of a comparison. BEQ and BNE look at the zero flag (cleared or set, respectively), BPL and BMI use the negative flag (cleared or set), and BCC and BCS examine the carry flag (cleared or set). Since all three flags are affected, we might need a couple of branch operations in sequence, to get where we want to go.
Lines 5770–5790 of Listing 1 provide an illustration. We’re comparing the contents of the accumulator (A) with the contents of ENDLOOP. If they’re equal or if A is smaller than ENDLOOP we want to loop again (go to @RELOOP). Otherwise we’re done (go to @LOOPDONE). Table 1 shows that the carry flag is clear only if A is less than ENDLOOP, so we must use a second test to distinguish between A equaling ENDLOOP and A being greater than ENDLOOP
In English, Line 5780 asks, “Does A equal ENDLOOP (is the zero flag set)?” If yes, go to @RELOOP If no, Line 5790 asks, “Is A greater than ENDLOOP (is the carry flag set)?” If yes, go to @LOOPDONE. If no, fall through to @RELOOP. You’ll have to use the old noodle to test these flags in the correct sequence when making such comparisons.
That’s what my girlfriend said when we visited my home state of Idaho, trying to get into the spirit of the Old West. Close enough. Our final macro for today is called MOVE, and it lives in Lines 5950–6170 of Listing 1. Actually the MOVE macro just handles the setup; the rather lengthy MOVESUB subroutine in Listing 2 does the hard part. The MOVE routines are great for such operations as copying the ROM character set into RAM or moving players vertically.
MOVE is used to transfer (copy, really) a block of bytes of any length from one part of memory (RAM or ROM) to another (RAM, obviously). The ranges can overlap, but of course, you’ll overwrite part of the source range if they do. Three parameters are needed: the starting address of the block to be copied; the address to which it is to be copied; and the number of bytes to transfer, a value. The MOVE macro just loads some work registers with these parameters, using the DPOKE macro, and calls MOVESUB. The equates for the work registers are at the top of Listing 1.
MOVESUB first determines if the destination address is at a higher or lower address in memory than the source, using Lines 2100–2150 in Listing 2. This is important. If moving data to a lower address, you want to begin with the first byte in the source block (lowest address). However, if moving to a higher address, move the highest byte in the range first. This prevents you from overwriting data in the source block in case the ranges overlap. The routine to move data to a higher address is in Lines 2300–2590, and the copying to a lower address takes place in Lines 2630–2930.
These routines are a little complicated. To handle any arbitrary number of bytes, we need a 2-byte register (I called it HOWMANY) to store the number of bytes to move. When moving to higher addresses, I first move the number of bytes indicated by the low-byte of HOWMANY. Then I use another loop to transfer the number of pages (256 bytes each) indicated by the high-byte of HOWMANY. Conversely, I move data in one-page blocks first when shifting to lower addresses (Lines 2710–2800), and wrap up the partial page, if any, in Lines 2810–2930. You might want to walk through this code and convince yourself it makes sense.
So far, I’ve just given you a bunch of tools and some thoughts on how to use them. The program in Listing 3 applies most of these new routines—and some old ones—to give you an idea of how easy it is to write assembly programs using all these shortcuts. Experiment!
Despite having a Ph.D. in organic chemistry, Karl Wiegers earns a living writing applications software for photographic research at Eastman Kodak Company, mostly on an IBM mainframe. He is also interested in educational applications of Atari 8-bit. Atari ST and Apple II computers.
Listing 1. Assembly listing. 0229 RTCLOK = $12 0241 MOVEFROM = $CB 0242 MOVETO = $CD 0351 I = $0681 0352 ENDLOOP = $0682 0353 INCLOOP = $0683 0354 LOOPADD = $0684 0355 HOWMANY = $0686 0845 ;or address 0892 .IF %2>256 0894 LDA %2 0896 STA %1 0898 .ELSE 0915 .ENDIF 3560 ; 3570 ;******************************* 3580 ; 3590 ;INPUT macro 3600 ; 3610 ;Usage: INPUT IOCB,address,length 3620 ; 3630 ;'IOCB' is the IOCB number to use 3640 ;'address' is a label or actual 3650 ; buffer address where the input 3660 ; string is to be stored 3670 ;'length' is the number of bytes 3680 ; to be input - if missing, then 3690 ; length is set to 255 bytes 3700 ; 3710 .MACRO INPUT 3720 .IF %0<2.OR%0>3 3730 .ERROR "Error in INPUT" 3740 .ELSE 3750 LDX #%1*16 3760 LDA #GETREC 3770 STA ICCOM,X 3780 LDA # <%2 3790 STA ICBAL,X 3800 LDA # >%2 3810 STA ICBAH,X 3820 .IF %0=2 3830 LDA #255 3840 STA ICBLL,X 3850 LDA #0 3860 STA ICBLH,X 3870 .ELSE 3880 LDA # <%3 3890 STA ICBLL,X 3900 LDA # >%3 3910 STA ICBLH,X 3920 .ENDIF 3930 JSR CIOV 3940 .ENDIF 3950 .ENDM 3960 ; 3970 ;******************************* 3980 ; 3990 ;PAUSE macro 4000 ; 4010 ;Usage: PAUSE jiffies 4020 ; 4030 ;'jiffies' is the number of 4040 ;jiffies (1/60 sec) to pause, a 4050 ;value up to 255, or an address 4060 ; 4070 .MACRO PAUSE 4080 .IF %0<>1 4090 .ERROR "Error in PAUSE" 4100 .ELSE 4110 CLC 4120 BCC @SKIPPAUSE 4130 @TIMER .BYTE 0 4140 @SKIPPAUSE 4150 LDA #0 4160 STA RTCLOK+2 4170 .IF %1>255 4180 LDA %1 4190 STA @TIMER 4200 .ELSE 4210 LDA # <%1 4220 STA @TIMER 4230 .ENDIF 4240 @DELAY 4250 LDA RTCLOK+2 4260 CMP @TIMER 4270 BNE @DELAY 4280 .ENDIF 4290 .ENDM 4300 ; 4310 ;******************************* 4320 ; 4330 ;ADD macro 4340 ; 4350 ;Usage: ADD first,second 4360 ; 4370 ;'first' is an address of a two- 4380 ;byte number 4390 ;'second' is either the address 4400 ;of a two-byte number, or a value 4410 ; 4420 ;first = first + second 4430 ; 4440 .MACRO ADD 4450 .IF %0<>2 4460 .ERROR "Error in ADD" 4470 .ELSE 4480 .IF %2<256 4490 CLC 4500 LDA %1 4510 ADC #%2 4520 STA %1 4530 BCC @SKIPADD 4540 INC %1+1 4550 @SKIPADD 4560 .ELSE 4570 CLC 4580 LDA %1 4590 ADC %2 4600 STA %1 4610 LDA %1+1 4620 ADC %2+1 4630 STA %1+1 4640 .ENDIF 4650 .ENDIF 4660 .ENDM 4670 ; 4680 ;******************************* 4690 ; 4700 ;SUBTRACT macro 4710 ; 4720 ;Usage: SUBTRACT first,second 4730 ; 4740 ;'first' is an address of a two- 4750 ;byte number 4760 ;'second' is either the address 4770 ;of a two-byte number, or a value 4780 ; 4790 ;first = first - second 4800 ; 4810 .MACRO SUBTRACT 4820 .IF %0<>2 4830 .ERROR "Error in SUBTRACT" 4840 .ELSE 4850 .IF %2<256 4860 SEC 4870 LDA %1 4880 SBC #%2 4890 STA %1 4900 BCS @SKIPSUB 4910 DEC %1+1 4920 @SKIPSUB 4930 .ELSE 4940 SEC 4950 LDA %1 4960 SBC %2 4970 STA %1 4980 LDA %1+1 4990 SBC %2+1 5000 STA %1+1 5010 .ENDIF 5020 .ENDIF 5030 .ENDM 5040 ; 5050 ;******************************* 5060 ; 5070 ;DPOKE macro 5080 ; 5090 ;Usage: DPOKE to,from 5100 ; 5110 ;'to' is a 2-byte destination 5120 ; address 5130 ;'from' is source value (0-65535) 5140 ; 5150 ; 5160 .MACRO DPOKE 5170 .IF %0<>2 5180 .ERROR "Error in DPOKE" 5190 .ELSE 5200 LDA # <%2 5210 STA %1 5220 LDA # >%2 5230 STA %1+1 5240 .ENDIF 5250 .ENDM 5260 ; 5270 ;******************************* 5280 ; 5290 ;FOR macro 5300 ; 5310 ;Usage: FOR label,start,stop,inc 5320 ; 5330 ;'label' is byte to hold index 5340 ;'start' is initial index value 5350 ;'stop' is final index value 5360 ;'inc' is optional step increment 5370 ; 5380 .MACRO FOR 5390 .IF %0<3 .OR %0>4 5400 .ERROR "Error in FOR" 5410 .ELSE 5420 LDA # <%2 5430 STA %1 5440 LDA # <%3 5450 STA ENDLOOP 5460 .IF %0=3 5470 LDA #1 5480 STA INCLOOP 5490 .ELSE 5500 LDA # <%4 5510 STA INCLOOP 5520 .ENDIF 5530 .ENDIF 5540 LDA # <@LOOPSTART 5550 STA LOOPADD 5560 LDA # >@LOOPSTART 5570 STA LOOPADD+1 5580 @LOOPSTART NOP 5590 .ENDM 5600 ; 5610 ;******************************* 5620 ; 5630 ;NEXT macro 5640 ; 5650 ;Usage: NEXT label 5660 ; 5670 ;'label' is byte holding index 5680 ; 5690 .MACRO NEXT 5700 .IF %0<>1 5710 .ERROR "Error in NEXT" 5720 .ELSE 5730 CLC 5740 LDA INCLOOP 5750 BMI @LOOPDOWN 5760 ADC %1 5770 CMP ENDLOOP 5780 BEQ @RELOOP 5790 BCS @LOOPDONE 5800 @RELOOP 5810 STA %1 5820 JMP (LOOPADD) 5830 @LOOPDOWN 5840 ADC %1 5850 CMP ENDLOOP 5860 BMI @LOOPDONE 5870 STA %1 5880 JMP (LOOPADD) 5890 @LOOPDONE 5900 .ENDIF 5910 .ENDM 5920 ; 5930 ;******************************* 5940 ; 5950 ;MOVE macro 5960 ; 5970 ;Usage: MOVE from,to,length 5980 ; 5990 ;'from' is starting address of 6000 ; block to be moved 6010 ;'to' is starting address where 6020 ; block is to be copied to 6030 ;'length' is number of bytes to 6040 ; be copied (value, not address) 6050 ; 6060 ;calls subroutine MOVESUB 6070 ; 6080 .MACRO MOVE 6090 .IF %0<>3 6100 .ERROR "Error in MOVE" 6110 .ELSE 6120 DPOKE MOVEFROM,%1 6130 DPOKE MOVETO,%2 6140 DPOKE HOWMANY,%3 6150 JSR MOVESUB 6160 .ENDIF 6170 .ENDM
1940 ; 1950 ;******************************* 1960 ; 1970 ;CLS (clear screen) subroutine 1980 ; 1990 CLS 2000 PRINT 0,CLEARSCR 2010 RTS 2020 CLEARSCR .BYTE 125,EOL 2030 ; 2040 ;******************************* 2050 ; 2060 ;sub. to move a block of data 2070 ;called by MOVE macro 2080 ; 2090 MOVESUB 2100 SEC ;find out if 2110 LDA MOVETO ;moving data up 2120 SBC MOVEFROM ;or down in RAM 2130 LDA MOVETO+1 2140 SBC MOVEFROM+1 2150 BMI MOVEDOWN 2160 ; 2170 ;move to higher RAM address 2180 ; 2190 CLC ;start with 2200 LDA MOVEFROM+1 ;byte having 2210 ADC HOWMANY+1 ;highest 2220 STA MOVEFROM+1 ;address 2230 CLC 2240 LDA MOVETO+1 2250 ADC HOWMANY+1 2260 STA MOVETO+1 2270 ; 2280 ;move block with < 256 bytes 2290 ; 2300 STARTUP 2310 LDY HOWMANY ;if low byte of 2320 BEQ FINISHUP ;HOWMANY=0, this 2330 LDX HOWMANY ;part is done 2340 PARTIAL1 2350 DEY 2360 LDA (MOVEFROM),Y 2370 STA (MOVETO),Y 2380 DEX 2390 BNE PARTIAL1 2400 ; 2410 ;move remainder in 1-page blocks 2420 ; 2430 FINISHUP 2440 DEC MOVEFROM+1 2450 DEC MOVETO+1 2460 DEC HOWMANY+1 ;when high byte 2470 BPL PAGEUP ;of HOWMANY=0 2480 RTS ;then all done 2490 PAGEUP 2500 LDY #0 ;use Y as index, 2510 LDX #0 ;X as counter 2520 NEXTUP 2530 DEY 2540 LDA (MOVEFROM),Y 2550 STA (MOVETO),Y 2560 INX 2570 BNE NEXTUP 2580 CLC 2590 BCC FINISHUP 2600 ; 2610 ;move to lower RAM address 2620 ; 2630 MOVEDOWN 2640 LDA HOWMANY+1 2650 BEQ FINISHDOWN 2660 LDY #0 2670 ; 2680 ;start with lowest address, move 2690 ;data in 1-page blocks 2700 ; 2710 NEXTDOWN 2720 LDA (MOVEFROM),Y 2730 STA (MOVETO),Y 2740 INY 2750 BNE NEXTDOWN 2760 INC MOVEFROM+1 2770 INC MOVETO+1 2780 DEC HOWMANY+1 2790 CLC 2800 BCC MOVEDOWN 2810 FINISHDOWN 2820 LDA HOWMANY 2830 BEQ DONEDOWN 2840 ; 2850 ;finish with block of < 256 bytes 2860 ; 2870 PARTIAL2 2880 LDA (MOVEFROM),Y 2890 STA (MOVETO),Y 2900 INY 2910 CPY HOWMANY 2920 BNE PARTIAL2 2930 DONEDOWN 2940 RTS
0100 ;Macro Workout Example 0110 ;Karl E. Wiegers 0120 ; 0130 .OPT OBJ,NO LIST 0140 ; 0150 ;pull in macro definitions 0160 ;and equates 0170 ; 0180 .INCLUDE #D8:MACRO.LIB 0190 ; 0200 ;some equates we need 0210 ; 0220 TEMP = $CB 0230 SDLSTL = $0230 0240 ; 0250 ;******************************* 0260 ;MAIN PROGRAM STARTS HERE 0270 ;******************************* 0280 ; 0290 *= $5000 0300 ; 0310 ;------------------------------- 0320 ;clear screen, set screen colors 0330 ;ask you to enter your name 0340 ;------------------------------- 0350 ; 0360 JSR CLS 0370 SETCOLOR 2,12,6 0380 SETCOLOR 1,0,0 0390 POSITION 8,5 0400 PRINT PROMPT 0410 POSITION 14,7 0420 INPUT 0,NAME,10 0430 ; 0440 ;------------------------------- 0450 ;go to Graphics 2 screen, change 0460 ;two lines to Graphics 1 by 0470 ;finding and changing display 0480 ;list 0490 ;------------------------------- 0500 ; 0510 GRAPHICS 2+16 0520 POKE TEMP,SDLSTL 0530 POKE TEMP+1,SDLSTL+1 0540 LDA #6 0550 LDY #7 0560 STA (TEMP),Y 0570 INY 0580 INY 0590 STA (TEMP),Y 0600 ; 0610 ;------------------------------- 0620 ;write some stuff on the screen 0630 ;change colors of your name 0640 ;using a loop 0650 ;------------------------------- 0660 ; 0670 POSITION 4,2 0680 PRINT 6,LINE1 0690 POSITION 5,4 0700 PRINT 6,LINE2 0710 POSITION 5,6 0720 PRINT 6,NAME 0730 POKE COLOR0,0 0740 FOR I,0,254 0750 INC COLOR0 0760 PAUSE 3 0770 NEXT I 0780 END JMP END 0790 ; 0800 ;------------------------------- 0810 ;text strings 0820 ;------------------------------- 0830 ; 0840 PROMPT .BYTE "Please enter " 0850 .BYTE "your name:",EOL 0860 LINE1 .BYTE "welcome to",EOL 0870 LINE2 .BYTE "boot camp",EOL 0880 NAME .DS 10 0890 .BYTE EOL 0900 ; 0910 ;------------------------------- 0920 ;bring in subroutines 0930 ;------------------------------- 0940 ; 0950 .INCLUDE #D8:SUBS.LIB
A.N.A.L.O.G. ISSUE 60 / MAY 1988 / PAGE 92
Recently, while I was talking to a regular Boot Camp reader, he made the interesting observation that the logical process of going from program idea to final product really isn’t discussed much in the public literature. You can read scads of articles about this language or that, programming tips from experts, and even books on the best ways to design complex software systems. However, very little is written about the thought processes other programmers go through in developing an idea.
Assembly language programming poses its own set of questions about the most effective sequence of program development steps. This is because you have to do a lot of the work in assembly (such as memory allocation) that the computer handles in high-level languages.
This month’s Boot Camp will be a little different. I’ll try to pass on to you the thoughts that flit through my mind, and the resulting activities, as I develop an assembly program. Along the way, we’ll actually generate a useful product.
The first thing I do when starting a MAC/65 session with my 130XE is set up a RAMdisk and copy my MACRO.LIB and SUBS.LIB files to it. Then I’m ready to roll. I’ve often wanted a program to handle this chore for me, and, by the end of this discussion, we’ll all have one in hand. The sample program reads a disk file called RAMDISK.FIL that contains a list of the files you want to place in the RAMdisk. Each of those files is copied in turn, and the MAC/65 editor magically appears afterward. You can name this file AUTORUN.SYS, so the whole process takes place automatically on booting.
This program uses several macros from earlier columns, as well as a few new ones. If you don’t have a macro assembler, you’ll need to modify the code to expand the macros out by hand and make any other changes specific to your assembler. If you don’t have an Atari 130XE with the RAMdisk, but have a second physical disk drive, you can change the references in the program from D8: to D2:. If you only have a 64K or smaller computer with a single drive, please bear with us and keep reading, since you may encounter some other useful information.
Quite often, the hardest part of writing a program is deciding what sort of program to write. Each month I have to think of something that at least some of you will find useful, informative, interesting and amusing enough to keep you entranced to the very last word. Your own program ideas are probably more goal oriented…How can I write the world’s best word processor program? How can I dazzle my friends with some graphics displays? How can I save twenty bucks and write my own checkbook balancing program? How can I make $300,000 quickly in the software business? This month’s idea came from my desire to find more ways to have the computer work for me, instead of the other way around.
I tend to think about my programming projects for quite a while before actually sitting down at the keyboard. This bit of wisdom leads to a discussion of the classic steps involved in software development (or any other problem solving exercise): analysis, design, programming, testing, debugging, release on an unsuspecting public. These steps tend to flow from one into another, and all too often they become hopelessly interwoven.
Analysis consists of making sure you understand the problem. A lot of people blithely skip this step and proceed with only a vague notion of what they’re trying to accomplish. If you aren’t sure what you’re trying to do, how will you know when you’re done? I always feel better if I know just where I’m heading before I write any code.
Design involves coming up with a solution to the problem. This is the most challenging part of the project, conceptually, and one of the most critical. In the years I’ve spent as a professional software developer in the Eastman Kodak Research Laboratories, I’ve learned that every minute you spend on design is worthwhile. For smaller projects like the RAMdisk copier program, I do much of the design in my head—over and over and over again—until I think I’ve figured out every angle. Then I put it on paper and see all the things I missed.
There are many techniques—all useful, but none perfect—for working out a design on paper. The traditional method is flowcharting. While flowcharting is still useful for figuring out the details of logic flow, it’s severely limited when dealing with any but the smallest programs. Many new methods for representing a system design have appeared in the last few years; I’m still looking into them and trying different approaches. All of them deal with the basic pieces of the puzzle: input (what data are we going to process?); a process (what are we going to do to the data?); and output (where do we put the results?).
Let’s think about this in the context of today’s program, and I’ll try to reconstruct some of my thought processes in a coherent fashion.
First, we’ll deal with the problem analysis. I want to write a program that will copy a specified group of files from the boot disk to a RAMdisk. The program should run automatically upon booting. Of course, the RAMdisk must be set up before this program can run. After completion, control of my Atari should be passed either to the cartridge (probably MAC/65) or the DOS menu, if no cartridge is present.
Now some thoughts on the system design, pretty much in the order in which I first had them. I need to have a list of the filenames to be copied. This list could be in a file on the boot disk. Let’s call it RAMDISK.FIL. The program will end after the last record in RAMDISK.FIL has been processed.
I could read the file in its entirety into memory, then copy it to the RAMdisk. This would require a lot of RAM for a big file. Alternatively, I could read it in little chunks, copying each chunk to the RAMdisk after reading it. The last might be shorter than earlier chunks. This method won’t require much RAM, since I can make the chunks as big or small as I like. I could even copy it 1 byte at a time. I decided to use the second method, of 255-byte chunks.
(So far, the problem analysis and design are really independent of the language and computer that will be used for the program. In fact, this analysis could apply just as well to a bunch of monks copying some documents by hand. We can describe this level of system design as being very abstract. Now, I’ll become more concrete and continue the design at a more detailed level.)
I’ll write this program on my Atari 130XE in assembly language, using MAC/65. I’ll need one IOCB (Input/Output Control Block) for reading the RAMDISK.FIL file, another IOCB for reading the file being copied, and a third for the output file on the RAMdisk. I’ll process chunks of files 255 bytes long at a time. The program will stop executing when an End-Of-File marker (EOF, for short) is read from RAMDISK.FIL. I’ll set this program up to autorun upon loading from disk. If the program file is named AUTORUN.SYS, it will load and execute upon booting.
How about memory allocation? I’ll begin this program at address $5000, just because I always do. I need to reserve a block of RAM 255 bytes long to hold each chunk of file as I read it (a “buffer”). I also need to reserve space to hold the name of the file being processed (which I just read from RAMDISK.FIL), and space for the output name (the same as input, except with a D8: drive specification), at 16 bytes each. (If I were writing a graphics program, I’d also decide how much RAM I needed for display list, character sets or whatever, and what addresses I’d use.)
Now, what sort of things might go wrong that I must teach my program to handle? Maybe the user’s computer doesn’t have a RAMdisk setup. Maybe the RAMDISK.FIL file is missing. Maybe the files listed in RAMDISK.FIL aren’t on the boot disk. In the first two cases, the program will simply stop executing. For the third situation, I’ll copy any files I find and print a message for any that aren’t found. I’ll use any macros I have lying around to save my typing in code wherever possible.
Whew! Did you follow all that? Maybe a diagram would clarify things somewhat. In Figure 1, I’ve drawn what’s called an “action diagram.” This is a simple way to sketch out the detailed logical structure of a program, as an alternative to a flowchart. I’m sure you could also draw a flowchart for a program this short, but let’s move into the 1980s.
+--- COPY FILES TO RAM DISK | | Open D1:RAMDISK.FIL | | +--- If error | | | | Print error message | | |<---+--- Escape | | | +--- Else | | | | +--- Loop 1 | | | | | | Read a record from D1:RAMDISK.FIL | | | | | | +--- If EOF | | | | | | | | Close D1:RAMDISK.FIL | | | | |<---+----+----+----Escape | | | | | | | +--- Else | | | | | | | | Set up output filename | | | | | | | | +--- Loop 2 | | | | | | | | | | Open input file | | | | | Read a chunk | | | | | | | | | | +--- If not EOF | | | | | | | | | | | | Write chunk to output file | | | | | | | | | | | +--- Else If EOF | | | | | | | | | | | | Get number of bytes read | | | | | | Write chunk to output file | | | | | | Close input and output files | | |<---+----+----+-----Escape | | | | | | | | | | | +---- End if | | | | | | | | | +--- End Loop 2 | | | | | | | +--- End if | | | | | +--- End Loop 1 | | | +--- End if | +--- End COPY FILES TO RAM DISK Figure 1
The brackets in the action diagram enclose logical blocks of operation. The IF/ELSE/ENDIF structures show conditional actions that depend on various situations. The brackets marked with a double line (equal signs) at the top indicate a repetitive (looping) action. The horizontal arrows labeled “Escape” show ways to exit from the inner bracketed actions when certain conditions (usually errors) are encountered. Sometimes we can use error conditions to our advantage, such as when an EOF condition error means we’re done with a particular operation.
After all this design work, I’m ready to begin writing a program. Again, let me stress that the more time you spend going over and over the design steps, the less aggravation you’ll encounter when coding, testing and debugging. I guarantee you’ll think of new angles, potential pitfalls, and so on, each time you iterate through the design process. Believe me, it’s a lot easier to modify an action diagram than rewrite a segment of your program because of a logic flaw or functional omission.
While working through the program design, I also think about what tools I’ll need to implement it. For a graphics program, I ponder such heavy questions as whether I need a vertical blank interrupt routine to move players, display list interrupts to make the screen look nice, a mixed-mode display list (build your own or modify the default?), or some redefined characters. I think about the macros and subroutines I have available that might do the job, and any others I’ll have to create.
For the file copier program, I need my OPEN macro to open some IOCBs, and, of course, the CLOSE macro when I’m finished with them. The PRINT macro would be nice for putting error messages on the screen. I can use INPUT (discussed in issue 58) to read the entries in RAMDISK.FIL. MOVE would be handy for copying the input filename to the output filename space, so I can change the drive designation to D8:.
Recall that I decided to read a chunk of the input file containing 255 characters and store it in a buffer. INPUT won’t work for this, since INPUT expects to see a carriage return (end-of-line character, EOL, $9B) at the end of the string being read. Similarly, PRINT adds an EOL to the output string, so that’s no good for writing to the output file. I guess I’ll have to write some new macros that don’t involve carriage returns. These are the PUT and GET macros fomid in Listing 1.
Please merge Listing 1 with your existing MACRO.LIB file, using the line numbers shown. The equates at the top of Listing 1 define the error number for an end-of-file condition (EOF $88) and the CIO command bytes for PUTCHAR and GETCHAR (put and get a string of characters, respectively) operations. RUNAD ($02E0) is a magic address for making an object code program run automatically upon loading, as we’ll see a little later.
The PUT macro sends a string of bytes out to a specified IOCB. PUT takes three parameters. The first is the IOCB number to use, and the second is the address of the buffer where the data to be output resides. Parameter 3 is an optional number of bytes to be output. If %3 is less than 256 it’s assumed to be a value. If greater, it’s assumed to be the address of a pair of bytes containing the number of bytes to be output. If %3 is missing, only 1 byte is output.
PUT is pretty straightforward. When I wrote PUT and GET, I found they had several statements in common, so I collected those into a separate macro called PGSETUP (Lines 6780–7070 of Listing 1). PUT and GET pass two parameters to PGSETUP, which sets up the IOCB buffer address and length for the specific operation being performed.
GET is the converse of PUT. The command value of GETCHAR ($07) is used in Line 6650, rather than the PUTCHAR ($0B) of Line 6360. GET and PUT both treat EOL like any other character. However, GET does recognize the end-of-file marker (CTRL-3), which terminates the input operation and retruns an error status of $88 (decimal 136).
There’s one more macro in Listing 1, OPENA (Lines 7110–7360). If you look back to the OPEN macro (issue 55), you’ll see that it expected parameter 4 to be a literal character string containing the name of the unit or file to open. However, in this program I’ll be opening some file whose name I just read from the RAMDISK.FIL file. Hence, I need a version of an OPEN macro that can accept an address as a parameter. OPENA corrects this limitation of my original OPEN macro.
If you’re like me, you’ll have to restrain yourself from the temptation to dive right in and start spewing out code as soon as you think you’re ready. The catch is that you probably aren’t ready as soon as you think you are. However, at this point I felt I understood the problem clearly and had a design in hand that was good enough to implement. Hence, I wrote the program you see in Listing 2. Type in Listing 2 and assemble it into a file called BC59.OBJ. Once you’ve tested BC59.OBJ enough to convince yourself that it works (just load it from the DOS menu with option L), you can safely rename it to AUTORUN.SYS, so it executes upon booting.
Okay, I admit it. My program didn’t look like Listing 2 when I first wrote it. I went through the usual series of testing, changing and retesting sections of code. Once I found the line I’d inadvertently deleted during editing, things went pretty smoothly. I also admit that, while coding, I found a few things I’d overlooked during the design phase, so I squeezed them into my action diagram. Incidentally, the action diagram (or other graphic representation of the program logic) will be mighty useful when you look at a program again a few months after writing it and want to figure out how in the world it works. Comments help too.
The BYTESREAD equate in Line 200 of Listing 2 is kind of interesting. Recall that the last chunk of data we read from the input file being copied may be shorter than 255 bytes, since GET stops whenever an EOF is encountered. We’ll want to know just how many bytes were read in that final chunk, so we can PUT exactly the right number out to the copy of that file on the RAMdisk. The other equate, INBUFF, says we want to use the 255 bytes, beginning at address $6000, as our buffer for data read from the input file.
The first thing our action diagram says to do is try to open the RAMDISK.FIL file. Lines 360–370 set the stage by clearing the decimal mode for arithmetic (not critical in this program, but a good practice) and clearing the display screen using the CLS subroutine from last time. Lines 380–480 attempt to open D1:RAMDISK.FIL for input using IOCB 2. If successful, we branch to label FOUNDIT and proceed. Otherwise, Lines 400–480 print an appropriate message on the screen and jump to the end of the program at label EXIT.
Recall that CIO errors leave their calling card in the Y-register. In Lines 400–410, I’m storing this value temporarily on the program stack, so I can write the first part of the error message (MISSING). The error number from the OPEN is retrieved in Lines 430–440. If I didn’t stash the Y-register contents like this, Y would otherwise contain the error status from the PUT in Line 420, which is probably 1 and therefore not very useful! Our old subroutine, STATUSERR, tries to figure out the problem with the OPEN and prints an appropriate message. I could have used PRINT for the MISSING message, but that would have caused a carriage return before STATUSERR told us what happened—strictly a cosmetic decision, but appearances are important.
The action diagram next says we should read a record from RAMDISK.FIL, as in Line 580. Lines 600–690 handle any error, which should just be an end-of-file condition. This isn’t really an error; it just indicates that the program’s job is complete. In any case, we close IOCB 2 and exit from the program. Remember to always close IOCBs you open.
The filename read from RAMDISK.FIL is stored at address FNAMEIN. Sixteen bytes were reserved for this purpose in Line 1450. In Lines 770–800, we copy that filename to the FNAMEOUT address reserved in Line 1460, and change the drive identifier to D8:. Note that this code segment assumes the records in RAMDISK.FIL are in the form D1:FILENAME.EXT, one filename per record; more about that later.
Line 810 prints the filename on-screen, so we can keep the user informed as to what’s going on. I’m a big believer in not making the user guess whether or not the computer is paying any attention to him. In Lines 820–870, we open the input file and handle any problems. This time we jump back to get the next filename, even if the present one caused some error.
The next section, at FOUNDINP, attempts to open an output file on the RAMdisk. This section will trap for situations such as no RAMdisk existing, or a full RAMdisk, or…Notice how many lines of code I’ve devoted to error handling? This is pretty typical for my programs. Just when you think you’ve covered every possible thing that might go wrong, either the user or the computer will get even more creatively nasty.
The actual copy routine in Lines 1080–1130 is ridiculously simple. It just alternates between reading a 255-byte chunk from the input file and writing it to the output file. When the EOF is detected, control branches to the FINISH routine at Line 1210. Now we find out how many bytes were actually read during the final GET operation, by checking the IOCB 3 buffer length, and put that number of bytes out to IOCB 4. This completes the copy process, so both IOCBs are closed. A message confirming that the copy was successful is printed, and Line 1310 jumps back to read another record from RAMDISK.FIL. What could be simpler?
If you want a binary (object code) program to run automatically upon loading, end it with an RTS (Return From Subroutine) instruction, as in Line 1380. This will return control to the environment from which the program was loaded, usually DOS. Setting up a file to “load-and-go” is very simple. Address RUNAD ($02E0) just has to be loaded with the address at which execution is to begin. In this program, like all of mine, the magic address is $5000. Note that, in Line 290 of Listing 2, I thoughtfully put the label START right at the beginning of the program. Then, at the very end of the program (Lines 1580–1590), I set the program counter to RUNAD and state that the 2-byte address defined by START is to be loaded into RUNAD. Very simple, very easy.
Now, you can assemble Listing 2 and save the object code under filename AUTORUN.SYS. If you aren’t using a RAMdisk to house your MACRO.LIB and SUBS.LIB files, change the drive numbers in Lines 180 and 1520. Each time you boot from that disk, the .LIB files will be copied onto the RAMdisk.
But wait! How do we get the RAMDISK.FIL file? The easiest method is to use the “Copy” function from the DOS menu. Select option C for copy, and copy from device E: (the screen editor) to device D1:RAMDISK.FIL. Each line you type at the cursor and end with a RETURN will now end up in the RAMDISK.FIL file. Enter the complete filespecs for the files you want to copy to the RAMdisk, in the form D1:MACRO.LIB. Press RETURN after each filespec. When you’re done, press CTRL-3 to create the end-of-file character, and RAMDISK.FIL is created. To verify the contents, simply copy from D1:RAMDISK.FIL to E:. Of course, you could also use any text editor that produces straight ATASCII text to create the RAMDISK.FIL entries.
I hope you found “A Trip Through Karl’s Brain” to be informative. Everyone writes programs a little differently, but my approach seems to get the job done. I have some other ideas for useful programs that I might present in future issues. Remember, ask not what you can do for your computer. Ask, rather, what your computer can do for you.
Despite having a Ph.D. in organic chemistry, Karl Wiegers earns a living writing applications software for photographic research at Eastman Kodak Company, mostly on an IBM mainframe. He is also interested in educational applications of Atari 8-bit, Atari ST and Apple II computers.
Listing 1. Assembly listing. 0165 EOF = $88 0195 GETCHAR = $07 0205 PUTCHAR = $0B 0265 RUNAD = $02E0 6180 ; 6190 ;******************************* 6200 ; 6210 ;PUT macro 6220 ; 6230 ;Usage: PUT IOCB,address,length 6240 ; 6250 ;'IOCB' is the IOCB number to use 6260 ;'address' is a label or buffer 6270 ;address where the output data is 6280 ;'length' is the number of bytes 6290 ;to be output-if missing then =1 6300 ; 6310 .MACRO PUT 6320 .IF %0<2.OR%0>3 6330 .ERROR "Error in PUT" 6340 .ELSE 6350 LDX #%1*16 6360 LDA #PUTCHAR 6370 STA ICCOM,X 6380 .IF %0=2 6390 PGSETUP %2,1 6400 .ELSE 6410 PGSETUP %2,%3 6420 .ENDIF 6430 JSR CIOV 6440 .ENDIF 6450 .ENDM 6460 ; 6470 ;******************************* 6480 ; 6490 ;GET macro 6500 ; 6510 ;Usage: GET IOCB,address,length 6520 ; 6530 ;'IOCB' is the IOCB number to use 6540 ;'address' is a label or buffer 6550 ;address where the input data 6560 ;should go 6570 ;'length' is the number of bytes 6580 ;to be input-if missing then =1 6590 ; 6600 .MACRO GET 6610 .IF %0<2.OR%0>3 6620 .ERROR "Error in GET" 6630 .ELSE 6640 LDX #%1*16 6650 LDA #GETCHAR 6660 STA ICCOM,X 6670 .IF %0=2 6680 PGSETUP %2,1 6690 .ELSE 6700 PGSETUP %2,%3 6710 .ENDIF 6720 JSR CIOV 6730 .ENDIF 6740 .ENDM 6750 ; 6760 ;******************************* 6770 ; 6780 ;PGSETUP macro 6790 ; 6800 ;Usage: PGSETUP address,length 6810 ; 6820 ;'address' is I/O buffer address 6830 ;'length' is number of bytes for 6840 ;PUT or GET operation (value<256 6850 ;or address) 6860 ; 6870 .MACRO PGSETUP 6880 .IF %0<>2 6890 .ERROR "Error in PGSETUP" 6900 .ELSE 6910 LDA # <%1 6920 STA ICBAL,X 6930 LDA # >%1 6940 STA ICBAH,X 6950 .IF %2<256 6960 LDA #%2 6970 STA ICBLL,X 6980 LDA #0 6990 STA ICBLH,X 7000 .ELSE 7010 LDA %2 7020 STA ICBLL,X 7030 LDA %2+1 7040 STA ICBLH,X 7050 .ENDIF 7060 .ENDIF 7070 .ENDM 7080 ; 7090 ;******************************* 7100 ; 7110 ;OPENA macro 7120 ; 7130 ;Usage: OPENA IOCB,ax1,ax2,add 7140 ; 7150 ;'IOCB' is IOCB number to use 7160 ;'ax1' is task number 7170 ;'ax2' is the 2nd auxiliary byte 7180 ;'add' is the address of the 7190 ;device name to be opened 7200 ; 7210 .MACRO OPENA 7220 .IF %0<>4 7230 .ERROR "Error in OPENA" 7240 .ELSE 7250 LDX #%1*16 7260 LDA #%2 7270 STA ICAX1,X 7280 LDA #%3 7290 STA ICAX2,X 7300 LDA # <%4 7310 STA ICBAL,X 7320 LDA # >%4 7330 STA ICBAH,X 7340 JSR OPENIOCB 7350 .ENDIF 7360 .ENDM
Listing 2. Assembly listing. 0100 ;Program to copy a list of files 0110 ;whose names are in a file named 0120 ;D1:RAMDISK.FIL from drive D1: 0130 ;to RAM disk drive D8: 0140 ; 0150 ;by Karl E. Wiegers 0160 ; 0170 .OPT OBJ,NO LIST 0180 .INCLUDE #D8:MACRO.LIB 0190 ; 0200 BYTESREAD = $4FFE 0210 INBUFF = $6000 0220 ; 0230 ;******************************* 0240 ; PROGRAM BEGINS HERE 0250 ;******************************* 0260 ; 0270 *= $5000 0280 ; 0290 START 0300 ; 0310 ;------------------------------ 0320 ;look for D1:RAMDISK.FIL; print 0330 ;error message if not found 0340 ;------------------------------ 0350 ; 0360 CLD 0370 JSR CLS 0380 OPEN 2,4,0,"D1:RAMDISK.FIL" 0390 BPL FOUNDIT 0400 TYA 0410 PHA 0420 PUT 0,MISSING,15 0430 PLA 0440 TAY 0450 JSR STATUSERR 0460 CLOSE 2 0470 JMP EXIT 0480 MISSING .BYTE "D1:RAMDISK.FIL " 0490 ; 0500 ;------------------------------- 0510 ;read a record from RAMDISK.FIL; 0520 ;if EOF is reached, program is 0530 ;complete; print message if 0540 ;some other error crops up 0550 ;------------------------------- 0560 ; 0570 FOUNDIT 0580 INPUT 2,FNAMEIN 0590 BPL NOTEOF 0600 CPY #EOF 0610 BNE OTHERERR 0620 CLOSE 2 0630 JMP EXIT 0640 OTHERERR 0650 PRINT UNKNOWNERR 0660 CLOSE 2 0670 JMP EXIT 0680 UNKNOWNERR .BYTE "Unknown error" 0690 .BYTE " on RAMDISK.FIL",EOL 0700 ; 0710 ;------------------------------ 0720 ;build the output file name, 0730 ;open input file, handle error 0740 ;------------------------------ 0750 ; 0760 NOTEOF 0770 MOVE FNAMEIN,FNAMEOUT,16 0780 LDX #1 0790 LDA #56 ;ATASCII '8' 0800 STA FNAMEOUT,X 0810 PRINT FNAMEIN 0820 OPENA 3,4,0,FNAMEIN 0830 BPL FOUNDINP 0840 JSR STATUSERR 0850 CLOSE 3 0860 CLOSE 4 0870 JMP FOUNDIT 0880 ; 0890 ;------------------------------ 0900 ;open output file, check for 0910 ;error with ramdisk 0920 ;------------------------------ 0930 ; 0940 FOUNDINP 0950 OPENA 4,8,0,FNAMEOUT 0960 BPL DOCOPY 0970 CLOSE 2 0980 CLOSE 3 0990 PRINT RAMDERROR 1000 JMP EXIT 1010 RAMDERROR .BYTE "Problem with" 1020 .BYTE " the ramdisk...",EOL 1030 ; 1040 ;------------------------------ 1050 ;copy file in blocks of 255 bytes 1060 ;------------------------------ 1070 ; 1080 DOCOPY 1090 GET 3,INBUFF,255 1100 BMI FINISH 1110 PUT 4,INBUFF,255 1120 CLC 1130 BCC DOCOPY 1140 ; 1150 ;------------------------------ 1160 ;write the remaining number of 1170 ;input bytes, close files, go 1180 ;get the next input filename 1190 ;------------------------------ 1200 ; 1210 FINISH 1220 LDX #$30 1230 LDA ICBLL,X 1240 STA BYTESREAD 1250 LDA ICBLH,X 1260 STA BYTESREAD+1 1270 PUT 4,INBUFF,BYTESREAD 1280 CLOSE 3 1290 CLOSE 4 1300 PRINT OKAY 1310 JMP FOUNDIT 1320 OKAY .BYTE "Copied okay",EOL 1330 ; 1340 ;------------------------------ 1350 ;RTS lets this be AUTORUN.SYS 1360 ;------------------------------ 1370 ; 1380 EXIT RTS 1390 ; 1400 ;------------------------------ 1410 ;space for input & output 1420 ;filenames 1430 ;------------------------------ 1440 ; 1450 FNAMEIN .DS 16 1460 FNAMEOUT .DS 16 1470 ; 1480 ;------------------------------ 1490 ;don't forget the subroutines! 1500 ;------------------------------ 1510 ; 1520 .INCLUDE #D8:SUBS.LIB 1530 ; 1540 ;------------------------------ 1550 ;set up for autorun on loading 1560 ;------------------------------ 1570 ; 1580 *= RUNAD 1590 .WORD START
A.N.A.L.O.G. ISSUE 61 / JUNE 1988 / PAGE 74
Quick, what values do you store at what addresses to enable player/missile graphics with single-line resolution? That’s okay, I don’t remember either. However, I learned long ago that the next best thing to knowing some useful tidbit of information is knowing where to find it when you need it.
There are two ways to remember the useful tidbits required for player/missile graphics. The first is to keep a copy of Mapping the Atari by Ian Chadwick (COMPUTE! Books) handy. This book is absolutely indispensible for anyone programming an 8-bit Atari in assembly language (or any other language).
Even better, use your computer’s memory instead of your own. Today I present a baker’s dozen of macros that help you use player/missile graphics (PMG), display list interrupts (DLI), and vertical blank interrupts (VBI) in assembly programs. These macros simulate some of the commands Atari BASIC should have had but didn’t. Many novice programmers are daunted by the minutiae associated with setting up PMG, but these macros are useful shortcuts to success. Along the way, we’ll see how to manipulate missiles too. You see, Bonzo (from “Attack of the Suicidal Road-Racing Aliens”) is fed up with being squashed. Today he shoots back.
We’d best begin with the “Whoops!” category. There’s a small bug in the MOVE macro from two months ago. Please add this line to your MACRO.LIB file:
6115 LDY #0
Sorry about that.
Listing 1 contains the promised 13 new graphics macros. I decided to begin a new file of macros to be .INCLUDEd in future assembly programs, since the old MACRO. LIB file has become pretty long. Please enter Listing 1 into a file named GRAPHICS.LIB. If you write a program that doesn’t use any of these macros, simply omit the .INCLUDE statement for this file.
If you’re using the RAM disk file copier from last time, you should add D1:GRAPHICS.LIB to the list of files to be copied from the boot disk to the RAM disk. We can use the append feature of the DOS menu selection for copying files. Go to the DOS menu, choose item C to copy a file, and type:
This notation means that we want to copy from the screen editor (that is, the keyboard) to file D:RAMDISK.FIL, appending whatever we type on the keyboard to the present contents of D:RAMDISK.FIL. The cursor will then move to the beginning of the next line. Type:
Press RETURN, and press control-3 to signify the end of the file. Your modified RAMDISK.FIL file should be written to the disk at this point. To verify that the change was made, copy from D:RAMDISK.FIL to E: and make sure all three lines appear:
D1:MACRO.LIB D1:SUBS.LIB D1:GRAPHICS.LIB
Let’s walk through the 13 macros in Listing 1. Most of the concepts will be familiar from our earlier graphics discussions, but I want to review a few points. These macros are all in MAC/65 format, but you should be able to adapt them to other macro assemblers with a little effort. The equates used by the macros are in Lines 170–280. You’ll get a duplicate label error if any of these equates also appear elsewhere in your program.
The first entry is VBION in Lines 320–490. This routine simply turns on a vertical blank interrupt routine in your program. It requires one parameter, the address of the beginning of the VBI. I always label the beginning of my VBI routines as (guess what) “VBI”; so my calls to this macro are in the form :VBION VBI. It seems redundant, but it really isn’t. All this routine does is insert your custom VBI routine into the deferred VBI vector so it gets executed every sixtieth of a second, as it should.
The obvious counterpart is the next macro, VBIOFF, which requires no parameters. It simply resets the deferred VBI vector to the system default, thereby disabling the user-written routine. For both VBION and VBIOFF, you can change the LDA #7 statement to LDA #6 if you wish to use an immediate, rather than deferred, VBI routine. See Boot Camp in issue 49 for a discussion of VBIs.
Similarly, the DLION macro (Lines 660–860) enables display list interrupts by setting bit 7 at address NMIEN ($D40E), Lines 780–800. DLION accepts one parameter, the address of your first DLI routine. I always call this (guess what) “DLI,” so my use of this macro is in the form: DLION DLI. That address is stored in locations VDSLST, $200–$201 (Lines 810–840). Recall that if you’re using multiple DLIs in the same screen, each DLI must itself store the address of the next DLI in VDSLST. Of course, it’s still up to you to indicate the mode lines where you want the DLIs to occur, by setting bit 7 of each mode line instruction in the display list. See issue 46 for a DLI refresher.
As you might expect, the DLIOFF macro simply clears bit 7 in NMIEN if it’s already set. Be careful, though. If you use DLIOFF before DLION, you can actually enable DLIs rather than disabling them. If you use these macros in the sensible order, all will be dandy.
Now to the player/missile graphics aids. SETPCOLOR (Lines 1020–1410) is virtually identical to our old SETCOLOR macro. However, SETPCOLOR sets one of the player color registers, whereas SETCOLOR processes a playfield color register. The four-player color registers are at addresses $2C0–$2C3, PCOLR0–PCOLR3. In case you ever need to change player colors using display list interrupts, these locations are the shadow registers for COLPM0–COLPM3 at $D012–$D015. Each color register controls the color of both a specific player and the missile associated with that player. Use SETPCOLOR just like you would SETCOLOR, with three parameters for the player number (0–3), hue (0–15) and luminance (0–15). Each parameter can be either a value or an address containing the values to be used.
The PWIDTH macro. Lines 1450–1660, lets you set each player independently to be normal (8 pixels), double (16 pixels), or quadruple (32 pixels) wide. Parameter 1 is the player number (0–3), and parameter 2 is the width to use (1, 2, or 4). The width of each player is determined by the bit pattern stored in bits and 1 at addresses SIZEP0–SIZEP3 ($D008–$D00B). A bit pattern of 00 or 10 selects normal width; 01 doubles the player’s width; and 11 produces quadruple width.
Several steps are required to actually enable player/missile graphics even after you’ve set up the player shapes, sizes and positions. Macro PMGON (Lines 1700–1880) does the dirty work. It takes one parameter, the address of the beginning of the block of RAM you reserved for PMG storage. Amazingly, I always give this address the label “PMG.” Lines 1810–1820 tell the operating system where to find the PMG data. Line 1830 turns on players and missiles by setting bits and 1 in GRACTL, $D01D. Lines 1840–1860 set bits 2 and 3 in SDMCTL, $22F, also required to activate PMG. Isn’t a single statement like “PMGON PMG” a lot easier to remember than all this other junk? That’s what macros are for. Of course, the next macro is called PMGOFF, in Lines 1920–2020. It simply undoes most of what PMGON accomplished. No parameters are needed.
You probably recall that players can be displayed in either single-line or double-line resolution. The default is double-line, which means that each bit pattern in the player shape definition table shows up on two adjacent scan lines. The PMGRES macro in Lines 2060–2230 lets you choose the desired resolution. The parameter can either be 1 for single-line or 2 for double-line players. Recall also that PMG RAM allocation and usage is different depending on the resolution you’re using. Refer to issue 48 to refresh your memory.
You can also control the horizontal position of each player and missile, independently. I have two macros for these purposes, HPLAYER in Lines 2270–2430 and HMISSILE in Lines 2470–2630. These work in exactly the same way. Two parameters are needed, the player number (0–3) and the desired horizontal position, a value from 0–255. It wouldn’t be difficult to modify these macros to accept as parameter 2 an address containing the desired horizontal position; give it a try. Remember that horizontal position values below about 48 and above 208 probably won’t be visible on your TV or monitor screen.
Two sets of addresses are used in each of these macros. Locations HPOSP0–HPOSP3 ($D000–$D003) control horizontal positions for players, and HPOSM0–HPOSM3 ($D004–$D008) are used for missiles. However, these addresses are “write-only.” You can’t find out where a player is by peeking at the contents of one of these addresses. Hence, I set up parallel sets of data storage locations called XPOSP0–XPOSP3 and XPOSM0–XPOSM3. The HPLAYER and HMISSILE macros assume that you’ve done the same, and you’ll get an undefined label error if you omit this step. Today’s sample program will show what I mean.
Setting the widths of missiles is a bit more convoluted. A missile is just a 2-bit wide analog of the 8-bit wide player. Only one address, SIZEM ($D00C), is devoted to controlling missile widths. Bits and 1 handle missile 0, bits 2–3 are for missile 1, bits 4–5 apply to missile 2, bits 6–7 take care of missile 3. The pattern in each pair of bits again controls the missile width: 00 and 10 are normal; 01 is double; and 11 is quadruple.
The MWIDTH macro first creates the desired bit pattern based on the value in parameter 2 (1, 2, or 4). The value of parameter 1 tells us which missile to set. The loop in Lines 2890–2960 shifts the desired bit pattern two bits to the left (more significant direction) until the bit pattern is in position corresponding to the correct missile. For example, for missile 0 we don’t do any shifting, and for missile 2 we shift the pattern a total of four times (two passes through the loop), until our pattern is in bits 4–5. The resulting bit pattern is stored temporarily at address @TEMP within the macro definition (Line 2800). Finally, Lines 2980–3000 take the current contents of SIZEM, use the ORA instruction to set the desired two bits based on the contents of @TEMP (leaving the other six bits of SIZEM unchanged), and store the result back in SIZEM.
Confused? So was I. That’s why I wrote the macro. Now I don’t have to remember how it works every time I want to set the width of a missile. I simply let the computer do the thinking, while I try to handle the creativity end of business.
Our final macro sets the width of the playfield to normal (40 Graphics characters), narrow (32 characters), or wide (48 characters) width. The playfield, of course, is the area of the monitor screen used for display of text, graphics and players. Our old friend SDMCTL ($22F) is the main actor here again. The PLFIELD macro in Lines 3060–3390 requires one parameter to specify the desired width. A parameter of 0 turns off the display screen entirely, 1 is for narrow, 2 for standard and 3 for the wide playfield. The bit pattern in bits 0 and 1 of SDMCTL controls the playfield setting. A value of 00 means off, 01 is narrow, 10 is standard and 11 is wide.
The logic in the PLFIELD macro gets a little harrowing. It turns out to be a little tricky to simply set and clear specific bits in a byte, without affecting other contents of the byte. The AND, ORA and EOR instructions are useful, but you have to think carefully about what they do and in what order to use them. In the case of the narrow playfield, for example, I want to clear bit 1 and set bit 0. I chose a rather odd method to do this, but it works. Lines 3230–3240 perform two LSR (Logical Shift Right) operations. This simply throws away the contents of bits 0 and 1, while shifting the remaining six bits two positions to the right. Then two ASL (Accumulator Shift Left) instructions put the six high-order bits back where they belong and clear both bits 0 and 1. After that I use the ORA instructions in Line 3270 to selectively set bit 0. Whew!
You may wonder why I gave this last macro the awkward name of PLFIELD. Why not just come right out and say PLAYFIELD? Well, I tried PLAYFIELD. Unfortunately, MAC/65 interpreted this as a PLA instruction followed by YFIELD as a piece of data. So, I tried PLYFIELD, thinking that at least PLY isn’t a 6502 mnemonic. Right, except that MAC/65 supports some extra opcodes that apply only to an enhanced NCR 65C02 microprocessor, and PLY happens to be such an instruction. It means to pull the Y-register from the stack. Hence, the more contrived PLFIELD. The moral is to be careful when naming macros, so MAC/65 doesn’t misinterpret your macro name as some bizarre kind of instruction.
So now your toolbox is crammed with even more goodies. Let’s see some of these babies in action.
Remember Bonzo? He’s the little guy with the death wish from “Attack of the Suicidal Road-Racing Aliens.” Bonzo’s changed his tune, and he’s out to get back at the cars that kept doing him in. Today’s sample program lets Bonzo shoot back at the cars. We’ll see how to manipulate missiles, and how easy it is to set up a graphics program using these new (and some old) macros. In fact,the program in Listing 2 uses about 20 macros. As a special treat, I’ll show you how to create the famous Atari rainbow character effect.
Please type in Listing 2. You’ll have to assemble this program to disk, rather than just to memory, which might slow things down a bit. If you’re using the RAM disk, assemble to some file on drive D8: using a command like: ASM,,#D8:BC58.OBJ. Don’t forget to save a copy of the source code on disk before you BLOAD the assembled object code. Otherwise, the object file might overwrite the tail end of your source code. If you aren’t using a RAM disk, change the drive designations for the .INCLUDE statements in Lines 210, 220, and 2850.
Here’s the plan. Bonzo will remain at the bottom of the screen, and you can move him left or right within specified boundaries using a joystick in Port 1. A car will move across the screen from left to right. Bonzo shoots a missile at the car whenever you press the fire button on the joystick. If Bonzo scores a hit, the car explodes and a message appears. You can then either press START to play again or press RESET to exit from the program.
We’ll use a VBI to handle movement of the car, Bonzo, and the missile. I’ve also created a special shape for Bonzo to assume when he’s actually firing the missile. The VBI will copy that form into the PMG RAM whenever you press the joystick fire button. Our main program sets up the PMG environment, waits for a collision, and handles the post-collision activities.
Of course, we need to .INCLUDE the two macro library files we’ve built. Lines 210–220. Some equates appear in Lines 260–330. You’ve seen most of these before. STRIG0 ($284) reads the joystick trigger (fire button). M0PL ($D008) checks for collisions between missile (fired by Bonzo as player 0) and players.
I put the PMG dedicated RAM block (2K for single-line resolution) at address $3000 in Line 390. The .DS directives reserve chunks of RAM for each player and the missiles. The three pages from PMG to MIS aren’t used in this program. My work variables which keep track of the horizontal and vertical positions of the players and missiles appear at the end of the PMG block, as do bytes to specify the limits of motion at the edges of the screen.
The VBI routine, begins at $4000 (Line 590). There’s quite a bit of unused space between the top of the PMG block and the beginning of the VBI, which might come in handy if you have a really large program. Much of the VBI code is adapted from the Boot Camp column in issue 49. Storing something in ATRACT (Line 620) prevents the computer from going into attract mode if no key is pressed for several minutes. Lines 630–650 move the car (Player 1) one pixel to the right. Lines 660–830 handle the left/right movement of Bonzo, making sure he doesn’t go past the boundaries I set in the main program.
The MOVEMISSILE routine beginning at Line 840 checks to see if the missile has been fired already, indicated if the horizontal position (XPOSM0) is not zero. If so, the missile is moved upward using the method we covered in previous issues (see Lines 1010–1140) until it hits the top boundary. When it hits the top. Lines 900–990 reset the horizontal position to zero (off-screen) and zero out the missile section in the PMG RAM block to clear out any junk. Then we go to CHKTRIG to see if the fire button is being pressed.
If the fire button is pressed, location STRIG0 will contain a 0. Otherwise, it contains a 1. If the button isn’t pressed. Line 1170 branches down to COPYBONZO at Line 1310. There the standard Bonzo shape is copied to the RAM block for player 0. 1 do this every time just in case the last shape displayed was the shooting form. We don’t wan’t the shooting shape to remain forever once it is first drawn, now, do we?
If you’re pressing the fire button, the shooting shape stored at address SHOOTER (Lines 2680–2710) is copied into PMG RAM using the MOVE macro. Line 1190. If the missile is already fired we don’t shoot another one. However, if it hasn’t been fired yet. Lines 1220–1270 copy the missile form (defined in Lines 2780–2790) into the PMG RAM block and set the horizontal position to look like Bonzo really fired it. As with any VBI routine, the gracefill way to exit is by jumping through the XITVBV ($E462) vector, Line 1350.
As usual, the main program begins execution at address $5000, line 1410. Since I’ve termed this starting point START (creative labels, eh?), you could make this program autorun on loading, using the method we discussed last month.
The first orders of business are to set up a full screen of Graphics 2 and set the boundaries for player and missile movement (Lines 1450–1480). Lines 1490–1580 zero the required portions of the reserved PMG RAM block. The statements in Lines 1680–1820 set up the PMG environment. The player shapes are defined in Lines 2580–2610 (Bonzo) and 2630–2660 (the car).
Bonzo is yellow and the car is pink. Both players are single resolution, on a standard width playfield. Bonzo is normal width and the car is double width. The missile Bonzo fires will be normal width. After enabling player/missile graphics in Line 1770, Bonzo is moved to the middle of the screen. Both the car and Bonzo’s missile begin offstage, at a horizontal position of 0. Finally, Line 1820 begins execution of the VBI routine, and the car starts to move across the screen. Now you can move Bonzo using the joystick and fire when ready, Gridley.
All we do now is wait until Bonzo hits the car with a missile. The loop in Lines 1910–1940 simply tests for this condition forever. Don’t forget to reset the collision registers as in Line 1900 before checking for a new collision. We talked about collision detection in issue 50.
When Bonzo scores a hit, the game is over. First I turn off the VBI routine in Line 2020 so all player and missile movement ceases. The missile is moved offscreen in Lines 2030–2040. I replace the car shape with a wrecked car shape (WRECK, defined in Lines 2730–2760), Line 2050. The FOR/ NEXT loop in Lines 2060–2110 simply changes the color of the wreck from bright to dark red and back rapidly ten times, pausing for three jiffies after each color change. This gives sort of a flashing explosion effect.
Lines 2220–2250 print some messages on the screen, which are defined in Lines 2520–2560. Notice that I’ve used characters in those lines to select different color registers for the different text lines.
Lines 2260–2340 are all it takes to generate the well-known Atari rainbow effect. It works by simply incrementing the value stored in a particular hardware color register. Line 2290 waits for horizontal synchronization before actually effecting the color change. The result is a new color on each scan line, moving down the screen at about 60 scan lines per second. By changing the offset in the Y-register (Line 2270) and/or the base address being affected (Line 2310), you can produce this effect in any of the playfield or player color registers.
The rainbow continues until you either press the START button to play again (Lines 2320–2330) or the RESET button to exit from the program entirely. We talked about reading the console buttons in issue 44. When START is pressed. Lines 2430–2460 close the screen IOCB, reset the collision registers, turn off player/missile graphics, and go back to let Bonzo get some more revenge.
As you can see, ladies and gentlemen of the jury, macros make assembly programming much faster, easier and cleaner. It doesn’t take an Atari expert to write effective graphics programs when the right macros are available. I ask you to find in favor of the macro assembler, and to purchase one if you plan to continue your pursuit of 6502 assembly language on the 8-bit Atari computers. I thank you.
0100 ;Graphics macros for MAC/65 0110 ;by Karl E. Wiegers 0120 ; 0130 ;******************************* 0140 ; 0150 ;equates needed by macros 0160 ; 0170 VDSLST = $0200 0180 SDMCTL = $022F 0190 PCOLR0 = $02C0 0200 HPOSP0 = $D000 0210 HPOSM0 = $D004 0220 SIZEP0 = $D008 0230 SIZEM = $D00C 0240 GRACTL = $D01D 0250 PMBASE = $D407 0260 NMIEN = $D40E 0270 SETVBV = $E45C 0280 XITVBV = $E462 0290 ; 0300 ;******************************* 0310 ; 0320 ;VBION macro 0330 ; 0340 ;Usage: VBION address 0350 ; 0360 ;'address' is the address or 0370 ;label for the beginning of your 0380 ;deferred VBI routine 0390 ; 0400 .MACRO VBION 0410 .IF %0<>1 0420 .ERROR "Error in VBION" 0430 .ELSE 0440 LDY # <%1 0450 LDX # >%1 0460 LDA #7 0470 JSR SETVBV 0480 .ENDIF 0490 .ENDM 0500 ; 0510 ;******************************* 0520 ; 0530 ;VBIOFF macro 0540 ; 0550 ;Usage: VBIOFF 0560 ; 0570 .MACRO VBIOFF 0580 LDY # <XITVBV 0590 LDX # >XITVBV 0600 LDA #7 0610 JSR SETVBV 0620 .ENDM 0630 ; 0640 ;******************************* 0650 ; 0660 ;DLION macro 0670 ; 0680 ;Usage: DLION address 0690 ; 0700 ;'address' is the starting 0710 ;address of the DLI routine to 0720 ;be executed 0730 ; 0740 .MACRO DLION 0750 .IF %0<>1 0760 .ERROR "Error in DLION" 0770 .ELSE 0780 LDA NMIEN 0790 ORA #$80 0800 STA NMIEN 0810 LDA # <%1 0820 STA VDSLST 0830 LDA # >%1 0840 STA VDSLST+1 0850 .ENDIF 0860 .ENDM 0870 ; 0880 ;******************************* 0890 ; 0900 ;DLIOFF macro 0910 ; 0920 ;Usage: DLIOFF 0930 ; 0940 .MACRO DLIOFF 0950 LDA NMIEN 0960 EOR #$80 0970 STA NMIEN 0980 .ENDM 0990 ; 1000 ;******************************* 1010 ; 1020 ;SETPCOLOR macro 1030 ; 1040 ;Usage: SETPCOLOR p#,hue,lum 1050 ; 1060 ;p# is player number (0-3) 1070 ;hue is color number (0-15) 1080 ;lum is luminance value (0-15) 1090 ;all can be values or addresses 1100 ; 1110 .MACRO SETPCOLOR 1120 .IF %0<>3 1130 .ERROR "Error in SETPCOLOR" 1140 .ELSE 1150 .IF %1>3 1160 LDX %1 1170 .ELSE 1180 LDX #%1 1190 .ENDIF 1200 .IF %2>15 1210 LDA %2 1220 ASL A 1230 ASL A 1240 ASL A 1250 ASL A 1260 .ELSE 1270 LDA #%2*16 1280 .ENDIF 1290 .IF %3>15 1300 LDY %3 1310 .ELSE 1320 LDY #%3 1330 .ENDIF 1340 STA PCOLR0,X 1350 TYA 1360 AND #$0F 1370 CLC 1380 ADC PCOLR0,X 1390 STA PCOLR0,X 1400 .ENDIF 1410 .ENDM 1420 ; 1430 ;******************************* 1440 ; 1450 ;PWIDTH macro 1460 ; 1470 ;Usage: PWIDTH p#,width 1480 ; 1490 ;p# is player number (0-3) 1500 ;width is width factor (1,2,4) 1510 ; 1520 .MACRO PWIDTH 1530 .IF %0<>2 1540 .ERROR "Error in PWIDTH" 1550 .ELSE 1560 LDX #%1 1570 LDA #0 1580 .IF %2=2 1590 LDA #1 1600 .ENDIF 1610 .IF %2=4 1620 LDA #3 1630 .ENDIF 1640 STA SIZEP0,X 1650 .ENDIF 1660 .ENDM 1670 ; 1680 ;******************************* 1690 ; 1700 ;PMGON macro 1710 ; 1720 ;Usage: PMGON address 1730 ; 1740 ;'address' is the address of the 1750 ;reserved PMG RAM block 1760 ; 1770 .MACRO PMGON 1780 .IF %0<>1 1790 .ERROR "Error in PMGON" 1800 .ELSE 1810 LDA # >%1 1820 STA PMBASE 1830 POKE GRACTL,3 1840 LDA SDMCTL 1850 ORA #$0C 1860 STA SDMCTL 1870 .ENDIF 1880 .ENDM 1890 ; 1900 ;******************************* 1910 ; 1920 ;PMGOFF macro 1930 ; 1940 ;Usage: PMGOFF 1950 ; 1960 .MACRO PMGOFF 1970 LDA #0 1980 STA GRACTL 1990 LDA SDMCTL 2000 EOR #$0C 2010 STA SDMCTL 2020 .ENDM 2030 ; 2040 ;******************************* 2050 ; 2060 ;PMGRES macro 2070 ; 2080 ;Usage: PMGRES res 2090 ; 2100 ;res is 1 for single-line, 2 2110 ;for double-line resolution 2120 ; 2130 .MACRO PMGRES 2140 .IF %0<>1 2150 .ERROR "Error in PMGRES" 2160 .ELSE 2170 .IF %1=1 2180 LDA SDMCTL 2190 ORA #$10 2200 STA SDMCTL 2210 .ENDIF 2220 .ENDIF 2230 .ENDM 2240 ; 2250 ;******************************* 2260 ; 2270 ;HPLAYER macro 2280 ; 2290 ;Usage: HPLAYER p#,X 2300 ; 2310 ;p# is player number (0-3) 2320 ;X is horizontal position 2330 ; 2340 .MACRO HPLAYER 2350 .IF %0<>2 2360 .ERROR "Error in HPLAYER" 2370 .ELSE 2380 LDX #%1 2390 LDA #%2 2400 STA HPOSP0,X 2410 STA XPOSP0,X 2420 .ENDIF 2430 .ENDM 2440 ; 2450 ;******************************* 2460 ; 2470 ;HMISSILE macro 2480 ; 2490 ;Usage: HMISSILE m#,X 2500 ; 2510 ;m# is missile number (0-3) 2520 ;X is horizontal position 2530 ; 2540 .MACRO HMISSILE 2550 .IF %0<>2 2560 .ERROR "Error in HMISSILE" 2570 .ELSE 2580 LDX #%1 2590 LDA #%2 2600 STA HPOSM0,X 2610 STA XPOSM0,X 2620 .ENDIF 2630 .ENDM 2640 ; 2650 ;******************************* 2660 ; 2670 ;MWIDTH macro 2680 ; 2690 ;Usage: MWIDTH m#,width 2700 ; 2710 ;m# is missile number (0-3) 2720 ;width is 1, 2, or 4 2730 ; 2740 .MACRO MWIDTH 2750 .IF %0<>2 2760 .ERROR "Error in MWIDTH" 2770 .ELSE 2780 CLC 2790 BCC @SKIPMWIDTH 2800 @TEMP .BYTE 0 2810 @SKIPMWIDTH 2820 LDA #0 2830 .IF %2=2 2840 LDA #1 2850 .ENDIF 2860 .IF %2=4 2870 LDA #3 2880 .ENDIF 2890 LDY #%1 2900 BEQ @SHDONE 2910 @SHLOOP 2920 ASL A 2930 ASL A 2940 DEY 2950 BNE @SHLOOP 2960 @SHDONE 2970 STA @TEMP 2980 LDA SIZEM 2990 ORA @TEMP 3000 STA SIZEM 3010 .ENDIF 3020 .ENDM 3030 ; 3040 ;****************************** 3050 ; 3060 ;PLFIELD macro 3070 ; 3080 ;Usage: PLFIELD width 3090 ; 3100 ;'width' is 0 to turn screen off, 3110 ;1 for narrow playfield, 2 for 3120 ;standard, 3 for wide 3130 ; 3140 .MACRO PLFIELD 3150 .IF %0<>1 3160 .ERROR "Error in PLFIELD" 3170 .ELSE 3180 LDA SDMCTL 3190 .IF %1=0 3200 LDA #0 3210 .ENDIF 3220 .IF %1=1 3230 LSR A 3240 LSR A 3250 ASL A 3260 ASL A 3270 ORA #1 3280 .ENDIF 3290 .IF %1=2 3300 LSR A 3310 ORA #1 3320 ASL A 3330 .ENDIF 3340 .IF %1=3 3350 ORA #3 3360 .ENDIF 3370 STA SDMCTL 3380 .ENDIF 3390 .ENDM
0100 ;Demonstration of player/missile 0110 ;graphics macros 0120 ; 0130 ;by Karl E. Wiegers 0140 ; 0150 .OPT OBJ,NO LIST 0160 ; 0170 ;****************************** 0180 ; PULL IN MACRO LIBRARIES 0190 ;****************************** 0200 ; 0210 .INCLUDE #D8:MACRO.LIB 0220 .INCLUDE #D8:GRAPHICS.LIB 0230 ; 0240 ;equates we need today 0250 ; 0260 ATRACT = $4D 0270 STICK0 = $0278 0280 STRIG0 = $0284 0290 M0PL = $D008 0300 COLPF0 = $D016 0310 HITCLR = $D01E 0320 CONSOL = $D01F 0330 WSYNC = $D40A 0340 ; 0350 ;******************************* 0360 ; SET UP PMG STORAGE 0370 ;******************************* 0380 ; 0390 *= $3000 0400 ; 0410 PMG .DS $0300 0420 MIS .DS $0100 0430 PL0 .DS $0100 0440 PL1 .DS $0100 0450 PL2 .DS $0100 0460 PL3 .DS $0100 0470 XPOSP0 .DS 4 0480 YPOSP0 .DS 4 0490 XPOSM0 .DS 4 0500 YPOSM0 .DS 4 0510 LEFT .DS 1 0520 RIGHT .DS 1 0530 TOP .DS 1 0540 ; 0550 ;******************************* 0560 ; VBI ROUTINE STARTS HERE 0570 ;******************************* 0580 ; 0590 *= $4000 0600 ; 0610 VBI 0620 POKE ATRACT,0 0630 INC XPOSP0+1 ;move car 1 0640 LDA XPOSP0+1 ;pixel to right 0650 STA HPOSP0+1 0660 LDA STICK0 ;get stick 1 0670 AND #4 ;left? 0680 BNE CHKRIGHT ;no,check right 0690 LDA XPOSP0 ;yes - at left 0700 CMP LEFT ;edge? 0710 BEQ MOVEMISSILE ;yes, go on 0720 DEC XPOSP0 ;no, move Bonzo 0730 POKE HPOSP0,XPOSP0 ;to left 0740 BNE MOVEMISSILE ;go on 0750 CHKRIGHT 0760 LDA STICK0 ;get stick 1 0770 AND #8 ;right? 0780 BNE MOVEMISSILE ;no, go on 0790 LDA XPOSP0 ;yes - at right 0800 CMP RIGHT ;edge? 0810 BEQ MOVEMISSILE ;yes, go on 0820 INC XPOSP0 ;no, move him to 0830 POKE HPOSP0,XPOSP0 ;right 0840 MOVEMISSILE 0850 LDA XPOSM0 ;missile fired? 0860 BEQ CHKTRIG ;no, check trig 0870 LDA YPOSM0 ;yes - at the 0880 CMP TOP ;top? 0890 BNE MOVEM ;no, move it 0900 POKE XPOSM0,0 ;yes - move 0910 POKE HPOSM0,0 ;missile 0920 POKE YPOSM0,$B8 ;offscreen 0930 LDX #0 ;zero out 0940 TXA ;missile 1 area 0950 ZMISSILE 0960 STA MIS,X 0970 INX 0980 CPX #$B8 0990 BNE ZMISSILE 1000 BEQ CHKTRIG ;check trigger 1010 MOVEM 1020 LDA # >MIS ;move missile 1030 STA MOVEFROM+1 ;up 1 scan 1040 POKE MOVEFROM,YPOSM0 ;line 1050 LDY #1 1060 LOOPUP 1070 LDA (MOVEFROM),Y 1080 DEY 1090 STA (MOVEFROM),Y 1100 INY 1110 INY 1120 CPY #10 ;missile is 10 1130 BNE LOOPUP ;bytes tall 1140 DEC YPOSM0 1150 CHKTRIG 1160 LDA STRIG0 ;trigger pressed? 1170 BNE COPYBONZO ;no, go on 1180 ;yes-copy shooting form of Bonzo 1190 MOVE SHOOTER,PL0+$C0,17 1200 LDA XPOSM0 ;missile fired? 1210 BNE VBIEXIT ;yes, exit 1220 LDX XPOSP0 ;no, copy missile 1230 INX ;form into PMG 1240 INX ;and move to 1250 STX HPOSM0 ;Bonzo's location 1260 STX XPOSM0 1270 MOVE MISSILE,MIS+$B8,10 1280 CLC 1290 BCC VBIEXIT 1300 ;copy normal Bonzo form 1310 COPYBONZO 1320 MOVE BONZO,PL0+$C0,17 1330 ;leave VBI routine 1340 VBIEXIT 1350 JMP XITVBV 1360 ; 1370 ;******************************* 1380 ; MAIN PROGRAM STARTS HERE 1390 ;******************************* 1400 ; 1410 *= $5000 1420 ; 1430 START 1440 CLD ;binary mode 1450 GRAPHICS 2+16 ;open screen 1460 POKE LEFT,56 ;set limits 1470 POKE RIGHT,191 1480 POKE TOP,30 1490 LDX #0 ;zero PMG area 1500 TXA 1510 INIT 1520 STA MIS,X 1530 STA PL0,X 1540 STA PL1,X 1550 STA PL2,X 1560 STA PL3,X 1570 INX 1580 BNE INIT 1590 ; 1600 ;------------------------------- 1610 ;now point to PMG area, move 1620 ;car and Bonzo shapes into PMG 1630 ;RAM, set colors, widths, and 1640 ;positions, and resolution, and 1650 ;turn on PMG and VBI 1660 ;------------------------------- 1670 ; 1680 MOVE BONZO,PL0+$C0,17 1690 MOVE CAR,PL1+$80,16 1700 SETPCOLOR 0,1,12 1710 SETPCOLOR 1,5,6 1720 PLFIELD 2 1730 PMGRES 1 1740 PWIDTH 0,1 1750 PWIDTH 1,2 1760 MWIDTH 0,1 1770 PMGON PMG 1780 HPLAYER 0,120 1790 HPLAYER 1,0 1800 HMISSILE 0,0 1810 POKE YPOSM0,$B8 1820 VBION VBI 1830 ; 1840 ;------------------------------- 1850 ;clear collision registers; 1860 ;loop until you get a collision 1870 ;between the missile and the car 1880 ;------------------------------- 1890 ; 1900 POKE HITCLR,0 1910 CHKCOL 1920 LDA M0PL 1930 AND #2 1940 BEQ CHKCOL 1950 ; 1960 ;------------------------------- 1970 ;when collide, turn off VBI, 1980 ;move missile offstage, copy 1990 ;wreck shape on car; flash colors 2000 ;------------------------------- 2010 ; 2020 VBIOFF 2030 POKE HPOSM0,0 2040 POKE XPOSM0,0 2050 MOVE WRECK,PL1+$80,18 2060 FOR I,1,10 2070 SETPCOLOR 1,4,12 2080 PAUSE 3 2090 SETPCOLOR 1,4,2 2100 PAUSE 3 2110 NEXT I 2120 ; 2130 ;------------------------------- 2140 ;change color registers, print 2150 ;messages, turn on rainbow for 2160 ;color register 0, wait for press 2170 ;of START or RESET keys 2180 ;------------------------------- 2190 ; 2200 SETCOLOR 1,5,8 2210 SETCOLOR 2,12,8 2220 POSITION 4,0 2230 PRINT 6,WINNER 2240 POSITION 1,2 2250 PRINT 6,WHATNEXT 2260 RAINBOW 2270 LDY #0 2280 INX 2290 STX WSYNC 2300 TXA 2310 STA COLPF0,Y 2320 LDA CONSOL 2330 CMP #6 2340 BNE RAINBOW 2350 ; 2360 ;------------------------------- 2370 ;close screen, reset collision 2380 ;registers, turn off PMG, go back 2390 ;and start it all over if START 2400 ;was pressed 2410 ;------------------------------- 2420 ; 2430 CLOSE 6 2440 POKE HITCLR,0 2450 PMGOFF 2460 JMP START 2470 ; 2480 ;------------------------------- 2490 ;lines to print, player shapes 2500 ;------------------------------- 2510 ; 2520 WINNER 2530 .BYTE "BONZO WINS!",EOL 2540 WHATNEXT 2550 .BYTE "start to go again " 2560 .BYTE " RESET TO EXIT",EOL 2570 ; 2580 BONZO 2590 .BYTE 0,60,24,126,189 2600 .BYTE 189,189,189,60,60 2610 .BYTE 36,36,36,102,0,0,0 2620 ; 2630 CAR 2640 .BYTE 0,0,126,195,219,219 2650 .BYTE 91,219,219,219,219 2660 .BYTE 91,219,219,195,126 2670 ; 2680 SHOOTER 2690 .BYTE 24,24,36,66,129,189 2700 .BYTE 153,126,60,60,60,60 2710 .BYTE 60,36,66,36,102 2720 ; 2730 WRECK 2740 .BYTE 20,89,98,86,156,41 2750 .BYTE 86,146,89,108,184,86 2760 .BYTE 40,84,86,8,16,32 2770 ; 2780 MISSILE 2790 .BYTE 1,1,1,1,1,1,1,1,1,0 2800 ; 2810 ;****************************** 2820 ; DON'T FORGET THE SUBS! 2830 ;****************************** 2840 ; 2850 .INCLUDE #D8:SUBS.LIB
A.N.A.L.O.G. ISSUE 62 / JULY 1988 / PAGE 76
From time to time I feel I need to remind you that computers were originally invented to perform computations. Yes, I know that spiffy visual and sound displays actually comprise the Atari appeal, but it all boils down to arithmetic in the end. Today I’d like to talk about different ways to store numbers in the computer, and present some methods for interconverting characters and numbers. A consequence of this discussion is that soon we’ll learn how to keep track of scores in such exciting games as “Attack of the Suicidal Road-Racing Aliens.”
So far, Boot Camp has focused on the most basic method for storing numbers in the computer, as binary integers. As you know, each byte of RAM can contain decimal value from 0 through 255 (hex $FF), based on the pattern of ones and zeros in the eight bits which make up the byte. If both positive and negative numbers must be accommodated, the most significant bit (bit 7) is reserved as a sign bit. If bit 7 is set, the number is negative; if cleared, the number is positive. This method leaves only seven data bits, so signed numbers ranging from -128 through +127 can be represented in this way.
Often we must deal with numbers larger than 255. We’ve used two adjacent bytes in RAM for this purpose, giving us 16 bits of unsigned data, or 15 bits for signed numbers. With 16 bits we can represent decimal numbers ranging from 0 through 65535 ($00 through $FFFF), or signed numbers from -32768 through +32767.
This is all very fine, but it doesn’t cover all our needs. Many numbers encountered in real life have fractional (decimal) parts, such as 7345.022. Obviously, the integer representation fails here. A more elaborate method for storing these so-called floating point numbers is used in the Atari, wherein each number occupies six bytes of RAM, regardless of its magnitude. Floating point storage uses a numeric representation called “binary-coded decimal” or BCD, which we’ll discuss more next month. The Boot Camp column in issue 43 covered floating point numbers and computations in grim detail.
Another problem arises when we wish to write a program in which the user enters a number that’s used in subsequent calculations, or when a calculated number needs to be output to the screen or printer. A number like 7239 is stored internally in only two bytes. With the hex value $1C47. But to print “7239” on the screen requires four characters. To further complicate the issue, to make the character “7” appear we actually have to print the ASCII character code 55 ($37).
(Things are even worse than they appear. The character with ASCII code 5 actually is stored internally in the Atari as character code 23. We won’t worry about this today.)
So, if we know that we want to print some numbers, we may want to choose another method for storing them internally, rather than using the standard two-byte integer. One possibility is to reserve one byte for each digit in our number. For the example of “7239,” we would use four bytes. But what to put in each byte? We could, of course, simply store “7” in the first byte, “2” in the second, and so on. But we still couldn’t print the number out this way. If we output an ASCII code of “7” to the screen, we get the same graphics symbol as you obtain by typing a control-G on the Atari keyboard (a diagonal slash). And you can’t even print an ASCII code 7 on a printer. Sending an ASCII 7 to an Epson printer makes the printer’s bell ring!
Here’s another option. Rather than storing “7” in the first byte, store the ASCII code for “7.” Table 1 lists the ASCII codes and bit patterns for the digits 0–9. Note that each digit has an ASCII code equal to the digit value plus $30. Hence, if we printed a byte containing $37, a 7 would indeed appear on the screen or printer.
Table 1. — ASCII / Character / Binary Equivalents. Character ASCII Code Binary Value 0 $30 0000 1 $31 0001 2 $32 0010 3 $33 0011 4 $34 0100 5 $35 0101 6 $36 0110 7 $37 0111 8 $38 1000 9 $39 1001
There are a few problems associated with storing numbers in ASCII form. First, this requires more RAM than does the binary integer form. Also, you can’t use the normal addition and subtraction operations, since they are designed to work with binary numbers.
One good solution to the problem is to go ahead and store numbers in two-byte integer format, and simply convert them to an ASCII string before printing. For input, we must convert the ASCII string typed by the user into its binary numeric representation. The Atari operating system contains built-in routines to convert ASCII strings into their floating point form and back again. Unfortunately, no such routines exist to interconvert integers and ASCII strings. Today I’ll present some macros and subroutines to perform all the necessary conversions.
Today’s example program lets you enter a number containing 1–5 digits at the keyboard. This number is checked to make sure it’s valid and then is converted to a two-byte binary integer. Then, the value 25 is added to the integer, and the result is converted to ASCII format and printed on the screen. Let’s dive in.
Listing 1 contains three macros (MAC/65 format) that should be appended to your by-now-enormous MACRO.LIB file, using the line numbers shown. These macros use some bytes for work space, which I’ve defined in the equates in Lines 7380–7400. ASCII is the address where the ASCII string being converted is stored, and NUM is the address where the binary integer value for the number resides. Six bytes are reserved for ASCII, five for digits (the maximum value that works correctly is 65535) and one for an end-of-line character, $9B. The input routine uses the EOL character to know when to stop converting digits, and the output routine adds an EOL so the result can be printed on the screen. COUNTER is just a one-byte work variable.
The first macro, ASC2INT, converts a numeric ASCII string into a two-byte binary integer. Parameter 1 is the address of the string to be converted (for example, an input buffer address), and parameter 2 is the address where the integer should be placed after conversion. This macro calls two subroutines that do most of the work, VALIDASC and ASC2INT (you can give a macro and a subroutine the same name). These and some other subroutines are found in Listing 2, which should be appended to your SUBS.LIB file using the line numbers shown.
The second macro, INT2ASC, converts a binary integer into a printable ASCII string. Parameter 1 is the address of the integer to convert, and parameter 2 is the address where the ASCII string should be placed. As you might expect, this macro calls subroutine INT2ASC, which is also found in Listing 2.
The ASCII string produced by the INT2ASC macro might not require all five characters reserved for it. For example, converting the number 43 to ASCII requires only two bytes for the character string. These digits are right-justified in the five-character ASCII string produced, so the result produced from INT2ASC would have the form 00043.
The LDGZERO macro (Lines 8110–8360 of Listing 1) can be used to convert any leading (that is, on the left) zeros into blanks for printing purposes. However, this macro does not left-justify the result in the five-character field, so if you printed the output ASCII string, you really would print three blanks in front of the 43. LDGZERO doesn’t call any subroutines. It takes two parameters. The first is the address of the string to be processed, and the second is the maximum number of digits to examine for leading zeros. Now let’s walk through a sample program and see how these conversion macros and subroutines do their stuff.
Please type in Listing 3, today’s sample program. Note the .INCLUDE directives in lines 160 and 650. If your MACRO.LIB and SUBS.LIB files are not on a RAM disk, change the drive designation from D8: to the correct drive number.
Almost every line in this example program is a macro call. This makes the source code much shorter and easier to understand than if we had to expand each procedure into its individual instructions. Also, notice my approach of using a macro in combination with one or more subroutines. The macro sets up the specifics of the particular operation, by virtue of addresses or values passed as parameters. I place the common details of the procedure into a subroutine wherever possible, using reserved pieces of RAM as general work variables. This method makes the resulting object code shorter and yet keeps the source code compact; a satisfactory compromise from my point of view.
Line 380 of Listing 3 makes sure we are in binary mode for arithmetic operations (more about this next month), and Line 390 clears the display screen. Line 400 prints a message prompting you to enter a number containing from one to five decimal digits. Lines 410–420 store your response at address ENTRY, a block of six bytes reserved in Line 540. Line 430 invokes the macro to convert this ASCII string to a two-byte binary integer stored at address INTEGER (defined in Line 550). If the carry flag is set upon completing the macro execution, we know an error has taken place, so Line 440 simply branches to the end of the program.
If we’ve ended up with a valid number at INTEGER, Line 450 adds 25 to that number. There’s nothing magical about this; it’s just a way to change the number you entered before I print it out again. Line 460 then converts that sum into an ASCII string at address ENTRY. Line 470 uses the LDGZERO macro to translate any leading zeros to blanks. You might try commenting Line 470 out and seeing what you get. Finally, Lines 480–520 print the resulting ASCII string on the screen and wait for you to press RESET. As usual, you can run this program from address $5000.
Let’s look at the ASCII to integer conversion in more detail. The first step is to make sure the user has entered a valid string of ASCII digits. Lines 7560–7640 in the ASC2INT macro definition in Listing 1 handle this chore. The loop simply looks through all the characters stored at the input buffer address (passed as parameter 1) until it finds an end-of-line character. Line 7600 stores each character in the appropriate position in the work variable called ASCII as the checking takes place. The subroutine VALIDASC is called to make sure the characters are all legitimate.
I apologize for bouncing you around the listings, but now we need to examine subroutine VALIDASC, starting at Line 2980 in Listing 2.Lines 3100–3160 pluck one character at a time out of the ASCII string and check for an EOL. If the first character found is an EOL, then the user just pressed RETURN without entering anything, so Line 3160 branches to an error routine at label INVALID (Line 3280). An error message is printed and the carry flag is set to indicate to the calling macro that an error took place.
The CHKASC routine beginning at Line 3190 tests whether each character has an ASCII value greater than $30 (decimal 0) and smaller than $3A (“:,” the first character past decimal 9). If not, control again branches to the INVALID routine. If the digit is okay. Lines 3240–3270 strip off the four high-order bits (thereby changing a $37 into a 7, for example), store the result back into the correct position in the ASCII string, and go get the next character.
This procedure underscores my contention that the largest portions of most good computer programs are devoted to input/output routines and error checking. If we knew our users would make only valid entries, our programs could be much shorter. Never make such a shaky assumption, though! Okay, now the string at address ASCII consists only of valid digits, from one to five of them. The next step is to convert these digits into a binary number. The ASC2INT subroutine (Lines 3390–3700 of Listing 2) does the trick.
Let’s contemplate the philosophy of number representation once again. A decimal number like 7239 actually means to multiply 1000 by 7, multiply 100 by 2, multiply 10 by 3, multiply 1 by 9, and add all these products together. To transform a bunch of characters from the ASCII string “7239” into the binary equivalent, we must perform precisely these same operations. The ASC2INT subroutine does the work, with the help of another subroutine called MULT10 (Lines 3740–4020 of Listmg 2). The MULT10 subroutine actually carries out the power of ten multiplications.
We begin with the most significant digit in the string to be converted. In the case of “7239,” this digit is a 7. Load the 7 into a byte and multiply by 10. This gives 70. Add the next digit in, yielding 72.
Multiply this result by 10 to get 720 and add in the next digit, giving 723. Multiply this result by 10 to get 7230 and add in the final digit, to wind up with 7239. Of course, this answer doesn’t look like 7239 in its binary representation. In binary it will look like 0001110001000111, and in hexadecimal it will be $1C47. There’s one final twist. The Atari stores two-byte integers in low-byte/high-byte format, so decimal 7239 is represented in two adjacent bytes of RAM in the Atari as hexadecimal 47 1C. And you thought this stuff was going to be simple!
Lines 3480–3510 of Listing 2 store a zero in the high-byte of our destination integer at address NUM and load the first (most significant) ASCII byte into the low-byte of NUM. If there’s only one ASCII character, our conversion is complete; Lines 3520–3550 check for this condition. If the second character is indeed the EOL, Lines 3560–3570 clear the carry flag (our signal to the calling macro that all is well) and return. Otherwise, we go on to the NEXTDIGIT label to continue processing.
The first step is to multiply this leftmost digit by 10. Subroutine MULT10 (Lines 3740–4020 of Listing 2) takes care of this for us. But how do we multiply using the 6502 processor? We’ve learned how to add and subtract using the ADC and SBC instructions. However, the 6502 contains no intrinsic multiplication or division instructions. You may recall that performing an ASL or Accumulator Shift Left operation is the same as multiplying the contents of a byte by two, and a LSR or Logical Shift Right operation divides the contents of a byte by two. Now we need to extend these concepts to handle a two-byte number and combine shift and add operations to perform integer multiplication.
Remember that multiplication is really just a bunch of sequential additions. The 6502 gives us an easy way to multiply by 2. To multiply some number by 10 we could multiply it by 2; multiply by 2 again (net result is multiply by 4); add the original number back to the result (net result is multiply by 5); and multiply by 2 once again, to give a net result of multiplying by 10. This is precisely what happens in subroutine MULT10.
One more point and then we’ll look at the code. Suppose our original number is decimal 150, stored in a single byte as $96. If we multiply that by 2 we get 300 in decimal terms ($012D), but the maximum value that fits in a single byte is 255. Whatever shall we do? When an overflow like this takes place, the carry flag in the processor status register is set, and the original byte contains the value of 300 minus the maximum 255, or 45 ($2D). This carry value must be added to the high-byte of our two-byte number, which also underwent a left shift operation during the multiply by 2 step. Fortunately, the 6502’s instruction set contains an instruction to handle all these details, the ROL or Rotate Left instruction.
Each bit shifts to the next higher order position (i.e., to the left). The carry flag shifts into bit 0, and bit 7 shifts into the carry flag. If the carry flag is cleared, ROL is the same as an ASL, simply multiplying the byte’s contents by 2. But if the carry is set, the ROL effectively multiplies by 2 and adds 1 to the original byte contents. Hence, a two-byte number can be multiplied by 2 simply by performing an ASL on the low-byte, followed by an ROL on the high-byte to account for the carry flag. I can’t believe you didn’t think of this solution immediately. (Wiegers’ First Law of Computing: Almost nothing you can do with a computer is difficult. Wiegers’ Second Law of Computing: Almost nothing you can do with a computer is obvious.)
In sum (pun intended), to multiply a two-byte binary integer by 2, you can simply perform an ASL operation on the low-byte, followed by an ROL operation on the high-byte.
As promised, you may now look at the MULT10 subroutine in Listing 2.Lines 3820–3830 store the high-byte of the original number on the stack so we can grab it for the necessary addition. Line 3840 places the original low-byte into the accumulator. Lines 3850–3860 multiply the original number by 2, and Lines 3870–3880 do it again. Lines 3890–3930 add in the original number, so now we’ve effectively multiplied it by 5. (Notice that all intermediate results are stored back in the original location at NUM and NUM+1.) Lines 3940–3950 complete the multiplication by 10. Lines 3960–4010 add in the next digit, as we discussed earlier.
The loop in Lines 3580–3640 of Listing 2 (subroutine ASC2INT) continue this monkey business until an EOL character is reached in the ASCII string, at which point the carry flag is cleared to indicate success and control returns to the calling ASC2INT macro.
We’re now back at Line 7660 of Listing 1, in the middle of the ASC2INT macro. If the carry flag is set, there was a problem with the conversion, and an appropriate error message (which lives at Lines 3680–3700 of Listing 2) is printed. Otherwise, the binary result in address NUM is moved to the location specified in the second parameter in the ASC2INT call (Lines 7670–7700), and we’re all done.
Whew! We finally got the simple number you entered stored in binary form. Now let’s see how to go the other way. Our sample program adds 25 to whatever number you enter, just to change it. The INT2ASC macro converts the number whose address is supplied in parameter 1 to a character string stored at the address specified in parameter 2. The INT2ASC macro is in Lines 7820–8070 of Listing 1.Lines 7940–7970 just copy the number to be transformed to our work space at address NUM.
Subroutine INT2ASC does all the work, creating a five-digit ASCII string of printable characters at address ASCII. Lines 7990–8050 copy this string, up through the EOL character, to the desired destination address in parameter 2. Subroutine INT2ASC is in Lines 4060–4590 of Listing 2. As with ASC2INT, this procedure is based on the fact that the position of a digit in a decimal number indicates the number of times a particular power of 10 must be added to zero to obtain that number. Algorithmically, it’s easier to work backwards, performing multiple subtractions. You keep subtracting a particular power of 10 (10, 100, 1000 or 10000) from the integer in question until you obtain a negative result. The number of subtractions you can do before going negative is equal to the value of the digit in a specific column (tens, hundreds, thousands, or ten thousands).
Here’s an illustration. Begin with the familiar integer 7239. Let’s set a counter equal to 0. How many times can you subtract 10000 from 7239 before you get a negative result? The answer is 0. Hence, the first of our five output digits (the ten thousands column) is 0. Next, how many times can you subtract 1000 from 7239 before obtaining a negative result? Seven, of course. Increment the counter for each successful subtraction. If your counter reaches 8 (representing 7239 minus 8000), the subtraction result is negative, and you know you’ve gone a digit too far. Add 1000 back in to get back to a positive number (7239−7000 = 239), and use the counter’s value of 7 for the second digit in the output ASCII string.
Continue this procedure until all powers often from 10000 to 10 have been done, and the remainder (the units column) is the fifth and final digit in the ASCII number. This is awkward to describe in words, but it actually makes some sense.
We’ll have to set bits 4 and 5 (ORA #$30) in our counter for the number of successful subtractions to convert it to the ASCII representation. If you walk through the commented INT2ASC subroutine you should understand this technique better. As you can see, it’s a pretty cumbersome way to turn a two-byte binary integer into a five-character ASCII string, but it’s just about the only way to do it. Lines 4500–4520 of the subroutine add an EOL to the end of the string so it can be printed properly using the PRINT macro, as done in the sample program of Listing 3.
The INT2ASC conversion routine produces a five character ASCII string, plus an EOL character. If the integer being converted is smaller than 10000 decimal, the first ASCII digit will be a zero. The number of leading zeros equals five minus the number of decimal digits in the number being converted. Often, you wish to print a number with just significant digits shown, that is, without any leading zeros appearing. The LDGZERO macro. Lines 8110–8360 of Listing 1, replaces leading zeros with spaces.
LDGZERO requires two parameters, the address of the string to be processed and the number of bytes to process before quitting. If a non-zero character (ASCII values $31–$39) is encountered, the routine terminates. The entire logic of this macro consists of looping through the bytes in the ASCII string replacing characters with ASCII code $30 (zero) with ASCII code $20 (blank or space character), until an end condition is satisfied.
As you see when you run the program in Listing 3, leading blanks do “print,” effectively shifting the significant digits to the right on the screen. You might want to write a macro or subroutine (or combination) to left-justify a string by simply removing leading zeros, rather than translating them into blanks. That’s not a hard exercise to do. While you’re at it, why not write a routine to right-justify a string in a field of some specified length? Don’t forget error checking. What would happen if you tried to right-justify a string of 11 characters in a field only 8 characters long? Oops.
I alluded to another numeric data storage format, binary-coded decimal. Next month we’ll take a close look at BCD and see some routines for converting ASCII strings to BCD and vice-versa.
Listing 1. Assembly listing. 7370 ; 7380 ASCII = $0690 7390 NUM = $0696 7400 COUNTER = $0698 7410 ; 7420 ;****************************** 7430 ; 7440 ;ASC2INT macro 7450 ; 7460 ;Usage: ASC2INT chars,number 7470 ; 7480 ;'chars' is address of ASCII 7490 ; string to convert,ending w/ EOL 7500 ;'number' is address of integer 7510 ; 7520 .MACRO ASC2INT 7530 .IF %0<>2 7540 .ERROR "Error in ASC2INT" 7550 .ELSE 7560 LDX #255 7570 @ASCLOOP 7580 INX 7590 LDA %1,X 7600 STA ASCII,X 7610 CMP #EOL 7620 BNE @ASCLOOP 7630 JSR VALIDASC 7640 BCS @DONE 7650 JSR ASC2INT 7660 BCS @ASCERROR 7670 LDA NUM 7680 STA %2 7690 LDA NUM+1 7700 STA %2+1 7710 CLC 7720 BCC @DONE 7730 @ASCERROR 7740 PRINT CONVERTMSG 7750 SEC 7760 @DONE 7770 .ENDIF 7780 .ENDM 7790 ; 7800 ;****************************** 7810 ; 7820 ;INT2ASC macro 7830 ; 7840 ;Usage: INT2ASC number,chars 7850 ; 7860 ;'number' is address of integer 7870 ;'chars' is address of resulting 7880 ; ASCII string, ending with EOL 7890 ; 7900 .MACRO INT2ASC 7910 .IF %0<>2 7920 .ERROR "Error in INT2ASC" 7930 .ELSE 7940 LDA %1 7950 STA NUM 7960 LDA %1+1 7970 STA NUM+1 7980 JSR INT2ASC 7990 LDX #255 8000 @INTLOOP 8010 INX 8020 LDA ASCII,X 8030 STA %2,X 8040 CMP #EOL 8050 BNE @INTLOOP 8060 .ENDIF 8070 .ENDM 8080 ; 8090 ;******************************* 8100 ; 8110 ;LDGZERO macro 8120 ; 8130 ;Usage: LDGZERO address,bytes 8140 ; 8150 ;'address' is beginning of ASCII 8160 ; string of digits 8170 ;'bytes' is max number of digits 8180 ; to check for leading zeros 8190 ; 8200 .MACRO LDGZERO 8210 .IF %0<>2 8220 .ERROR "Error in LDGZERO" 8230 .ELSE 8240 LDX #255 8250 @SUPZERO 8260 INX 8270 LDA %1,X 8280 CMP #$30 8290 BNE @LZDONE 8300 LDA #$20 8310 STA %1,X 8320 CPX #%2 8330 BNE @SUPZERO 8340 @LZDONE 8350 .ENDIF 8360 .ENDM
Listing 2. Assembly listing. 2950 ; 2960 ;******************************* 2970 ; 2980 ;subroutine VALIDASC 2990 ;called by ASC2INT,ASC2BCD macros 3000 ; 3010 ;makes sure all characters in 3020 ;string beginning at address 3030 ;ASCII are valid ASCII codes for 3040 ;numeric digits; looks until it 3050 ;hits an EOL; error message is 3060 ;printed and carry flag is set 3070 ;if an invalid char. is found 3080 ; 3090 VALIDASC 3100 LDX #0 3110 LOOPASC 3120 LDA ASCII,X ;get a char 3130 CMP #EOL ;EOL? 3140 BNE CHKASC ;no,go check it 3150 CPX #0 ;yes, 1st char? 3160 BEQ INVALID ;yes,null entry 3170 CLC ;no, all done 3180 RTS ;go back 3190 CHKASC 3200 CMP #$30 ;less than 0? 3210 BCC INVALID ;yes, no good 3220 CMP #$3A ;greater than 9? 3230 BCS INVALID ;yes, no good 3240 AND #$0F ;clear 4 hi bits 3250 STA ASCII,X ;save 4 lo bits 3260 INX ;ready for next 3270 BCC LOOPASC ;char 3280 INVALID 3290 PRINT ASCERRMSG 3300 SEC ;set carry to 3310 RTS ;show an error 3320 ; 3330 ASCERRMSG 3340 .BYTE "Non-numeric " 3350 .BYTE "character found",EOL 3360 ; 3370 ;****************************** 3380 ; 3390 ;subroutine ASC2INT 3400 ;called by ASC2INT macro 3410 ; 3420 ;converts string of ASCII digits 3430 ;at address ASCII to a 2-byte 3440 ;binary integer at address NUM; 3450 ;carry flag set if error 3460 ; 3470 ASC2INT 3480 LDA #0 ;zero hi byte 3490 STA NUM+1 3500 LDA ASCII ;get first char 3510 STA NUM 3520 LDX #1 ;next char EOL? 3530 LDA ASCII,X ;next char EOL? 3540 CMP #EOL 3550 BNE NEXTDIGIT ;no, go on 3560 CLC ;yes all done 3570 RTS 3580 NEXTDIGIT 3590 JSR MULT10 ;multiply by 10 3600 BCS ABORT ;carry set? error 3610 INX 3620 LDA ASCII,X ;next char EOL? 3630 CMP #EOL 3640 BNE NEXTDIGIT ;no, go on 3650 CLC 3660 ABORT 3670 RTS ;exit 3680 CONVERTMSG 3690 .BYTE "ASCII conversion" 3700 .BYTE " error...",EOL 3710 ; 3720 ;******************************* 3730 ; 3740 ;subroutine MULT10 3750 ;called by subroutine ASC2INT 3760 ; 3770 ;multiplies binary integer at 3780 ;address NUM and NUM+1 by 10 3790 ;and adds the next digit in 3800 ; 3810 MULT10 3820 LDA NUM+1 ;save high byte 3830 PHA 3840 LDA NUM ;get low byte 3850 ASL NUM ;multiply x 2 3860 ROL NUM+1 3870 ASL NUM ;times 2 again 3880 ROL NUM+1 3890 ADC NUM ;add to self to 3900 STA NUM ;effectively 3910 PLA ;multiply x 5 3920 ADC NUM+1 3930 STA NUM+1 3940 ASL NUM ;times 2 again, 3950 ROL NUM+1 ;total is now x10 3960 LDA ASCII,X ;add in next char 3970 ADC NUM 3980 STA NUM 3990 LDA #0 ;adding 0 to high 4000 ADC NUM+1 ;byte just pulls 4010 STA NUM+1 ;in carry value 4020 RTS 4030 ; 4040 ;******************************* 4050 ; 4060 ;subroutine INT2ASC 4070 ;called by INT2ASC macro 4080 ; 4090 ;converts a 2-byte binary integer 4100 ;at address NUM to a string of 4110 ;ASCII digits at address ASCII 4120 ; 4130 INT2ASC 4140 LDY #0 ;pointer to table 4150 STY COUNTER ;of powers of 10 4160 NEXTDIGIT2 4170 LDX #0 ;digit counter 4180 SUBLOOP 4190 LDA NUM ;get low byte 4200 SEC ;subtract lo byte 4210 SBC DECTABLE,Y ;of current 4220 STA NUM ;power of 10 4230 LDA NUM+1 ;now subtract hi 4240 INY ;byte of current 4250 SBC DECTABLE,Y ;power of 10 4260 BCC ADDITBACK ;if neg,restore 4270 STA NUM+1 ;save hi byte 4280 INX ;digit counter 4290 DEY ;point to lo-byte 4300 CLC ;of current power 4310 BCC SUBLOOP ;of 10 again 4320 ADDITBACK 4330 DEY ;point to lo byte 4340 LDA NUM ;add lo byte of 4350 ADC DECTABLE,Y ;power of 10 4360 STA NUM ;back in 4370 TXA ;convert digit 4380 ORA #$30 ;counter to ASCII 4390 LDX COUNTER ;and store at 4400 STA ASCII,X ;next position 4410 INC COUNTER 4420 INY ;point to next 4430 INY ;power of 10 4440 CPY #8 ;at end of table? 4450 BCC NEXTDIGIT2 ;no, go on 4460 LDA NUM ;get units column 4470 ORA #$30 ;convert to ASCII 4480 LDX COUNTER ;store it 4490 STA ASCII,X 4500 INX 4510 LDA #EOL ;add an EOL in 4520 STA ASCII,X ;next position 4530 RTS ;all done 4540 ; 4550 DECTABLE 4560 .WORD 10000 4570 .WORD 1000 4580 .WORD 100 4590 .WORD 10
0100 ;Example 1. Interconverting ASCII 0110 ;strings and 2-byte integers 0120 ; 0130 ;by Karl E. Wiegers 0140 ; 0150 .OPT NO LIST,OBJ 0160 .INCLUDE #D8:MACRO.LIB 0170 ; 0180 ;------------------------------- 0190 ; PROGRAM STARTS HERE 0200 ; 0210 ;You'll be prompted to enter a 0220 ;number with 1-5 digits. This 0230 ;is stored at address ENTRY. 0240 ;The binary integer produced 0250 ;is stored at address INTEGER. 0260 ;If the number is too large, 0270 ;missing (null entry) or has non- 0280 ;digits in it, you'll get an 0290 ;error message. 25 will be added 0300 ;to the value you entered, and 0310 ;the result will be converted to 0320 ;ASCII and printed on the screen 0330 ; 0340 ;------------------------------- 0350 ; 0360 *= $5000 0370 ; 0380 CLD 0390 JSR CLS 0400 PRINT PROMPT 0410 POSITION 5,5 0420 INPUT 0,ENTRY 0430 ASC2INT ENTRY,INTEGER 0440 BCS END 0450 ADD INTEGER,25 0460 INT2ASC INTEGER,ENTRY 0470 LDGZERO ENTRY,5 0480 POSITION 2,8 0490 PRINT AFTER 0500 POSITION 5,10 0510 PRINT ENTRY 0520 END JMP END 0530 ; 0540 ENTRY .DS 6 0550 INTEGER .DS 2 0560 ; 0570 PROMPT 0580 .BYTE "Enter a number " 0590 .BYTE "with 1-5 digits:",EOL 0600 AFTER 0610 .BYTE "After adding 25:",EOL 0620 ; 0630 ;------------------------------- 0640 ; 0650 .INCLUDE #D8:SUBS.LIB
A.N.A.L.O.G. ISSUE 64 / SEPTEMBER 1988 / PAGE 45
The rather cryptic title of this article can be translated as “Binary-Coded Decimal and You.” Last month we discussed some routines for interconverting strings of numeric ASCII characters and their binary representations as integers.
This time we tackle another commonly used method for storing numbers in computers: binary-coded decimal, or BCD for short. After I explain the BCD representation, well see how to change an ASCII string into a BCD storage format. I also have some examples of how to do arithmetic with numbers stored in BCD from, and some traps you can fall into if you don’t keep your wits about you.
Look at the bit patterns for digits 0–9 shown in Table 1. Notice that they range form 0000 to 1001. The point here is that we need only four bits to represent any one of the ten decimal digits. You no doubt recall that the standard byte contains a grand total of eight bits. If we think of subdividing a byte, we could make a duplex with each unit containing four bits. A 4-bit unit is sometimes referred to as a “nybble” (a small byte-get it?). I’ve seen it spelled more conventionally as nibble, but I’ll use the “y” so the non-computer whizzes who read this will think I’m talking about something really obscure and hence important.
Since we can store the binary representation of any one decimal digit in each nybble, the largest value that could be stored in a single byte this way is 99. This corresponds to a bit pattern of 1001 in each nybble; the entire storage contains 10011001. This two-digit-per-byte data storage method is the infamous binary-coded decimal.
There are two ways to interpret a bit pattern of 10011001. In pure hexadecimal, it is $99, which corresponds to decimal 153. But if we think of it as two decimal digits, that bit pattern means decimal 99. We need some way to tell the computer which meaning we have in mind at any given time.
Doing arithmetic on BCD numbers is different from processing binary numbers also. In binary, adding 1 to a byte containing the value 00001001 ($09) produces the value 00001010 ($0A). In BCD, adding 1 to 00001001 (09) would result in 00010000 (10). Similarly, adding 1 to 10011001 in hex terms produces 10011010 ($99 to $9A). But in BCD we should wind up with 00000000 in this byte and the carry flag set to indicate that a higher order byte must be incremented. This is a fancy way of saying that 99 plus 1 equals 100.
The 6502 microprocessor in the Atari 8-bit computers can perform either decimal or binary arithmetic, thereby handling either of the two conditions from the previous paragraph. Bit 3 in the processor status register controls whether decimal mode (bit set) or binary mode (bit cleared) is selected. So far, we’ve performed only binary arithmetic operations, so most of our programs have begun with a CLD (CLear Decimal mode) instruction. To choose decimal mode, use the SED (SEt Decimal mode) instruction. You will get very strange results if the decimal flag isn’t set the way you think it is; so it’s always a good idea to explicitly select the desired mode.
Actually, it’s a little worse than “very strange.” If you try to do things like print to the screen when the decimal flag is set, you can wind up in computer never-never land, with a coldstart being the only way back. Always clear decimal mode with the CLD instruction when you’ve finished your decimal arithmetic operations.
Today’s example is similar in format to last month’s discussion of how to interconnect ASCII and binary storage formats for integers. Listing 1 contains two macros in MAC/65 format that should be appended to your MACRO.LIB file using the line numbers shown. Similarly, Listing 2 contains a pair of subroutines called by these macros; append Listing 2 to your SUBS.LIB file.
My MACRO.LIB file is now an even 100 single-density sectors long. If you’re using a RAM disk for assemblies, this is only a minor nuisance. However, reading a file that large from a physical disk each time you do an assembly takes a long time, and it doesn’t do your disk drive any good. You may want to think about splitting the MACRO.LIB file into several smaller library files, perhaps grouped logically by function. You can do this any way you like, and just .INCLUDE the ones you need for your current project. Be sure to keep the equates needed by the macros accessible (and unduplicated). In fact, you might just collect all the equates into a separate EQUATES.LIB file. I’ll leave the details of the MACRO.LIB dissection to each of you.
The two new macros, and their corresponding subroutines, are named ASC2BCD and BCD2ASC. These complement the ASC2INT and INT2ASC routines from the previous Boot Camp. ASC2BCD takes a string of up to six numeric characters and converts it into a three-byte BCD number. Not surprisingly, BCD2ASC takes a three-byte BCD number and transforms it right back into a printable ASCII string. The macros themselves do some error checking and use parameters to handle ASCII strings and BCD numbers stored at any address, while their subroutine partners do most of the real work.
We’ll start at the beginning. Please turn your attention to the ASC2BCD macro in Listing 1. ASC2BCD expects two parameters, the address of the ASCII string to convert, and the address where the resulting three-byte BCD number is to be stashed. An error message appears if the number of parameters is not two (Lines 8130–8140).
This macro begins just like the ASC2INT macro from last time. Lines 8160–8220 copy the characters from the input string at the address specified in parameter % 1 to a work address labeled ASCII. The ASCII address was defined in Listing 1 from last month as $0690. The input string must terminate with an end-of-line character ($9B). In our sample program today, the numeric string to convert is read from the keyboard using our INPUT macro, which automatically guarantees that an EOL character will be present.
Line 8230 calls the VALIDASC subroutine from last month, which makes sure that all the characters in the string are in fact digits in the range 0–9. If not, the carry flag is set in the subroutine to indicate an error. Line 8240 handles this condition by simply short-circuiting around the rest of the macro code. The main program that invoked this macro handles the error condition, as we’ll see a little later. I don’t have any provision for handling negative numbers.
Subroutine VALIDASC retains only the lower four bits from the ASCII character. That is, if you entered the digit “7” at the keyboard, the ASCII value is $37, and VALIDASC changes this back into a plain “7” after confirming that it is a legal entry.
After the conversion, the BCD number resides at a work location called NUM, defined as $0696 last month. Lines 8280–8350 copy the BCD result to the desired output address specified in parameter % 2.
After all this monkey business, we wind up with a string of characters at address ASCII which looks exactly like what we typed at the keyboard. Let’s pretend we typed the number “7239.” Our goal is to convert the input string, now typed in five bytes like this (showing both nybbles in each byte):
07 02 03 09 9B
into BCD format stored in three bytes like this:
00 72 39
Notice that this numeric storage format is different from the low-byte/high-byte format used for binary integers.
Line 8250 of Listing 1 calls the ASC2BCD subroutine in Listing 2 to handle the details of the conversion. Now please direct your attention to Listing 2.
First we need to know how many input digits to convert to BCD. When we get to today’s example in Listing 3, you’ll see that the value we need was stored in a work address labeled CHARCTR. The value in CHARCTR includes the EOL character, so it is one larger than the actual number of digits in the input number. Lines 4720–4740 of Listing 2 set up the Xregister as an offset for the characters for the BCD bytes. Here’s the conversion plan.
We’ll begin by zeroing the three bytes where the BCD result will be stored. Lines 4760–4790 handle this task. The conversion step will begin with the least significant (rightmost) digit in the entered ASCII string. This number becomes the low-order nybble in the least significant (rightmost) BCD byte (Lines 4810–4820).
If the ASCII string to be converted contains an odd number of digits, the highorder nybble of one of the the BCD bytes will remain zero. This should be apparent to you. Line 4830 in Listing 2 points to the next ASCII character, which is destined to go into the high-order nybble of the current BCD byte. Line 4840 checks to see if we’ve reached the end of the ASCII string yet. If not, fetch the contents of the next ASCII byte (Line 4850). Remember that we’ve already changed this from the original ASCII value to the value of the digit itself (e.g., $37 was changed to 7).
Lines 4860–4890 shift this number four bits to the left, thereby relocating it to the high-order nybble of the accumulator. Line 4900 combines the result with the low-order nybble from the previous ASCII digit, and the completed BCD byte (now containing two digits) is stored back where it belongs (Line 4910). Lines 4920–4960 check to see if were done with the ASCII string yet and loop back to continue if not.
This discussion is a little confusing. You might find it illuminating to use your debugger to trace through a stepwise processing of a sample input number after entering Listing 3, and see how the ASC2BCD subroutine does its thing.
I know you’re eager to dive into the sample program for today, but I’m going to hold you back a little longer. The sample program goes through a bunch of BCD arithmetic examples, which we’ll get to in a moment. But while the details of the ASCII-to-BCD conversion are fresh in your mind, I want to tackle the reverse process. Bear with me.
I’m sure you can figure out what we must do to change a number stored in BCD format into a printable ASCII string. There are two basic steps. First, split the high and low nybbles of each byte in the BCD number into separate bytes in the output string. And second, convert the digits into their corresponding ASCII values. As an additional cosmetic nicety, we’ll also convert any leading zeros to leading blanks.
The BCD2ASC macro begins at line 8540 of Listing 1, and the complementary BCD2ASC subroutine starts at line 5150 of Listing 2. The macro again requires two parameters, the address of the BCD number to be converted and the address where the resulting ASCII string should be stored. Three bytes at address NUM and six bytes at address ASCII are again used as work locations. Lines 8580–8630 of the macro copy the contents of the BCD number into work location NUM. The subroutine BCD2ASC is then called. Lines 8650–8710 then copy the resulting string from address ASCII into the address specified in parameter %2. Lines 8720–8740 tack an EOL character on the end so the string can be printed.
This time the conversion proceeds from left to right (high order to low order). In the BCD2ASC subroutine, I’ve set aside one byte (called ZEROBLANK, defined in Line 5570 of Listing 2) to indicate whether a zero digit is to be represented as a zero ASCII character ($30) or as a blank ($20). ZEROBLANK is initially set to $20 in Lines 5160–5170 so as to print leading zeros as blanks. However, as soon as a non-zero digit is encountered, ZEROBLANK is set to $30 so that zeros in the middle of the number appear properly.
Line 5210 gets the first (leftmost) BCD digit, which is saved temporarily on the program stack (Line 5220). (The Xregister is used as an offset into the BCD number, and the Y-register as an offset into the ASCII string.) The high nybble is moved into the low nybble with a series of four right shifts; this is the opposite of the four ASLs we used in the ASC2BCD process. If the result is a zero, Lines 5290–5300 store the current value of ZEROBLANK into the next position in the output string. If the digit is not a zero, Lines 5330–5360 convert the digit to ASCII by adding $30 to it, store the result in the output string, and set the value of ZEROBLANK to an ASCII zero.
Lines 5380–5400 point to the next output character, retrieve the BCD byte, and strip off the four most significant bits. This leaves just the low nybble, which is the second of the two digits in the BCD byte. Then the same activities are performed as for the first digit, depending on whether the digit is a zero or not (Lines 5410–5440). After processing all three BCD bytes, we wind up with a printable ASCII string. Voila.
Now for the interesting part. The 6502 microprocessor knows how to do arithmetic on numbers stored in both binary and decimal modes. There are a few differences you should keep in mind, and Listing 3 will help you out.
The program in Listing 3 asks you to enter a number up to six digits long, verifies that you entered only digits, converts the string of ASCII characters to a threebyte BCD number, and performs some representative arithmetic operations in both BCD and decimal mode. The results from each operation are printed on the screen in a little table. Let’s walk though Listing 3 now.
Line 160 pulls in the macros from our library file. Be sure to change this statement if you are using a real disk drive instead of the D8: RAM disk, or if you segmented the MACRO.LIB file as I suggested earlier. Some work variables are defined in Lines 280–310. BCD is the home of the BCD number. CHARCTR contains the number of ASCII characters you entered (including the EOL character). INBUF is an input buffer for the number you enter, and OUTBUF is an output buffer for the printable ASCII result.
As usual, the executable code begins at address $5000. Lines 520–590 clear decimal mode (for now), clear the screen, prompt you to enter a number, store the number at INBUF, and store the number of characters you entered at CHARCTR. Lines 650–760 set up the column and row headings for the output table; the text strings to be printed are stored in Lines 2350–2480. Line 830 converts the input string in INBUF to BCD representation at address BCD. Well have to repeat this after each sample calculation to make sure the BCD number starts out the same way every time. If there’s an error in the BCD conversion, the carry flag will be set and the program terminates due to Lines 840–850.
The program has four sample calculations: increment the lowest BCD byte; add decimal 25 to the BCD number; add hex 25 to the BCD number; and add the contents of the middle BCD byte to the whole BCD number. Each calculation is done in both binary and decimal modes. I suggest you try this program with several sample entries, to see what happens. Press return after the output appears to try another number. Four interesting numbers to try are 0, 1234, 999 and 7239. On the off chance that you don’t really want to spend the time typing in the listings, I’ve included tables to the output you would see for each of these test cases (Tables 2–5).
The simplest arithmetic operation you can do in 6502 assembly language is to increment the contents of a byte. The opcode for this is, of course, INC. Lines 980–1030 increment the least significant byte of the BCD number (BCD$2) in binary mode, and Lines 1070–1130 do the same in decimal mode. Notice the SED instruction in Line 1070 to set the decimal mode flag. Line 1090 clears the flag immediately after the arithmetic is done to avoid problems with subsequent operations. After each operation, the resulting value at address BCD is converted to ASCII at address OUTBUF and printed on the screen.
The first line of Table 2 shows that INC works just fine when the target number is 0, giving the expected result of 1 in each case. Table 3 shows that INC works fine for the number 1234 also. But wait! An input value of 999 gives the bizarre result of 99:. Something similar happens in Table 5 with 7239. How can this be?
Well, for Table 4, BCD + 2 contains “99;” which is incremented to “9A.” Converting to ASCII gives two bytes, containing $39 (prints as a 9) and $3A (prints as a colon, :). Hmmmm. We really wanted the BCD number “99” to increment to “00;” setting the carry flag to indicate that the next higher order byte should also be incremented. It appears that the INC instruction has the same effect in decimal mode as it does in binary. Moral: Don’t use INC to add 1 to a BCD number. Instead, go through the cumbersome motions of actually adding 1.
Okay, so let’s add something to a BCD number. Lines 1210–1330 add an immediate value of 25 decimal to the BCD number you entered in binary mode. Lines 1370–1490 do the same in decimal mode. These routines use a subroutine called INCREMBCD (Lines 2630–2750 of Listing 3) to handle the case where the carry flag is set after the addition, so that the next higher order byte must be incremented (by adding 1 to it, of course). This in itself might reset the carry flag, so that the highest order BCD byte also has to be incremented. These operations should make sense to you by now.
Let’s do it. Now look at the second output line in Tables 2–5 to see how our sample numbers respond. A problem is immediately apparent in Table 2. Adding 25 to 0 gave 19, not 25. Why? Well, the hex equivalent of 25 is $19. Last month we added decimal 25 (using an ADC #25 instruction) to a number stored as binary, went through the binary-to-ASCII conversion, and got the right answer. But we’ve scrambled our conventions here. We added a decimal number (stored internally as hex, of course) to a BCD number, using binary mode, and converted the presumed BCD result to ASCII for printing. It’s not surprising that the wrong result shows up.
The same thing happens with all the other input numbers. The weird characters in Tables 3 and 4 appear again because the addition results have gone out of the legal 0–9 BCD range, into values which print as other ASCII characters. Check out your table of hex codes for ASCII characters if you don’t believe me.
The correct method for adding an immediate value to a stored BCD number is to use the desired decimal digits for the immediate number, but tell the computer that it’s a hex number. That is, to add decimal 25 to a BCD number, use an ADC #$25 instruction. The third output line in each table shows that this approach does indeed produce the result of adding 25. The initial entry of 1234 fortuitously gives the correct answer in either binary or decimal modes (Table 3). However, an entry of 999 works right only in decimal mode (Table 4). In binary mode, the computer sets the carry flag when the byte’s contents exceed $FF, not $99 as it does in decimal.
The final line of each table shows the result of adding the middle byte of the BCD number to the entire number, just to show how things work when you add together two stored values. Table 2 correctly shows no output for this line, since 0 + 0 = 0, which we print as all blanks. For the other three cases, the correct answer is always obtained when the decimal flag is set, and only in some cases (e.g., Table 4) when in binary mode.
Now you know more about binary-coded decimal than you ever dreamed possible. But why should you care? Burrow back through your archives to the yellowed, brittle pages of ANALOG #43. The Boot Camp in that issue discussed floating point numbers and mathematics in the Atari. Floating point numbers use the BCD representation as a compact way to stuff several digits of precision into a minimum number (six) of bytes. A special notation is used to keep track of the decimal point, exponent and negative sign in floating point numbers. BCD turns out to be a pretty efficient storage format for base-10-type numbers, and most computers use some form of BCD for floating point storage.
You may recall that the main alternative character-coding method in common computer use is called EBCDIC (pronounced ebb-see-dick), used mainly by IBM mainframe computers. That acronym stands for “Extended Binary-Coded Decimal Interchange Code.” See? You can run, but you just can’t hide from binary-coded decimal.
There’s another advantage. In today’s example program, we converted BCD numbers to ASCII strings and printed them on the screen. However, you could also take each BCD digit, convert it to the Atari internal character code by ANDing it with $10 (as opposed to $30, which converts it to ASCII), and poke the result directly into the screen RAM for the current display. This is simpler and faster than printing on the screen, and the visual result is the same. A good example of this technique can be found in James Hague’s Streamliner from ANALOG #56. See the right column of page 37 in that issue.
I promise: no more hard-core computing for awhile. We’ll get back to some graphics (warm know how to draw circles?), sound effects and real-time clocks (how about a metronome program?), and maybe even the kernel of an adventure program; a simple vocabulary parser. Stay tuned.
Table 1. ASCII Codes for
0 $30 0000 1 $31 0001 2 $32 0010 3 $33 0011 4 $34 0100 5 $35 0101 6 $36 0110 7 $37 0111 8 $38 1000 9 $39 1001
Table 2. Sample Output From Input
Number of 0
Binary Mode Decimal Mode INC 1 1 Add 25 19 19 Add $25 25 25 Add 2nd
Table 3. Sample Output From Input
Number of 1234
Binary Mode Decimal Mode INC 1235 1235 Add 25 124= 1253 Add $25 1259 1259 Add 2nd
Table 4. Sample Output From Input
Number of 999
Binary Mode Decimal Mode INC 99: 99: Add 25 9;2 1018 Add $25 9; > 1024 Add 2nd
Table 5. Sample Output From Input
Number of 7239
Binary Mode Decimal Mode INC 723: 723: Add 25 7252 7258 Add $25 725> 7264 Add 2nd
Listing 1. Assembly listing. 8010 ; 8020 ;******************************* 8030 ; 8040 ;ASC2BCD macro 8050 ; 8060 ;Usage: ASC2BCD chars,number 8070 ;'chars' is address of ASCII 8080 ; string to convert,ending w/ EOL 8090 ;'number' is address of BCD 8100 ; representation of the string 8110 ; 8120 .MACRO ASC2BCD 8130 .IF X0<>2 8140 .ERROR "Error in ASC2BCD" 8150 .ELSE 8160 LDX #255 8170 @ASCLOOP2 8180 INX 8190 LDA %1,X 8200 STA ASCII,X 8210 CMP #EOL 8220 BNE @ASCLOOP2 8230 JSR VALIDASC 8240 BCS @DONE2 8250 JSR ASC2BCD 8260 BCS @BCDERROR 8270 LDX #0 8280 @ASCLOOP3 8290 LDA NUM,X 8300 STA %2,X 8310 INX 8320 CPX #3 8330 BNE @ASCLOOP3 8340 CLC 8350 BCC @DONE2 8360 @BCDERROR 8370 PRINT CONVERTMSG2 8380 SEC 8390 @DONE2 8400 .ENDIF 8410 .ENDM 8420 ; 8430 ;*************************** 8440 ; 8450 ;BCD2ASC macro 8460 ; 8470 ;Usage: BCD2ASC number,chars 8480 ;'number' is address of BCD 8490 ; number to convert 8510 ;'chars' is address of resulting 8520 ; ASCII string, ending with EOL 8530 ; 8540 .MACRO BCD2ASC 8550 .IF %0<>2 8560 .ERROR "Error in BCD2ASC" 8570 .ELSE 8580 LDA %1 8590 STA NUM 8600 LDA %1+1 8610 STA NUM+1 8620 LDA %1+2 8630 STA NUM+2 8640 JSR BCD2ASC 8650 LDX #255 8660 @BCDLOOP 8670 INX 8680 LDA ASCII,X 8690 STA %2,X 8700 CPX #5 8710 BNE @BCDLOOP 8720 INX 8730 LDA #EOL 8740 STA %2,X 8750 .ENDIF 8760 .ENDM
Listing 2. Assembly listing. 4600 ; 4610 ;******************************* 4620 ; 4630 ;subroutine ASC2BCD 4640 ;called by ASC2BCD macro 4650 ; 4660 ;converts string of ASCII digits 4670 ;at address ASCII to a 3-byte 4680 ;binary-coded decimal represen- 4690 ;tation at address NUM 4700 ; 4710 ASC2BCD 4720 LDX CHARCTR ;how many chars 4730 DEX ;to convert? 4740 DEX 4750 LDY #2 4760 LDA #0 ;zero 3 bytes 4770 STA NUM ;where BCD value 4780 STA NUM+1 ;will go 4790 STA NUM+2 4800 NXTDIG 4810 LDA ASCII,X ;get next char 4820 STA NUM,Y ;low BCD digit 4830 DEX ;point to next 4840 BMI BCDDONE ;done yet? 4850 LDA ASCII,X ;get new char 4860 ASL A ;shift into 4870 ASL A ;high nybble 4880 ASL A 4890 ASL A 4900 ORA NUM,Y ;becomes high 4910 STA NUM,Y ;BCD digit 4920 DEX ;point to prev. 4930 BMI BCDDONE ;done yet? 4940 DEY ;point to next 4950 CLC ;BCD digit 4960 BCC NXTDIG ;go get it 4970 BCDDONE 4980 CLC ;all done, so 4990 RTS ;leave 5000 CONVERTMSG2 5010 .BYTE "ASCII to BCD cone 5020 .BYTE "version error",EOL 5030 ; 5040 ;******************************* 5050 ; 5060 ;subroutine BCD2ASC 5070 ;called by BCD2ASC macro 5080 ; 5090 ;converts 3-byte BCD number at 5100 ;address NUM to a 6-byte ASCII 5110 ;string at address ASCII 5120 ;leading zeros are changed to 5130 ;leading blanks 5140 ; 5150 BCD2ASC 5160 LDA #$20 ;init leading 5170 STA ZEROBLANK ;char to blank 5180 LDX #0 ;pointer to digit 5190 LDY #0 ;pointer to char 5200 NXTDIG2 5210 LDA NUM,X ;get 1st digit 5220 PHA ;stash on stack 5230 CLC 5240 LSR A ;move high nybble 5250 LSR A ;into low nybble 5260 LSR A 5270 LSR A 5280 BNE NONZERO1 ;equal to 0? 5290 LDA ZEROBLANK ;yes, set to 5300 STA ASCII,Y ;leading char 5310 BPL DOLOW ;do low half 5320 NONZERO1 5330 ORA #$30 ;change to ASCII 5340 STA ASCII,Y ;add to string 5350 LDA #$30 ;set leading 5360 STA ZEROBLANK ;char to '0' 5370 DOLOW 5380 INY ;aim at next char 5390 PLA ;get BCD digit 5400 AND #$0F ;keep low nybble 5410 BNE NONZERO2 ;equal to 0? 5420 LDA ZEROBLANK ;yes, set to 5430 STA ASCII,Y ;leading char 5440 BPL BCDDONE2 ;all done 5450 NONZERO2 5460 ORA #$30 ;conver to ASCII 5470 STA ASCII,Y ;add to string 5480 LDA #$30 ;set leading char 5490 STA ZEROBLANK ;to zero 5500 BCDDONE2 5510 INY ;point to next 5520 INX ;digit and char 5530 CPX #3 ;done 3 digits? 5540 BNE NXTDIG2 ;no, continue 5550 CLC ;yes, all done 5560 RTS ;exit 5570 ZEROBLANK .DS 1
Listing 3. Assembly listing. 0100 ;Example 1. Interconverting ASCII 0110 ;strings and BCD numbers 0120 ; 0130 ;by Karl E. Wiegers 0140 ; 0150 .OPT NO LIST,OBJ 0160 .INCLUDE #D8:MACRO.LIB 0170 ; 0180 ;------------------------------- 0190 ; 0200 ;store some work variables at 0210 ;$4FE0 so you can examine them 0220 ;if you like 0230 ; 0240 ;------------------------------- 0250 ; 0260 *= $4FE0 0270 ; 0280 BCD .DS 3 0290 CHARCTR .DS 1 0300 INBUF .DS 7 0310 OUTBUF .DS 7 0320 ; 0330 ;------------------------------- 0340 ; 0350 ; PROGRAM STARTS HERE 0360 ; 0370 ;You'll be prompted to enter a 0380 ;number with 1-6 digits. This 0390 ;is stored at address INBUF. 0400 ;The BCD number produced is 0410 ;stored in 3 bytes starting at 0420 ;address BCD. Then several 0430 ;arithmetic operations are done 0440 ;in both binary and decimal mode, 0450 ;and a table of results is 0460 ;printed out. 0470 ; 0480 ;------------------------------- 0490 ; 0500 *= $5000 0510 ; 0515 START 0520 CLD ;binary mode! 0530 JSR CLS ;clear screen 0540 PRINT PROMPT ;get input 0550 POSITION 2,2 ;number 0560 INPUT 0,INBUF 0570 LDX #$00 ;get number of 0580 LDA ICBLL,X ;chars entered 0590 STA CHARCTR 0600 ; 0610 ;------------------------------ 0620 ; lay out the table of results 0630 ;------------------------------ 0640 ; 0650 POSITION 12,5 0660 PRINT TITLE 0670 POSITION 12,6 0680 PRINT HYPHENS 0690 POSITION 2,8 0700 PRINT INCRE 0710 POSITION 2,10 0720 PRINT DEC25 0730 POSITION 2,12 0740 PRINT HEX25 0750 POSITION 2,14 0760 PRINT ADDBYTE 0770 ; 0780 ;------------------------------ 0790 ;convert string to BCD, abort if 0800 ;have a conversion problem 0810 ;------------------------------ 0820 ; 0830 ASC2BCD INBUF,BCD 0840 BCC NOPROBLEM 0850 JMP END 0860 ; 0870 ;------------------------------ 0880 ;First line: increment the BCD 0890 ;number in binary and decimal 0900 ;modes; be sure to set back to 0910 ;binary before doing anything 0920 ;else! 0930 ;reconvert from input string to 0940 ;BCD after each operation 0950 ;------------------------------ 0960 ; 0970 NOPROBLEM 0980 CLD 0990 INC BCD+2 1000 BCD2ASC BCD,OUTBUF 1010 POSITION 14,8 1020 PRINT OUTSUF 1030 ASC2BCD INBUF,BCD 1040 ; 1050 ;increment in decimal mode 1060 ; 1070 SED 1080 INC BCD+2 1090 CLD 1100 BCD2ASC BCD,OUTBUF 1110 POSITION 29,8 1120 PRINT OUTBUF 1130 ASC2BCD INBUF,BCD 1140 ; 1150 ;------------------------------ 1160 ;Second line: add 25 to the BCD 1170 ;number in binary and decimal 1180 ;modes 1190 ;------------------------------ 1200 ; 1210 CLD 1220 CLC 1230 LDA BCD+2 1240 ADC #25 1250 STA BCD+2 1260 BCC NOINC1 1270 JSR INCREMBCD 1280 NOINC1 1290 CLD 1300 BCD2ASC BCD,OUTBUF 1310 POSITION 14,10 1320 PRINT OUTBUF 1330 ASC2BCD INBUF,BCD 1340 ; 1350 ;add 25 in decimal mode 1360 ; 1370 SED 1380 CLC 1390 LDA BCD+2 1400 ADC #25 1410 STA BCD+2 1420 BCC NOINC2 1430 JSR INCREMBCD 1440 NOINC2 1450 CLD 1460 BCD2ASC BCD,OUTBUF 1470 POSITION 29,10 1480 PRINT OUTBUF 1490 ASC2BCD INBUF,BCD 1500 ; 1510 ;------------------------------- 1520 ;Third line: add hexadecimal 25 1530 ;to the BCD number in binary and 1540 ;binary modes 1550 ;------------------------------- 1560 ; 1570 CLD 1580 CLC 1590 LDA BCD+2 1600 ADC #$25 1610 STA BCD+2 1620 BCC NOINC3 1630 JSR INCREMBCD 1640 NOINC3 1650 CLD 1660 BCD2ASC BCD,OUTBUF 1670 POSITION 14,12 1680 PRINT OUTBUF 1690 ASC2BCD INBUF,BCD 1700 ; 1710 ;add $25 in decimal mode 1720 ; 1730 SED 1740 CLC 1750 LDA BCD+2 1760 ADC #$25 1770 STA BCD+2 1780 BCC NOINC4 1790 JSR INCREMBCD 1800 NOINC4 1810 CLD 1820 BCD2ASC BCD,OUTBUF 1830 POSITION 29,12 1840 PRINT OUTBUF 1850 ASC2BCD INBUF,BCD 1860 ; 1870 ;------------------------------- 1880 ;Fourth line: add second byte 1890 ;of BCD number to the entire 1900 ;number, in binary and decimal 1910 ;modes. If number was 1-2 digits 1920 ;long, will just add zero 1930 ;------------------------------- 1940 ; 1950 ;ADD 2ND BYTE TO 3RD - BINARY 1960 CLD 1970 CLC 1980 LDA BCD+1 1990 ADC BCD+2 2000 STA BCD+2 2010 BCC NOINC5 2020 JSR INCREMBCD 2030 NOINC5 2040 CLD 2050 BCD2ASC BCD,OUTBUF 2060 POSITION 14,14 2070 PRINT OUTBUF 2080 ASC2BCD INBUF,BCD 2090 ; 2100 ;add 2nd byte to total - decimal 2110 ; 2120 SED 2130 CLC 2140 LDA BCD+1 2150 ADC BCD+2 2160 STA BCD+2 2170 BCC NOINC6 2180 JSR INCREMBCD 2190 NOINC6 2200 CLD 2210 BCD2ASC BCD,OUTBUF 2220 POSITION 29,14 2230 PRINT OUTBUF 2240 END 2244 INPUT 0,INBUF 2248 JMP START 2250 ; 2260 ;------------------------------ 2270 ;text lines for prompt and for 2280 ;output table 2290 ;------------------------------ 2300 ; 2310 PROMPT 2320 .BYTE "Enter a number " 2330 .BYTE "up to 6 digits " 2340 .BYTE "long:",EOL 2350 TITLE 2360 .BYTE "Binary Mode " 2370 .BYTE "Decimal Mode",EOL 2380 HYPHENS 2390 .BYTE "----------- " 2400 .BYTE "------------",EOL 2410 INCRE 2420 .BYTE "INC",EOL 2430 DEC25 2440 .BYTE "Add 25",EOL 2450 HEX25 2460 .BYTE "Add $25",EOL 2470 ADDBYTE 2480 .BYTE "Add 2nd byte",EOL 2490 ; 2500 ;------------------------------- 2510 ;don't forget the subroutines! 2520 ;------------------------------- 2530 ; 2540 .INCLUDE #D8:SUBS.LIB 2550 ; 2560 ;******************************* 2570 ;subroutine do handle carry if 2580 ;adding to the third BCD byte 2590 ;went above 99; can't increment, 2600 ;so must add 1 to higher order 2610 ;bytes as needed 2620 ; 2630 INCREMBCD 2640 SED ;still in decimal 2650 CLC 2660 LDA #1 ;add 1 to second 2670 ADC BCD+1 ;BCD byte 2680 STA BCD+1 ;and store 2690 BCC NOMOREINC ;cause carry? 2700 CLC 2710 LDA #1 ;yes, so add 1 to 2720 ADC BCD ;first BCD byte 2730 STA BCD ;and store 2740 NOMOREINC 2750 RTS ;all done, exit
A.N.A.L.O.G. ISSUE 66 / NOVEMBER 1988 / PAGE 25
How many of you have ever played a computer adventure game of some sort? I see a lot of hands in the air. (Great eyes, no?) And if you’ve ever tried to write one yourself, you quickly discovered that even a simple adventure game involves some pretty sophisticated programming. The ever-adventurous Clayton Walnum once wrote an insightful three-part series on how to design and program your own adventure games.
Blow the cobwebs off issues 39, 40 and 41 of ANALOG Computing, from early 1986, and re-read what Clayton had to say. It’s okay, I’ll wait here until you’re done.
Back already? Then you’ve learned many things (or else you were watching Dallas reruns while you were supposed to be studying). Clayton’s articles told you (among other useful stuff) that the heart of an adventure game is its “parser.” The parser is the program code that lets the computer interpret commands you type and take some appropriate action. It is the parser that gives the computer some appearance of being intelligent. Of course, computers aren’t intelligent in the least; good parser programmers are.
In reality, parsers are useful for much more than simply exploring dungeons. Many kinds of computer applications can benefit from a user interface that at least attempts to understand natural language communications. While it’s pretty hard to get a computer to understand spoken instructions, the written word can be interpreted a bit more easily.
In the next Boot Camp or two, we’ll see how a very simple parser can be implemented on the 8-bit Atari, using some assembly language for the time-intensive parts. You really aren’t likely to write a complete application program in assembler around this word-searcher nucleus. Hence, we’ll set up a simple BASIC program structure that interfaces to the machine-language parser routine, to show you how it all fits together.
Now, what subject area should we use to illustrate the fine art of parsing? Adventure games are kind of passe by now. Wait! I’ve got it! Imagine the kitchen of the future, automatically assembling ingredients in the quantities and sequence you specify, popping the result into the oven, and doing everything for you except eating the food. Let’s write a general-purpose parser in assembly language, then cook up a BASIC program that might be used someday in Karl’s Komputerized Kitchen.
There are three main aspects to a natural language-processing program or parser: 1) to take the input string apart into separate words and/or numbers, 2) to attempt to identify the individual words by looking them up in a vocabulary list, and 3) to try to understand what the input “means”; that is, see if the words identified in the input string constitute a recognizable instruction that can be executed by the program.
Let’s look at all three of these functions in more detail.
The basic idea of natural language processing is that the user (who is presumably a human being of some sort) can present instructions or queries to the computer much as he would communicate with another human being. I’m sure you recognize how amazingly complex this kind of communication really is. After all, you use all sorts of shortcuts in your verbal and written communications, yet other people who know the same language can usually figure out what you’re trying to say. We have to be pretty creative to do something similar with a microcomputer.
Take a simple instruction of the sort you might give to a computerized kitchen when you want to bake a cake: “Slowly mix in two cups of brown sugar.” Most of you should have a picture in your mind of what this means. But how do we get the computer to understand it?
The first step is to break the input string into separate words. The simplest way to do this is to look for blank characters as delimiters between words. But what if the program user entered more than one blank between words, or used a punctuation mark (comma, semicolon, period, etc.) to separate words instead of (or in addition to) the blank? For simplicity, we’ll decree that only single instructions can be entered. This means we don’t have to look for complex sentences such as, “Melt the butter, then stir in the flour.” So, most punctuation marks can be disregarded.
However, we can’t just ignore periods. What if you want to add 3.5 cups of flour? The period here is really a decimal point in a number. Obviously, when we split the input string into words, we must distinguish numbers from true words that we’ll be trying to find in the vocabulary list.
The moral of the story is that the natural language interface involves some “preprocessing” of the user’s input. This step discards symbols like certain punctuation marks and builds a list of words for which we must search in the known vocabulary. Any numbers or other anticipated special character strings will be identified and set aside until we get to Step 3, in which we try to make some sense out of the input.
The preprocessor can be a part of the parser code itself or it can be a separate routine. For simplicity in this example, I’ll put the preprocessor into the BASIC program. If execution time is critical, you would want to recode it into assembler, but BASIC will be fine for our purposes.
Once your preprocessing step has come up with a list of words from the user’s input string, we need to see if the words are “known” to the program. Your vocabulary list should be separate from the parser code itself, since a general-purpose parser routine could then be used for many applications having different vocabularies. The limited RAM in 8-bit microcomputers can really restrict the size of the vocabulary in a given program, because you still need some memory for the rest of the program.
The simplest approach is to put all the words you want the program to recognize into the vocabulary list. Some alternative techniques provide more efficient use of memory, thus allowing larger vocabularies. Have you ever noticed that some spelling-checker programs appear to require far less memory than it seems like they need to handle; say, 30,000 words? Data compression methods and clever algorithms can be used to substantially reduce the amount of memory consumed by a block of information. However, we’ll leave such techniques to the experts and stick to the brute force approach.
Another consideration is how much latitude you wish to give the user regarding different ways he can enter equivalent commands. For example, do you wish to distinguish between uppercase and lowercase letters? This can be important for proper names, like in a quiz on U.S. presidents. Do you want the user to be able to get away with a certain degree of misspelling? One way to handle this is to add anticipated misspellings of particular words to the vocabulary. A more sophisticated approach uses some algorithm to determine just how closely words must match vocabulary entries to be considered acceptable.
In today’s example, we’ll translate all lowercase letters to uppercase, and the vocabulary entries will all be in uppercase. Only exact matches with vocabulary entries will be accepted.
Yet another one of many characteristics of human, that is, interpersonal communication is that we tend to be more or less, I mean pretty much, wordy lots of the time, you know? Think back to “slowly mix in two cups of brown sugar.” The words “in” and “of” certainly are superfluous to the meaning of this instruction. Prepositions and articles (a, an, the) can generally be ignored without disturbing the meaning of an instruction. Hence, we’ll leave them out of the vocabulary. A word of warning: Be very careful with negation words like “not” and “don’t.” “Don’t boil the milk” is rather different from “boil the milk”!
How about words like “slowly” and “brown”? Adjectives and adverbs like these can be important, but not necessarily. The specific application dictates whether the actions to be taken depend on the presence of modifiers like these in the command string.
Another important aspect of building a vocabulary is the handling of synonyms. Instructions to “add flour” and “add sugar” are obviously different. But are there differences between “add sugar,” “mix sugar,” “stir sugar,” “beat sugar,” “fold sugar” and so on? If not, these instructions are all equivalent. So, even though our parser has to locate the verb (add, mix, etc.) in the vocabulary, only one piece of program logic is required to handle all these inputs.
Okay, so we’ve split the input string into words and found the words in the vocabulary list. Each word is assigned a numeric value, or “token.” Each category of vocabulary entries will have a different set of tokens, and specific arrangements of tokens will make up valid instructions. Synonyms are given the same token.
Listing 1 illustrates what I mean. This is an Atari BASIC program that creates the vocabulary file (VOCAB.DAT) for our computerized kitchen example. You can modify this program to create vocabulary files for other applications by changing the DATA statements in Lines 1000–1120. The DIM statements in Line 100 limit the length of a single vocabulary entry to 20 characters and the total length of the vocabulary file to 2,000 bytes, but of course you could change these restrictions.
The first block of vocabulary words (Lines 1000–1030) pertains to ingredients that we think someone might want to use in describing a recipe. FLOUR is assigned the token 1, SUGAR is 2 and so on. Notice that I’m regarding BUTTER, MARGARINE and SHORTENING as equivalent ingredients, so they all are given the same token, a 5 (Line 1010). In the adventure game sense, these words correspond to the nouns that could be entered in a simple two-word command.
Another section of our vocabulary concerns operations (verbs) the user might want to perform while cooking up something tasty. All of these have tokens in the range 20–29 (Lines 1040–1060). Again, some of them are considered to be synonymous (MIX, STIR, ADD, FOLD), and instructions containing any of these words will be handled in exactly the same way by the evaluator part of the parser.
Vocabulary words with tokens in the 30–39 range refer to the units on some meaningful numbers that could be part of a command. These units refer to cooking time (HOURS, MINUTES) or temperature (DEGREES). Words with tokens in the 40s are units pertaining to the quantities of ingredients that are to be added (CUPS, TSP and so forth).
When we actually get to the part of the parser that determines whether a valid instruction was entered, the program will be looking at tokens, not at actual words. Certain patterns of tokens constitute valid commands. For example, suppose the instruction string entered said, “ADD 2 COCOA.” The parser would tokenize this into ingredient = 8, amount = 2 (identified as a number), and operation = 21. However, the parser logic should recognize that something is missing: units. “ADD 2 what COCOA?” Cups? Ounces? Tablespoons? It makes a difference in the final product, or so I’ve heard. Hence, “ADD 2 COCOA” would be flagged as an invalid instruction, because no units were specified. More about the third portion of the parser next time.
Enough preliminaries; let’s look at some more code! I said that Listing 1 is a utility program for creating a file containing a vocabulary list. The entire vocabulary list is treated as one giant character string variable, VOCAB$. For convenience of editing and for ease of reading the file, the contents of the string are written out to the VOCAB.DAT file in 40-byte records, in Lines 220–300.
The data statements in Listing 1 contain the individual vocabulary entries and their corresponding tokens. A complete vocabulary entry in string VOCAB$ consists of: one character whose ATASCII value equals the number of characters in the word (Lines 130–140); the word itself (Line 150); and a character whose ATASCII value equals the token value for that word (Line 160). This method for storing the vocabulary limits you to 255 unique tokens, but if you needed more, you could go to a two-byte representation for the tokens; 65,535 tokens should be adequate.
An example: MARGARINE has a length of nine characters and a token value of 5. The vocabulary entry for this word consists of CHR$(9) (Control-I), MARGARINE and CHR$(5) (Control-E). Make sense?
Line 1130 marks the end of the vocabulary data with an exclamation mark and a token value of 0. If the vocabulary searching part of the parser gets to the end of the vocabulary list without a match, a token of 0 is returned.
Next month we’ll look at the preprocessing and evaluator parts of the parser. For now, you’ll have to settle for something simpler. Listing 2 is a BASIC program that simply loads the word-finder machine-language parser routine into RAM, reads the VOCAB.DAT file into string variable VOCAB$, lets you enter a word at the keyboard, and tells you if the word you entered is found in the vocabulary list. (Enter QUIT to exit.) This is a useful way to test whether there are any errors in your vocabulary file. For today, Listing 2 will serve as a framework for illustrating how the machine-language routine operates.
Line 40 of Listing 2 DIMS the VOCAB$ variable to the actual length of the vocabulary file or thereabouts. The 40-byte records from VOCAB.DAT are read in Lines 140–190.
Oh, yeah, I almost forgot: Boot Camp is supposed to be about 6502 assembly language. Well, look at Listing 3. This is the kernel of the parser, the routine that searches for a particular word in the vocabulary file. It produces only 79 bytes of object code. Shorter than you expected, eh? Well, it really isn’t doing anything particularly fancy. Of course, if you were to write the entire preprocessor and evaluator parts of the parser in assembly language, you’d be talking about some serious code. The preprocessor could be written so as to be generally useful in any natural language program. However, the evaluation code is necessarily specific to each application.
The machine-language routine in Listing 3 is intended to be called from BASIC by means of the USR function. It is relocatable, so it can be loaded at any address you like. Assemble this listing and create a disk file called PARSER.OBJ.
Listing 2 reads the machine-language code from PARSER.OBJ. You may recall that binary (object code) files contain six bytes of header information. Lines 60–80 of Listing 2 read these six bytes and throw them away. Lines 90–120 read the actual object code and load it into a string variable called ML$. An alternative approach is to read the object code and poke it into some safe place in RAM. Since the code is relocatable, this can be secure anywhere, so long as you know the starting address.
The assembly routine uses six RAM locations for specific purposes. Zero-page bytes $CB–$CE (decimal 203–206) are needed for the indirect indexed addressing mode, as we have seen so many times before. Location $6FE (decimal 1790) contains the length of the word being sought in the vocabulary; this serves as input into the machine-language routine. Location $6FF (1791) contains the word’s token value (or a zero if not found); this is the parser’s output. You can change these if they conflict with other uses for those addresses in your own programs.
To call this machine-language routine, first poke the length of the target word (in variable WORD$ in Listing 2) into location 1790 (Line 240 of Listing 2). Then call the machine-language routine with the USR function as shown in Line 250, specifying the addresses of the ML routine itself, the vocabulary variable string, and the targetword variable string. Faster than a speeding bullet, the token value for the word appears at Address 1791, unless the contents of WORD$ aren’t found in VOCAB$, in which case a zero appears in address 1791. Pretty simple, eh?
Let’s look at the assembly code a little more closely. Lines 450–530 in Listing 3 illustrate how to handle arguments passed from BASIC to machine language. These, you may recall, are passed via the stack, in two-byte chunks. The PLA in Line 450 removes a onebyte counter of how many arguments were actually passed. It’s not a bad idea to do some error checking to make sure that the accumulator contains a “2” after the PLA operation; otherwise, a computer lockup is likely. Next on the stack are the high byte and low byte of Argument 1 (address of VOCAB$), followed by the high byte and low byte of Argument 2 (address of WORD$).
The searching algorithm is really very simple. It begins by comparing the length of the target word to the length of the current vocabulary entry being pointed to by VOCAB (Lines 680–700, with a branch down to Line 790). If the lengths are different, the words obviously don’t match, so control passes from Line 810 to label NEXTWORD at Line 1090. Lines 1100–1320 simply change the contents of VOCAB to point to the next word in VOCAB$, by skipping ahead a number of bytes equal to the length of the current word plus 2 (one for the length, one for the token).
If the target length did match the current vocabulary entry length, a character-by-character comparison is done in Lines 820–960. This comparison actually starts with the last character in the word and works backwards. If all characters match (Ta-da!), we have a hit. Lines 970–1010 fetch the token value at the end of the current vocabulary entry, store it at address RESULT ($6FF, 1791), and return to the BASIC program. If the entire vocabulary list is searched with no match, RESULT contains a zero (Line 710).
So there you have it. A very simple assembly-language program for searching an arbitrary list of vocabulary entries to see if a target character string can be identified. Next time, we’ll see a way to package this vocabulary searching part of the parser with preprocessor and evaluator routines in BASIC to show just how smart a program has to be to make Karl’s Komputerized Kitchen a reality.
I’m indebted to Dr. Bruce Argyle of Mad Scientist Software for sharing his parser code and concepts with me. Thanks, Bruce.
Listing 1. BASIC listing. 10 REM Program name: VOCAB.BAS 20 REM Utility program to build 30 REM Vocabulary list file for parser 40 REM demo program in "Boot Camp" 50 REM 60 REM by Karl E. Wiegers 70 REM 80 REM First build VOCABS string 90 REM 100 DIM TERM$(20),VOCAB$(2000),TEMP$(41) 110 A=1:PRINT "Building vocabulary..." 120 READ TERM$,TOKEN 130 INLEN=LEN(TERM$) 140 VOCAB$(A,A)=CHR$(INLEN) 150 VOCAB$(A+1,A+INLEN)=TERM$ 160 VOCAB$(A+1+INLEN,A+1+INLEN)=CHR$(TOKEN) 170 IF TERMS="!" THEN GOTO 200 180 A=A+INLEN+2 190 GOTO 120 200 REM Then print data to file 210 PRINT "Saving in file...." 220 OPEN #2,8,6,"D:VOCAB.DAT" 230 MAX=INT(LEN(VOCAB$)/41)+1 240 FOR I=1 TO MAX-1 250 TEMP$=VOCAB$(40*I-39,40*I) 260 PRINT #2;TEMP$ 270 NEXT I 280 TEMP$=VOCAB$(40*MAX-39,LEN(VOCAB$)) 290 PRINT #2;TEMP$ 300 CLOSE #2 310 PRINT "All done!" 320 END 1000 DATA FLOUR,1,SUGAR,2,OIL,3,MILK,4 1010 DATA BUTTER,5,MARGARINE,5,SHORTENING,5 1020 DATA NUTS,6,WATER,7,COCOA,8 1030 DATA EGG,10,EGGS,16 1040 DATA MIX,21,STIR,21,FOLD,21,ADD,21 1050 DATA WHIP,22,BEAT,22 1060 DATA PREHEAT,23,COOK,24,BAKE,24 1070 DATA HOURS,31,MINUTES,32,DEGREES,32 1080 DATA CUP,41,CUPS,41,C,41 1090 DATA TEASPOON,42,TEASPOONS,42,TSP,42,TSPS,42 1100 DATA TABLESPOON,43,TABLESPOONS,43 1110 DATA TBSP,43,TBSPS,43 1120 DATA OUNCE,44,OUNCES,44,OZ,44,OZS,44 1130 DATA !,0
Listing 2. BASIC listing. 10 REM Sample program to demonstrate 20 REM vocabulary searching for words 30 REM entered by user 35 REM 40 DIM VOCAB$(280),TEMP$(40),WORD$(20),ML$(79) 50 OPEN #2,4,0,"D:PARSER.OBJ" 60 FOR I=1 TO 6 70 GET #2,A 80 NEXT I 90 FOR I=1 TO 79 100 GET #2,A 110 ML$(I)=CHR$(A) 120 NEXT I 130 CL05E #2 140 OPEN #2,4,0,"D:VOCAB.DAT" 150 FOR I=0 TO 6 160 INPUT #2,TEMP$ 170 VOCAB$(I*40+1)=TEMP$ 180 NEXT I 190 CLOSE #2 200 PRINT CHR$(125) 210 PRINT "Enter a word (QUIT to exit):" 220 INPUT WORD$ 230 IF WORDS="QUIT" THEN STOP 240 POKE 1790,LEN(WORD$) 250 X=USR(ADR(ML$),ADR(VOCAB$),ADR(WORD$)) 260 IF PEEK(1791)=0 THEN PRINT "Sorry, I don't know ";WORD$ 270 IF PEEK(1791)>0 THEN PRINT "Token for ";WORD$;" is ";PEEK(1791) 280 PRINT 290 GOTO 210
Listing 3. Assembly listing. 0100 .OPT OBJ,NO LIST 0110 ; 0120 ;Vocabulary searching routine, to 0130 ;be used as part of a natural 0140 ;language parser program 0150 ; 0160 ;by Karl E. Wiegers 0170 ; 0180 ;This machine language subroutine 0190 ;is designed to be called from a 0200 ;BASIC program. It takes two 0210 ;arguments: the address of the 0220 ;Vocabulary data string, and the 0230 ;address of the variable that 0240 ;contains the word being searched 0250 ;for, like this: 0260 ; 0270 ;X=USR(loc,ADR(VOCAB$),ADR(WORD$) 0280 ; 0290 ; 0300 VOCAB = $CB 0310 WORD = $CD 0320 LENGTH = $06FE 0330 RESULT = $06FF 0340 ; 0350 ;routine is orged at $600, but is 0360 ;relocatable 0370 ; 0380 *= $0600 0390 ; 0400 ;------------------------------- 0410 ;set up arguments passed from 0420 ;BASIC, into page 0 variables 0430 ;------------------------------- 0440 ; 0450 PLA 0460 PLA ;pointer to start 0470 STA VOCAB+1 ;of vocabulary 0480 PLA ;character string 0490 STA VOCAB 0500 PLA ;pointer to start 0510 STA WORD+1 ;of word being 0520 PLA ;searched for in 0530 STA WORD ;Vocabulary list 0540 ; 0550 ;------------------------------- 0560 ;search routine starts here with 0570 ;next word being pointed to by 0580 ;VOCAB Variable; branch to label 0590 ;ANALYZE to look for match; if 0600 ;no match, return to here; last 0610 ;'entry' in VOCAB$ has token of 0620 ;0, so store that in RESULT and 0630 ;return to BASIC program 0640 ;------------------------------- 0650 ; 0660 CLD 0670 BEGIN 0680 LDY #0 0690 LDA (VOCAB),Y 0700 BNE ANALYZE 0710 STA RESULT 0720 RTS 0730 ; 0740 ;------------------------------- 0750 ;see if length matches that of 0760 ;next word in vocabulary 0770 ;------------------------------- 0780 ; 0790 ANALYZE 0800 CMP LENGTH ;lengths match? 0810 BNE NEXTWORD ;no, go on 0820 LDY LENGTH ;yes,check chars 0830 ; 0840 ;------------------------------- 0850 ;compare characters in target 0860 ;word with those in current word 0870 ;in vocabulary 0880 ;------------------------------- 0890 ; 0900 CYCLE 0910 LDA (VOCAB),Y ;get next char 0920 DEY ;and compare to 0930 CMP (WORD),Y ;target word 0940 BNE NEXTWORD ;no match,go on 0950 TYA ;matches, check 0960 BNE CYCLE ;next char 0970 LDY LENGTH ;found! point to 0980 INY ;token value, get 0990 LDA (VOCAB),Y ;it, and store 1000 STA RESULT ;in RESULT byte 1010 RTS ;all done, so exit 1020 ; 1030 ;------------------------------- 1040 ;skip to next word by adding 1050 ;length of current word to 1060 ;pointer to vocabulary list 1070 ;------------------------------- 1080 ; 1090 NEXTWORD 1100 CLC 1110 LDY #0 1120 LDA VOCAB 1130 ADC (VOCAB),Y 1140 STA VOCAB 1150 BCC NOINC1 1160 INC VOCAB+1 1170 NOINC1 1180 CLC 1190 LDA VOCAB 1200 ADC #2 1210 STA VOCAB 1220 BCC NOINC2 1230 INC VOCAB+1 1240 NOINC2 1250 ; 1260 ;------------------------------- 1270 ;continue search with next word 1280 ;in the vocabulary 1290 ;------------------------------- 1300 ; 1310 CLC 1320 BCC BEGIN
A.N.A.L.O.G. ISSUE 69 / FEBRUARY 1989 / PAGE 24
When last we met, we were discussing how to get our Atari to understand instructions presented to it in normal English sentences-natural-language communication with the computer. The software that gives the computer the ability to at least partially understand natural-language input is called a “parser.” The parser, as we discovered, has three principal functions: to break (or parse) the user’s input into individual words and numbers; to search for these words in a preset vocabulary list; and to determine if a valid instruction has been entered and take appropriate action.
We chose as an example of natural-language communication a hypothetical kitchen of the future: Karl’s Komputerized Kitchen. In this dream kitchen, we simply have to tell the computer what ingredients to combine and how to cook them, and the automated kitchen will do the rest. (My fiancee thinks this is the greatest idea since take-out pizza.)While I haven’t yet connected my Atari to my Mixmaster, we can at least think about how we’d like to communicate with this komputerized kitchen.
Last time we focused on the second function of the parser, the act of looking up words from the user’s command in a vocabulary list. A sample BASIC program was included to build a small vocabulary file with ordinary cooking kinds of words: FLOUR, SUGAR, BAKE, PREHEAT, STIR, and so on. Each word is assigned a number, or “token,” to make it easier to handle the program logic when we get to the stage of trying to make sense out of the user’s input. Synonyms, such as BUTTER, MARGARINE and SHORTENING, all have the same token. For simplicity, our vocabulary contains only words with all uppercase letters, and only exact matches to vocabulary words will be accepted. This rules out misspellings or plural forms, but it keeps our sample program simpler.
Whereas the core of the previous column was an assembler routine for searching the vocabulary list for a specific word, today’s program is written entirely in Atari BASIC. It illustrates the other two functions of a full-parser program. I called the initial step of splitting the user’s input into words and numbers a “preprocessor” function. After preprocessing, the machine-language routine we looked at last time is used to look for individual words and assign tokens to them. Step 3 is to evaluate the tokenized input and see if it makes any sense, in the context of what the computer has been programmed to “understand.” If we are skillful, the result will be a simple BASIC program that appears to be remarkably intelligent.
We begin with a minor correction to Listing 1 from last time. This was program VOCAB.BAS, which created the VOCAB.DAT vocabulary file. In Line 1070, please change the number following the word “DEGREES” to a 33. You may recall that these numbers are the tokens assigned to each vocabulary word. Rerun this program to create a new VOCAB.DAT file before proceeding further. Sorry about that.
Before getting too deep into the code, we’d better establish some ground rules for the komputerized kitchen program. First, I want to limit the number of commands entered before you can throw the pot into the oven to a maximum of ten. As valid commands are entered, I’ll save them in a string pseudo-array and write them at the top of the screen before prompting for the next command. You’ll have to preheat the oven to the desired temperature before baking your mixture. And the final command will be to bake whatever you have thrown together, no matter how disgusting it may appear.
Our vocabulary is rather limited for a full-blown automated kitchen, but this program should at least illustrate the power of a parser program. A real application program would of course include other useful terms in the vocabulary, such as “HELP” and “QUIT.” I’ve omitted such user aids for conciseness, but don’t ever leave these important functions out of an actual program!
As we parse each entered command, any words or numbers identified will be printed on the screen. This way you can watch as the computer tries to make sense out of whatever you tell it to do.
There are four classes of meaningful words that a user could enter in a command in this program. First is the name of an ingredient. The VOCAB.BAS program showed that ingredients have tokens in the numeric range of 0–19. Instructions have tokens in the range of 20–29. Units associated with cooking time or temperature have tokens in the 30s. And units associated with ingredients have tokens in the 40s. Also, each command entered in this particular program must contain a number, to indicate the amount of an ingredient to add, a preheating temperature, or a cooking time.
In brief, our preprocessing function will separate words in the input string by looking for blank (space) characters in the command. As each word is isolated, it will be looked up in the vocabulary list. If the first character in a word is in the ATASCII range for a digit from 0–9, that entire “word” is considered to be a number, and a separate processing routine is used. Note that this method won’t handle numbers that begin with either a minus sign (hyphen) or a decimal point (period), but it could be changed easily to accommodate these.
Please turn your attention to the BASIC program in Listing 1. At first glance, this program may appear to contain a lot of spaghetti code, what with all the GOTOs, but I think it’s better than it looks. Before we go any further, let’s define what some key variables in this program represent.
- entire vocabulary data string.
- temporary variable for reading from VOCAB.DAT.
- target word being searched for in VOCAB$.
- command entered by user after removing unrecognized words.
- machine-language code for the parser.
- command string entered by user.
- string representation of any number present in the command entered.
- temporary variable.
- string pseudo-array of valid instructions entered so far.
- flag for whether oven is preheated or not.
- number of valid commands entered so far.
- numeric form of number in NUM$.
- token value of any ingredient in command entered.
- token value of any instruction in command entered.
- token value if units for cooking time or temperature were included in command entered.
- token for ingredient units included in command entered.
- cooking temperature entered in a BAKE or COOK command.
Lines 10–140 of this program are pretty much the same as Listing 3 from last time. Some string variables are dimensioned, then the vocabulary-searcher machine-language code is read from file PARSER.OBJ (Lines 30–90). Next, the vocabulary data is read into variable VOCAB$ from file VOCAB.DAT (Lines 100–140). Some of the work variables needed are initialized in Line 150.
Lines 160–200 clear the screen and print any valid commands entered so far on the screen. If the oven has already been preheated, Line 205 prints a message to that effect. Lines 210–220 accept the next command from the user. If he just pressed Return without an entry, Line 230 tells him to try it again. Line 240 initializes the values for the four classes of vocabulary entries to zeros. If any words in these categories are found in the command entered, these variables will be set to the corresponding token values for the vocabulary words. The evaluator part of the program will examine the pattern of the tokens stored in these variables when it attempts to interpret the user’s command.
Line 300 sets a variable called I to 1. This variable always points to the character we are currently processing from the command entered. After processing each character, I will be incremented to point to the next character. (A new experience for me-I’ve frequently been insulted, but never incremented. Does it hurt?) The variable MAX equals the number of characters in the command string. When I is greater than MAX, the entire command has been processed, and this preprocessor loop terminates (Line 310). The evaluator function begins in Line 800; more about that later.
Line 320 plucks the next character from the command string, and Line 330 checks to see if this character is a space. If so, we could be at the end of a word. Fall through to Line 340 to see if the WORD$ variable contains anything yet. If not, we’ve probably detected a leading blank in the input string, so go back to Line 310 to get the next character. However, if WORD$ does contain something, we’re ready to see if that word is in our vocabulary, so Line 350 calls a subroutine at Line 600 for this purpose. More about this later, too.
If our current character is not a blank, we must already be in the middle of a word. Line 360 tests whether this character is in the ATASCII range for a digit. If so, a subroutine at Line 500 is called to handle it. (We’ll get to this in just a bit.) If the current character is a lowercase letter, Line 370 translates it to uppercase. Line 380 adds this character to the end of our WORD$ variable and Line 390 says to go get the next character.
Number processing is dealt with in a subroutine beginning in Line 500. The digit character is added to the end of variable NUM$ and also is printed on the screen in Line 510. Line 520 points to the next character in the input string, and Line 530 tests to see if we have completed the command parsing yet. Line 540 fetches the next character from the command string. Line 550 tests whether this one is also a digit or a decimal point. If so, add it to NUM$ and keep going. If not, we must be at the end of the number, so print a trailing blank on the screen in Line 560.
Lines 570–580 append the string representation of our number to the end of variable PARSED$, which contains the interpreted form of the command entered. Line 590 converts the string form of the number into its numeric equivalent in variable AMOUNT. This number will be used during the evaluation step.
Recall that the parsing process is interrupted whenever we hit either a blank or the end of the command string, provided that the variable WORD$ contains something. Control is then passed to the subroutine at Line 600, where the contents of WORD$ are matched against the vocabulary list in VOCAB$ to see if we “know” the target word. But first, Line 610 extracts the final character from WORD$, and Line 620 discards that character if it is an anticipated punctuation mark. This gets around the problem of the user entering a command ending with a period or other symbol, yet it allows the user the flexibility of using punctuation in a natural kind of way in his commands.
Lines 630–650 should be familiar from last month. POKE the length of the target word into location 1790, call the machinelanguage vocabulary searcher routine, and get the corresponding token for the word out of location 1791. If the token is zero, Line 660 concludes that we don’t know the word and says to proceed with the next character in the command string. Otherwise, we have a hit! Line 670 prints the word on the screen. Lines 680–710 classify the word into one of the four vocabulary categories I mentioned earlier and assign the word’s token value to the appropriate variable. Lines 720–740 add the identified word to the PARSED$ variable and return to continue parsing the command string.
Hey, we’re two-thirds of the way home. So far, all we have for our trouble are a few variables whose values correspond to vocabulary-word tokens, and perhaps a string representing a number entered by the user as part of his command. Now we have to see if the pattern of variable values makes any sense. We need some more rules to determine what constitutes a valid command. Try these on for size:
Whew! And these are just the ones that come to mind right away. Clearly, we need a lot of program code to test the numeric values of the key variables (INSTRUCT, UNITS, INGRED, TIMETEMP, and AMOUNT) to look for these conditions. Most real-life programs involving this sort of natural-language communication will have even more rules. Larger vocabularies, more complex command structures, and more classes of vocabulary terms all will increase the required sophistication of the evaluator part of the parser. You’ll have to apply all of your error-checking skills when you design this section of your program.
Line 800 processes the final contents of WORD$ after control is transferred here when the end of the command string is reached. The rest of this section is somewhat hamstrung by the unstructured nature of Atari BASIC. For consistency, I used the approach of testing for “not” each error condition, and branching around the error handling code if no problem was encountered.
For example, Line 810 handles Rule 1. If no instruction was found in the parsed command, the value of variable INSTRUCT still will be zero. Otherwise, it will have the token value of the instruction entered. Line 810 ’says if we do have an instruction, go on to Line 830. Otherwise, fall through to Line 820 where the error message is printed. The subroutine at Line 10000 just waits for the user to press Return. After any such error, control returns to Line 160, where the command entry process begins again.
In this way, we keep leapfrogging through the evaluator code, until we figure out if the command entered is okay and what to do about it. A couple of special cases are handled in separate sections of code. The subroutine to handle a BAKE (or COOK) instruction begins at Line 2000, and a subroutine for the PREHEAT instruction is in Lines 2500–2620. By now you should understand why we are using tokens to represent the vocabulary words, rather than doing complex string comparisons for all these tests.
Without going in detail through all the code, I’ll just indicate the line numbers where some of the rules are being tested. (The limit of ten commands before baking isn’t being enforced, but the program will probably crash if you exceed this.)
Rule 1-Lines 810–820
Rule 2-Lines 830–840
Rule 3-Lines 2030–2040
Rule 4-Lines 2530–2540
Rule 5-Lines 2550–2580
Rule 7-Lines 2050–2060
Rule 8-Lines 2070–2110
Rule 9-Lines 2010–2020
Rule 10-Lines 910–920
Rule 11-Lines 890–900
If the command turns out to be valid, the contents of variable PARSED$ are copied to the appropriate section of the string pseudoarray variable INSTR$ in Lines 950–960. This is so the accumulated instructions can be printed at the top of the screen to remind you of what has been entered already.
And when you finally get your mixture into the oven, the program is over! The all-important END statement appears in Line 2160. You mean that wasn’t the first place you looked for it? Oh, well, think of it as an Easter egg hunt.
In your classic adventure game, which expects simple two-word commands, it’s safe to insist that the first word be a verb and the second be a noun. “LIGHT TORCH” makes some sense then, and “TORCH LIGHT” does not. We can use the simplicity of the parser to impose some syntax restrictions on the kinds of commands that can be entered.
However, the parser I’ve described in these two issues of Boot Camp is a little more flexible in terms of the kinds of commands that can be entered. The downside is that we haven’t placed any restrictions on syntax; command interpretation depends only upon the presence of keywords. A command like “COCOA TEASPOONS 11 BEAT” is perfectly comprehensible to our parser. It contains an instruction (BEAT), a number (11), an ingredient (COCOA), and units suitable for an ingredient (TEASPOONS). The fact that no one above the age of two would utter such a jumbled sentence doesn’t faze the parser one bit. Obviously, a really sophisticated natural-language processor program would include more rules to govern usage and structure, but that’s a topic best left to the specialists for now.
Notice how we had to set up a bunch of rules to determine what constitutes legitimate commands for this application area, which commands had to be entered prior to others, and so on. The assembler and BASIC programs shown here handle all of these tasks in a detailed, brute-force manner, with explicit procedures for every single case. In fact, most traditional computer languages are called “procedural” languages, for this very reason.
Anytime you have a computer application that involves rules, concepts or patterns like we’re dealing with here, the term “artificial intelligence” is sure to crop up. Two of the hot, current areas of interest in artificial (computer-based) intelligence are rule-based systems and natural-language processors. Our parser and komputerized kitchen examples clearly fit right into both of those realms. New programming languages like PROLOG let you program the rules directly into the computer, rather than having to explicitly write all the nitty-gritty code to handle each rule the way we did today. However, most of these require some pretty hefty hardware to do a passable job.
But even those of us stuck with our trusty old 8-bit Ataris might be impressed with the flexible human-computer dialogs that can be created using the ideas discussed in these two articles. The speed with which these simple programs interpret your commands is pretty remarkable. If you’re ambitious, you might try recoding the pre-processor into assembler to see if you enjoy much of an execution speed increase. And if you’re a hardware type, let me know if you can get the computer to do the dishes, too.
Listing 1. BASIC listing. 1 REM Parser program for Karl's Komputerized 2 REM Kitchen. Enter up to 10 instructions, 3 REM then bake it! 4 REM 5 REM by Karl E. Wiegers 6 REM 8 REM ------------------------------ 9 REM 10 DIM VOCAB$(280),TEMP$(40),WORD$(20),PARSED$(30) 20 DIM ML$(79),IN$(40),NUM$(6),A$(1),INSTR$(300),PREHEAT$(1) 30 OPEN #2,4,0,"D:PARSER.OBJ" 40 FOR I=1 TO 6:GET #2,A:NEXT I 50 FOR I=1 TO 79 60 GET #2,A 70 ML$(I)=CHR$(A) 80 NEXT I 90 CLOSE #2 100 OPEN #2,4,0,"D:VOCAB.DAT" 110 FOR I=0 TO 6 120 INPUT #2, TEMP$ 138 VOCAB$(I*40+1)=TEMP$ 140 NEXT I:CLOSE #2 150 NINS=0:INSTR$="":PREHEAT$="N":BAKETEMP=0 157 REM 158 REM ---------------------------- 159 REM 160 PRINT CHR$(125) 170 IF NINS=0 THEN GOTO 205 180 FOR I=1 TO NIN$ 190 PRINT INSTR$(30*I-29,30*I) 200 NEXT I 205 IF PREHEAT$="Y" THEN PRINT "OVEN IS PREHEATED TO ";BAKETEMP;" DEGREES" 210 PRINT PRINT "Next instruction, please:":PRINT 220 INPUT IN$:PARSED$="":AMOUNT=0 230 IF IN$="" THEN GOTO 210 240 INGRED=0:INSTRUCT=0:TIMETEMP=0:UNITS=0 300 I=1:WORD$="":MAX=LEN(IN$):NUM$="" 310 IF I>MAX THEN GOTO 800 320 A$=IN$(I,I):A=ASC(A$) 330 IF A$<>" " THEN GOTO 360 340 IF WORD$="" THEN I=I+1:GOTO 310 350 GOSUB 600:I=I+1:WORD$="":GOTO 310 360 IF A>47 AND A<58 THEN GOSUB 500:GOTO 310 370 IF A>96 AND A<123 THEN A$=CHR$(A-32) 380 WORD$(LEN(WORD$)+1)=A$ 390 I=I+1:GOTO 310 498 REM 499 REM ----------------------------- 500 REM Subroutine to process numbers 501 REM ----------------------------- 502 REM 510 NUMS(LEN(NUM$)+1)=A$:PRINT A$; Q1 520 I=I+1 530 IF I>MAX THEN GOTO 570 540 A$=IN$(I,I):A=ASC(A$) 550 IF (A>47 AND A<58) OR A$="." THEN GOTO 510 560 PRINT " "; 570 PARSED$(LEN(PARSED$)+1)=NUM$ 580 PARSED$(LEN(PARSED$)+1)=" " 590 AMOUNT=VAL(NUM$):RETURN 598 REM 599 REM ------------------------------------ 600 REM Subroutine to search for current word 601 REM ------------------------------------ 602 REM 610 A$=WORD$(LEM(WORD$)) 620 IF A$="." OR AS="," OR AS="?" OR A$=";" OR A$="!" OR A$=::" THEN WORD$=WORD$(1,LEN(WORD$)-1):GOTO 610 630 POKE 1790,LEN(WORD$):POKE 1791,0 640 X=USR(ADR(ML$),ADR(VOCAB$),ADR(WORD$)) 650 X=PEEK(1791) 660 IF X=0 THEN RETURN 670 PRINT WORDS;" "; 680 IF X<20 THEN INGRED=X 690 IF X>19 AND X<30 THEN INSTRUCT=X 700 IF X>29 AND X<40 THEN TIMETEMP=X 710 IF X>39 AND X<50 THEN UNITS=X 720 PARSED$(LEM(PARSED$)+1)=WORD$ 730 PARSED$(LEN(PARSED$)+1)=" " 740 RETURN 795 REM 796 REM ----------------------------- 797 REM Evaluator Routine 798 REM ----------------------------- 799 REM 800 IF WORD$<>"" THEN GOSUB 600 810 IF INSTRUCT>0 THEN GOTO 830 820 ? :? :? "Please include an instruction.":GOSUB 10000:GOTO 160 830 IF AMOUNT>0 THEN GOTO 850 840 ? :? :? "Please include a numeric amount.":GOSUB 10000:GOTO 160 850 IF INSTRUCT=24 THEN GOSUB 2000:GOTO 160 860 IF INSTRUCT=23 THEN GOSUB 2500:GOTO 160 870 IF INGRED>0 THEN GOTO 890 880 ? :? :? PARSED$;" of what??":GOSUB 10000:GOTO 160 890 IF INGRED>9 AND UNITS=0 THEN GOTO 930 900 IF INGRED>9 AND UNITS>0 THEN ? :? :? "Eggs don't have units!":GOSUB 10000:GOTO 160 910 IF UNITS>0 THEN GOTO 930 920 ? :? :? "Please include some units":GOSUB 10000:GOTO 160 930 ? :? :? "Okay!!" 940 NINS=NINS+1:L=LEN(PARSED$) 950 IF L<36 THEN FOR I=L TO 30:PARSED$(I,I)=" ":NEXT I 960 INSTR$(30*NINS-29,30*NINS)=PARSER$ 970 GOSUB 10000:GOTO 160 1998 REM 1999 REM ------------------------------ 2000 REM Sub to handle bake instruction 2001 REM ------------------------------ 2002 REM 2010 IF NINS>0 THEN GOTO 2030 2020 ? :? :? "You don't have anything to bake yet!":GOSUB 10000:RETURN 2030 IF PREHEAT$="Y" THEN GOTO 2050 2040 ? :? :? "You need to preheat the oven first.":GOSUB 10000:RETURN 2050 IF TIMETEMP>0 THEN GOTO 2070 2060 ? :? : "Please include baking time and units.":GOSUB 10000:RETURN 2070 IF TIMETEMP=31 THEN AMOUNT=AMOUNT*60 2080 IF AMOUNT>=5 THEN GOTO 2100 2090 ? :? ? "Shortest legal cooking time is 5 min.":GOSUB 10000:RETURN 2100 IF AMOUNT<=688 THEN GOTO 2120 2110 ? :? :? "Longest legal cooking time is 10 hrs.":GOSUB 10000:RETURN 2120 ? :? :? "Okay!" 2130 ? :? "Your concoction is in the oven," 2140 ? "and this program is now finished." 2150 GOSUB 10000:PRINT CHR$(125) 2160 END 2498 REM 2499 REM --------------------------- 2500 REM Subroutine to preheat oven 2501 REM --------------------------- 2502 REM 2510 IF PREHEATS="N" THEN GOTO 2530 2520 ? :? :? "The oven is already preheated!":GOSUB 10000:RETURN 2530 IF TIMETEMP=33 THEN GOTO 2550 2540 ? :? .? "Include units for preheating.":GOSUB 10000:RETURN 2550 IF AMOUNT>=300 THEN GOTO 2570 2560 ? :? :? "Minimum temperature is 300 degrees.":GOSUB 10000:RETURN 2570 IF AMOUNT<=500 THEN GOTO 2590 2580 ? :? :? "Maximum temperature is 500 dearees.":GOSUB 10000:RETURN 2590 ? :? :? "Okay!!" 2600 PREHEAT$="Y" 2610 BAKETEMP=AMOUNT 2620 GOSUB 10000:RETURN 3000 REM Handle unitless ingredients, like eggs 9998 REM 9999 REM ---------------------------- 10000 REM Sub to get RETURN press 10001 REM ---------------------------- 10002 REM 10010 POSITION 7,23 10020 PRINT " Press RETURN to continue "; 10030 OPEN #1,4,0,"K:" 10040 GET #1,A 10050 IF A<>155 THEN GOTO 10040 10060 CLOSE #1 10070 RETURN