ML Equivalents of BASIC Commands

What follows is a small dictionary, arranged alphabetically, of the major BASIC commands. If you need to accomplish something in ML - TAB for example - look it up in this chapter to see one way of doing it in ML. Often, because ML is so much freer than BASIC, there will be several ways to go about a given task. Of these choices, one might work faster, one might take up less memory, and one might be easier to program and understand. When faced with this choice, I have selected example routines for this chapter which are easier to program and understand. At ML speeds, and with increasingly inexpensive RAM memory available, it will be rare that you will need to opt for velocity or memory efficiency.


In BASIC, this clears all variables. Its primary effect is to reset pointers. It is a somewhat abbreviated form of NEW since it does not "blank out" your program, as NEW does.

     We might think of CLR, in ML, as the initialization routine which erases (zeros) the memory locations you've set aside to hold your ML flags, pointers, counters, etc. Before your program RUNs, you may want to be sure that some of these "variables" are set to zero. If they are in different places in memory, you will need to zero them individually:

2000 LDA # 0
2002 STA 1990 (put zero into one of the "variables")
2005 STA 1994 (continue putting zero into each byte which needs to be initialized)

     On the other hand, maybe you've got your tables, flags, etc., all lined up together somewhere in a data table at the start or end of your ML program. It's a good idea. If your table is in one chunk of RAM, say from 1985 to 1999, then you can use a loop to zero them out:

2000 LDA # 0

2002 LDY # 15 
(Y will be the counter. There are 15 bytes to zero out in this example.)
2004 STA 1985,Y
(the lowest of the 15 bytes)
2007 DEY

2008 BNE 2004
(let Y count down to zero, BNEing until Y is zero, then the Branch if Not Equal will let the program fall through to the next instruction at 2010)


This word allows your program to pick up where it left off after a STOP command (or after hitting the system break key). You might want to look at the discussion of STOP, below. In ML, you can't usually get a running program to stop with the BREAK (or STOP) key. If you like, you could write a subroutine which checks to see if a particular key is being held down on the keyboard and, if it is, BRK:

3000 LDA 96
(or whatever your map says is the "key currently depressed" location for your machine)
3002 CMP # 13
(this is likely to be the RETURN key on your machine, but you'll want CMP here to the value that appears in the "currently pressed" byte for the key you select as your STOP key. It could be any key. If you want to use "A" for your "stop" key, try CMP #65.)
3004 BNE 3007
(if it's not your target key, jump to RTS)
3006 BRK
(if it is the target, BRK)
3007 RTS
(back to the routine which called this subroutine)

     The 6502 places the Program Counter (plus two) on the stack after a BRK.

     A close analogy to BASIC is the placement of BRK within ML code for a STOP and then typing. G or GO or RUN - whatever your monitor recognizes as the signal to start execution of an ML program - to CONT.


In BASIC, DATA announces that the items following the word DATA are to be considered pieces of information (as opposed to being thought of as parts of the program). That is, the program will probably use this data, but the data are not BASIC commands. In ML, such a zone of "non-program" is called a table. It is unique only in that the program counter never starts trying to run through a table to carry out instructions. Program control is never transferred to a table since there are no meaningful instructions inside a table. Likewise, BASIC slides right over its DATA lines.

     To keep things simple, tables of data are usually stored together either below the program or above it in memory. (See Figure 9-1.)

     From within the program, tables can be used to print messages to the screen, update or examine flags, etc. If you disassemble your BASIC in ROM, you'll find the words STOP, RUN, LIST, and so forth, gathered together in a table. You can suspect a data table when your disassembler starts giving lots of error messages. It cannot find groups of meaningful opcodes within tables.

Figure 9-1. Typical ML program organization with data tables-one at top or bottom of program.



 <----  Bottom of Memory
 <----  Start Of ML Program

 <----  Subroutines


With its automatic string handling, array management, and error messages, BASIC makes life easy for the programmer. The price you pay for this "hand-holding" is that a program is slow when it's RUN. In ML, the DIMensioning of space in memory for variables is not explicitly handled by the computer. You must make a note that you are setting aside memory from 6000 to 6500, or whatever, to hold variables. It helps to make a simple map of this "dimensioned" memory so you know where permanent strings, constants, variable strings, and variables, flags, etc., are within the dimensioned zone.

     A particular chunk of memory (where, and how much, is up to you) is set aside, that's all. You don't write any instructions in ML to set aside the memory; you just jot it down so you won't later use the reserved space for some other purpose. Managing memory is left up to you. It's not difficult, but it is your responsibility.


There are several ways to make a graceful exit from ML programs. You can look for the "warm start" address on your particular computer (in the map of its BASIC locations) and JMP to that address. Or you can go to the "cold start" address. This results in the computer resetting itself as if you had turned the power off and then back on again.

     If you went into the ML from BASIC (with a USR or SYS), you can return to BASIC with an RTS. Recall that every JSR matches up with its own RTS. Every time you use a JSR, it shoves its "return here" address onto the top of the stack. If the computer finds another JSR (before any RTS's), it will shove another return address on top of the first one. So, after two JRS's, the stack contains two return addresses. When the first RTS is encountered, the top return address is lifted from the stack and put into the program counter so that the program returns control to the current instruction following the most recent JSR.

     When the next RTS is encountered, it pulls its appropriate return (waiting for it on the stack) and so on. The effect of a SYS or USR from BASIC is like a JSR from within ML. The return address to the correct spot within BASIC is put on the stack. In this way, if you are within ML and there is an RTS (without any preceding JSR), what's on the stack had better be a return-to-BASIC address left there by SYS or USR when you first went into ML.

     Another way to END is to put a BRK in your ML code. This drops you into the machine's monitor. Normally, you put BRKs in during program development and debugging. When the program is finished, though, you would not want to make this ungraceful exit any more than you would want to end a BASIC program with STOP.

     In fact, many ML programs, if they stand alone and are not part of a larger BASIC program, never END at all! They are an endless loop. The main loop just keeps cycling over and over. A game will not end until you turn off the power. After each game, you see the score and are asked to press a key when you are ready for the next game. Arcade games which cost a quarter will ask for another quarter, but they don't end. They go into "attract mode." The game graphics are left running on screen to interest new customers.

     An ML word processor will cycle through its main loop, waiting for keys to be pressed, words to be written, format or disk instructions to be given. Here, too, it is common to find that the word processor takes over the machine, and you cannot stop it without turning the computer off. Among other things, such an endless loop protects software from being easily pirated. Since it takes control of the machine, how is someone going to save it or examine it once it's in RAM? Some such programs are "auto-booting" in that they cannot be loaded without starting themselves running.

     BASIC, itself a massive ML program, also loops endlessly until you power down. When a program is RUNning, all sorts of things are happening. BASIC is an interpreter, which means that it must look up each word (like INT) it comes across during a RUN (interpreting it, or translating its meanings into machine-understandable JSRs). Then BASIC executes the correct sequence of ML actions from its collection of routines.

     In contrast to BASIC RUNS, BASIC spends 99 percent of its time waiting for you to program with it. This waiting for you to press keys is its "endless" loop, a tight, small loop indeed. It would look like our "which key is pressed?" routine.

2000 LDA 96
(or wherever your machine's map shows that the "which key down" value is stored)
2002 CMP #255
(or whatever value is normally left in this address by default when no key is being pressed)
2004 BEQ 2000
(if it says "no key down," cycle back and wait for one)


Everyone has used "delay loops" in BASIC (FOR T=1 TO 1000: NEXT T). These are small loops, sometimes called do-nothing loops because nothing happens between the FOR and the NEXT except the passage of time. When you need to let the user read something on the screen, it's sometimes easier just to use a delay loop than to say "When finished reading, press any key."

     In any case, you'll need to use delay loops in ML just to slow ML itself down. In a game, the ball can fly across the screen. It can get so fast, in fact, that you can't see it. It just "appears" when it bounces off la wall. And, of course, you'll need to use loops in many other situations. Loops of all kinds are fundamental programming techniques.

     In ML, you don't have that convenient little counter ("T" in the BASIC FOR/NEXT example above) which decides when to stop the loop. When T becomes 1000, go to the instructions beyond the word NEXT. Again, you must set up and check your counter variable by yourself.

     If the loop is going to be smaller than 255 cycles, you can use the X register as the counter (Y is saved for the very useful indirect indexed addressing discussed in Chapter 4: LDA (96),Y). So, using X, you can count to 200 by:

 2000 LDX #200 (or $C8 hex)
 2002 DEX
 2003 BNE 2002

     For loops involving counters larger than 255, you'll need to use two bytes to count down, one going from 255 to zero and then clicking (like a gear) the other (more significant) byte. To count to 512:

2000 LDA # 2

2002 STA 0
(put the 2 into address zero, our MSB, Most Significant Byte, counter)
2004 LDX #0
(set X to zero so that its first DEX will make it 255. Further DEX's will count down again to zero, when it will click the MSB down from 2 to 1 and then finally 0)
2006 DEX

2007 BNE 2006

2009 DEC 0
(click the number in address zero down 1)
2011 BNE 2006

     Here we used the X register as the LSB (least significant byte) and address zero as the MSB. We could use addresses zero and one to hold the MSB/LSB if we wanted. This is commonly useful because then address zero (or some available, two-byte space in zero page) can be used for LDA (0),Y. You would print a message to the screen using the combination of a zero page counter and LDA (zero page address),Y.


Here you would just increase your counter (usually X or Y) more than once. To create FOR I =100 TO 1 STEP -2 you could use:

 2000 LDX # 100
 2002 DEX
 2003 DEX
 2004 BCC

     For larger numbers you create a counter which uses two bytes working together to keep count of the events. Following our example above for FOR-NEXT, we could translate FOR I =512 TO 0 STEP -2:

 2000 LDA # 2
 2002 STA 0  
   (this counts the MSB)
 2004 LDX # 0    (X counts the LSB)
 2006 DEX
 2007 DEX    
   (here we click X down a second time, for --2)
 2008 BNE 2006
 2010 DEC 0
 2012 BNE 2006

     To count up, use the CoMPare instruction. FOR I=1 TO 50 STEP 3:

 2000 LDX # 0
 2002 INX
 2003 INX
 2004 INX
 2005 CPX # 50
 2007 BNE 2002

     For larger STEP sizes, you can use a nested loop within the larger one. This would avoid a whole slew of INX's. To write the ML equivalent of FOR I =1 TO 50 STEP 10:

 2000 LDX #0
 2002 LDY #0
 2004 INX
 2005 INY
 2006 CPY #10
 2008 BNE 2004
 2010 CPX #50
 2012 BNE 2002


Each computer model has its own "which key is being pressed?" address, where it holds the value of a character typed in from the keyboard. To GET, you create a very small loop which just keeps testing the first address in the buffer.

     For Atari (in decimal):

2000 LDA 764
("which key pressed" decimal address. In advanced assemblers, you could freely mix decimal with hex, but not in the Simple Assembler.)
2003 CMP #255
(when an FF value is in this address, it means that no key is pressed)
2005 BEQ 2000
(keep going back and looking until there is some key pressed)

     For PET (Upgrade and 4.0) (in decimal)

 2000 LDA 151   ("which key pressed" decimal address)
 2003 CMP #255
 2005 BEQ 2000

     For PET (Original):

 2000 LDA 515   ("which key pressed" decimal address)
 2003 CMP #255
 2005 BEQ 2000

     For Apple II (hex):

 2000 LDA 0000  ("which key pressed" - note: this is in hex)
 2003 BPL 2000
 2005 STA C010 
(clears the keyboard)
 2008 AND #7F   (to give you the correct character value)

     For VIC and 64 (decimal):

 2000 LDA 197
 2003 CMP #255
 2008 BEQ 2000

     The Commodore computers have a GET routine similar to the one illustrated by these examples, which is built in at $FFE4 which can be used for all ROM versions (all models of CBM) because it is a fixed JMP table which does not change address when new BASIC versions are introduced. See your BASIC's map for Print a Byte to the Screen, GET a Byte, and other routines in the Commodore Jump Tables. They start at $FFBD.

     The examples above do not conform to PET BASIC's GET. In this version of BASIC, the computer does not "wait" for a character. If no key is being held down during a GET, the computer moves on and no GET takes place. In our ML GETs above, we loop until some character is actually pressed.

     For most programming purposes, though, you want to wait until a key has actually been pressed. If your program is supposed to fly around doing things until a key is pressed, you might use the above routines without the loop structure. Just use a CMP to test for the particular key that would stop the routine and branch the program somewhere else when a particular key is pressed. How you utilize and construct a GET-type command in ML is up to you. You can, with ML's flexibility, make special adjustments to use the best kind of GET for each different application.


This is nearly identical to BASIC in ML. Use JSR $NNNN and you will go to a subroutine at address NNNN instead of a line number, as in BASIC. ("NNNN" just means you can put any hex number in there you want to.) Some assemblers allow you to give "labels," names to JSR to instead of addresses. The Simple Assembler does not allow labels. You are responsible (as with DATA tables, variables, etc.) for keeping a list on paper of your subroutine addresses and the parameters involved.

     Parameters are the number or numbers handed to a subroutine to give it information it needs. Quite often, BASIC subroutines work with the variables already established within the BASIC program. In ML, though, managing variables is up to you. Subroutines are useful because they can perform tasks repeatedly without needing to be programmed into the body of the program each time the task is to be carried out. Beyond this, they can be generalized so that a single subroutine can act in a variety of ways, depending upon the variable (the parameter) which is passed to it.

     A delay loop to slow up a program could be general in the sense that the amount of delay is handed to the subroutine each time. The delay can, in this way, be of differing durations, depending on what it gets as a parameter from the main routine. Let's say that we've decided to use address zero to pass parameters to subroutines. We could pass a delay of "five" cycles of the loop by:

2000 LDA # 5

The Main Program
2002 STA 0

2004 JSR 5000

The Subroutine
5000 DEC 0

5002 BEQ 5012
(if address zero has counted all the way down from five to zero, RTS back to the Main Program)

5004 LDY # 0

5006 DEY

5007 BNE 5006

5009 JMP 5000

5012 RTS

     A delay which lasted twice as long as the above would merely require a single change: 2000 LDA # 10.


In ML, it's JMP. JMP is like JSR, except the address you leap away from is not saved anywhere. You jump, but cannot use an RTS to find your way back. A conditional branch would be CMP #0 BEQ 5000. The condition of equality is tested by BEQ, Branch if EQual. BNE tests a condition of inequality, Branch if Not Equal. Likewise, BCC (Branch if Carry is Clear) and the rest of these branches are testing conditions within the program.

     GOTO and JMP do not depend on any conditions within the program, so they are unconditional. The question arises, when you use a GOTO: Why did you write a part of your program that you must always (unconditionally) jump over? GOTO and JMP are sometimes used to patch up a program, but, used without restraint, they can make your program hard to understand later. Nevertheless, JMP can many times be the best solution to a programming problem. In fact, it is hard to imagine ML programming without it.

     One additional note about JMP: it makes a program non-relocatable. If you later need to move your whole ML program to a different part of memory, all the JMP's (and JSR's) need to be checked to see if they are pointing to addresses which are no longer correct (JMP or JSR into your BASIC ROM's will still be the same, but not those which are targeted to addresses within the ML program). This can be an important consideration if you are going to use an ML subroutine in other programs where the locations might well differ. Fully relocatable ML routines can be convenient if you like to program by drawing from a personal collection of solved problems.

 2000 JMP 2005
 2003 LDY #3
 2005 LDA #5

     If you moved this little program up to 5000, everything would survive intact and work correctly except the JMP 2005 at address 2000. It would still say to jump to 2005, but it should say to jump to 5005, after the move. You have to go through with a disassembly and check for all these incorrect JMP's. To make your programs more "relocatable," you can use a special trick with unconditional branching which will move without needing to be fixed:

2000 LDY #0

2002 BEQ 2005
(since we just loaded Y with a zero, this Branch-if-EQual-to-zero instruction will always be true and will always cause a pseudo-JMP)
2004 NOP

2005 LDA #5

     This works because we set the Z flag. Then, when BEQ tests the zero flag, it will pass the test, it will find that flag "up" and will branch. If you load X, Y, or A with a zero, the zero flag goes up.

     Various monitors and assemblers include a "move it" routine, which will take an ML program and relocate it somewhere else in memory for you. On the Apple, you can go into the monitor and type *5000 < 2000.2006M (although you will have to give the monitor these numbers in hex). The first number is the target address. The second and third are the start and end of the program you want to move.

     On CBM computers, the built-in monitor (the VIC-20 and the Original 2001 ROM set do not have a built-in monitor) does not have a Move it command. However, it is easy to add a "monitor extension" program to the built-in monitor. Supermon and Micromon are such extensions. The format for Moveit in Commodore machines is. T 2000 2006 5000 (start and end of the program to be moved, followed by the target address). Again, these numbers must be in hex. The T stands for transfer.

     The Atari Assembler Editor Cartridge follows a convention similar to Apple's: M 5000 < 2000, 2006.


This familiar and primary computing structure is accomplished in ML with the combination of CMP-BNE or any other conditional branch: BEQ, BCC, etc. Sometimes, the IF half isn't even necessary. Here's how it would look:

 2000 LDA 57   (what's in address 57?)
 2002 CMP #15  (is it 15?)
 2004 BEQ 2013 (IF it is, branch up to 2013)
 2006 LDA #10   (or ELSE, put a 10 into address 57)
 2008 STA 57
 2010 JMP 2017
(and jump over the THEN part)
 2013 LDA #20  (THEN, put a 20 into address 57)
 2015 STA 57
(continue with the program ... )

     Often, though, your flags are already set by an action, making the CMP unnecessary. For example, if you want to branch to 2013 if the number in address 57 is zero, just LDA 57 BEQ 2013. This is because the act of loading the accumulator will affect the status register flags. You don't need to CMP #0 because the zero flag will be set if a zero was just loaded into the accumulator. It won't hurt anything to use a CMP, but you'll find many cases in ML programming where you can shorten and simplify your coding. As you gain experience, you will see these patterns and learn how and what affects the status register flags.


This is a series of GETS, echoed to the screen as they are typed in, which end when the typist hits the RETURN key. The reason for the echo (the symbol for each key typed is reproduced on the screen) is that few people enjoy typing without seeing what they've typed. This also allows for error correction using cursor control keys or DELETE and INSERT keys. To handle all of these actions, an INPUT routine must be fairly complicated. We don't want, for example, the DELETE to become a character within the string. We want it to immediately act on the string being entered during the INPUT, to erase a mistake.

     Our INPUT routine must be smart enough to know what to add to the string and what keys are intended only to modify it. Here is the basis for constructing your own ML INPUT. It simply receives a character from the keyboard, stores it in the screen RAM cells, and ends when the RETURN key is pressed. This version is for Upgrade and 4.0 CBM/PETs and we'll write it as a subroutine. That simply means that when the 13 (ASCII for carriage return) is encountered, we'll perform an RTS back to a point just following the main program address which JSRed to our INPUT routine:

5000 LDY #0
(Y will act here as an offset for storing the characters to the screen as they come in)
5002 LDA 158
(this is the "number of keys in the keyboard buffer' location. If it's zero, nothing has been typed yet)
5004 BNE 5002
(so we go back to 5002)
5006 LDA 623
(get the character from the keyboard buffer)
5009 CMP #13
(is it a carriage return?)
5011 BNE 5014
(if not, continue)
5013 RTS
(otherwise return to the main program)
5014 STA 32768,Y
(echo it to the screen)
5017 INY

5018 LDA #0

5020 STA 158
(reset the "number of keys" counter to zero)
5022 JMP 5002
(continue looking for the next key)

     This INPUT could be made much larger and more complex. As it stands, it will contain the string on the screen only. To save the string, you would need to read it from screen RAM and store it elsewhere where it will not be erased. Or, you could have it echo to the screen, but (also using Y as the offset) store it into some safe location where you are keeping string variables. The routine above does not make provisions for DELETE or INSERT either. The great freedom you have with ML is that you can redefine anything you want. You can softkey: define a key's meaning via software; have any key perform any task. You might use the $ key to DELETE.

     Along with this freedom goes the responsibility for organizing, writing, and debugging these routines.


Although this word is still available on most BASICs, it is a holdover from the early days of computing. It is supposed to remind you that a statement like LET NAME =NAME +4 is an assignment of a value to a variable, not an algebraic equation. The two numbers on either side of the "equals" sign, in BASIC, are not intended to be equal in the algebraic sense. Most people write NAME =NAME +4 without using LET. However, the function of LET applies to ML as well as to BASIC: we must assign values to variables.

     In the Atari, VIC, and Apple, for example, where the address of the screen RAM can change depending on how much memory is in the computer, etc. - there has to be a place where we find out the starting address of screen RAM. Likewise, a program will sometimes require that you assign meanings to string variables, counters, and the like. This can be part of the initialization process, the tasks performed before the real program, your main routine, gets started. Or it can happen during the execution of the main loop. In either case, there has to be an ML way to establish, to assign, variables. This also means that you must have zones of memory set aside to hold these variables.

     For strings, you can think of LET as the establishment of a location in memory. In our INPUT example above, we might have included an instruction which would have sent the characters from the keyboard to a table of strings as well as echoing them to the screen. If so, there would have to be a way of managing these strings. For a discussion on the two most common ways of dealing with strings in ML, see Chapter 6 under the subhead "Dealing With Strings. "

     In general, you will probably find that you program in ML using somewhat fewer variables than in BASIC. There are three reasons for this:

     1. You will probably not write many programs in ML such as data bases where you manipulate hundreds of names, addresses, etc. It might be somewhat inefficient to create an entire data base management program, an inventory program for example, in ML. Keeping track of the variables would be a nightmare. An important benefit of ML is its speed of execution, but a drawback is that it slows programming down. So, for an inventory program, you could write the bulk of the program in BASIC and simply attach ML routines for sorting and searching tasks within the program.

     2. Also, the variables in ML are often handled within a series of instructions (not held elsewhere as BASIC variables are). FOR I =1 TO 10: NEXT I becomes LDY #1, INY, CPY #10, BNE. Here, the BASIC variable is counted for you and stored outside the body of the program. The ML "variable," though, is counted by the program itself. ML has no interpreter which handles such things. If you want a loop, you must construct all of its components yourself.

     3. In BASIC, it is tempting to assign values to variables at the start of the program and then to refer to them later by their variable names, as in: 10 BALL= 79. Then, any time you want to PRINT the BALL to the screen, you could say, PRINT CHR$(BALL). Alternatively, you might define it this way in BASIC: 10 BALL$= "0". In either case, your program will later refer to the word BALL. In this example we are assuming that the number 79 will place a ball character on your screen.

     In ML we are not free to use variable names except when using a complicated, advanced assembler. With the Simple Assembler, you will find it easier just to LDA #79, STA (screen position) each time. Some people like to put the 79 into their zone of variables (that arbitrary area of memory set up at the start of a program to hold tables, counters, and important addresses). They can pull it out of that zone whenever it's needed. That is somewhat cumbersome, though, and slower. You would LDA 1015, STA (screen position), assuming you had put a 79 into this "ball" address earlier.

     Obviously a value like BALL will remain the same throughout a program. A ball will look like a ball in your game, whatever else happens. So, it's not a true variable, it does not vary. It is constant. A true variable must be located in your "zone of variables," your variable table. It cannot be part of the body of your program itself (as in: LDA #79) because it will change. You don't know when writing your program what the variable will be. So you can't use immediate mode addressing because it might not be a #79. You have to LDA 1015 (or whatever) from within your table of variables.

     Elsewhere in the program you have one or more STA 1015's or INC 1015's or some other manipulation of this address which keeps updating this variable. In effect, ML makes you responsible for setting aside areas which are safe to hold variables. What's more, you have to remember the addresses, and update the variables in those addresses whenever necessary. This is why it is so useful to keep a piece of paper next to you when you are writing ML. The paper lists the start and end addresses of the zone of variables, the table. You also write down the specific address of each variable as you write your program.


This is done via a disassembler. It will not have line numbers (though, again, advanced assembler-disassembler packages do have line numbers). Instead, you will see the address of each instruction in memory. You can look over your work and debug it by working with the disassembler, setting BRKs into problem areas, etc. See Appendix D.


The method of saving and loading an ML program varies from computer to computer. Normally, you have several options which can include loading: from within the monitor, from BASIC, or even from an assembler. When you finish working on a program, or a piece of a program, on the Simple Assmbler you will be given the starting and ending addresses of your work. Using these, you can save to tape or disk in the manner appropriate to your computer. To LOAD, the simplest way is just to LOAD as if you were bringing in a BASIC program. Unfortunately, this only works on Commodore machines. You'll get your ML program, not a BASIC program, so it won't start at the normal starting address for BASIC unless you wrote and saved it at that address. You should type NEW after loading it, however, to reset some pointers in the computer. That will not NEW out the ML program.

     To save from within the monitor on Commodore machines:

  .S "PROGRAM NAME", 01,NNNN,NNNN* (for tape)
  .L "PROGRAM NAME",01 (for tape)
  .S "0:PROGRAM NAME",08,NNNN,NNNN* (for disk)
  .L "0:PROGRAM NAME",08 (for disk)

     *You should add one to the hex number for the end of your program or the SAVE will clip off the last byte. If your program exists in RAM from $0300 to $0350, you save it like this: S "PROGRAM NAME", 01, 0300, 0351.

     On the Apple, you must BLOAD from disk. On the Atari, if you have DOS you can use the "L" command from the DOS menu to LOAD in an ML program. If you don't, you need to use a short BASIC program that grabs in the bytes via a series of GETs:

 10 OPEN#1,4,0,"C:"
 40 START =LO+256*HI
 60 FIN =LO+256*HI
 70 TRAP 100
 90 GOTO 30
 100 END

     Note: This will not work correctly if the START and FIN addresses overlap this BASIC program in memory. It would then load in on top of itself.


In Microsoft BASIC, this has the effect of resetting some pointers which make the machine think you are going to start over again. The next program line you type in will be put at the "start-of-a-BASIC-program" area of memory. Some computers, the Atari for example, even wash memory by filling it with zeros. There is no special command in ML for NEWing an area of memory, though some monitors have a "fill memory" option which will fill a block of memory as big as you want with whatever value you choose.

     The reason that NEW is not found in ML is that you do not always write your programs in the same area of memory (as you do in BASIC), building up from some predictable address. You might have a subroutine floating up in high memory, another way down low, your table of variables just above the second subroutine, and your main program in the middle. Or you might not. We've been using 2000 as our starting address for many of the examples in this book and 5000 for subroutines, but this is entirely arbitrary.

     To "NEW" in ML, just start assembling over the old program. Alternatively, you could just turn the power off and then back on again. This would, however, have the disadvantage of wiping out your assembler along with your program.


In BASIC, you are expecting to test values from among a group of numbers: 1,2,3,4,5 .... The value of X must fall within this narrow range: ON X GOSUB 100, 200, 300 ... (X must be 1 or 2 or 3 here). In other words, you could not conveniently test for widely separated values of X (18, 55, 220). Some languages feature an improved form of ON GOSUB where you can test for any values. If your computer were testing the temperature of your bathwater:

         80 OF GOSUB HOT ENDOF

     ML permits you the greater freedom of the CASE structure. Using CMP, you can perform a multiple branch test:

2000 LDA 150
(get a value, perhaps input from the keyboard)
2002 CMP # 80

2004 BNE 2009

2006 JSR 5000
(where you would print "hot," following your example of CASE)
2009 CMP # 100

2011 BNE 2016

2013 JSR 5020
(print "very hot")
2016 CMP # 120

2018 BNE 2023

2020 JSR 5030
(print "intolerable")

     Since you are JSRing and then will be RTSing back to within the multiple branch test above, you will have to be sure that the subroutines up at 5000 do not change the value of the accumulator. If the accumulator started out with a value of 80 and, somehow, the subroutine at 5000 left a 100 in the accumulator, you would print "hot" and then also print "very hot." One way around this would be to put a zero into the accumulator before returning from each of the subroutines (LDA #0). This assumes that none of your tests, none of your cases, responds to a zero.


This is more common in ML than the ON GOSUB structure above. It eliminates the need to worry about what is in the accumulator when you return from the subroutines. Instead of RTSing back, you jump back, following all the branch tests.

2000 LDA 150

2002 CMP # 80

2004 BNE 2009

2006 JMP 5000
(print "hot")
2009 CMP # 100

2011 BNE 2016

2013 JMP 5020
(print "very hot")
2016 CMP # 120

2018 BNE 2023

2020 JMP 5030 (print "intolerable")
(all the subroutines JMP 2023 when they finish)

     Instead of RTS, each of the subroutines will JMP back to 2023, which lets the program continue without accidentally "triggering" one of the other tests with something left in the accumulator during the execution of one of the subroutines.


You could print out a message in the following way:

2000 LDY #0

2002 LDA #72
(use whatever your computer's screen POKE value is for the letter "H")
2004 STA 32900,Y
(an address on the screen)
2007 INY

2008 LDA #69
(the letter "E")
2010 STA 32900,Y

2013 INY

2014 LDA #76
(the letter "L")
2016 STA 32900,Y

2019 INY

2020 LDA #76
(the letter "L")
2022 STA 32900,Y

2025 INY

2026 LDA #79
(the letter "O")
2028 STA 32900,Y


     But this is clearly a cumbersome, memory-eating way to go about it. In fact, it would be absurd to print out a long message this way. The most common ML method involves putting message strings into a data table and ending each message with a zero. Zero is never a printing character in computers (excepting Atari which cannot use the technique described here). To print the ASCII number zero, you use 48: LDA #48, STA 32900. So, zero itself can be used as a delimiter to let the printing routine know that you've finished the message. In a data table, we first put in the message "hello". Recall that you should substitute your own computer's screen POKE code:

 1000 72 H
 1001 69 E
 1002 76 L
 1003 76 L
 1004 79 O
 1005    0  
(the delimiter, see Chapter 6)
 1006 72 H
 1007 73 I
(another message)
 1008 0     (another delimiter)

     Such a message table can be as long as you need; it holds all your messages and they can be used again and again:

2000 LDY #0

2002 LDA 1000,Y

2005 BEQ 2012
(if the zero flag is set, it must mean that we've reached the delimiter, so we branch out of this printing routine)
2005 STA 39000,Y
(put it on the screen)
2008 INY

2009 JMP 2002
(go back and get the next letter in the message)
(continue with the program.)

     Had we wanted to print "HI," the only change necessary would have been to put 1006 into the LDA at address 2003. To change the location on the screen that the message starts printing, we could just put some other address into 2006. The message table, then, is just a mass of words, separated by zeros, in RAM memory.

     The easiest way to print to the screen, especially if your program will be doing a lot of printing, is to create a subroutine and use some bytes in zero page (addresses 0 to 255) to hold the address of the message and the screen location you want to send it to. This is one reason why hex numbers can be useful. To put an address into zero page, you will need to put it into two bytes. It's too big to fit into one byte. With two bytes together forming an address, the 6502 can address any location from $0000 to the top $FFFF. So, if the message is at decimal location 1000 like "HELLO" above, you should turn 1000 into a hex number. It's $03E8.

     Then you split the hex number in two. The left two digits, $03, are the MSB (the most significant byte) and the right digits, $E8, make up the LSB (least significant byte). If you are going to put this target address into zero page at 56 (decimal):

 2000 LDA #232    (LSB, in decimal)
 2002 STA 56
 2004 LDA #3     
 2006 STA 57
 2008 JSR 5000   
(printout subroutine)

 5000 LDY #0
 5002 LDA (56),Y
 5004 BEQ 5013    
(if zero, return from subroutine)
 5006 STA 32900,Y  (to screen)
 5009 INY
 5010 JMP 5002
 5013 RTS

     One drawback to the subroutine is that it will always print any messages to the same place on the screen. That 32900 (or whatever you use there) is frozen into your subroutine. Solution? Use another zero page pair of bytes to hold the screen address. Then, your calling routine sets up the message address, as above, but also sets up the screen address.

     The Atari contains the address of the first byte of the screen addresses in zero page for you at decimal 88 and 89. You don't need to set up a screen address byte pair on the Atari. We are using the Apple II's low resolution screen for the examples in this book, so you will want to put 0 and 4 into the LSB and MSB respectively. The PET's screen is always located in a particular place, unlike the Atari, Apple, VIC, and 64 screen RAM locations which can move, so you can put a $00 and an $80 into LSB and MSB for PET. The following is in decimal:

2000 LDA #232
2002 STA 56
(set up message address)
2004 LDA #3
2006 STA 57

2008 LDA # 0
(LSB for PET and Apple)
2010 STA 58
(we'll just use the next two bytes in zero page above our message address for the screen address)
2012 LDA # 4
(this is for Apple II; use 128 ($80) for PET)
2014 STA 59

2016 JSR 5000


5000 LDY #0

5002 LDA (56),Y

5004 BEQ 5013
(if zero, return from subroutine)
5006 STA (58),Y
(to screen)
5009 INY

5010 JMP 5002

5013 RTS

     For Atari: 5006 STA (88),Y. You have less flexibility because you will always be printing your messages to the first line on screen, using address 88 as your screen storage target. To be able to put the message anywhere on screen, Atari users will have to use some other zero page for the screen address, as we did for Apple II and PET above. Atari users would have to keep track of the "cursor position" for themselves in that case.


There is no reason for a reading of data in ML. Variables are not placed into ML "DATA statements." They are entered into a table when you are programming. The purpose of READ, in BASIC, is to assign variable names to raw data or to take a group of data and move it somewhere, or to manipulate it into an array of variables. These things are handled by you, not by the computer, in ML programming.

     If you need to access a piece of information, you set up the addresses of the datum and the target address to which you are moving it. See the "PRINT" routines above. As always, in ML you are expected to keep track of the locations of your variables. You keep a map of data locations, vectors, tables, and subroutine locations. A pad of paper is always next to you as you program in ML. It seems as if you would need many notes. In practice, an average program of say 1000 bytes could be mapped out and commented on, using only one sheet.


You do this on a pad of paper, too. If you want to comment or make notes about your program - and it can be a necessary, valuable explanation of what's going on - you can disassemble some ML code like a BASIC LISTing. If you have a printer, you can make notes on the printed disassembly. If you don't have a printer, make notes on your pad to explain the purpose of each subroutine, the parameters it expects to get, and the results or changes it causes when it operates.

     Complex, large assemblers often permit comments within the source code. As you program with them, you can include REMarks by typing a semicolon, or parentheses, or some other signal to the assembler to ignore the REMarks when it is assembling your program. In these assemblers, you are working much closer to the way you work in BASIC. Your remarks remain part of the source program and can be listed out and studied.


RTS works the same way that RETURN does in BASIC: it takes you back to just after the JSR (GOSUB) that sent control of the program away from the main program and into a subroutine. JSR pushes, onto the stack, the address which immediately follows the JSR itself. That address then sits on the stack, waiting until the next RTS is encountered. When an RTS occurs, the address is pulled from the stack and placed into the program counter. This has the effect of transferring program control back to the instruction just after the JSR.


There are several ways to start an ML program. If you are taking off into ML from BASIC, you just use SYS or USR or CALL. They act just like JSR and will return control to BASIC, just like RETURN would, when there is an unmatched RTS in the ML program. By unmatched we mean the first RTS which is not part of a JSR/RTS pair. USR and SYS and CALL can be used either in immediate mode (directly from the keyboard) or from within a BASIC program as one of the BASIC commands.

   USR is just like SYS and CALL except that you can "send" values from BASIC to ML by attaching them to the USR (  ) within the parentheses. In Microsoft BASIC (Apple, PET/CBM, etc.), you must set up the location of your target ML program in special USR addresses, before exiting BASIC via USR. For example, to "gosub" to an ML routine located at $0360 (hex), you want to put a $60 (hex) into address 1 and an 03 into address 2. The 03 is obvious, just POKE 2,3. Atari goes from BASIC to ML via USR. The USR's argument may place several parameters on the stack along with the "count," the number of parameters which were passed.

     The hex 60 means that you would multiply 16 x 6, since the second column in hex is the "16's" column. So you would POKE 1, 96. Recall that we always set up ML addresses to be used by "indirect indexed addressing" (LDA (00),Y) by putting the LSB (least significant byte) first. To set up 0360, then, you first separate the hex number into its two bytes, 03 60. Then you translate them into decimal since we're in BASIC when we use USR: 3 96. Then you switch them so that they conform to the correct order for ML: LSB/MSB 96 3. Finally, you POKE them into memory locations 1 and 2.

     If this seems rather complex, it is. In practice, Microsoft BASIC users rarely use USR. The number which is "passed" to ML from within the parentheses is put into the floating point accumulator. Following this you must JSR to FPINT, a BASIC ROM routine which converts a floating point value into an integer that you could work with in ML. As we mentioned, working with floating point arithmetic in ML is an arcane art. For most applications which must pass information from BASIC to ML, it is far easier to use ordinary "integer" numbers and just POKE them into some predetermined ML variable zone that you've set aside and noted on your workpad. Then just SYS to your ML routine, which will look into the set-aside, POKEd area when it needs the values from BASIC.

     In Atari BASIC, USR works in a more simplified and more convenient way. For one thing, the target ML address is contained within the argument of the USR command: USR (address). This makes it nearly the exact parallel of BASIC's GOSUB. What's more, USR passes values from BASIC by putting them on the stack as a two-byte hex number. USR (address,X) does three things. 1. It sends program control to the ML routine which starts at "address." 2. It pushes the number X onto the stack where it can be pulled out with PLA's. 3. Finally, it pushes the total number of passed values onto the stack. In this case, one value, X, was passed to ML. All of these actions are useful and make the Atari version of USR a more sensible way of GOSUBing from BASIC to ML.

     If you are not going between BASIC and ML, you can start (RUN) your ML program from within your "monitor." The PET/CBM and the Apple have built-in monitor programs in their ROM chips. On the Atari, a monitor is available as part of a cartridge. On the "Original" PET/CBM (sometimes called BASIC 2.0), there is no built-in monitor. A cassette with a program called TIM (terminal interface monitor) can be LOADed, though, and used in the same way that the built-in versions are on later models. Neither the VIC nor the 64 has a built-in monitor.

     To enter "monitor mode" (as opposed to the normal BASIC mode), you can type SYS 1024 or SYS 4 on the PET/CBM. These locations always contain a zero and, by "landing" on a zero in ML, you cause a BRK to take place. This displays the registers of your 6502 and prints a dot on the screen while waiting for your instructions to the monitor. To enter the monitor on Apple II, type CALL -151 and you will see an asterisk (instead of PET's period) as your prompt. From within Atari's Assembler Cartridge, you would type BUG to enter the equivalent of the Apple and PET monitor. The Atari will print the word DEBUG and then the cursor will wait for your next instruction.

     To RUN an ML program, all five computers use the abbreviation G to indicate "goto and run" the hex address which follows the G. Unfortunately, the format of the ML RUN (G), as always, differs between machines. To run a program which starts at address $2000:

  Apple II,          you type: 2000G    (8192 in decimal)
  PET, VIC,64,  you type: G 2000
  Atari,               you type: G 2000

     One other difference: the Apple II expects to encounter an unmatched RTS to end the run and return control to the monitor. Put another way, it will think that your ML program is a subroutine and 2000G causes it to JSR to the subroutine at address (in hex.) 2000. The Commodores and the Atari both look for a BRK instruction (00) to throw them back into monitor mode.


When you SAVE a BASIC program, the computer handles it automatically. The starting address and the ending address of your program are calculated for you. In ML, you must know the start and end yourself and let the computer know. From the Apple II monitor, you type the starting and ending address of what you want saved, and then "W" for write:

  2000.2010W (This is only for cassette and these commands are in hex. These addresses are 8192.8208, in decimal.)

     From BASIC to disk use:

  B SAVE Name, A, L (A=address, L=length)

     On the VIC, 64, and PET, the format for SAVE is similar, but includes a filename:

  .S "PROGRAM NAME",01,2000,2010 (the 01 is the "device number" of the tape player)

     To save to disk, you must change the device number to 08 and start the filename with the number of the drive you are SAVEing to:

  .S "0: NAME", 08,2000,2010

     (Always add one to the "finish" address; the example above saves from 2000 to 200F.)

     With the Atari Assembler Cartridge, you:

  SAVE#C: NAME < 2000,2010 (do this from the EDIT, not DEBUG, mode). The NAME is not required with cassette.

     To write Atari source code to cassette, type: .SAVE#C. For disk, type SAVE#D:FILENAME.EXT or use DOS.


BRK (or an RTS with no preceding JSR, on the Apple) throws you back into the monitor mode after running an ML program. This is most often used for debugging programs because you can set "breakpoints" in the same way that you would use STOP to examine variables when debugging a BASIC program.

String Handling


In BASIC, this will give you the number of the ASCII code which stands for the character you are testing. ?ASC("A") will result in a 65 being displayed. There is never any need for this in ML. If you are manipulating the character A in ML, you are using ASCII already. In other words, the letter A is 65 in ML programming. If your computer stores letters and other symbols in nonstandard ways (such as Commodore character codes for lowercase, and Atari's ATASCII), you will need to write a special program to be able to translate to standard ASCII if you are using a modem or some other peripheral which uses ASCII. See your computer's manual, the Atari BASIC Reference Manual for example, for information on your computer's internal character code.


This is most useful in BASIC to let you use characters which cannot be represented within normal strings, will not show up on your screen, or cannot be typed from the keyboard. For example, if you have a printer attached to your computer, you could "send" CHR$(13) to it, and it would perform a carriage return. (The correct numbers which accomplish various things sometimes differ, though decimal 13 - an ASCII code standard - is nearly universally recognized as carriage return.) Or, you could send the combination CHR$(27)CHR$(8) and the printer would backspace.

     Again, there is no real use for CHR$ within ML. If you want to specify a carriage return, just LDA #13. In ML, you are not limited to the character values which can appear on screen or within strings. Any value can be dealt with directly.

     The following string manipulation instructions are found in Microsoft BASIC:


As usual in ML, you are in charge of manipulating data. Here's one way to extract a five-character-long "substring" from out of the left side of a string as in the BASIC statement: LEFT$ (X$,5)

2000 LDY #5

2002 LDX #0
(use X as the offset for buffer storage)
2004 LDA 1000,Y
(the location of X$)
2007 STA 4000,X
(the "buffer," or temporary storage area for the substring)
2010 INX

2011 DEY

2012 BNE 2004


In some cases, you will already know the length of a string in ML. One of the ways to store and manipulate strings is to know beforehand the length and address of a string. Then you could use the subroutine given for LEFT$ above. More commonly, though, you will store your strings with delimiters (zeros, except in Atari) at the end of each string. To find out the length of a certain string:

2000 LDY #0

2002 LDA 1000,Y
(the address of the string you are testing)
2003 BEQ 2009
(remember, if you LDA a zero, the zero flag is set. So you don't really need to use a CMP #0 here to test whether you've loaded the zero delimiter)
2005 INY

2006 BNE 2002
(we are not using a JMP here because we assume that all your strings are less than 256 characters long.)
2008 BRK
(if we still haven't found a zero after 256 INY's, we avoid an endless loop by just BRKing out of the subroutine)
2009 DEY
(the LENgth of the string is now in the Y register)

     We had to DEY at the end because the final INY picked up the zero delimiter. So, the true count of the LENgth of the string is one less than Y shows, and we must DEY one time to make this adjustment.


To extract a substring which starts at the fourth character from within the string and is five characters long (as in MID$(X$,4,5) ):

2000 LDY #5
(the size of the substring we're after)
2002 LDX #0
(X is the offset for storage of the substring)
2004 LDA 1003,Y
(to start at the fourth character from within the X$ located at 1000, simply add three to that address. Instead of starting our LDA,Y at 1000, skip to 1003. This is because the first character is not in position one. Rather, it is at the zeroth position, at 1000.)
2007 STA 4000,X
(the temporary buffer to hold the substring)
2010 INX

2011 DEY

2012 BNE 2004


This, too, is complicated because normally we do not know the LENgth of a given string. To find RIGHT$(X$,5) if X$ starts at 1000, we should find the LEN first and then move the substring to our holding zone (buffer) at 4000:

2000 LDY #0

2002 LDX #0

2004 LDA 1000,Y

2007 BEQ 2013
(the delimiting zero is found, so we know LEN)
2009 INY

2010 JMP 2004

2013 TYA
(put LEN into A to subtract substring size from it)
2014 SEC
(always set carry before subtraction)
2015 SBC #5
(subtract the size of the substring you want to extract)
2017 TAY
(,put the offset back into Y, now adjusted to point to five characters from the end of X$)
2018 LDA 1000,Y

2021 BEQ 2030
(we found the delimiter, so end)
2023 STA 4000,X

2026 INX

2027 DEY

2028 BNE 2018

2030 RTS

The above does not apply to Atari since it cannot use zero as a delimiter.


This formatting instruction is similar to TAB. The difference is that SPC(10) moves you ten spaces to the right from wherever the cursor is on screen at the time. TAB(10) moves ten spaces from the left-hand side of the screen. In other words, TAB always counts over from the first column on any line; SPC counts from the cursor's current position.

     In ML, you would just add the amount you want to SPC over. If you were printing to the screen and wanted ten spaces between A and B so it looked like this (A    B), you could write:

 2000 LDA #65 (A)
 2002 STA 32768    
(screen RAM address)
 2005 LDA #66 (B)
 2007 STA 32778    
(you've added ten to the target address)

     Alternatively, you could add ten to the Y offset:

2000 LDY #0
2002 LDA #65
2004 STA 32768,Y
2007 LDY #10      
(add ten to Y)
2009 LDA #66
2011 STA 32768,Y

     If you are printing out many columns of numbers and need a subroutine to correctly space your printout, you might want to use a subroutine which will add ten to the Y offset each time you call the subroutine:

5000 TYA
5001 CLC
5002 ADC #10
5004 TAY
5005 RTS

     This subroutine directly adds ten to the Y register whenever you JSR 5000. To really do this job, however, you should use a two-byte register to keep track of the cursor.


Quite similar to SPC, except that you don't add the offset from the cursor position (whatever location you most recently printed). Rather, TAB(X) moves ten over from the left side of the screen, or, if you are using a printer, from the left margin on the piece of paper. There is no particular reason to use TAB in ML. You have much more direct control in ML over where characters are printed out.

Return to Table of Contents | Previous Chapter | Next Chapter