The Instruction Set
There are 56 instructions (commands) available in 6502 machine language. Most versions of BASIC have about 50 commands. Some BASIC instructions are rarely used by the majority of programmers: USR, END, SGN, TAN, etc. Some, such as END and LET, contribute nothing to a program and seem to have remained in the language for nostalgic reasons. Others, like TAN, have uses that are highly specialized. There are surplus commands in computer languages just as there are surplus words in English. People don't often say culpability. They usually say guilt. The message gets across without using the entire dictionary. The simple, common words can do the job.
Machine language is the same as any other language in this respect. There are around 20 heavily used instructions. The 36 remaining ones are far less often used. Load the disassembler program in Appendix D and enter the starting address of your computer's BASIC in ROM. You can then read the machine language routines which comprise it. You will quickly discover that the accumulator is heavily trafficked (LDA and STA appear frequently), but you will have to hunt to find an ROR, SED, CLV, RTI, or BVC.
ML, like BASIC, offers you many ways to accomplish a given job. Some programming solutions, of course, are better than others, but the main thing is to get the job done. An influence still lingers from the early days of computing when memory space was rare and expensive. This influence - that you should try to write programs using up as little memory as possible - is usually safely ignored. Efficient memory use will often be low on your list of objectives. It could hardly matter if you used up 25 instead of 15 bytes to print a message to your screen when your computer has space for programs which exceeds 30,000 bytes.
Rather than memorize each instruction individually, we will concentrate on the workhorses. Bizarre or arcane instructions will get only passing mention. Unless you are planning to work with ML for interfacing or complex mathematics and such, you will be able to write excellent machine language programs for nearly any application with the instructions we'll focus on here.
For each instruction group, we will describe three things before getting down to the details about programming with them. 1. What the instructions accomplish. 2. The addressing modes you can use with them. 3. What they do, if anything, to the flags in the Status Register. All of this information is also found in Appendix A.
The Six Instruction Groups
The best way to approach the "instruction set" might be to break it down into the following six categories which group the instructions according to their functions: 1. The Transporters 2. The Arithmetic Group 3. The Decision-makers 4. The Loop Group 5. The Subroutine and Jump Group and 6. The Debuggers. We will deal with each group in order, pointing out similarities to BASIC and describing the major uses for each.
As always, the best way to learn is by doing. Move bytes around. Use each instruction, typing a BRK as the final instruction to see the effects. If you LDA #65, look in the A register to see what happened. Then STA $12 and check to see what was copied into address $12. If you send the byte in the accumulator (STA), what's left behind in the accumulator? Is it better to think of bytes as being copied rather than being sent?
Play with each instruction to get a feel for it. Discover the effects, qualities, and limitations of these ML commands.
LDA, LDX, LDY
STA, STX, STY
These instructions move a byte from one place in memory to another. To be more precise, they copy what is in a source location into a target location. The source location still contains the byte, but after a "transporter" instruction, a copy of the byte is also in the target. This does replace whatever was in the target.
All of them affect the N and Z flags, except STA, STX, and STY which do nothing to any flag.
There are a variety of addressing modes available to different instructions in this group. Check the chart in Appendix A for specifics.
Remember that the computer does things one at a time. Unlike the human brain which can carry out up to 1000 different instructions simultaneously (walk, talk, and smile, all at once) - the computer goes from one tiny job to the next. It works through a series of instructions, raising the program counter (PC) each time it handles an instruction.
If you do a TYA, the PC goes up by one to the next address and the computer looks at that next instruction. STA $80 is a two-byte long instruction, it's zero page addressing, so the PC =PC+2. STA $8500 is a three-byte long absolute addressing mode and PC =PC+3.
Recall that there's nothing larger than a three-byte increment of the PC. However, in each case, the PC is cranked up the right amount to make it point to the address for the next instruction. Things would get quickly out of control if the PC pointed to some argument, thinking it was an instruction. It would be incorrect (and soon disastrous) if the PC landed on the $15 in LDA $15.
If you type SYS 1024 (or USR or CALL), the program counter is loaded with $0400 and the computer "transfers control" to the ML instructions which are (we hope!) waiting there. It will then look at the byte in $0400, expecting it to be an ML instruction. It will do that job and then look for the next instruction. Since it does this very fast, it can seem to be keeping score, bouncing the ball, moving the paddle, and everything else - simultaneously. It's not, though. It's flashing from one task to another and doing it so fast that it creates the illusion of simultaneity much the way that 24 still pictures per second look like motion in movies.
The Programmer's Time Warp
Movies are, of course, lots of still pictures flipping by in rapid succession. Computer programs are composed of lots of individual instructions performed in rapid succession.
Grasping this sequential, step-by-step activity makes our programming job easier: we can think of large programs as single steps, coordinated into meaningful, harmonious actions. Now the computer will put a blank over the ball at its current address, then add 40 to the ball's address, then print a ball at the new address. The main single-step action is moving information, as single-byte numbers, from here to there, in memory. We are always creating, updating, modifying, moving and destroying single-byte variables. The moving is generally done from one double-byte address to another. But it all looks smooth to the player during a game.
Programming in ML can pull you into an eerie time warp. You might spend several hours constructing a program which executes in seconds. You are putting together instructions which will later be read and acted upon by coordinated electrons, moving at electron speeds. It's as if you spent an afternoon slowly and carefully drawing up pathways and patterns which would later be a single bolt of lightning.
In ML there are three primary places where variables rest briefly on their way to memory cells: the X, the Y, and the A registers. And the A register (the accumulator) is the most frequently used. X and Y are used for looping and indexing. Each of these registers can grab a byte from anywhere in memory or can load the byte right after its own opcode (immediate addressing):
LDX $8000 (puts the number at hex address 8000 into X, without destroying it at $8000)
LDX #65 (puts the number 65 into X)
LDA and LDY work the same.
Be sure you understand what is happening here. LDX $1500 does not copy the "byte in the X register into address $1500." It's just the opposite. The number (or "value" as it's sometimes called) in $1500 is copied into the X register.
To copy a byte from X, Y, or A, use STX, STY, or STA. For these "store-bytes" instructions, however, there is no immediate addressing mode. No STA #15. It would make no sense to have STA #15. That would be disruptive, for it would modify the ML program itself. It would put the number 15 into the next cell beyond the STA instruction within the ML program itself.
Another type of transporter moves bytes between registers - TAY, TAX, TYA, TXA. See the effect of writing the following. Look at the registers after executing this:
1000 LDA #65
The number 65 is placed into the accumulator, then transferred to the Y register, then sent from the accumulator to X. All the while, however, the A register (accumulator) is not being emptied. Sending bytes is not a "transfer" in the usual sense of the term "sending." It's more as if a Xerox copy were made of the number and then the copy is sent. The original stays behind after the copy is sent.
LDA #15 followed by TAY would leave the 15 in the accumulator, sending a copy of 15 into the Y register.
Notice that you cannot directly move a byte from the X to the Y register, or vice versa. There is no TXY or TYX.
Flags Up And Down
Another effect of moving bytes around is that it sometimes throws a flag up or down in the Status Register. LDA (or LDX or LDY) will affect the N and Z, negative and zero, flags.
We will ignore the N flag. It changes when you use "signed numbers," a special technique to allow for negative numbers. For our purposes, the N flag will fly up and down all the time and we won't care. If you're curious, signed numbers are manipulated by allowing the seven bits on the right to hold the number and the leftmost bit stands for positive or negative. We normally use a byte to hold values from 0 through 255. If we were working with "signed" numbers, anything higher than 127 would be considered a negative number since the leftmost bit would be "on" - and an LDA #255 would be thought of as -1. This is another example of how the same things (the number 255 in this case) could signify several different things, depending on the context in which it is being interpreted.
The Z flag, on the other hand, is quite important. It shows whether or not some action during a program run resulted in a zero. The branching instructions and looping depend on this flag, and we'll deal with the important zero-result effects below with the BNE, INX, etc., instructions.
No flags are affected by the STA, STX, or STY instructions.
The Stack Can Take Care Of Itself
There are some instructions which move bytes to and from the stack. These are for advanced ML programmers. PHA and PLA copy a byte from A to the stack, and vice versa. PHP and PLP move the status register to and from the stack. TSX and TXS move the stack pointer to or from the X register. Forget them. Unless you know precisely what you are doing, you can cause havoc with your program by fooling with the stack. The main job for the stack is to keep the return addresses pushed into it when you JSR (Jump To Subroutine). Then, when you come back from a subroutine (RTS), the computer pulls the addresses off the stack to find out where to go back to.
The one major exception to this warning about fiddling with the stack is Atari's USR instruction. It is a worthwhile technique to master. Atari owners can move between BASIC and ML programs fairly easily, passing numbers to ML via the stack. The parameters (the passed numbers) must be pulled off the stack when the ML program first takes control of the computer.
For most ML programming, on the other hand, avoid stack manipulation until you are an advanced programmer. If you manipulate the stack without great care, you'll give an RTS the wrong address and the computer will travel far, far beyond your control. If you are lucky, it sometimes lands on a BRK instruction and you fall into the monitor mode. The odds are that you would get lucky roughly once every 256 times. Don't count on it. Since BRK is rare in your BASIC ROM, the chances are pretty low. If your monitor has a FILL instruction which lets you put a single number into large amounts of RAM memory, you might want to fill the RAM with "snow." FILL 1000 8000 00 would put zeros into every address from 1000 to 8000. This greatly improves the odds that a crash will hit a BRK.
As an aside, there is another use for a blanket of "zero page snow." Many Atari programs rely on the fact that the computer leaves page six ($0600-06FF) pretty much alone. The PET doesn't make much use of the second cassette buffer. So, you can safely put an ML subroutine in these places to, for example, add a routine which customizes an ML word processor. Does your Atari's ML word-processing program use any memory space in page six? Probably. What locations does it use? Fill page six with 00's, put the word-processor through its paces, then look at the tracks, the non-zeros, in the snow.
ADC, SBC, SEC, CLC
Here are the commands which add, subtract, and set or clear the carry flag. ADC and SBC affect the N, Z, C, and V (overflow) flags. CLC and SEC, needless to say, affect the C flag and their only addressing mode is Implied.
ADC and SBC can be used in eight addressing modes: Immediate, Absolute, Zero Page, (Indirect,X), (Indirect),Y, Zero Page,X, and Absolute,X and Y.
Arithmetic was covered in the previous chapter. To review, before any addition, the carry flag must be cleared with CLC. Before any subtraction, it must be set with SEC. The decimal mode should be cleared at the start of any program (the initialization): CLD. You can multiply by two with ASL and divide by two with LSR. Note that you can divide by four with LSR LSR or by eight with LSR LSR LSR. You could multiply a number by eight with ASL ASL ASL. What would this do to a number: ASL ASL ASL ASL? To multiply by numbers which aren't powers of two, use addition plus multiplication. To multiply by ten, for example: copy the original number temporarily to a vacant area of memory. Then ASL ASL ASL to multiply it by eight. Then multiply the stored original by two with a single ASL. Then add them together.
If you're wondering about the V flag, it is rarely used for anything. You can forget about the branch which depends on it, BVC, too. Only five instructions affect it and it relates to "twos complement" arithmetic which we have not touched on in this book. Like decimal mode or negative numbers, you will be able to construct your ML programs very effectively if you remain in complete ignorance of this mode. We have largely avoided discussion of most of the flags in the status register: N, V, B, D, and I. This avoidance has also removed several branch instructions from our consideration: BMI, BPL, BVC, and BVS. These flags and instructions are not usually found in standard ML programs and their use is confined to specialized mathematical or interfacing applications. They will not be of use or interest to the majority of ML programmers.
The two flags of interest to most ML programmers are the Carry flag and the Zero flag. That is why, in the following section, we will examine only the four branch instructions which test the C and Z flags. They are likely to be the only branching instructions that you'll ever find occasion to use.
CMP, BNE, BEQ, BCC, BCS
The four "branchers" here - they all begin with a "B" - have only one addressing mode. In fact, it's an interesting mode unique to the "B" instructions and created especially for them: relative addressing. They do not address a memory location as an absolute thing; rather, they address a location which is a certain distance from their position in the ML code. Put another way, the argument of the "B" instructions is an offset which is relative to their position. You never have to worry about relocating "B" instructions to another part of memory. You can copy them and they will work just as well in the new location. That's because their argument just says "add five to the present address" or "subtract twenty-seven," or whatever argument you give them. But they can't branch further back than 127 or further forward than 128 bytes.
None of the brancher instructions have any effect whatsoever on any flags; instead, they are the instructions which look at the flags. They are the only instructions that base their activity on the condition of the status register and its flags. They are why the flags exist at all.
CMP is an exception. Many times it is the instruction that comes just before the branchers and sets flags for them to look at and make decisions about. Lots of instructions - LDA is one - will set or "clear" (put down) flags - but sometimes you need to use CMP to find out what's going on with the flags. CMP affects the N, Z, and C flags. CMP has many addressing modes available to it: Immediate, Absolute, Zero Page, (Indirect, X), (Indirect), Y, Zero Page, X, and Absolute, X and Y.
The Foundations Of Computer Power
This decision-maker group and the following group (loops) are the basis of our computers' enormous strength. The decision-makers allow the computer to decide among two or more possible courses of action. This decision is based on comparisons. If the ball hits a wall, then reverse its direction. In BASIC, we use IF-THEN and ON-GOTO structures to make decisions and to make appropriate responses to conditions as they arise during a program run.
Recall that most micros use memory mapped video, which means that you can treat the screen like an area of RAM memory. You can PEEK and POKE into it and create animation, text, or other visual events. In ML, you PEEK by LDA $VIDEO MEMORY and examine what you've PEEKed with CMP. You POKE via STA $VIDEO MEMORY.
CMP does comparisons. This tests the value at an address against what is in the accumulator. Less common are CPX and CPY. Assume that we have just added 40 to a register we set aside to hold the current address-location of a ball on our screen during a game. Before the ball can be POKEd into that address, we'd better make sure that something else (a wall, a paddle, etc.) is not sitting there. Otherwise the ball would pass right through walls.
Since we just increased the location register (this register, we said, was to be at $80,81), we can use it to find out if there is blank space (32) or something else (like a wall). Recall that the very useful "indirect Y" addressing mode allows us to use an address in zero page as a pointer to another address in memory. The number in the Y register is added to whatever address sits in 80,81; so we don't LDA from 80 or 81, but rather from the address that they contain, plus Y's value.
To see what's in our potential ball location, we can do the following:
|(we want to fetch from the
ball address itself, so we don't want to add anything to it.
Y is set to zero.)
||(fetch whatever is sitting
where we plan to next send the ball. To review Indirect, Y addressing
once more: say that the address we are fetching from here is $1077.
Address $80 would hold the LSB ($77) and address $81 would hold the
MSB ($10). Notice that the argument of an Indirect, Y instruction only
mentions the lower address of the two-byte pointer, the $80. The computer
knows that it has to combine $80 and $81 to get the full address -
and does this automatically.)
At this point in your game, there might be a 32 (ASCII for the space or blank character) or some other number which we would know indicated a wall, another player, a paddle, etc. Now that this questionable number sits in the accumulator, we will CMP it against a space. We could compare it with the number which means wall or the other possibilities - it doesn't matter. The main thing is to compare it:
| 2000 CMP #32
||(is it a space?)
||(Branch if Not Equal [if not
32] to address 200A, which contains the first of a series of comparisons
to see if it's a wall, a paddle, etc. On the other hand, if the comparison
worked, if it was a 32 (so we didn't Branch Not Equal), then the next
thing that happens is the instruction in address 2004. We "fall through"
the BNE to an instruction which jumps to the subroutine (JSR), which
moves the ball into this space and then returns to address 2007,
which jumps over the series of comparisons for wall, paddle, etc.)
||(the ball printing subroutine)
||(jump over the rest of the
||(is it our paddle symbol?)
||(if not, continue to next comparison)
||(do the paddle-handling subroutine
||(jump over the rest, as before
||(is it a wall ... and so forth
with as many comparisons as needed)
This structure is to ML what ON-GOTO or ON-GOSUB is to BASIC. It allows you to take multiple actions based on a single LDA. Doing the CMP only once would be comparable to BASIC's IF-THEN.
Other Branching Instructions
In addition to the BNE we just looked at, there are BCC, BCS, BEQ, BMI, BPL, BVC, and BVS. Learn BCC, BCS, BEQ, and BNE and you can safely ignore the others.
All of them are branching, IF-THEN, instructions. They work in the same way that BNE does. You write BEQ followed by the address you want to go to. If the result of the comparison is "yes, equal-to-zero is true," then the ML program will jump to the address which is the argument of the BEQ. "True" here means that something EQuals zero. One example that would send up the Z flag (thereby triggering the BEQ) is: LDA #00. The action of loading a zero into A sets the Z flag up.
You are allowed to "branch" either forward or backward from the address that holds the "B-" instruction. However, you cannot branch any further than 128 bytes in either direction. If you want to go further, you must JMP (JuMP) or JSR (Jump to SubRoutine). For all practical purposes, you will usually be branching to instructions located within 30 bytes of your "B" instruction in either direction. You will be taking care of most things right near where a CoMPare, or other flag-setting event, takes place.
If you need to use an elaborate subroutine, simply JSR to it at the target address of your branch:
2000 LDA 65
||(is what was in address 65 equal
to what was in address 85?)
||(if Not Equal, branch over the next
three bytes which perform some elaborate job)
||(at 4000 sits an elaborate subroutine
to take care of cases where addresses 65 and 85 turn out to be equal)
||(continue with the program here)
If you are branching backwards, you've written that part of your program, so you know the address to type in after a BNE or one of the other branches. But, if you are branching forward, to an address in part of the program not yet written - how do you know what to give as the address to branch to? In complicated two-pass assemblers, you can just use a word like "BRANCHTARGET", and the assembler will "pass" twice through your program when it assembles it. The first "pass" simply notes that your BNE is supposed to branch to "BRANCHTARGET," but it doesn't yet know where that is.
When it finally finds the actual address of "BRANCHTARGET," it makes a note of the correct address in a special label table. Then, it makes a second "pass" through the program and fills in (as the next byte after your BNE or whatever) the correct address of "BRANCHTARGET". All of this is automatic, and the labels make the program you write (called the source code) look almost like English. In fact, complicated assemblers can contain so many special features that they can get close to the higher-level languages, such as BASIC:
| (These initial
definitions of labels
| are sometimes
BRANCHTARGET 200D ... etc.
Instead of using lots of numbers (as we do when using the Simple Assembler) for the target/argument of each instruction, these assemblers allow you to define ("equate") the meanings of words like "TESTBYTE" and from then on you can use the word instead of the number. And they do somewhat simplify the problem of forward branching since you just give (as above) address 200D a name, "BRANCHTARGET," and the word at address 2009 is later replaced with 200D when the assembler does its passes.
This is how the example above looks as the source code listing from a two-pass, deluxe assembler:
; (IMMEDIATE ADDRE
||CMP *NEWBYTE ;
(ZERO PAGE ADDRESSING)
; (RELATIVE ADDRES
20 10 20
AD 00 04
; YOU CAN FREELY MIX
AND SUBROUTINES. ALSO, COMMENTS
BE IGNORED BY THE ASSEMBLER AND CAN
||; BE STUCK
ANYWHERE, AS YOU SEE.
AD 21 00
|| ; ETC. ETC.
Actually, we should note in passing that a 200D will not be the number which finally appears at address 2009 to replace "BRANCHTARGET". To save space, all branches are indicated as an "offset" from the address of the branch. The number which will finally replace "BRANCHTARGET" at 2009 above will be three. This is similar to the way that the value of the Y register is added to an address in zero page during indirect Y addressing (also called "indirect indexed"). The number given as an argument of a branch instruction is added to the address of the next instruction. So, 200A+3=200D. Our Simple Assembler will take care of all this for you. All you need do is give it the 200D and it will compute and put the 3 in place for you.
Forward Branch Solutions
There is one responsibility that you do have, though. When you are writing 2008 BNE 200D, how do you know to write in 200D? You can't yet know to exactly which address up ahead you want to branch. There are two ways to deal with this. Perhaps easiest is to just put in BNE 2008 (have it branch to itself). This will result in a FE being temporarily left as the target of your BNE. Then, you can make a note on paper to later change the byte at 2009 to point to the correct address, 200D. You've got to remember to "resolve" that FE to POKE in the number to the target address, or you will leave a little bomb in your program - an endless loop. The Simple Assembler has a POKE function. When you type POKE, you will be asked for the address and value you want POKEd. So, by the time you have finished coding 200D, you could just type POKE and then POKE 2009,3.
The other, even simpler, way to deal with forward branch addresses will come after you are familiar with which instructions use one, two, or three bytes. This BNE-JSR-TARGET construction is common and will always be six away from the present address, an offset of 6. If the branch instruction is at 2008, you just count off three: 200A, 200B, 200C and write BNE 200D. Other, more complex branches such as ON-GOTO constructions will also become easy to count off when you're familiar with the instruction byte-lengths. In any case, it's simple enough to make a note of any unsolved branches and correct them before running the program.
Alternatively, you can use a single "unresolved" forward branch in the Simple Assembler; see its instructions. You just type BNE FORWARD.
Recall our previous warning about staying away from the infamous BPL and BMI instructions? BPL (Branch on PLus) and BMI (Branch on MInus) sound good, but should be avoided. To test for less-than or more-than situations, use BCC and BCS respectively. (Recall that BCC is alphabetically less-than BCS - an easy way to remember which to use.) The reasons for this are exotic. We don't need to go into them. Just be warned that BPL and BMI, which sound so logical and useful, are not. They can fail you and neither one lives up to its name. Stick with the always trustworthy BCC, BCS.
Also remember that BNE and the other three main "B" group branching instructions often don't need to have a CMP come in front of them to set a flag they can test. Many actions of many opcodes will automatically set flags during their operations. For example, LDA $80 will affect the Z flag so you can tell if the number in address $80 was or wasn't zero by that flag. LDA $80 followed by BNE would branch away if there were anything besides a zero in address $80. If in doubt, check the chart of instructions in Appendix A to see which flags are set by which instructions. You'll soon get to know the common ones. If you are really in doubt, go ahead and use CMP.
DEY, DEX, INY, INX, INC, DEC
INY and INX raise the Y and X register values by one each time they are used. If Y is a 17 and you INY, Y becomes an 18. Likewise, DEY and DEX decrease the value in these registers by one. There is no such increment or decrement instruction for the accumulator.
Similarly, INC and DEC will raise or lower a memory address by one. You can give arguments to them in four addressing modes: Absolute, Zero Page, Zero Page,X and Absolute,X. These instructions affect the N and Z flags.
The Loop Group are usually used to set up FOR-NEXT structures. The X register is used most often as a counter to allow a certain number of events to take place. In the structure FOR I =1 TO 10: NEXT I, the value of the variable I goes up by one each time the loop cycles around. The same effect is created by:
2000 LDX #10
2002 DEX ("DEcrement" or "DEcrease X" by 1)
2003 BNE 2002 (Branch if Not Equal [to zero] back up to address 2002)
Notice that DEX is tested by BNE (which sees if the Z flag, the zero flag, is up). DEX sets the Z flag up when X finally gets down to zero after ten cycles of this loop. (The only other flag affected by this loop group is the N [negative] flag for signed arithmetic.)
Why didn't we use INX, INcrease X by 1? This would parallel exactly the FOR I =1 TO 10, but it would be clumsy since our starting count which is #10 above would have to be #245. This is because X will not become a zero going up until it hits 255. So, for clarity and simplicity, it is customary to set the count of X and then DEX it downward to zero. The following program will accomplish the same thing as the one above, and allow us to INX, but it too is somewhat clumsy:
2000 LDX #0
2003 CPX #10
2005 BNE 2002
Here we had to use zero to start the loop because, right off the bat, the number in X is INXed to one by the instruction at 2002. In any case, it is a good idea to just memorize the simple loop structure in the first example. It is easy and obvious and works very well.
How would you create a loop which has to be larger than 256 cycles? When we examined the technique for adding large numbers, we simply used two-byte units instead of single-byte units to hold our information. Likewise, to do large loops, you can count down in two bytes, rather than one. In fact, this is quite similar to the idea of "nested" loops (loops within loops) in BASIC.
2000 LDX #10
(start of 1st loop)
2002 LDY #0 (start of 2nd loop)
2005 BNE 2004 (if Y isn't yet zero, loop back to DEcrease Y again - this is the inner loop)
2007 DEX (reduce the outer loop by one)
2008 BNE 2002 (if X isn't yet zero, go through the entire DEY loop again)
200A (continue with the rest of the program ...)
One thing to watch out for: be sure that a loop BNE's back up to one address after the start of its loop. The start of the loop sets a number into a register and, if you keep looping up to it, you'll always be putting the same number into it. The DEcrement (decrease by one) instruction would then never bring it down to zero to end the looping. You'll have created an endless loop.
The example above could be used for a "timing loop" similarly to the way that BASIC creates delays with: FOR T=1 TO 2000: NEXT T. Also, sometimes you do want to create an endless loop (the BEGIN... UNTIL in "structured programming"). A popular "endless" loop structure in BASIC waits until the user hits any key: 10 GET K$: IF K$ = " " THEN 10.
10 IF PEEK (764)=255 THEN 10 is the way to accomplish this on the Atari; it will cycle endlessly unless a key is pressed. The simplest way to accomplish this in ML is to look on the map of your computer to find which byte holds the "last key pressed" number. On Upgrade and 4.0 CBM/PET, it's address 151. On Atari, it's 764. On Apple II, it's -16384. On VIC and Commodore 64, it's 203 with a 64 in that location if no key is pressed. In any event, when a key is pressed, it deposits its special numerical value into this cell. If no key is pressed, some standard value stays there all the time. We'll use the CBM as our model here. If no key is pressed, location 151 will hold a 255:
2000 LDA 151
2002 CMP #255
2004 BEQ 2000
If the CMP is EQual, this means that the LDA pulled a 255 out of address 151 and, thus, no key is pressed. So, we keep looping until the value of address 151 is something other than 255. This setup is like GET in BASIC because not only does it wait until a key is pressed, but it also leaves the value of the key in the accumulator when it's finished.
Recall that a CMP performs a subtraction. It subtracts the number in its argument from whatever number sits in the accumulator at the time. LDA #12 CMP $15 would subtract a 5 from 12 if 5 is the number "held" in address 15. This is how it can leave flags set for testing by BEQ or BNE. The key difference between this "subtraction" and SBC is that neither the accumulator nor the argument is affected at all by it. They stay what they were. The result of the subtraction is "thrown away," and all that happens is that the status flags go up or down in response to the result. If the CMP subtraction causes an answer of zero, the Z flag flips up. If the answer is not zero, the Z flag flips down. Then, BNE or BEQ can do their job - checking flags.
Dealing With Strings
You've probably been wondering how ML handles strings. It's pretty straightforward. There are essentially two ways: known-length and zero-delimit. If you know how many characters there are in a message, you can store this number at the very start of the text: "5ERROR." (The number 5 will fit into one byte, at the start of the text of the message.) If this little message is stored in your "message zone" - some arbitrary area of free memory you've set aside to hold all of your messages - you would make a note of the particular address of the "ERROR" message. Say it's stored at 4070. To print it out, you have to know where you "are" on your screen (cursor position). Usually, the cursor address is held in two bytes in zero page so you can use Indirect,Y addressing.
Alternatively, you could simply set up your own zero-page pointers to the screen. For Apple II and Commodore 64, the screen memory starts at 1024; for CBM/PET it's 32768. In any case, you'll be able to set up a "cursor management" system for yourself. To simplify, we'll send our message to the beginning of the Apple's screen:
2000 LDX 4070
||(remember, we put the length of the
message as the first byte of the message, so we load our counter
with the length)
||(Y will be our message offset)
||(gets the character at the address plus
Y. Y is zero the first time through the loop, so the "e" from here
lands in the accumulator. It also stays in 4071. It's just being
copied into the accumulator.)
||(we can make Y do double duty as the offset
for both the stored message and the screen-printout. Y is still
zero the first time through this loop, so the "e" goes to 1024.)
||(prepare to add one to the message-storage
location and to the screen-print location)
||(lower the counter by one)
||(if X isn't used up yet, go back and
get-andprint the next character, the "r")
If The Length Is Not Known
The alternative to knowing the length of a string is to put a special character (usually zero) at the end of each message to show its limit. This is called a delimiter. Note that Atari users cannot make zero the delimiter because zero is used to represent the space character. A zero works well for other computers because, in ASCII, the value 0 has no character or function (such as carriage return) coded to it. Consequently, any time the computer loads a zero into the accumulator (which will flip up the Z flag), it will then know that it is at the end of your message. At 4070, we might have a couple of error messages: "Ball out of range0Time nearly up!0". (These are numeric, not ASCII, zeros. ASCII zero has a value of 48.)
To print the time warning message to the top of the CBM/PET screen (this is in decimal):
2000 LDY #0
||(get the "T")
||(the LDA just above will flip the zero
flag up if it loads a zero, so we forward branch out of our message-printing
loop. "BEQ 2005" is a dummy target, used until we know the actual
target and can POKE it into 2006.)
||(we're using the Y as a double-duty offset
||(in this loop, we always jump back. Our exit
from the loop is not here, at the end. Rather, it is the Branch
if EQual which is within the loop.)
||(continue with another part of the program)
By the way, you should notice that the Simple Assembler will reject the commas in this example and, if you've forgotten to set line 10 to accept decimal, it will not accept the single zero in LDY #0. Also, if you get unpredictable results, maybe decimal 2000 is not a safe address to store your ML. You might need to use some other practice area.
Now that we know the address which follows the loop (2014), we can POKE that address into the "false forward branch" we left in address 2006. What number do we POKE into 2006? Just subtract 2007 from 2014, which is seven. Using the Simple Assembler, type POKE and you can take care of this while you remember it. The assembler will perform the POKE and then return to wait for your next instruction.
Both of these ways of handling messages are effective, but you must make a list on paper of the starting addresses of each message. In ML, you have the responsibility for some of the tasks that BASIC (at an expense of speed) does for you. Also, no message can be larger than 255 using the methods above because the offset and counter registers count only that high before starting over at zero again. Printing two strings back-to-back gives a longer, but still under 255 byte, message:
2000 LDY #0
||(in this example, we use X as a counter
which represents the number of messages we are printing)
||(get the "B" from "Ball out of . . .
||(go to reduce [and check] the value of
||(we're using the Y as a double-duty offset
||(we need to raise Y since we skipped that
step when we branched out of the loop)
||(at the end of the first message, X will
be a "1"; at the end of the second message, it will be zero)
||(if X isn't down to zero yet, re-enter
the loop to print out the second message)
To fill your screen with instructions instantly (say at the start of a game), you can use the following mass-move. We'll assume that the instructions go from 5000 to 5400 in memory and you want to transfer them to the PET screen (at $8000). If your computer's screen RAM moves around (adding memory to VIC will move the screen RAM address), you will need to know and substitute the correct address for your computer in these examples which print to the screen. This is in hex:
2000 LDY #0
||(if Y hasn't counted up to zero - which comes
just above 255 - go back and load-store the next character in each
quarter of the large message )
This technique is fast and easy any time you want to mass-move one area of memory to another. It makes a copy and does not disturb the original memory. To mass-clear a memory zone (to clear the screen, for example), you can use a similar loop, but instead of loading the accumulator each time with a different character, you load it at the start with the character your computer uses to blank the screen. (Commodore including VIC and Apple =decimal 32; Atari =0):
2000 LDA #20
(this example, in hex, blanks the PET screen)
2002 LDY #0
2004 STA 8000,Y
2007 STA 5100,Y
200A STA 8200,Y
200D STA 8300,Y
2011 BNE 2004
Of course, you could simply JSR to the routine which already exists in your BASIC to clear the screen. In Chapter 7 we will explore the techniques of using parts of BASIC as examples to learn from and also as a collection of ready-made ML subroutines. Now, though, we can look at how subroutines are handled in ML.
JMP, JSR, RTS
JMP has only one useful addressing mode: Absolute. You give it a firm, two-byte argument and it goes there. The argument is put into the Program Counter and control of the computer is transferred to this new address where an instruction there is acted upon. (There is a second addressing mode, JMP Indirect, which, you will recall, has a bug and is best left unused.)
JSR can only use Absolute addressing.
RTS's addressing mode is Implied. The address is on the stack, put there during the JSR.
None of these instructions has any effect on the flags.
JSR (Jump to SubRoutine) is the same as GOSUB in BASIC, but instead of giving a line number, you give an address in memory where the subroutine sits. RTS (ReTurn from Subroutine) is the same as RETURN in BASIC, but instead of returning to the next BASIC command, you return to the address following the JSR instruction (it's a three-byte-long ML instruction containing JSR and the two-byte target address). JMP (JuMP) is GOTO. Again, you JMP to an address, not a line number. As in BASIC, there is no RETURN from a JMP.
Some Further Cautions About The Stack
The stack is like a pile of coins. The last one you put on top of the pile is the first one pulled off later. The main reason that the 6502 sets aside an entire page of memory especially for the stack is that it has to know where to go back to after GOSUBs and JSRs.
A JSR instruction pushes the correct return address onto the "stack" and, later, the next RTS "pulls" the top two numbers off the stack to use as its argument (target address) for the return. Some programmers, as we noted before, like to play with the stack and use it as a temporary register to PHA (PusH Accumulator onto the stack). This sort of thing is best avoided until you are an advanced ML programmer. Stack manipulations often result in a very confusing program. Handling the stack is one of the few things that the computer does for you in ML. Let it.
The main function of the stack (as far as we're concerned) is to hold return addresses. It's done automatically for us by "pushes" with the JSR and, later, "pulls" (sometimes called pops) with the RTS. If we don't bother the stack, it will serve us well. There are thousands upon thousands of cells where you could temporarily leave the accumulator - or any other value - without fouling up the orderly arrangement of your return addresses.
Subroutines are extremely important in ML programming. ML programs are designed around them, as we'll see. There are times when you'll be several subroutines deep (one will call another which calls another); this is not as confusing as it sounds. Your main Player-input routine might call a print-message subroutine which itself calls a wait-until-key-is-pressed subroutine. If any of these routines PHA (PusH the Accumulator onto the stack), they then disturb the addresses on the stack. If the extra number on top of the stack isn't PLA-ed off (Pull, Accumulator), the next RTS will pull off the number that was PHA'ed and half of the correct address. It will then merrily return to what it thinks is the correct address: it might land somewhere in the RAM, it might go to an address at the outer reaches of your operating system - but it certainly won't go where it should.
Some programmers like to change a GOSUB into a GOTO (in the middle of the action of a program) by PLA PLA. Pulling the two top stack values off has the effect of eliminating the most recent RTS address. It does leave a clean stack, but why bother to JSR at all if you later want to change it to a GOTO? Why not use JMP in the first place?
There are cases, too, when the stack has been used to hold the current condition of the flags (the Status Register byte). This is pushed/pulled from the stack with PHP (PusH Processor status) and PLP (PulL Processor status). If you should need to "remember" the condition of the status flags, why not just PHP PLA STA $NN? ("NN" means the address is your choice.) Set aside a byte somewhere that can hold the flags (they are always changing inside the Status Register) for later and keep the stack clean. Leave stack acrobatics to FORTH programmers. The stack, except for advanced ML, should be inviolate.
FORTH, an interesting language, requires frequent stack manipulations. But in the FORTH environment, the reasons for this and its protocol make excellent sense. In ML, though, stack manipulations are a sticky business.
Saving The Current Environment
There is one exception to our leave-the-stack-alone rule. Sometimes (especially when you are "borrowing" a routine from BASIC) you will want to take up with your own program from where it left off. That is, you might not want to write a "clear the screen" subroutine because you find the address of such a routine on your map of BASIC. However, you don't know what sorts of things BASIC will do in the meantime to your registers or your flags, etc. In other words, you just want to clear the screen without disturbing the flow of your program by unpredictable effects on your X, Y, A, and status registers. In such a case, you can use the following "Save the state of things" routine:
||(push the status register onto the stack)
||(to the clear-the-screen routine in BASIC.
The RTS will remove the return address , and you'll have a
mirror image of the things you had pushed onto the stack. They are
pulled out in reverse order, as you can see below. This is because the
first pull from the stack will get the most recently pushed number.
If you make a little stack of coins, the first one you pull off will
be the last one you put onto the stack.)
||(now we reverse the order to get them back)
||(this one stays in A)
|| (the status register)
Saving the current state of things before visiting an uncharted, unpredictable subroutine is probably the only valid excuse for playing with the stack as a beginner in ML. The routine above is constructed to leave the stack intact. Everything that was pushed on has been pulled back off.
The Significance Of Subroutines
Maybe the best way to approach ML program writing - especially a large program - is to think of it as a collection of subroutines. Each of these subroutines should be small. It should be listed on a piece of paper followed by a note on what it needs as input and what it gives back as parameters. "Parameter passing" simply means that a subroutine needs to know things from the main program (parameters) which are handed to it (passed) in some way.
The current position of the ball on the screen is a parameter which has its own "register" (we set aside a register for it at the start when we were assigning memory space on paper). So, the "send the ball down one space" subroutine is a double-adder which adds 40 or whatever to the "current position register." This value always sits in the register to be used any time any subroutine needs this information. The "send the ball down one" subroutine sends the current-position parameter by passing it to the current-position register.
This is one way that parameters are passed. Another illustration might be when you are telling a delay loop how long to delay. Ideally, your delay subroutine will be multi-purpose. That is, it can delay for anywhere from 1/2 second to 60 seconds or something. This means that the subroutine itself isn't locked into a particular length of delay. The main program will "pass" the amount of delay to the subroutine.
3000 LDY #0
3003 BNE 3002
3006 BNE 3000
Notice that X never is initialized (set up) here with any particular value. This is because the value of X is passed to this subroutine from the main program. If you want a short delay, you would:
2000 LDX #5 (decimal)
2002 JSR 3000
And for a delay which is twice as long as that:
2000 LDX #10
2002 JSR 3000
In some ways, the less a subroutine does, the better. If it's not entirely self-sufficient, and the shorter and simpler it is, the more versatile it will be. For example, our delay above could function to time responses, to hold sounds for specific durations, etc. When you make notes, write something like this: 3000 DELAY LOOP (Expects duration in X. Returns 0 in X.). The longest duration would be LDX #0. This is because the first thing that happens to X in the delay subroutine is DEX. If you DEX a zero, you get 255. If you need longer delays than the maximum value of X, simply:
3000 LDX #0
|| (notice that we don't need to set X to zero
this second time. It returns from the subroutine with a zeroed X.)
You could even make a loop of the JSR's above for extremely long delays. The point to notice here is that it helps to document each subroutine in your library: what parameters it expects, what registers, flags, etc., it changes, and what it leaves behind as a result. This documentation - a single sheet of paper will do - helps you remember each routine's address and lets you know what effects and preconditions are involved.
Like BASIC's GOTO, JMP is easy to understand. It goes to an address: JMP 5000 leaps from wherever it is to start carrying out the instructions which start at 5000. It doesn't affect any flags. It doesn't do anything to the stack. It's clean and simple. Yet some advocates of "structured programming" suggest avoiding JMP (and GOTO in BASIC). Their reasoning is that JMP is a shortcut and a poor programming habit.
For one thing, they argue, using GOTO makes programs confusing. If you drew lines to show a program's "flow" (the order in which instructions are carried out), a program with lots of GOTO's would look like boiled spaghetti. Many programmers feel, however, that JMP has its uses. Clearly, you should not overdo it and lean heavily on JMP. In fact, you might see if there isn't a better way to accomplish something if you find yourself using it all the time and your programs are becoming impossibly awkward. But JMP is convenient, often necessary in ML.
A 6502 Bug
On the other hand, there is another, rather peculiar JMP form which is hardly ever used in ML: JMP (5000). This is an indirect jump which works like the indirect addressing we've seen before. Remember that in Indirect, Y addressing (LDA (81),Y), the number in Y is added to the address found in 81 and 82. This address is the real place we are LDAing from, sometimes called the effective address. If 81 holds a 00, 82 holds a 40, and Y holds a 2, the address we LDA from is going to be 4002. Similarly (but without adding Y), the effective address formed by the two bytes at the address inside the parentheses becomes the place we JMP to in JMP (5000).
There are no necessary uses for this instruction. Best avoid it the same way you avoid playing around with the stack until you're an ML expert. If you find it in your computer's BASIC ROM code, it will probably be involved in an "indirect jump table," a series of registers which are dynamic. That is, they can be changed as the program progresses. Such a technique is very close to a self-altering program and would have few uses for the beginner in ML programming. Above all, there is a bug in the 6502 itself which causes indirect JMP to malfunction under certain circumstances. Put JMP ($NNNN) into the same category as BPL and BMI. Avoid all three.
If you decide you must use indirect JMP, be sure to avoid the edge of pages: JMP ($NNFF). The "NN" means "any number." Whenever the low byte is right on the edge, if $FF is ready to reset to 00, this instruction will correctly use the low byte (LSB) found in address $NNFF, but it will not pick up the high byte (MSB) from $NNFF plus one, as it should. It gets the MSB from NN00!
Here's how the error would look if you had set up a pointer to address $5043 at location $40FF:
Your intention would be to JMP to $5403 by bouncing off this pointer. You would write JMP ($40FF) and expect that the next instruction the computer would follow would be whatever is written at $5043. Unfortunately, you would land at $0043 instead (if address $4000 held a zero). It would get its MSB from $4000.
BRK and NOP
BRK and NOP have no argument and are therefore members of that class of instructions which use only the Implied addressing mode. They also affect no flags in any way with which we would be concerned. BRK does affect the I and B flags, but since it is a rare situation which would require testing those flags, we can ignore this flag activity altogether.
After you've assembled your program and it doesn't work as expected (few do), you start debugging. Some studies have shown that debugging takes up more than fifty percent of programming time. Such surveys might be somewhat misleading, however, because "making improvements and adding options" frequently takes place after the program is allegedly finished, and would be thereby categorized as part of the debugging process.
In ML, debugging is facilitated by setting breakpoints with BRK and then seeing what's happening in the registers or memory. If you insert a BRK, it has the effect of halting the program and sending you into your monitor where you can examine, say, the Y register to see if it contains what you would expect it to at this point in the program. It's similar to BASIC's STOP instruction:
2000 LDA #15
If you run the above, it will carry out the instructions until it gets to BRK when it will put the program counter plus two on the stack, put the status register on the stack, and load the program counter with whatever is in addresses $FFFE, $FFFF. These are the two highest addresses in your computer and they contain the vector (a pointer) for an interrupt request (IRQ).
These addresses will point to a general interrupt handler and, if your computer has a monitor, its address might normally be found here. Remember, though, that when you get ready to CONT, the address on the top of the stack will be the BRK address plus two. Check the program counter (it will appear when your monitor displays the registers) to see if you need to modify it to point to the next instruction instead of pointing, as it might be, to an argument. Some monitors adjust the program counter when they are BRKed to so that you can type g (go) in the same way that you would type CONT in BASIC. See the instructions for your particular monitor.
In effect, you debug whenever your program runs merrily along and then does something unexpected. It might crash and lock you out. You look for a likely place where you think it is failing and just insert a BRK right over some other instruction. Remember that in the monitor mode you can display a hex dump and type over the hex numbers on screen, hitting RETURN to change them. In the example above, imagine that we put the BRK over an STY 8000. Make a note of the hex number of the instruction you covered over with the BRK so you can restore it later. After checking the registers and memory, you might find something wrong. Then you can fix the error.
If nothing seems wrong at this point, restore the original STY over the BRK, and insert a BRK in somewhere further on. By this process, you can isolate the cause of an oddity in your program. Setting breakpoints (like putting STOP into BASIC programs) is an effective way to run part of a program and then examine the variables.
If your monitor or assembler allows single-stepping, this can be an excellent way to debug, too. Your computer performs each instruction in your program one step at a time. This is like having BRK between each instruction in the program. You can control the speed of the stepping from the keyboard. Single-stepping automates breakpoint checking. It is the equivalent of the TRACE command sometimes used to debug BASIC programs.
Like BRK ($00), the hex number of NOP ($EA) is worth memorizing. If you're working within your monitor, it will want you to work in hex numbers. These two are particularly worth knowing. NOP means No OPeration. The computer slides over NOP's without taking any action other than increasing the program counter. There are two ways in which NOP can be effectively used.
First, it can be an eraser. If you suspect that STY 8000 is causing all the trouble, try running your program with everything else the same, but with STY 8000 erased. Simply put three EA's over the instruction and argument. (Make a note, though, of what was under the EA's so you can restore it.) Then, the program will run without this instruction and you can watch the effects.
Second, it is sometimes useful to use EA to temporarily hold open some space. If you don't know something (an address, a graphics value) during assembly, EA can mark that this space needs to be filled in later before the program is run. As an instruction, it will let the program slide by. But, remember, as an address or a number, EA will be thought of as 234. In any case, EA could become your "fill this in" alert within programs in the way that we use self-branching (leaving a zero after a BNE or other branch instruction) to show that we need to put in a forward branch's address.
When the time comes for you to "tidy up" your program, use your monitor's "find" command, if it has one. This is a search routine: you tell it where to start and end and what to look for, and it prints out the addresses of any matches it finds. It's a useful utility; if your monitor does not have a search function, you might consider writing one as your first large ML project. You can use some of the ideas in Chapter 8 as a starting point.
Less Common Instructions
The following instructions are not often necessary for beginning applications, but we can briefly touch on their main uses. There are several "logical" instructions which can manipulate or test individual bits within each byte. This is most often necessary when interfacing. If you need to test what's coming in from a disk drive, or translate on a bit-by-bit level for I/O (input/output), you might work with the "logical" group.
In general, this is handled for you by your machine's operating system and is well beyond beginning ML programming. I/O is perhaps the most difficult, or at least the most complicated, aspect of ML programming. When putting things on the screen, programming is fairly straightforward, but handling the data stream into and out of a disk is pretty involved. Timing must be precise, and the preconditions which need to be established are complex.
For example, if you need to "mask" a byte by changing some of its bits to zero, you can use the AND instruction. After an AND, both numbers must have contained a 1 in any particular bit position for it to result in a 1 in the answer. This lets you set up a mask: 00001111 will zero any bits within the left four positions. So, 00001111 AND 11001100 result in 00001100. The unmasked bits remained unchanged, but the four high bits were all masked and zeroed. The ORA instruction is the same, except it lets you mask to set bits (make them a 1). 11110000 ORA 11001100 results in 11111100. The accumulator will hold the results of these instructions.
EOR (Exclusive OR) permits you to "toggle" bits. If a bit is one it will go to zero. If it's zero, it will flip to one. EOR is sometimes useful in games. If you are heading in one direction and you want to go back when bouncing a ball off a wall, you could "toggle." Let's say that you use a register to show direction: when the ball's going up, the byte contains the number 1 (00000001), but down is zero (00000000). To toggle this least significant bit, you would EOR with 00000001. This would flip 1 to zero and zero to 1. This action results in the complement of a number. 11111111 EOR 11001100 results in 00110011.
To know the effects of these logical operators, we can look them up in "truth tables" which give the results of all possible combinations of zeros and ones:
|0 AND 0=0
||0 OR 0=0
||0 EOR 0=0
||0 OR 1=1
||0 EOR 1=1
|1 AND 0=0
||1 OR 0=1
||1 EOR 0=1
|1 AND 1=1
||1 OR 1=1
||1 EOR 1=0
Another instruction, BIT, also tests (it does an AND), but, like CMP, it does not affect the number in the accumulator - it merely sets flags in the status register. The N flag is set (has a 1) if bit seven has a 1 (and vice versa). The V flag responds similarly to the value in the sixth bit. The Z flag shows if the AND resulted in zero or not. Instructions, like BIT, which do not affect the numbers being tested are called non-destructive.
We discussed LSR and ASL in the chapter on arithmetic: they can conveniently divide and multiply by two. ROL and ROR rotate the bits left or right in a byte but, unlike with the Logical Shift Right or Arithmetic Shift Left, no bits are dropped during the shift. ROL will leave the 7th (most significant) bit in the carry flag, leave the carry flag in the 0th (least significant bit), and move every other bit one space to the left:
ROL 11001100 (with the carry
flag set) results in
10011001 (carry is still set, it got the leftmost 1)
If you disassemble your computer's BASIC, you may well look in vain for an example of ROL, but it and ROR are available in the 6502 instruction set if you should ever find a use for them. Should you go into advanced ML arithmetic, they can be used for multiplication and division routines.
Three other instructions remain: SEI (SEt Interrupt), RTI (ReTurn from Interrupt), and CLI (CLear Interrupt). These operations are, also, beyond the scope of a book on beginning ML programming, but we'll briefly note their effects. Your computer gets busy as soon as the power goes on. Things are always happening: timing registers are being updated; the keyboard, the video, and the peripheral connectors are being refreshed or examined for signals. To "interrupt" all this activity, you can SEI, perform some task, and then CLI to let things pick up where they left off.
SEI sets the interrupt flag. Following this, all maskable interruptions (things which can be blocked from interrupting when the interrupt status flag is up) are no longer possible. There are also non-maskable interrupts which, as you might guess, will jump in anytime, ignoring the status register.
The RTI instruction (ReTurn from Interrupt) restores the program counter and status register (takes them from the stack), but the X, Y, etc., registers might have been changed during the interrupt. Recall that our discussion of the BRK involved the above actions. The key difference is that BRK stores the program counter plus two on the stack and sets the B flag on the status register. CLI puts the interrupt flag down and lets all interrupts take place.
If these last instructions are confusing to you, it doesn't matter. They are essentially hardware and interface related. You can do nearly everything you will want to do in ML without them. How often have you used WAIT in BASIC?
Return to Table of Contents | Previous Chapter | Next Chapter