A.N.A.L.O.G. ISSUE 36 / NOVEMBER 1985 / PAGE 97

Boot Camp

by Matthew J.W. Ratcliff

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.

How it works.

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:

BCCBranch on Carry Clear
BCSBranch on Carry Set
BEQBranch if EQual
BNEBranch if Not Equal
BPLBranch if PLus
BMIBranch 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.
FLAGSNZC
A, X or Y < Memory1*00
A, X or Y = Memory011
A, X or Y > Memory0*01
* 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 ifFollow compare instruction with:
For Unsigned #sFor Signed #s
Register is LESS THAN dataBCC TRUEBMI TRUE
Register is EQUAL TO dataBEQ TRUEBEQ TRUE
Register is GREATER THAN dataBEQ FALSE
BCS TRUE
BEQ FALSE
BPL TRUE
Register is LESS THAN or EQUAL TO dataBCC TRUE
BEQ TRUE
BMI TRUE
BEQ TRUE
Register is NOT EQUAL to dataBNE TRUEBNE 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

Boot Camp

by Karl E. Wiegers

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.

The graphics statement.

To begin, let’s see just what happens when a graphics statement is executed in Atari BASIC. Here’s what that simple statement causes:

  1. A block of RAM is allocated to hold the data to be displayed on-screen, and the entire block is zeroed (i.e., the screen is cleared). The size and location of this RAM block depend on the graphics mode.
  2. A “display list” is set up in RAM immediately below the screen RAM block. This tells the computer how many lines to show on the screen, what graphics mode to use for each line, where to find display data in memory, and gives other userful information. It’s different for each graphics mode.
  3. The five playfield-color registers at locations 708–712 decimal ($2C4–$2C8 hex) are set to their default values of 40, 202, 148, 70 and 0, respectively.
  4. The text cursor is enabled (turned on) by setting location 752 ($2F0) to 0.
  5. The screen tab positions are initialized to their default values.
  6. Various registers are set to tell the OS what graphics mode it’s in, whether or not a text window is present, where in RAM to find display data for the graphics screen and text window, and so on.
  7. The text cursor is moved to the top line of the screen (graphics 0) or text window (other modes), at the default left margin stop.
  8. A communications channel is opened to permit the display of text in graphics or text window regions. A second channel may be opened for printing or plotting on screens with other graphics screens.

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.

The IOCB.

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 #0Equate NameFunction
$342ICCOMCode for command requested by user.
$344ICBALLow byte of buffer address for device name, text to print, etc.
$345ICBAHHigh byte of buffer address.
$348ICBLLLow byte of buffer length; specifies number of bytes to be transferred in input or output operation.
$349ICBLHHigh byte for buffer length; if less than 256 bytes are involved.
$34AICAX1Auxiliary byte 1; used to specify kind of file access needed in open operation; controls screen clear and text window in graphics screen.
$34BICAX2Auxiliary 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.

The BASIC equivalent.

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

Boot Camp

by Karl E. Wiegers

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.

Plotting points and drawing lines.

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.

The BASIC equivalent.

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.

Conclusion.

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

Boot Camp

by Karl E. Wiegers

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.

Integer vs. floating point.

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.

FP storage format.

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.

The floating point routines.

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.

Table 1.
FLOATING POINT ROUTINES IN THE ATARI OS
NAMEADDRESSFUNCTION PERFORMED
AFP$0800Convert ATASCII string in LBUFF to FP number in FR0
FASC$D8E6Convert FP number In FR0 to ATASCII string in LBUFF
IFP$D9AAConvert integer number in FR0 to FP number in FR0
FPI$D9D2Convert FP number in FR0 to integer number in FR0
FSUB$DA60FR0 = FR0 - FR1
FADD$DA66FR0 = FR0 + FR1
FMUL$DADBFR0 = FR0 * FR1
FDIV$DB28FR0 = FR0 / FR1
EXP$DDC0FR0 = e ** FR0
EXP10$DDCCFR0 = 10 ** FR0
LOG$DECDFR0 = natural (base e) logarithm of FR0
LOG10$DED1FR0 = common (base 10) logarithm of FR0
SIN$BDA7FR0 = SIN(FR0) (in BASIC)
COS$BDB1FR0 = COS(FR0) (in BASIC)
ATAN$BE77FR0 = ATAN(FR0) (in BASIC)
SQR$BEE5FR0 = SQR(FR0) (in BASIC)
FLD0R$DD89Load FR0 from memory using X and Y registers
FLD1R$DD98Load FR1 from memory using X and Y registers
FST0R$DDA7Store value in FR0 from memory using X and Y registers
FMOVE$DDB6Move FP number from FR0 to FR1
ZFR0$DA46Set 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.

Conclusion.

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

Boot Camp

by Karl E. Wiegers

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.

Mixed-mode displays.

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.

Table 1.
ATARI GRAPHICS MODES
ANTIC ModeBASIC ModeScan Lines/Mode LineMode Lines/ScreenBytes/Mode Line
2082440
3NONE10about 1940
412 (XL)82440
513 (XL)161240
6182420
72161220
8382410
9444810
10544820
11629620
1214 (XL)119220
13729640
1415 (XL)119240
15*8119240
*GTIA modes also use ANTIC mode 15.

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:

  1. Decimal 112 (hex $70) is stored in 3 consecutive bytes. This tells ANTIC to skip twenty-four scan lines, to make sure the display is positioned properly on the monitor screen.
  2. There’s a byte equal to the ANTIC mode number of your first segment plus decimal 64 (hex $40), followed by the beginning address of screen RAM in low-byte, high-byte format. The addition of 64 to the ANTIC mode number identifies this byte as a “load memory scan” (or LMS) instruction. ANTIC looks at the next 2 bytes to find the screen RAM area.
    Okay, I’ll tell you the trick I postponed earlier. If your screen RAM region crosses a 4K boundary, you’ll need to put an extra LMS instruction in your display list, right where the 4K boundary occurs. Let’s not worry about LMS instructions for now, though.
  3. Next is a list of the ANTIC mode numbers for all the other mode lines in your screen display, in sequence, from the top of the screen to the bottom.
  4. Finally, we have a byte set to decimal 65 ($41), followed by the address of the beginning of your display list. The 65 is a jump instruction, and tells ANTIC where to go for the next display list instruction. In our case, it will always point to the first byte of the display list.

Our title screen.

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.

Table 2.
CONSOL VALUES
ValueButtons Pressed
0OPTION, SELECT, START
1OPTION, SELECT
2OPTION, START
3OPTION
4SELECT, START
5SELECT
6START
7none

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.

Epilogue.

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

Boot Camp

by Karl E. Wiegers

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.

Display list interrupts.

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.

Writing a DLI.

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!

Chasing your shadow.

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.
HardwareShadowPurpose
$D016–$D01A$2C4–$2C8Playfield colors
$D012–$D015$2C0–$2C3Player/missile colors
$D409$2F4Character set base address
$D01B$26FPlayer priorities

DLI setup.

Besides writing the DLI routine itself, we have to tell the Atari what to do with it. There are three steps:

  1. Select the mode line on the screen where you’d like the interrupt to take place. Then go to the display list and set bit 7 of the display list byte after which the interrupt is to be executed. This is the same as adding 128 to the value of the display list byte in question.
  2. Store the starting address of your DLI routine into locations $200 and $201 (low-byte, high-byte), called VDSLST.
  3. Enable DLIs by storing a decimal 192 into address $D40E, also known as NMIEN (nonmaskable interrupt enable).

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:

LDA ($CD),Y

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.

Sneak preview.

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

Boot Camp

by Karl E. Wiegers

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.

PMG primer.

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.

Getting started.

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.

Table 1.
Usage of the RAM block allocated to player/missile graphics, in bytes offset from PMBASE.

Function
Double-Line
Resolution
Single-Line
Resolution
Unused0 - 3830 - 767
Missiles384 - 511768 - 1023
Player 0512 - 8391024 - 1279
Player 1640 - 7671280 - 1535
Player 2768 - 8951536 - 1791
Player 3896 - 10231792 - 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.

Table 2.
Important PMG registers.
NameHex AddressFunction
SDMCTL$022F62 for single-line, 46 for double-line resolution
PCOLR0–3$02C0–$02C3color of players 0–3
HPOSP0–3$D000–$D003horizontal positions of players 0–3
SIZEP0–3$D008–$D00Bplayer size: 0=normal, 2=double, 3=quadruple
GRACTL$D01Dstore 3 to enable PMG, to disable
PMBASE$D407store high byte of PMBASE here

Example: an alien and his car.

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.

Things to come.

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

Boot Camp

Reading the Joystick and smoothing out player movement via vertical blanks.

by Karl E. Wiegers

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.

Vertical blank interrupts.

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.

Deciphering the joystick.

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.

Assembly listing.

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.

The BASIC bonus.

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

Boot Camp

by Karl E. Wiegers

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.

The many faces of Bonzo.

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.

Handling collisions.

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.

Table 1.
Missile/Playfield and Player/Playfield Collision Registers
AddressColor Reg. 0Color Reg. 1Color Reg. 2Color Reg. 3
Missiles
$D000, M0PF1248
$D001, M1PF1248
$D002, M2PF1248
$D003, M3PF1248
Players
$D004, P0PF1248
$D005, P1PF1248
$D006, P2PF1248
$D007, P3PF1248

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.

Table 2.
Player/Playfield and Player/Missile Collision Registers.
AddressPlayer 0Player 1Player 2Player 3
Missiles
$D008, M0PL1248
$D009, M1PL124
$D00A, M2PL1248
$D00B, M3PL1248
Players
$D00C, P0PL0248
$D00D, P1PL1048
$D00E, P2PL1208
$D00F, P3PL1240

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.

PMG wrap-up.

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.

Some programming pointers.

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

Boot Camp

by Karl E. Wiegers

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…

Character set fundamentals.

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

Memory usage.

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 AddressATASCII ValuesPrincipal Contents
$E00032–63numbers, punctuation marks
$E10064–95capital letters
$E2000–31control characters
$E30096–127lowercase 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?

Redefining the character set.

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.

Sequential disk access with CIO.

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.

Now you do some work.

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.

Colored characters in graphics 0.

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.

Aliens reprise.

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.

Yet to come…

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

Boot Camp

Another step along on the animation trail.

by Karl E. Wiegers

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…

Character animation.

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.

Setting the scene.

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)

Ready, set, animate!

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.

Wrapping up.

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

Boot Camp

Macromania

by Karl E. Wiegers

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!

I give up; what’s a macro?

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.

Macros vs. subroutines.

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.

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

Macro ecology.

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.

Macros with parameters.

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

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.

A macro of a different color.

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.

Son of Macromania.

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

Boot Camp

Son of Macromania

by Karl E. Wiegers

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.

Macro refresher.

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!

A macro library.

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.

Example 1.—Text.

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.

OPEN.

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?

GRAPHICS.

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.

Macros calling macros.

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.

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.

PRINT.

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)?

CLOSE.

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.

Example 2.—Graphics.

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.

COLOR.

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.

PLOT.

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.

DRAWTO.

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.

Tarot cards.

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

Boot Camp

Everywhere a macro, macro.

by Karl E. Wiegers

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.

X-I, X-I-O.

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:

XIO command,#IOCB,aux1,aux2,filespec

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.
NameHex CodeFunction.
OPEN$03Open an IOCB.
GET RECORD$05Input a record up lo EOL or end of buffer.
GET CHARACTERS$07Input a specified number of characters.
PUT RECORD$09Output a record up to EOL or end of buffer.
PUT CHARACTERS$0BOutput a specified number of characters.
CLOSE$0CClose an IOCB.
GET STATUS$0DReturn the status of a device or file.
DRAW$11Draw a straight line on a graphics screen.
FILL$12Fill a polygonal area on graphics screen.
RENAME$20Rename a disk file.
DELETE$21Erase a disk file.
LOCK$23Lock a disk file.
UNLOCK$24Unlock a disk file.
POINT$25Move file pointer to specific sector and byte.
NOTE$26See where file pointer is now.
FORMAT$FEFormat a disk

Example 1—Lockup.

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 CodeDecimalMeaning
11No error—everything’s cool.
80128BREAK key pressed during I/O operation.
81129IOCB is already in use.
82130Device specified does not exist.
83131Attempt to read from an IOCB opened only for output.
84132CIO command is invalid or syntax is bad.
85133Attempt to use an IOCB that isn’t open.
86134IOCB number isn’t in the range 1–7.
87135Attempt to write on an IOCB opened only for input.
88136End of file was reached.
89137Data record longer than 256 bytes was truncated.
8A138Device does not respond, causing a timeout.
8B139Device malfunction.
90144Disk is write protected, or directory is garbled.
92146Attempted an invalid operation on the device.
A0160Disk drive number error.
A1161Too many disk files are open at once.
A2162The disk is full.
A3163Unspecified fatal disk error.
A4164Disk file is garbled, or file pointer is not pointing to part of the open file.
A5165Filename error.
A6166POINT error—pointing to nonexistent byte number.
A7167File is locked.
A8168Attempt to use invalid or unknown CIO command.
A9169Disk directory is full (64 tiles maximunn).
AA170File is not found.
AB171POINT 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…

MacroMan to the rescue!

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…

Example 2—Disk gymnastics.

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!

Example 3—Fill me in.

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.

Example 4—Disk directory.

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:

Byte 1
*if locked, else blank
Byte 2
Blank
Bytes 3–10
Filename
Bytes 11–13
Extension
Byte 14
Blank
Bytes 15–17
Number of sectors occupied
Byte 18
End-of-line marker ($9B)

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

Boot Camp

Revenge of Macromania.

by Karl E. Wiegers

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.

Philosophy of the month.

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.

Format reprise.

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

New macros.

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

Readin’.

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.

Take a break.

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.

’Rithmetic.

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.

Poke two, they’re small.

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.

Loop-the-loop.

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.

Table 1.
Flags set by compare operations: CMP operand.
SituationNegativeZeroCarry
A < operand100
A = operand011
A > operand001

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.

Head ’em out, move ’em up.

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.

A demo program.

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

Boot Camp

RAMdisk file copier

by Karl E. Wiegers

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.

Getting started

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.

Analysis and design

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.

Picture perfect?

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.

Toolbox

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.

New macros

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.

Programming, at last!

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?

Autorunning

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.

Wrap-up

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

Boot Camp

by Karl E. Wiegers

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.

Insecticide

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.

Getting Started

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:

E:,D:RAMDISK.FIL/A

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:

D1:GRAPHICS.LIB

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

Graphics Shortcuts

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.

Revenge of Bonzo

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.

Vertical Blanking

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.

The Main Routine

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.

Closing Argument

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

Boot Camp

by Karl E. Wiegers

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

Storing Numbers

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.
CharacterASCII CodeBinary Value
0$300000
1$310001
2$320010
3$330011
4$340100
5$350101
6$360110
7$370111
8$381000
9$391001

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.

Interconverting ASCII and Binary

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.

ASCII to Integer

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.

Integer to ASCII

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.

Zero Zapper

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.

Decimal Pointers

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

Boot Camp

BCD N U

by Karl E. Wiegers

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.

Binary-Coded Decimal

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.

Interconverting ASCII and BCD

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.

ASCII to BCD

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.

BCD to ASCII

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.

BCD Arithmetic

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

Incrementing

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.

Adding 25

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.

Adding $25

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.

Adding Two Stored Numbers

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.

So What?

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.

Promise

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
Decimal Characters
Character ASCII
Value

Binary
Values
0$300000
1$310001
2$320010
3$330011
4$340100
5$350101
6$360110
7$370111
8$381000
9$391001
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
Byte
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
Byte

1246

1246
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
Byte

9:2

1008
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
Byte

72:;

7311
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

Boot Camp

Light Torch…Grid Loins…Boot Assembler

by Karl E. Wiegers

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.

The joy of parsing

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.

Dissecting the input

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.

Do I know you?

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.

A token gesture

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.

Vocabulary building

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.

The word quest

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

Conclusion

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.

Acknowledgement

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

Boot Camp

Karl’s Komputerized Kitchen

by Karl E. Wiegers

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.

Erratum

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.

Recipe Rules

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.

Warming Up

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.

VOCAB$
entire vocabulary data string.
TEMP$
temporary variable for reading from VOCAB.DAT.
WORD$
target word being searched for in VOCAB$.
PARSED$
command entered by user after removing unrecognized words.
ML$
machine-language code for the parser.
IN$
command string entered by user.
NUM$
string representation of any number present in the command entered.
A$
temporary variable.
INSTR$
string pseudo-array of valid instructions entered so far.
PREHEAT$
flag for whether oven is preheated or not.
NINS
number of valid commands entered so far.
AMOUNT
numeric form of number in NUM$.
INGRED
token value of any ingredient in command entered.
INSTRUCT
token value of any instruction in command entered.
TIMETEMP
token value if units for cooking time or temperature were included in command entered.
UNITS
token for ingredient units included in command entered.
BAKETEMP
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.

Batter Up. Hit or Miss?

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.

Now We’re Cooking!

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:

  1. Every command must include an instruction.
  2. Every command must include a number.
  3. The PREHEAT instruction must be entered before the BAKE instruction.
  4. The PREHEAT instruction must specify the oven temperature.
  5. The oven temperature must be between 300° and 500° .
  6. The BAKE instruction is the final instruction.
  7. The BAKE instruction must specify the cooking time.
  8. The cooking time must be between five minutes and ten hours.
  9. At least one ingredient must be added before baking.
  10. If an ingredient is specified, units also must be included.
  11. Exception to Rule 10: an ingredient whose token value is in the range 10 to 19 does not require units.

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.

Torch Light. Assembler Boot.

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.

The AI Angle

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