A.N.A.L.O.G. ISSUE 78 / NOVEMBER 1989 / PAGE 60
I teethed on the Atari Assembler/Editor (Asm/Ed) cartridge way back in 1982. Now, nearly seven years later, it is becoming more popular than ever. Why? Well, I have seen several mail order ads over the past year offering Asm/Ed for only $10, and sometimes even as little as $5. But there’s a hitch: no documentation. A lot of Atari cartridges were left over from the Warner days when the Tramiel family took over the company. It seems that thousands of cartridges were sold “by the pound,” with no boxes or documentation, just to clear out the warehouses. Now many of you Atarians have decided that Asm/Ed was too good to pass up at such a low price, because you just know that someday you will learn assembly language programming.
This article is for you, the Atarian who took the plunge and bought the Asm/Ed cartridge without documentation. I won’t pretend to teach you how to write your own assembly language programs (although I’m bound to toss in a bit of free advice along the way); our Boot Camp series will do that job well enough. I hope to give you a quick comparison between BASIC and assembly language to illustrate the major speed differences. I will also cover the mechanics of writing a USR routine to help speed up BASIC programs and a stand-alone assembly program that may be executed from DOS.
The Asm/Ed cartridge will work with any DOS (disk operating system) from Atari DOS 2.0S all the way through DOS-XE and even the SpartaDOS X cartridge. The Asm/Ed cartridge itself is made up of three basic components: the Editor, Assembler and Debugger. I will present a quick reference for all the commands of each section of the cartridge and then lead you through the creation of your first program.
After booting your Atari with Asm/Ed installed and control is sent to the cartridge; the EDIT prompt will appear. From here you can begin entering your assembly “source code” with line numbers, assembly mnemonics and comments. The editor is line oriented, requiring line numbers. They are used for reference while editing only and are not used as part of the program itself, as they are in BASIC.
NEW—The NEW command clears all assembly source code from memory, providing a clean slate for entry of a new program. NEW will irrevocably erase your program from memory, so always be certain to save important code before using this command. I highly recommend using a comment as the first line of every program, similar to the following:
It’s easy to forget what file you are working on, or make a typographical error when using the LIST command. Saving a file to the wrong place can ruin a lot of work in a big hurry. By placing the LIST command, followed by the correct file name, in a comment, you may simply display that line and use the full-screen editor to eliminate the line number and comment character (the semicolon), press Return, and execute the proper save command every time. This good habit has saved me countless hours of frustration. Use it!
DEL—This command deletes lines of code. The format is DEL xx, where xx is the line you wish to delete, or DEL xx,yy, where xx is the first line of a block you wish to delete and yy is the last line. Example: DEL 50,100.
NUM—Number lines automatically, for fast entry of source code. If no starting number is specified, the program begins with the last line number currently in your program, plus 10. Type your code and comments, pressing Return when each line is complete. Press Return on a blank line to stop auto-entry of code. Do not use fullscreen editing functions to change lines previously entered while still in the NUM mode. This will give unpredictable results. The command format is NUM, to increment by 10 after each line; NUM nn, to begin line entry at Line nn with an increment of 10; or NUM nn,mm, which forces the next statement number to be nn and the increment to be mm. Example: NUM 100,20.
The last command format may be used to insert new lines between code that already exists (e.g. NUM 11,1 to enter up to nine lines of code between Lines 10 and 20).
REN—Renumber the file. It will resequence all the line numbers of the source file. The command format is REN, to renumber starting with 10 and using an increment of 10; REN nn, to renumber with an increment of nn starting with 10; or REN nn,mm, to renumber with an increment of nn starting with mm. Example: REN 100,10.
When inserting a lot of new code, REN may be used to space the line numbers wider apart, thus allowing entry of more new code with the NUM entry method between consecutive lines of source code.
FIND—The FIND command can be used to help locate any string of text anywhere in the program. This command format is FIND/string/, to find the first occurrence of string, and display the line it is found on; FIND/string/A, to find all occurrences of string. Each line is listed to the display as it is located. Pressing Control-1 will halt screen scrolling; FIND/string/xx to find string on Line number xx. The line is listed, if found; or FIND/string/xx,yy,A, to find all occurrences of string from Lines xx through yy, inclusive. Example: FIND/LABEL1/1000,5000,A.
Note that when searching, the line numbers themselves are ignored (you don’t search the line numbers, when looking for a particular number). In the examples above, the string of interest is delimited by the slash (/) character. Any matched pair of characters may be used as delimiters. The following would be used to find all occurrences of the slash character in your program:
REP—Replace strings in the file. The command format is REP/oldstring/newstring/, to replace the first occurrence of oldstring with newstring; REP/oldstring/newstring/,A, to replace all occurrences of oldstring with newstring (use the “all” option with care); REP/oldstring/newstring/xx,yy, to replace the first occurrence of oldstring with newstring in the line number range xx to yy; REP/oldstring/newstring/xx,yy,A, to replace all occurrences of oldstring with newstring in Lines xx through yy; or REP/oldstring/newstring/xx,yy,Q, to replace with query. You will be prompted to press “Y” for each replace. Example: REP/LDA #$02/LDA #$04/100,200,Q.
LIST—The LIST command is used to display, print or save assembly source code. The format is LIST, to list the entire program to the screen; LIST nn,mm, to display Lines nn through mm; LIST #P:, to send the entire source program to the printer; LIST #C:, to list the source code to cassette in ASCII form; LIST #D:filename.ext to list the entire program to the file filename.ext on Disk Drive 1; LIST #D:file-name.ext,nn,mm to list Lines nn through mm to the file filename.ext. Example: LIST#D:MYPROG.SRC,100,1000.
Any filename or device specification may be followed by the line number range specification.
PRINT—The PRINT command functions exactly like LIST, except the line numbers are not output. Since assembly source files require line numbers, it won’t be very useful to PRINT your program to disk and attempt to ENTER it later. This would hopelessly confuse Asm/Ed. Always LIST source code to disk.
ENTER—The ENTER command is used to retrieve a previously listed source file. A valid input device must be specified such as ENTER#D:MYFILE.ASM or ENTER#C:.
If you wish to merge a program, append a “,M” to the ENTER command, like ENTER #D:ROUTINES.ASM,M.This merge works the same as the following sequence would in Atari BASIC:
LOAD "D:MYPROG.BAS" ENTER "D:NEWSUBS.LST"
The lines are merged. If any line numbers in the file to be merged match those of the file already in memory, the merge file takes precedence. If you wish to append a file to a current working program, it may be best to ENTER the merge file first, renumber it with some large range such as 20000,1 and then LIST it out to a temporary file. Then ENTER your main program, and finally ENTER with the merge option this renumbered file.
SAVE—Use the SAVE command to write a block of memory, such as an object program, to a file. Let’s say your program begins at $4000 (with an =$4000 at the top of your assembly code). After the ASM command, you see the final address was $41FE. Then, to create a binary image of this file, which may be loaded and run later, enter SAVE #D:MYFILE.OBJ x 4000,41FE.
Note that the addresses are always assumed to be in hexadecimal, and you do not specify a dollar sign ($) to indicate this on the SAVE command line. You may also SAVE to the cassette (#C:). With the proper ASM command, your object files may be created automatically, as we will see. Note that you may assemble your program in memory, and then go to DOS and use the DOS memory save command to create this object file as well. (The advantage of the DOS memory save command is that you can specify the RUN address as well, so that your program automatically executes when you perform a binary load. There are ways to set this up with the ASM command as well.)
In BASIC, filenames are enclosed by quotes, such as SAVE "D:MYFILE.BAS". In Asm/Ed, the filename is preceded by the pound sign (#); no quotes are used. Filenames you may use are shown here:
E: The screen editor, used by default with some commands such as LIST.
#P: Refers to the printer.
#C: This is used in reference to the Atari program recorder.
#Dn:filename.ext This is a disk file. The n is the drive number, which may be from 1 to 8, depending on the DOS and drive configuration you employ. If no drive number is specified, drive 1 is assumed. The name of the file may be up to eight alphanumeric characters, followed by a period and an optional three-character extender. The extender may be anything you wish. ASM or SRC is generally used for assembly source files, and OBJ or COM for executable object files.
ASM—Once you have created your assembly source code and listed it to a file for safe keeping, it is time to assemble it. This is Asm/Ed’s primary function, to convert your source code into executable object code. When you issue the ASM command, the current file in memory is scanned for syntax errors. If it understands all your source code, all the assembly mnemonics are converted into equivalent binary codes and written to memory or a file.
Care must be taken that your code assembles to an area of memory that does not conflict with your source code. Before assembling a program for the first time always enter the SIZE command. Three hexadecimal numbers will be displayed, such as:
10F4 1345 9C1F
The first number indicates where in memory your source code begins, just above DOS’s basic memory requirements. The second number is where, in memory, your source code ends. The final address is the top of usable RAM. At the top of your program will always be a statement similar to the following:
This tells the assembler to start building your object code at memory location 4000 hexadecimal, the program origin. This address may be any number between the second and third numbers reported by the SIZE command, with some notable limitations, when assembling to memory. It may be any value above the first number, so long as you assemble to a file.
If you wish for your program to assemble into lower memory, you may use the LOMEM command. This must be the first command entered, once you start up Asm/Ed and receive the EDIT prompt. The format is LOMEM xxxx, where xxxx is the hexadecimal address to set the new low memory value. This is the first address value reported by the SIZE command detailed above. For example, if you want your program to load at address $2400, and you know the object code will be 4K or less, then use LOMEM $3400 ($1000 is 4K bytes). Then ENTER your program, and use =$2400 at the top of the file to set the origin. Then the program may be assembled in RAM to RAM safely, so long as your object code does not grow beyond 4K.
If you plan to write stand-alone assembly programs, which may be loaded from DOS with the binary-load command (option “L”), I recommend an origin of $3400. This will set the start address of your code above both DOS.SYS and DUP.SYS RAM in Atari DOS, any version through DOSXE, as well as any version of SpartaDOS.
Unlike BASIC, you must manage memory yourself. If your program origin is too close to the second number from the SIZE command, the assembler may get confused. The assembler must build a symbol table and assign some temporary storage as it processes your source code. It starts building this information from the end of your source code and grows upward. If the symbol table runs into the area where the object code is being stored in RAM, the assembler is likely to generate a lot of erroneous phase errors. If your origin is set too high, your object code will run into display memory and eventually run out of room.
These problems may be avoided in several ways. The general form of the ASM command is ASM #D:SOURCE,#D:LIST# D: OBJECT.
The first filename in the ASM command represents the file your assembly source code is stored in. This allows you to assemble from disk (but not cassette, since Asm/Ed requires multiple passes through the file). If this field is empty, simply place a comma immediately after the ASM command; then the source code is assumed to be in memory. The second filename specifies a listing file, where a complete “assembled listing” is routed. This will usually be the printer (#P:). If this field is left empty (you must still include the comma, though), the listing goes to the screen. The listing always goes somewhere; it cannot be turned off as it can in MAC/65. However, assembler directives may be used to control the output of a listing, as we shall see. The third field is the filename where the object code will be stored. If this field is not specified, your program is assembled to memory. Always make a current listing of your program on disk or cassette before issuing the ASM command. If you have set up memory mapping improperly, the source code will get clobbered in a big hurry.
Directives, or pseudo operations (pseudoops), are special instructions to the assembler. They can be used to control listing format, program title for listing, allocation of memory, and more. In general, assembler directives begin with a period (.), followed by some key word and associated parameters.
OPT—The options directive controls assembler output. They are .OPT NOLIST, to suppress the output of the listing during assembly; .OPT LIST, to output assembly listing (default); .OPT NOOBJ, to not generate object code during assembly; .OPT OBJ, to output object code (default); .OPT NOERR, to not display error messages while assembling (there is no good reason to ever use this option); .OPT ERR, to display error messages when assembling (default); .OPT NOEJECT for no margin at the bottom of each page when outputting the listing; .OPT EJECT, to skip four lines at the end of each page (default).
More than one option may be placed on a single line, such as .OPT NOLIST,NOOBJ. Note that the MAC/65 assembler defaults to .OPT NOOBJ; it does not generate object code unless explicitly told to with the .OPT OBJ directive. Asm/Ed is just the opposite. Whenever you are assembling your program frequently, working out syntax and undefined label errors, it is generally wise to have a .OPT NOOBJ near the top of your program. When you are ready to generate code and start test running it, then change it back to .OPT OBJ.
TITLE and PAGE—The title and page directives are designed to make your assembly listings easier to read. The title directive is generally used to specify the name of your program, revision and date. The page directive can be used to force a page break and optionally output some text. For example:
10 .TITLE "Attack Of The Dweebies" 20 .PAGE "Program equates" . . . 200 .PAGE "Graphics Routines" . . . 300 .PAGE "High Score Routine"
TAB—The TAB directive is used to set the spacing of the fields of your assembly code for listings. The command format is .TAB 12,17,27 This example illustrates the defaults used by Asm/Ed. These may be set to any position you find most suitable for your printer listings. The first number indicates the field where the mnemonics (assembly op codes) will appear, the second for the operands, and the third for the comment field. For example, suppose your program has a lot of long labels with a maximum of 15 characters. Then you may wish to set the tabs as .TAB 20, 25, 40, which would make for a prettier listing on the printer.
BYTE, DBYTE and WORD—The BYTE, DBYTE, and WORD directives are used to reserve storage in memory, similar to variables in BASIC. Labels may be associated with these directives for easy reference. For example:
100 LDA STORAGE 110 LDX STORAGE+1 . . . 500 STORAGE .BYTE 34,$45
In the above the statement, Line 100 will fetch the first value at location STORAGE, which is the number 34 following the BYTE directive. In Line 110 the X register will receive the data value 45 hexadecimal. Note that the assembler will perform the address arithmetic STORAGE+1 automatically. The BYTE directive may also be used to reserve storage for strings:
100 LDA #STRING/$100 110 LDX #STRING&$FF . . . 320 STRING .BYTE "This is a test",155
In Line 320 the BYTE directive reserves storage for a string initialized to “This is a test” followed by a 155 (ATASCII carriage return). The code in Lines 100 and 110 fetches the address of the label STRING, placing its address high byte in A and the low byte in X. This technique is commonly used to pass the address of data collections (such as strings or data tables) to subroutines.
The DBYTE directive reserves two consecutive memory locations, generally used for numbers greater than 256, in high-byte low-byte order. For example:
1000 DATA .DBYTE 258
The above will result in two bytes of memory being reserved at location DATA, with the values 1 and 2 respectively (1256 + 2 equals 258). Addresses are stored in low-byte high-byte format as expected by the 6502 microprocessor. The WORD directive is used for this, such as:
100 *=$3400 110 START LDA #45 . . . 290 RTS ;End of program 300 *=$2E0 310 .WORD START
In Line 100 the origin of the program, or program counter, is set to 3400 hexadecimal. The first line of code, with the label START, will then be assembled into your computer’s memory at $3400. At Line 300 the program counter is reset to $2E0. At Line 310 we have the WORD directive, immediately followed by the label START. The assembler will `backtrack` as it processes your source code, realize that START refers to memory location $3400, and place this value (low-byte high-byte order) in memory at $2E0, $2E1 respectively. This is a special location, commonly referred to by name as RUNAD in Atari memory maps. When you assemble a program to disk which will be loaded and run from DOS, you use the above technique to set the run address of your program. When the program ends with an RTS, control is returned to DOS. Most game programs do not end, but you will use this technique for many utilities. As we will see later, a BRK instruction is used, instead of RTS, when testing programs from Asm/Ed’s debugger.
Label Directive—You do not have string, integer and floating point variables in assembly language, the way you do with Atari BASIC. As we saw above, you must set up your own storage and interpret it properly. There are no automatic mechanisms in assembly language for managing variables. To make life easier, you will want to attach meaningful labels to constant values, such as:
10 RUNAD = $2E0 . . . 1000 *=RUNAD 1010 .WORD START
It is much easier to tell from this example that the intended run address of our program is defined at the label START. In the previous example for the WORD directive, we simply had the number $2E0. Unless you want to memorize a lot of memory locations, employ meaningful labels wherever practical.
Labels are used for reference when you want to GOTO (JMP) or GOSUB (JSR) in assembly language. For example:
10 PROMPT .BYTE "PRESS RETURN TO CONTINUE",155 . . . 100 LDA #PROMPT/$100 110 LDX #PROMPT&$FF 120 JSR PRINTSTRING . . . 500 PRINTSTRING STA ICBADR+1 510 STX ICBADR
Labels may also be used in branch instructions such as:
50 CONTINUE LDA TABLE,X . . . 100 DEX 110 BEQ EXITLOOP 120 BNE CONTINUE 130 EXITLOOP STA RESULT
In the above example, we have set up a loop, similar to a BASIC FOR/NEXT loop, between the labels CONTINUE and EXITLOOP. In Line 100 the X register is decremented by 1 (we assume it was initialized by some code previous to Line 50). If the result of the DEX instruction is zero (BEQ) then control will be passed to EXITLOOP. If the X register has not gone to zero (BNE) the control is sent back up to CONTINUE. As a result of DEX, the zero flag can only be set (BEQ) or cleared (BNE), so we have exhausted the possibilities. It would have been equally valid to use:
120 JMP CONTINUE
Generally, whenever you have the choice between a JMP and Bxx (branch) instruction, use the branch. It will require less memory and work faster. The problem is that a branch is limited to plus or minus 127 bytes from the current position. If you try to branch too far, you will get an assembly error. Then JMP instructions, or combinations of JMP and branch instructions may be required.
Origin Directive—We have already used the origin directive “[ ]=” in many of the previous examples. This tells the assembler “set the program counter to the following address.” The address may be some number, or a label, or some expression (so long as the assembler may resolve it to a fixed value). Some examples are:
100 *=$3400 300 START = $4400 310 *=START . . . 500 *=START + 439 . . . 600 HERE *=*+45
Take note of the spacing used in all of our examples. Any label always begins one space after the line number, referred to as the label field. The op code field begins at least one space after the start of the label field. If a line of code has no label on it, then your assembly mnemonics may begin two spaces after the line number. At least one space after the op code field will begin the operand field. This field is optional since not all assembly mnemonics have an op code (such as DEX or INY). Anything after the operand field is ignored by the assembler and assumed to be the comment field. A comment can take up an entire line, when the label field begins with a semicolon.
IF—The IF directive is used for “conditional assembly.” This may be used to enable or disable the generation of some test code, for example, based on the value of a number, label or expression. For example:
10 DEBUG = 0 ;Enable test code . . . 300 IF DEBUG @ENDOFDEBUG 310 ; Debugging test routines . . . 500 ENDOFDEBUG
If the expression (DEBUG above) is equal to zero, then everything from the line following the IF directive to the specified label (ENDOFDEBUG) is assembled. When you are satisfied that your code works, don’t throw away all that useful testing code. Simply change Line 10 to DEBUG=1 and reassemble your program. If you do not understand the use of conditional assembly, don’t worry. I have only used it a few times in the past seven years, and generally you don’t need it at all.
END—The Asm/Ed manual recommends that every program have one .END directive, as the last line. It really isn’t necessary, since the assembler knows when to stop (it runs out of source code to assemble). If you place an .END in the middle of your program inadvertently, all code after it will be ignored and not assembled. I seldom use a .END in any of my assembly code.
Next month, we’ll finish up our quick reference to the Atari Asm/Ed cartridge by discussing error codes, expressions and the debugger, among other things.
A.N.A.L.O.G. ISSUE 79 / DECEMBER 1989 / PAGE 12
Your assembly language programs are bound to have some bugs. Luckily, Asm/Ed provides a method for testing assembled object code. When at the Asm/Ed EDIT prompt, type BUG and press Return. You’ll be presented with the DEBUG prompt.
The debugger uses short one-or two-letter commands, some followed by an optional hexadecimal address. Below is a complete list of the commands.
X—Exit from the debugger.
DR—Display the contents of the 6502 registers:
DR A=00 X=10 Y=20 P=B0 S=DF
A is the accumulator, X and Y are the index registers, P is the processor status register (which includes the carry flag, zero flag, etc.) and S is the stack pointer.
CR—Change the contents of any of the 6502 registers.
The specified values are stored in the registers in the same order the registers are displayed by the DR command. In the above example, the accumulator is unchanged, the X register receives a 1, the Y register a 2, the status register remains unchanged and the stack pointer is adjusted to DE.
“D3000,0” displays memory location $3000. When the second parameter is less than or equal to the first, only one location is shown. “D3000,3010” displays memory from locations $3000 through $3010. Enter D by itself, and the next eight locations (in this case, $3011 through $3018) will be displayed. If only the second parameter is omitted, a default of eight memory locations is displayed:
D3000 3000 10 40 28 22 14 11 12 FE
Note that the output of the debugger is always in hexadecimal. All input addresses and register values must be specified in hex as well.
The command itself is immediately followed by the starting hexadecimal address to change. The values to be placed in memory, starting at the first location, are separated by commas. Two consecutive commas tell the debugger to skip over that memory location, leaving it unchanged. In the above example, memory location $3034 receives $21, $3035 receives $23 and $3037 gets $2E.
The above tells the debugger to move memory from locations $0700 through $0800 to memory beginning at location $0600. The destination address ($0600 in this case) must be less than the first source address ($0700) or greater than the ending source address ($0800). If the source and destination areas of memory overlap, you may get unexpected results.
V—Compare two blocks of memory (verify).
You might, for example, use this to compare two slightly different versions of the same program to see where something has changed.
The above command tells the debugger to compare memory from $7100 through $7123 to memory at $7000. Any memory locations that do not match are shown side by side:
In the above comparison, all memory from $7100 through $7123 matched memory from $7000 through $7023, except at one location. Memory location $7101 contained a $0, while $7001 contained a $21.
L—List memory with disassembly.
The “L” command is one of Asm/Ed’s most powerful. It can be used to disassemble your operating system ROM (beginning at $0000) to see what some of the routines look like. It can be used to disassemble object files loaded into memory to see how they work. Here are some examples:
L7000—List memory with disassembly (as many lines that will fit on the screen) beginning at $7000.
L—List memory with disassembly starting at the next location (picking up where the previous L command left off).
L7000,0 or L7000,7000—List and disassemble $7000 only.
L2300,2400—List and disassemble memory from $2300 through $2400.
When the debugger comes across data that cannot be disassembled (such as data tables or strings, for example), it will print a series of question marks. Otherwise, the data is shown in hexadecimal, as well as in its equivalent assembly mnemonic form:
L50000,00 A9 8A LDA #$8A
A—Assemble a single instruction.
This comes in handy when you want to test a small patch to a program. Simply type A and press return to get into the singleline assembly mode. You must first specify an address, followed by a less-than character (<) and the assembly instruction. To assemble to successive memory locations, subsequent entries require only the less-than character followed by the assembly instructions. For example:
A 5001<LDY $1234 5001 AC3412 <INY 5004 CB
In the above example, we have assembled LDY $1234 and INY into consecutive memory, starting at $5001. Note that here your assembly instructions must use the dollar sign to indicate hexadecimal. Press Return on an empty line to exit the mini-assembler. You cannot refer to labels in the program, since the debugger doesn’t keep track of them. If you do not know the absolute address of a needed label, then you should go back to the source code, make the changes there and reassemble.
G—Execute instructions beginning at a particular address.
Type the letter “G” followed by the first execution address. The program will continue to run until the system crashes, you press the break key, or a BRK (break) instruction is executed.
Type the letter “T” followed by the address at which to begin execution. The instruction will be executed, immediately followed by a dump of the instruction (list a single line with disassembly) and the CPU registers. This continues until a BRK instruction is executed or you press the break key.
S—Step singly through instructions.
Sometimes you need to test a single instruction at a time. The debugger’s step command is used for this task. Enter “S” followed by the address to begin execution. The effects are the same as the TRACE command, except the debugger stops execution after each assembly instruction. Type “S” and Return repeatedly to continue single-stepping through the program.
X—Exit the debugger and return to Asm/Ed’s editor.
The error codes between 128 and 255 are the same as those in your Atari BASIC reference manual. These are generally input/output errors associated with CIO (central input/output) utility operations, the heart of your Atari’s operating system. There are 19 other error codes that you may encounter while assembling or debugging your programs:
1—The memory available is insufficient for the program to be assembled.
2—For the command “DEL xx,yy,” the line number xx cannot be found.
3—There is an error in specifying an address (mini-assembler).
4—The file named cannot be loaded (wrong file format).
5—Undefined label reference (you probably misspelled a label in your program).
6—Error in syntax of a statement (missing operand or misspelled assembly mnemonic).
7—Label defined more than once.
8—Buffer overflow. (I’m not certain what this means.)
9—There is no label or “*” before “=”. (An equals sign was found in the first field of a line of code. All equals signs must be preceded by either a valid label or an asterisk.)
10—The value of an expression is greater than 255 where only one byte was required. (e.g., LDA #LABEL, where label is an address of some memory location greater than 255).
11—A null string has been used where invalid.
12—The address or address type specified is incorrect (e.g., LDA (PGZRO),Y would result in this assembly error if the label PGZRO was not an address of a memory location less than 256).
13—Phase error. An inconsistent result has been found from pass 1 to pass 2 (e.g., two bytes were reserved for some label on the first pass, but on the second pass only one byte was needed. This is avoided by minimizing forward references and by defining all known labels at the top of the file before any assembly code. You will get this error a lot as you learn the language).
14—Undefined forward reference (e.g., misspelled label or reference to a label not defined).
15—Line is too large.
16—Assembler does not recognize the source statement.
17—The line number is too large (32767 is maximum).
18—LOMEM command was attempted after other command(s) or instruction(s). LOMEM, if used, must be the first command after entering the Asm/Ed editor.
19—There is no starting address (e.g., you forgot the directive at the top of your program).
The assembler can perform many useful computations for you. The operators recognized and the operations they perform are as follows:
+ Addition - Subtraction * Multiplication / Division & Logical AND
Expressions may not contain parentheses, and they are always evaluated left to right. (There is no precedence placed on operators.) Some examples follow:
100 STORAGE = $4000 110 *= STORAGE + $10 200 JMP START+20 300 LDA t#STORAGE&$0FF 310 LDX #STORAGE/$100 320 LDA #3*15
The USR command of Atari BASIC allows you to call assembly language routines. These routines can perform special functions to vastly improve the performance of BASIC. For example, assembly USR routines may be implemented for Player/Missile graphics movement, sort algorithms or high-speed disk I/O functions.
Assembly code won’t normally be loaded as part of your BASIC program. It must be loaded using a routine in BASIC, placing the data values into strings or POKEing it into safe RAM, for example. You may place up to 256 bytes of assembly code into Page 6 (beginning at memory location 1536). If you do not use the cassette (C:), up to 128 bytes of code can go into Page 4 (beginning at memory location 1024), the cassette buffer. If your code is “position independent” (relocatable), it may be loaded into a BASIC string.
What is position-independent assembly code? Such a program may have no JMP or JSR instructions (with the exception of JSR’s to ROM addresses that are guaranteed not to move). So how do you implement loops? Use branch instructions. If your code gets much larger than 256 bytes, writing position-independent code can be difficult. The largest routine I’ve ever written of this type was 410 bytes long. You may also “relocate” your code. This requires a foreknowledge of all the JMP and JSR instructions in your code. You may then load the object code into a string, determine its starting address, and then POKE adjusted address values in for all the JMP and JSR instructions. This is no small task and is seldom used. Generally, your USR routines will be fairly small and can be written in a position-independent manner.
The format of a BASIC USR command is:
The first parameter, ADR, is the starting address of the assembly code you wish to execute. The values following are parameters that are passed to the assembly code on the system stack after being converted to integers. The variable A takes on an integer from memory locations $D4 and $D5 (low byte, high byte). This is how you return a value to BASIC.
Let’s write a USR routine to add two integers and return the result. Our BASIC program might look like this:
10 TRAP 1000 20 OPEN #1,4,0,"D:MYUSR.OBJ":REM Our USR Code in a file 30 TRAP 70 40 FOR I=1 TO 6:GET #1,A:NEXT I:REM Ignore 6-byte load header of file 50 I=1536:REM USR routine was assembled for Page 6 60 GET #1,A:POKE I,A:I=I+1:GOTO 60:REM End of file error will terminate our entry of the program 70 CLOSE #1 80 PRINT "INPUT NUMBER 1 ";:INPUT N1 90 IF N1<0 OR N1>65535 THEN ? OUT OF RANGE":GOTO 80 100 PRINT "INPUT NUMBER 2 ";:INPUT N2 110 IF N2<0 OR N2>65535-N1 THEN ? "OUT OF RANGE":GOTO 100 120 SUM=USR(15361,N1,N2) 130 PRINT "NUMBER ";N1;" PLUS ";N2;" EQUALS ";SUM 140 END 1000 PRINT "COULD NOT FIND USR ROUTINE FILE" 1010 PRINT "MYUSR.OBJ" 1020 END
Now we need to write an assembly language program with Asm/Ed that implements this USR routine. It will accept parameters Nl and N2 off the stack (two two-byte integers), add them, and return the result to SUM through memory locations $D4 and $D5. Our code might appear as shown in Listing 1.
Enter this program with Asm/Ed and execute the instructions in the first two comment lines. When you get an assembly with no errors, your file D:MYUSR.OBJ should be ready to test with the BASIC program.
Work at this until it performs as expected. As you become more adept at writing USR routines, you may wish to develop utilities for converting OBJ files into a series of BASIC DATA statements, so you can simply READ and POKE them without using messy file I/O to initialize the USR routine. It takes a relatively long time to install USR routines by poking them into memory or strings, but once in place, they execute amazingly fast.
You will find that USR routines are difficult to debug since you need to initialize and call them from BASIC. If you mess up the stack or some other operation, the computer usually crashes inexplicably. It isn’t easy to debug USR routines from DEBUG, because you will have to write sophisticated test routines to stuff all sorts of test values on the stack.
Sooner or later, you’ll get tired of USR routines (mostly because they are so difficult to debug). When you do, it is time to take the plunge into writing a stand-alone assembly language program. Then you will get into the complexities of keyboard input, screen output, disk I/O and printer output from the Asm/Ed environment. Complete libraries of routines, such as a “graphics package” that performs the equivalent of BASIC’s GRAPHICS, COLOR, PLOT and DRAWTO, will become a necessity. This is where ANALOG’s Boot Camp series will help the most. In the months to come you will learn everything from keyboard input to floating-point processing, all from the assembly language level.
As an example of a stand-alone assembly language program, and an illustration of its raw speed, we present the following demonstration. First, type this BASIC program and run it. While it executes (it will take about 12 minutes), read the remainder of this article to see how the same functions can be performed in assembly language:
10 DINDEX=88:REM Screen RAM pointer 20 SCREEN=PEEK(DINDEX)+256*PEEK(DINDEX+1) 30 FOR X=0 TO 255 40 A=X 50 FOR Y=0 TO 255 60 POKE SCREEN+Y,A 70 NEXT Y 80 NEXT X
At location DINDEX is a two-byte “pointer.” Memory locations 88 and 89 hold the address of the beginning of screen RAM. The equation in line 20 calculates the variable SCREEN, which we use as a direct pointer for the POKE in line 60. In our assembly language equivalent of the above program, this problem is even easier to solve. (This is seldom the case, however; most things are harder to do in assembly language. This demonstration is designed specifically to show the strengths and speed of assembly language.)
Next, two loops are set up. The inner Y loop is used to POKE the current value of X into the first 256 screen RAM locations. You will see these characters fill the top portion of your display. All ATASCII values from zero through 255 are POKEd, with the help of the X loop. The variable A was used simply for a more symmetrical comparison with the assembly code to follow.
Let this BASIC program run to completion. Time it carefully. When you finally get the READY prompt, reboot your computer with Asm/Ed and enter this equivalent assembly language program:
0 ;LIST#D:SCREEN.ASM 1 ;ASM,,#D:SCREEN.OBJ 2 *=$3400 3 RUNAD=$2E0 10 DINDEX = 88 ; Screen RAM pointer 20 ; We don't have to compute SCREEN; we use post indexed addressing 30 START LDX #0 ; Initialize variables for loops 40 LDY #0 50 STORE TXA ; Place screen character into A register 60 PUTIT STA (DINDEX),Y ; Place character on screen 70 INY ; Next screen location 80 BNE PUTIT ; Y register "wraps around" to zero after 255 90 INX 100 BNE STORE ; NEXT X 110 RTS ; Return control to DOS 120 *= RUNAD 130 .WORDSTART ; So we can l oad and run from DOS
Now execute the two commands in the first two comment lines at the top of the listing. If you get no assembly errors, you will have a file SCREEN.OBJ that is ready to load and run. Go to DOS and execute a binary load of the file SCREEN.OBJ. It will run immediately after loading and return control back to DOS after performing all 65,536 “POKES” of characters to screen memory. Did you catch it? You probably didn’t if you blinked. This version of the program takes barely a second to run! If you want to watch the show for a while and exit to DOS when a key is pressed, for example, modify your program as follows:
15 CH = 764 ; Keyboard buffer 101 LDA #255 102 CMP CH ; keypressed? 103 BEQ START ; Nope, loop 104 STA CH ; Yes, clear out key buffer and exit to DOS
List this version to disk and reassemble it. When loaded from DOS, it will POKE all those ATASCII patterns to the screen continuously until you press a key on the keyboard. To randomize the show, make these changes:
16 RANDOM = 53770 ; Always a random number here 50 STORE 60 PUTIT LDA RANDOM ; Get a random fill character 61 STA (DINDEX),Y ; Place character on screen/PX
Notice how I always added a meaningful label for each important memory location. Avoid the use of code, such as LDA 53770. The proper use of labels makes it much easier to see exactly what your program does and how it does it.
If you didn’t pay much attention to ANALOG’s “Master Memory Map” series, I strongly recommend that you go back and read it. Even if you do not understand it all, you will learn a lot. A good memory map is the key to unleashing all the power of your computer. As a 6502 assembly language reference manual, I use 6502 Assembly Language Programming by Leventhal. This is a general reference for the 6502 microprocessor and does not have any specifics on the Atari computer. It does detail all the 6502 assembly mnemonics and provides examples of multiply, divide and other useful routines.
When you find that Asm/Ed is too slow to suit your tastes as you build larger and more sophisticated programs, consider upgrading to MAC/65 (sold by ICD). This macro assembler supports the use of INCLUDE files, allowing you to easily import “canned” routines that have already been debugged. Its MACRO capabilities allow you to define high-level constructs that vastly simplify the development of assembly programs. With a good MACRO library (such as the MAC/65 Toolkit from ICD or QuickCode from Stardust Software), your assembly source code will resemble BASIC or some other high-level language while retaining all the power and speed of pure assembly language. MAC/65 is the fastest native 6502 assembler I have ever used, bar none. (Mad Mac for the Atari ST will assemble 6502 code at a speed that blows the doors off MAC/65; but that’s a whole new ball game.)
Welcome to the fast and complicated world of assembly language programming. I hope this guide will inspire you to put that inexpensive Asm/Ed cartridge to work on all those fantastic ideas that the old faithful Atari BASIC could never handle.
Matthew J.W. Ratcliff is an electrical engineer at McDonnell Aircraft in St. Louis, Missouri. An experienced assembly language, C and Ada programmer on IBM and main frame computers, he still enjoys developing new programs and articles for the 8 bit Atari home computer. He has been an Atari enthusiast since 1982.