6
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.
1. The Transporters: LDA, LDX,
LDY STA, STX, STY TAX,
TAY TXA, TYA
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.
Registers 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 TAY
TAX
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.
2. The Arithmetic Group: 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.
3. The Decision-Makers: 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:
LDY #0
|
(we want to fetch from the ball address itself,
so we don't want to add anything to it. Y is set to
zero.)
|
LDA (80),Y
|
(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?)
|
2002 BNE
200A
|
(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.)
|
2004 JSR
3000
|
(the ball printing subroutine)
|
2007 JMP
2020
|
(jump over the rest of the
comparisons)
|
200A CMP
#128
|
(is it our paddle symbol?)
|
200C BNE
2014
|
(if not, continue to next comparison)
|
200E JSR
3050
|
(do the paddle-handling subroutine and
...)
|
2011 JMP
2020
|
(jump over the rest, as before in
2007)
|
2014 CMP
#144
|
(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
|
|
2002 CMP
85
|
(is what was in address 65 equal to what was in
address 85?)
|
2004 BNE
2009
|
(if Not Equal, branch over the next three bytes
which perform some elaborate job)
|
2006 JSR
4000
|
(at 4000 sits an elaborate subroutine to take
care of cases where addresses 65 and 85 turn out to be
equal)
|
2009
|
(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
|
TESTBYTE=80
|
are sometimes called "equates.")
|
NEWBYTE=99
|
2004
LDA
|
TESTBYTE
|
2006
CMP
|
NEWBYTE
|
2008
BNE
|
BRANCHTARGET
|
200A JR
|
SPECIALSUBROUTINE
|
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:
Program 6- I .
|
0010
|
|
.BA
$2004
|
|
0020
|
TESTBYTE
|
.DE $80
|
|
0030
|
NEWBYTE
|
.DE $99
|
|
0040
|
;
|
|
2004- A9
80
|
0050
|
START
|
LDA #TESTBYTE ;
(IMMEDIATE ADDRE
|
2006- C5
99
|
0060
|
|
CMP *NEWBYTE ; (ZERO
PAGE ADDRESSING)
|
2008- D0
03
|
0070
|
|
BNE BRANCHTARGET
; (RELATIVE ADDRES
|
200A- 20 10
20
|
0080
|
|
JSR
SPECIALSUBROUTINE
|
200D- AD 00
04
|
0090
|
BRANCHTARGET
|
LDA $400 ;
YOU CAN FREELY MIX
|
|
0100
|
; LABLES AND
SUBROUTINES. ALSO, COMMENTS
|
|
0110
|
; WILL BE IGNORED
BY THE ASSEMBLER AND CAN
|
|
0120
|
; BE STUCK
ANYWHERE, AS YOU SEE.
|
|
0130
|
;
|
|
2010- AD 21
00
|
0140
|
SPECIALSUBROUTINE
|
LDA 33
|
|
0150
|
; ETC.
ETC.
|
|
|
0160
|
|
.EN
|
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.
4. The Loop Group: 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 2002
INX 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.
Big Loops 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) 2004 DEY 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)
|
2003 LDY
#0
|
(Y will be our message offset)
|
2005 LDA
4071,Y
|
(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.)
|
2008 STA
1024,Y
|
(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.)
|
2011
INY
|
(prepare to add one to the message-storage
location and to the screen-print location)
|
2012
DEX
|
(lower the counter by one)
|
2013 BNE
2005
|
(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
|
|
2002 LDA
4088,Y
|
(get the "T")
|
2005 BEQ
2005
|
(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.)
|
2007 STA
32768,Y
|
(we're using the Y as a double-duty offset
again)
|
2010
INY
|
|
2011 JMP
2002
|
(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.)
|
2014
|
(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
|
|
2002 LDX
#2
|
(in this example, we use X as a counter which
represents the number of messages we are printing)
|
2004 LDA
4000,Y
|
(get the "B" from "Ball out of . . .
")
|
2007 BEQ
2016
|
(go to reduce [and check] the value of
X)
|
2009 STA
32768,Y
|
(we're using the Y as a double-duty offset
again)
|
2012
INY
|
|
2013 JMP
2004
|
|
2016
INY
|
(we need to raise Y since we skipped that step
when we branched out of the loop)
|
2017
DEX
|
(at the end of the first message, X will be a
"1"; at the end of the second message, it will be
zero)
|
2018 BNE
2004
|
(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
|
|
2002 LDA
5000,Y
|
|
2005 STA
8000,Y
|
|
2008 LDA
5100,Y
|
|
200B STA
8100,Y
|
|
200E LDA
5200,Y
|
|
2011 STA
8200,Y
|
|
2014 LDA
5300,Y
|
|
2017 STA
8300,Y
|
|
201A
INY
|
|
201B BNE 2002
|
(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 2010 DEY 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.
5. The Subroutine and Jump Group: 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:
2000
PHP
|
(push the status register onto the
stack)
|
2001
PHA
|
|
2002
TXA
|
|
2003
PHA
|
|
2004
TYA
|
|
2005
PHA
|
|
2006
JSR
|
(to the clear-the-screen routine in BASIC. The
RTS will remove the return address [2009], 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.)
|
2009
PLA
|
(now we reverse the order to get them
back)
|
2010
TAY
|
|
2011
PLA
|
|
2012
TAX
|
|
2013
PLA
|
(this one stays in A)
|
2014
PLP
|
(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 3002 INY 3003 BNE 3002 3005
DEX 3006 BNE 3000 3008
RTS
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
|
|
3002 JSR
3000
|
|
3005 JSR
3000
|
(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.
JMP 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:
$40FF
43 $4100 50
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.
6. Debuggers: 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 2002 TAY 2003
BRK
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.
Debugging
Methods 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:
AND
|
OR
|
EOR
|
0 AND 0=0
|
0 OR 0=0
|
0 EOR 0=0
|
0 AND1=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
| BIT Tests 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 |
|