There'll be many things you'll want to do in ML, but complicated math is not one of them. Mathematics beyond simple addition and subtraction (and a very easy form of elementary division and multiplication) will not be covered in this book. For most games and other ML for personal computing, you will rarely need to program with any complex math. In this chapter we will cover what you are likely to want to know. BASIC is well-suited to mathematical programming and is far easier to program for such tasks.
Before we look at ML arithmetic, it is worth reviewing an important concept: how the computer tells the difference between addresses, numbers as such, and instructions. It is valuable to be able to visualize what the computer is going to do as it comes upon each byte in your ML routine.
Even when the computer is working with words, letters of the alphabet, graphics symbols and the like - it is still working with numbers. A computer works only with numbers. The ASCII code is a convention by which the computer understands that when the context is alphabetic, the number 65 means the letter A. At first this is confusing. How does it know when 65 is A and when it is just 65? The third possibility is that the 65 could represent the 65th cell in the computer's memory.
It is important to remember that, like us, the computer means different things at different times when it uses a symbol (like 65). We can mean a street address by it, a temperature, the cost of a milk shake, or even a secret code. We could agree that whenever we used the symbol "65" we were ready to leave a party. The point is that symbols aren't anything in themselves. They stand for other things, and what they stand for must be agreed upon in advance. There must be rules. A code is an agreement in advance that one thing symbolizes another.
The Computer's Rules
Inside your machine, at the most basic level, there is a stream of input. The stream flows continually past a "gate" like a river through a canal. For 99 percent of the time, this input is zeros. (BASICs differ; some see continuous 255's, but the idea is the same.) You turn it on and the computer sits there. What's it doing? It might be updating a clock, if you have one, and it's holding things coherent on the TV screen - but it mainly waits in an endless loop for you to press a key on your keyboard to let it know what it's supposed to do. There is a memory cell inside (this, too, varies in its location) which the computer constantly checks. On some computers, this cell always has a 255 in it unless a key is pressed. If you press the RETURN key, a 13 will replace the 255. At last, after centuries (the computer's sense of time differs from ours) here is something to work with! Something has come up to the gate at long last.
You notice the effect at once - everything on the screen moves up one line because 13 (in the ASCII code) stands for carriage return. How did it know that you were not intending to type the number 13 when it saw 13 in the keyboard sampling cell? Simple. The number 13, and any other keyboard input, is always read as an ASCII number. In ASCII, the digits from 0 through 9 are the only number symbols. There is no single symbol for 13. So, when you type in a 1 followed immediately by a 3, the computer's input-from-the-keyboard routine scans the line on the screen and notices that you have not pressed the "instant action" keys (the STOP, BREAK, ESC, TAB, cursor-control keys, etc.). Rather, you typed 1 and 3 and the keyboard sampling cell (the "which key pressed" address in zero page) received the ASCII value for one and then for three. ASCII digits are easy to remember in hex: zero is 30, 1 is 31, and up to 39 for nine. In decimal, they are 48 through 57.
The computer decides the "meaning" of the numbers which flow into and through it by the numbers' context. If it is in "alphabetic" mode, the computer will see the number 65 as "a"; or if it has just received an "a," it might see a subsequent number 65 as an address to store the "a". It all depends on the events that surround a given number. We can illustrate this with a simple example:
2000 LDA #65 A9
(169) 41 (65)
2000 STA $65 85 (133) 41 (65)
This short ML program (the numbers in parentheses are the decimal values) shows how the computer can "expect" different meanings from the number 65 (or 41 hex). When it receives an instruction to perform an action, it is then prepared to act upon a number. The instruction comes first and, since it is the first thing the computer sees when it starts a job, it knows that the A9 (169) is not a number. It has to be one of the ML instructions from its set of instructions (see Appendix A).
Instructions And Their Arguments
The computer would no more think of this first 169 as the number 169 than you would seal an envelope before the letter was inside. If you are sending out a pile of Christmas cards, you perform instruction-argument just the way the computer does: you (1) fill the envelope (instruction) (2) with a card (argument or operand). All actions do something to something. A computer's action is called an instruction (or, in its numeric form inside the computer's memory it's called an opcode for operation code). The target of the action is called the instruction's argument (operand). In our program above, the computer must LoaD Accumulator with 65. The # symbol means "immediate"; the target is right there in the next memory cell following the mnemonic LDA, so it isn't supposed to be fetched from a distant memory cell.
Then the action is complete, and the next number (the 133 which means STore Accumulator in zero page, the first 256 cells) is seen as the start of another complete action. The action of storing always signals that the number following the store instruction must be an address of a cell in memory to store to.
Think of the computer as completing each action and then looking for another instruction. Recall from the last chapter that the target can be "implied" in the sense that INX simply increases the X register by one. That "one" is "implied" by the instruction itself, so there is no target argument in these cases. The next cell in this case must also contain an instruction for a new instruction-argument cycle.
Some instructions call for a single-byte argument. LDA #65 is of this type. You cannot LoaD Accumulator with anything greater than 255. The accumulator is only one byte large, so anything that can be loaded into it can also be only a single byte large. Recall that $FF (255 decimal) is the largest number that can be represented by a single byte. STA $65 also has a one byte argument because the target address for the STore Accumulator is, in this case, in zero page. Storing to zero page or loading from it will need only a one byte argument - the address. Zero page addressing is a special case, but an assembler program will take care of it for you. It will pick the correct opcode for this addressing mode when you type LDA $65. LDA $0065 would create ML code that performs the same operation though it would use three bytes instead of two to do it.
The program counter is like a finger that keeps track of where the computer is located in its trip up a series of ML instructions. Each instruction takes up one, two, or three bytes, depending on what type of addressing is going on.
Context Defines Meaning
TXA uses only one byte so the program counter (PC) moves ahead one byte and stops and waits until the value in the X register is moved over to the accumulator. Then the computer asks the PC, "Where are we?" and the PC is pointing to the address of the next instruction. It never points to an argument. It skips over them because it knows how many bytes each addressing mode uses up in a program.
Say that the next addresses contain an LDA $15. This is two bytes long (zero page addressing). The PC is raised by two. The longest possible instruction would be using three bytes, such as LDA $5000 (absolute addressing). Here the argument takes up two bytes. Add that to the one byte used by any instruction and you have a total of three bytes for the PC to count off. Zero page LDA is represented by the number A5 and Absolute LDA is AD. Since the opcodes are different, even though the mnemonics are identical, the computer can know how many bytes the instruction will use up.
Having reviewed the way that your computer makes contextual sense out of the mass of seemingly similar numbers of which an ML program is composed, we can move on to see how elementary arithmetic is performed in ML.
Arithmetic is performed in the accumulator. The accumulator holds the first number, the target address holds the second number (but is not affected by the activities), and the result is left in the accumulator. So:
LDA #$40 (remember, the # means immediate,
the $ means hex)
will result in the number 41 being left in the accumulator. We could then STA that number wherever we wanted. Simple enough. The ADC means ADd with Carry. If this addition problem resulted in a number higher than 255 (if we added, say, 250+6), then there would have to be a way to show that the number left behind in the accumulator was not the correct result. What's left behind is the carry. What would happen after adding 250+6 is that the accumulator would contain a 1. To show that the answer is really 256 (and not 1), the "carry flag" in the status register flips up. So, if that flag is up, we know that the real answer is 255 plus the 1 Left in the accumulator.
To make sure that things never get confused, always put in a CLC (CLear Carry) before any addition problems. Then the flag will go down before any addition and, if it is up afterward, we'll know that we need to add 256 to whatever is in the accumulator. We'll know that the accumulator holds the carry, not the total result.
One other point about the status register: there is another flag, the "decimal" flag. If you ever set this flag up (SED), all addition and subtraction is performed in a decimal mode in which the carry flag is set when addition exceeds 99. In this book, we are not going into the decimal mode at all, so it's a good precaution to put a CLear Decimal mode (CLD) instruction as the first instruction of any ML program you write. After you type CLD, the flag will be put down and the assembler will move on to ask for your next instruction, but all the arithmetic from then on will be as we are describing it.
Adding Numbers Larger Than 255
We have already discussed the idea of setting aside some memory cells as a table for data. All we do is make a note to ourselves that, say, $80 and $81 are declared a zone for our personal use as a storage area. Using a familiar example, let's think of this zone as the address that holds the address of a ball-like character for a game. As long as the addresses are not in ROM, or used by our program elsewhere, or used by the computer (see your computer's memory map), it's fine to declare any area a data zone. It is a good idea (especially with longer programs) to make notes on a piece of paper to show where you intend to have your subroutines, your main loop, your initialization, and all the miscellaneous data - names, messages for the screen, input from the keyboard, etc. This is one of those things that BASIC does for you automatically, but which you must do for yourself in ML.
When BASIC creates a string variable, it sets aside an area to store variables. This is what DIM does. In ML, you set aside your own areas by simply finding a safe and unused memory space and then not writing a part of your program into it. Part of your data zone can be special registers you declare to hold the results of addition or subtraction. You might make a note to yourself that $80 and $81 will hold the current address of the bouncing ball in your game. Since the ball is constantly in motion, this register will be changing all the time, depending on whether the ball hit a wall, a paddle, etc. Notice that you need two bytes for this register. That is because one byte could hold only a number from 0 to 255. Two bytes together, though, can hold a number up to 65535.
In fact, a two-byte register can address any cell in most microcomputers because most of us have machines with a total of 65536 memory cells (from zero to 65535). So if your ball is located (on your screen) at $8000 and you must move it down one, just change the ball-address register you have set up. If your screen has 40 columns, you would want to add 40 to this register.
The ball address register now looks like this: $0080 00 80 (remember that the higher, most significant byte, comes after the LSB, the least significant byte in the 6502's way of looking at pointers). We want it to be: $0080 28 80. (The 28 is hex for 40.) In other words, we're going to move the ball down one line on a 40-column screen.
Remember the "indirect Y" addressing mode described in the previous chapter? It lets us 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 STA to $80 or $81, but rather to the address that they contain. STA ($80),Y or, using the simplified punctuation rules of the Simple Assembler: STA (80)Y.
Moving A Ball Down
How to add $28 to the ball address register? First of all, CLC, clear the carry to be sure that flag is down. To simplify our addition, we can set aside another special register which serves only to hold the $28 as a double-byte number all through the game: $4009 28 00. This is the size of one screen line in our 40-column computer and it won't change. Since it moves the ball down one screen line, it can be used equally well for a subtraction that would move the ball up one screen line as well. Now to add them together:
1000 CLC (1000
is our "add 40 to ball address" subroutine)
1001 LDA $80 (we fetch the LSB of ball address)
1003 ADC $4009 (LSB of our permanent screen line size)
1006 STA $80 (put the new result into the ball address)
1008 LDA $81 (get the MSB of ball address)
100A ADC $400A (add with carry to the MSB of screen value)
100D STA $81 (update the ball address MSB)
That's it. Any carry will automatically set the carry flag up during the ADC action on the LSB and will be added into the result when we ADC to the MSB. It's all quite similar to the way that we add ordinary decimal numbers, putting a carry onto the next column when we get more than a 10 in the first column. And this carrying is why we always CLC (clear the carry flag, putting it down) just before additions. If the carry is set, we could get the wrong answer if our problem did not result in a carry. Did the addition above cause a carry?
Note that we need not check for any carries during the MSB+MSB addition. Any carries resulting in a screen address greater than $FFFF (65535) would be impossible on our machines. The 6502 is permitted to address $FFFF tops, under normal conditions.
As you might expect, subtracting single-byte numbers is a snap:
results in a $40 being left in the accumulator. As before, though, it is good to make it a habit to deal with the carry flag before each calculation. When subtracting, however, you set the carry flag: SEC. Why is unimportant. Just always SEC before any subtractions, and your answers will be correct. Here's double subtracting that will move the ball up the screen one line instead of down one line:
$1020 SEC ($1020
is our "take 40 from ball address" subroutine)
1021 LDA $80 (get the LSB of ball address)
1023 SBC $4009 (LSB of our permanent screen line value)
1026 STA $80 (put the new result into the ball address)
1028 LDA $81 (get the MSB of ball address)
102A SBC $400A (subtract the MSB of screen value)
102D STA $81 (update the ball address MSB)
Multiplication And Division
Multiplying could be done by repeated adding. To multiply 5 x 4, you could just add 4+4+4+4+4. One way would be to set up two registers like the ones we used above, both containing 04, and then loop through the addition process five times. For practical purposes, though, multiplying and dividing are much more easily accomplished in BASIC. They simply are often not worth the trouble of setting up in ML, especially if you will need results involving decimal points (floating point arithmetic). Perhaps surprisingly, for the games and personal computing tasks where creating ML routines is useful, there is little use either for negative numbers or arithmetic beyond simple addition and subtraction.
If you find that you need complicated mathematical structures, create the program in BASIC, adding ML where super speeds are necessary or desirable. Such hybrid programs are efficient and, in their way, elegant. One final note: an easy way to divide the number in the accumulator by two is to LSR it. Try it. Similarly, you can multiply by two with ASL. We'll define LSR and ASL in the next chapter.
One rather tricky technique is used fairly often in ML and should be learned. It is tricky because there are two branch instructions which seem to be worth using in this context, but they are best avoided. If you are trying to keep track of the location of a ball on the screen, it will have a two-byte address. If you need to compare those two bytes against another two-byte address, you need a "double compare" subroutine. You might have to see if the ball is out of bounds or if there has been a collision with some other item flying around on screen. Double compare is also valuable in other kinds of ML programming.
The problem is the BPL (Branch on PLus) and BMI (Branch on MInus) instructions. Don't use them for comparisons. In any comparisons, single- or double-byte, use BEQ to test if two numbers are equal; BNE for not equal; BCS for equal or higher; and BCC for lower. You can remember BCS because its "S" is higher and BCC because its "C" is lower in the alphabet. To see how to perform a double-compare, here's one easy way to do it. (See Program 5-1.)
Program 5-I. Double Compare.
||; ------- STORAGE AREAS -------
||TESTED .DE $1000
||SECOND .DE $1002
||; ------- LANDING PLACES ------
||LOWER .DE $1004
||EQUAL .DE $1005
||HIGHER .DE $1006
|1011- AD 00 10
LDA TESTED ; COMPARE THE LOW BYTES
|1014- ED 02 10
|1017- 8D 08 10
|101A- AD 01 10
LDA TESTED+1 ; COMPARE THE HIGH BYTES
|101D- ED 03 10
|1020- 0D 08 10
|1023- F0 E0
BEQ EQUAL ; TESTED = SECOND
|1025- 90 DD
BCC LOWER ; TESTED < SECOND
|1027- B0 DD
BCS HIGHER ; TESTED > SECOND
This is a full-dress, luxurious assembler at work. With such assemblers you can use line numbers and labels, add numbers to labels (see TESTED +1 in line 150), add comments, and all the rest. To try this out, type in the hex bytes on the left, starting at address $1010, which make up the program itself. Then fill bytes $1000-100f with zeros - that's your storage area for the numbers you are comparing as well as a simulated "landing place" where your computer will branch, demonstrating that the comparison worked correctly.
Now try putting different numbers into the two-byte zones called TESTED and SECOND. TESTED, at $1000, is the first, the tested, number. It's being tested against the second number, called SECOND. As you can see, you've got to keep it straight in your mind which number is the primary number. There has to be a way to tag them so that it means something when you say that one is larger (or smaller) than the other.
When you've set up the numbers in their registers ($1000 to $1003), you can run this routine by starting at $1010. All that will happen is that you will land on a BRK instruction. Where you land tells you the result of the comparison. If the numbers are equal, you land at $1005. If the TESTED number is less than the SECOND number, you'll end up at $1004. If all you needed to find out was whether they were unequal, you could use BNE. Or you could leave out branches that you weren't t interested in. Play around with this routine until you've understood the ideas involved.
In a real program, of course, you would be branching to the addresses of subroutines which do something if the numbers are equal or greater or whatever. This example sends the computer to $1004, $1005, or $1C06 just to let you see the effects of the double-compare subroutine. Above all, remember that comparing in ML is done with BCS and BCC (not BPL or BMI).
Return to Table of Contents | Previous Chapter | Next Chapter