9
ML Equivalents
Of BASIC Commands
What follows
is a small dictionary, arranged alphabetically, of the major BASIC
commands. If you need to accomplish something in ML - TAB for
example - look it up in this chapter to see one way of doing it in
ML. Often, because ML is so much freer than BASIC, there will be
several ways to go about a given task. Of these choices, one might
work faster, one might take up less memory, and one might be easier
to program and understand. When faced with this choice, I have
selected example routines for this chapter which are easier to
program and understand. At ML speeds, and with increasingly
inexpensive RAM memory available, it will be rare that you will need
to opt for velocity or memory efficiency.
CLR
In
BASIC, this clears all variables. Its primary effect is to reset
pointers. It is a somewhat abbreviated form of NEW since it does not
"blank out" your program, as NEW does. We
might think of CLR, in ML, as the initialization routine which
erases (zeros) the memory locations you've set aside to hold your ML
flags, pointers, counters, etc. Before your program RUNs, you may
want to be sure that some of these "variables" are set to zero. If
they are in different places in memory, you will need to zero them
individually:
2000 LDA #
0 2002 STA 1990
(put zero into one of the "variables") 2005 STA 1994 (continue putting zero
into each byte which needs to be initialized)
On the other hand, maybe you've got your tables, flags, etc.,
all lined up together somewhere in a data table at the start or end
of your ML program. It's a good idea. If your table is in one chunk
of RAM, say from 1985 to 1999, then you can use a loop to zero them
out:
2000 LDA #
0
|
|
2002 LDY #
15
|
(Y will be the counter. There are 15 bytes to
zero out in this example.)
|
2004 STA
1985,Y
|
(the lowest of the 15 bytes)
|
2007
DEY
|
|
2008 BNE
2004
|
(let Y count down to zero, BNEing until Y is
zero, then the Branch if Not Equal will let the program fall
through to the next instruction at
2010)
| CONT
This word
allows your program to pick up where it left off after a STOP
command (or after hitting the system break key). You might want to
look at the discussion of STOP, below. In ML, you can't usually get
a running program to stop with the BREAK (or STOP) key. If you like,
you could write a subroutine which checks to see if a particular key
is being held down on the keyboard and, if it is, BRK:
3000 LDA
96
|
(or whatever your map says is the "key
currently depressed" location for your machine)
|
3002 CMP #
13
|
(this is likely to be the RETURN key on your
machine, but you'll want CMP here to the value that appears in
the "currently pressed" byte for the key you select as your
STOP key. It could be any key. If you want to use "A" for your
"stop" key, try CMP #65.)
|
3004 BNE
3007
|
(if it's not your target key, jump to
RTS)
|
3006
BRK
|
(if it is the target, BRK)
|
3007
RTS
|
(back to the routine which called this
subroutine)
| The
6502 places the Program Counter (plus two) on the stack after a
BRK. A close analogy to BASIC is the
placement of BRK within ML code for a STOP and then typing. G or GO
or RUN - whatever your monitor recognizes as the signal to start
execution of an ML program - to CONT.
DATA
In BASIC,
DATA announces that the items following the word DATA are to be
considered pieces of information (as opposed to being thought of as
parts of the program). That is, the program will probably use this
data, but the data are not BASIC commands. In ML, such a zone of
"non-program" is called a table. It is unique only in that the
program counter never starts trying to run through a table to carry
out instructions. Program control is never transferred to a table
since there are no meaningful instructions inside a table. Likewise,
BASIC slides right over its DATA lines. To
keep things simple, tables of data are usually stored together
either below the program or above it in memory. (See Figure
9-1.) From within the program, tables can be
used to print messages to the screen, update or examine flags, etc.
If you disassemble your BASIC in ROM, you'll find the words STOP,
RUN, LIST, and so forth, gathered together in a table. You can
suspect a data table when your disassembler starts giving lots of
error messages. It cannot find groups of meaningful opcodes within
tables.
Figure
9-1. Typical ML program organization with data tables-one at top or
bottom of program.
DATA
|
INITIALIZATION
|
MAIN LOOP
|
|
DATA
|
|
| <----
Bottom of Memory
<---- Start Of ML Program
<----
Subroutines
DIM
With its
automatic string handling, array management, and error messages,
BASIC makes life easy for the programmer. The price you pay for this
"hand-holding" is that a program is slow when it's RUN. In ML, the
DIMensioning of space in memory for variables is not explicitly
handled by the computer. You must make a note that you are setting
aside memory from 6000 to 6500, or whatever, to hold variables. It
helps to make a simple map of this "dimensioned" memory so you know
where permanent strings, constants, variable strings, and variables,
flags, etc., are within the dimensioned zone.
A particular chunk of memory (where, and how much, is up to
you) is set aside, that's all. You don't write any instructions in
ML to set aside the memory; you just jot it down so you won't later
use the reserved space for some other purpose. Managing memory is
left up to you. It's not difficult, but it is your
responsibility.
END
There are
several ways to make a graceful exit from ML programs. You can look
for the "warm start" address on your particular computer (in the map
of its BASIC locations) and JMP to that address. Or you can go to
the "cold start" address. This results in the computer resetting
itself as if you had turned the power off and then back on
again. If you went into the ML from BASIC
(with a USR or SYS), you can return to BASIC with an RTS. Recall
that every JSR matches up with its own RTS. Every time you use a
JSR, it shoves its "return here" address onto the top of the stack.
If the computer finds another JSR (before any RTS's), it will shove
another return address on top of the first one. So, after two JRS's,
the stack contains two return addresses. When the first RTS is
encountered, the top return address is lifted from the stack and put
into the program counter so that the program returns control to the
current instruction following the most recent JSR.
When the next RTS is encountered, it pulls its appropriate
return (waiting for it on the stack) and so on. The effect of a SYS
or USR from BASIC is like a JSR from within ML. The return address
to the correct spot within BASIC is put on the stack. In this way,
if you are within ML and there is an RTS (without any preceding
JSR), what's on the stack had better be a return-to-BASIC address
left there by SYS or USR when you first went into ML.
Another way to END is to put a BRK in your ML code.
This drops you into the machine's monitor. Normally, you put BRKs in
during program development and debugging. When the program is
finished, though, you would not want to make this ungraceful exit
any more than you would want to end a BASIC program with
STOP. In fact, many ML programs, if they
stand alone and are not part of a larger BASIC program, never END at
all! They are an endless loop. The main loop just keeps cycling over
and over. A game will not end until you turn off the power. After
each game, you see the score and are asked to press a key when you
are ready for the next game. Arcade games which cost a quarter will
ask for another quarter, but they don't end. They go into "attract
mode." The game graphics are left running on screen to interest new
customers. An ML word processor will cycle
through its main loop, waiting for keys to be pressed, words to be
written, format or disk instructions to be given. Here, too, it is
common to find that the word processor takes over the machine, and
you cannot stop it without turning the computer off. Among other
things, such an endless loop protects software from being easily
pirated. Since it takes control of the machine, how is someone going
to save it or examine it once it's in RAM? Some such programs are
"auto-booting" in that they cannot be loaded without starting
themselves running. BASIC, itself a massive
ML program, also loops endlessly until you power down. When a
program is RUNning, all sorts of things are happening. BASIC is an
interpreter, which means that it must look up each word (like INT)
it comes across during a RUN (interpreting it, or translating its
meanings into machine-understandable JSRs). Then BASIC executes the
correct sequence of ML actions from its collection of
routines. In contrast to BASIC RUNS, BASIC
spends 99 percent of its time waiting for you to program with it.
This waiting for you to press keys is its "endless" loop, a tight,
small loop indeed. It would look like our "which key is pressed?"
routine.
2000 LDA
96
|
(or wherever your machine's map shows that the
"which key down" value is stored)
|
2002 CMP
#255
|
(or whatever value is normally left in this
address by default when no key is being pressed)
|
2004 BEQ
2000
|
(if it says "no key down," cycle back and wait
for one)
| FOR-NEXT
Everyone
has used "delay loops" in BASIC (FOR T=1 TO 1000: NEXT T). These are
small loops, sometimes called do-nothing loops because nothing
happens between the FOR and the NEXT except the passage of time.
When you need to let the user read something on the screen, it's
sometimes easier just to use a delay loop than to say "When finished
reading, press any key." In any case, you'll
need to use delay loops in ML just to slow ML itself down. In a
game, the ball can fly across the screen. It can get so fast, in
fact, that you can't see it. It just "appears" when it bounces off
la wall. And, of course, you'll need to use loops in many other
situations. Loops of all kinds are fundamental programming
techniques. In ML, you don't have that
convenient little counter ("T" in the BASIC FOR/NEXT example above)
which decides when to stop the loop. When T becomes 1000, go to the
instructions beyond the word NEXT. Again, you must set up and check
your counter variable by yourself. If the
loop is going to be smaller than 255 cycles, you can use the X
register as the counter (Y is saved for the very useful indirect
indexed addressing discussed in Chapter 4: LDA (96),Y). So, using X,
you can count to 200 by:
2000 LDX
#200 (or $C8 hex) 2002
DEX 2003 BNE 2002
For loops involving counters larger than 255, you'll need to
use two bytes to count down, one going from 255 to zero and then
clicking (like a gear) the other (more significant) byte. To count
to 512:
2000 LDA #
2
|
|
2002 STA
0
|
(put the 2 into address zero, our MSB, Most
Significant Byte, counter)
|
2004 LDX
#0
|
(set X to zero so that its first DEX will make
it 255. Further DEX's will count down again to zero, when it
will click the MSB down from 2 to 1 and then finally
0)
|
2006
DEX
|
|
2007 BNE
2006
|
|
2009 DEC
0
|
(click the number in address zero down
1)
|
2011 BNE
2006
|
|
Here we used the X register as the LSB (least significant
byte) and address zero as the MSB. We could use addresses zero and
one to hold the MSB/LSB if we wanted. This is commonly useful
because then address zero (or some available, two-byte space in zero
page) can be used for LDA (0),Y. You would print a message to the
screen using the combination of a zero page counter and LDA (zero
page address),Y.
FOR-NEXT-STEP
Here
you would just increase your counter (usually X or Y) more than
once. To create FOR I =100 TO 1 STEP -2 you could
use:
2000 LDX # 100 2002
DEX 2003 DEX 2004
BCC 2002
For
larger numbers you create a counter which uses two bytes working
together to keep count of the events. Following our example above
for FOR-NEXT, we could translate FOR I =512 TO 0 STEP
-2:
2000 LDA # 2 2002 STA
0 (this counts the
MSB) 2004 LDX #
0 (X counts the LSB) 2006 DEX 2007 DEX
(here we click X down a second
time, for --2) 2008 BNE
2006 2010 DEC 0 2012 BNE
2006
To count up, use the
CoMPare instruction. FOR I=1 TO 50 STEP 3:
2000 LDX # 0 2002 INX 2003
INX 2004 INX 2005 CPX # 50 2007 BNE
2002
For larger STEP
sizes, you can use a nested loop within the larger one. This would
avoid a whole slew of INX's. To write the ML equivalent of FOR I =1
TO 50 STEP 10:
2000 LDX
#0 2002 LDY #0 2004 INX 2005
INY 2006 CPY #10 2008 BNE 2004 2010 CPX
#50 2012 BNE 2002
GET
Each
computer model has its own "which key is being pressed?" address,
where it holds the value of a character typed in from the keyboard.
To GET, you create a very small loop which just keeps testing the
first address in the buffer. For Atari (in
decimal):
2000 LDA
764
|
("which key pressed" decimal address. In
advanced assemblers, you could freely mix decimal with hex,
but not in the Simple Assembler.)
|
2003 CMP
#255
|
(when an FF value is in this address, it means
that no key is pressed)
|
2005 BEQ
2000
|
(keep going back and looking until there is
some key pressed)
|
For PET (Upgrade and 4.0) (in decimal)
2000 LDA 151 ("which key
pressed" decimal address) 2003 CMP
#255 2005 BEQ 2000
For PET (Original):
2000 LDA
515 ("which key pressed" decimal
address) 2003 CMP #255 2005
BEQ 2000
For Apple II
(hex):
2000 LDA 0000
("which key pressed" - note: this is in
hex) 2003 BPL 2000 2005 STA
C010 (clears the keyboard) 2008 AND #7F (to give
you the correct character value)
For VIC
and 64 (decimal):
2000 LDA
197 2003 CMP #255 2008 BEQ
2000
The Commodore
computers have a GET routine similar to the one illustrated by these
examples, which is built in at $FFE4 which can be used for all ROM
versions (all models of CBM) because it is a fixed JMP table which
does not change address when new BASIC versions are introduced. See
your BASIC's map for Print a Byte to the Screen, GET a Byte, and
other routines in the Commodore Jump Tables. They start at
$FFBD. The examples above do not conform to
PET BASIC's GET. In this version of BASIC, the computer does not
"wait" for a character. If no key is being held down during a GET,
the computer moves on and no GET takes place. In our ML GETs above,
we loop until some character is actually pressed.
For most programming purposes, though, you want to wait until
a key has actually been pressed. If your program is supposed to fly
around doing things until a key is pressed, you might use the above
routines without the loop structure. Just use a CMP to test for the
particular key that would stop the routine and branch the program
somewhere else when a particular key is pressed. How you utilize and
construct a GET-type command in ML is up to you. You can, with ML's
flexibility, make special adjustments to use the best kind of GET
for each different application.
GOSUB
This is
nearly identical to BASIC in ML. Use JSR $NNNN and you will go to a
subroutine at address NNNN instead of a line number, as in BASIC.
("NNNN" just means you can put any hex number in there you want to.)
Some assemblers allow you to give "labels," names to JSR to instead
of addresses. The Simple Assembler does not allow labels. You are
responsible (as with DATA tables, variables, etc.) for keeping a
list on paper of your subroutine addresses and the parameters
involved. Parameters are the
number or numbers handed to a subroutine to give it information it
needs. Quite often, BASIC subroutines work with the variables
already established within the BASIC program. In ML, though,
managing variables is up to you. Subroutines are useful because they
can perform tasks repeatedly without needing to be programmed into
the body of the program each time the task is to be carried out.
Beyond this, they can be generalized so that a single
subroutine can act in a variety of ways, depending upon the variable
(the parameter) which is passed to it. A
delay loop to slow up a program could be general in the sense that
the amount of delay is handed to the subroutine each time. The delay
can, in this way, be of differing durations, depending on what it
gets as a parameter from the main routine. Let's say that we've
decided to use address zero to pass parameters to subroutines. We
could pass a delay of "five" cycles of the loop by:
|
2000 LDA #
5
|
|
The Main Program
|
2002 STA
0
|
|
|
2004 JSR
5000
|
|
|
…
|
|
The Subroutine
|
5000 DEC
0
|
|
|
5002 BEQ
5012
|
(if address zero has counted all the way down
from five to zero, RTS back to the Main Program)
|
|
5004 LDY #
0
|
|
|
5006
DEY
|
|
|
5007 BNE
5006 |
|
|
5009 JMP
5000
|
|
|
5012
RTS
|
|
A delay which lasted twice as long as the above would merely
require a single change: 2000 LDA # 10.
GOTO
In ML,
it's JMP. JMP is like JSR, except the address you leap away from is
not saved anywhere. You jump, but cannot use an RTS to find your way
back. A conditional branch would be CMP #0 BEQ 5000. The condition
of equality is tested by BEQ, Branch if EQual. BNE tests a condition
of inequality, Branch if Not Equal. Likewise, BCC (Branch if Carry
is Clear) and the rest of these branches are testing conditions
within the program. GOTO and JMP do not
depend on any conditions within the program, so they are
unconditional. The question arises, when you use a GOTO: Why did you
write a part of your program that you must always (unconditionally)
jump over? GOTO and JMP are sometimes used to patch up a program,
but, used without restraint, they can make your program hard to
understand later. Nevertheless, JMP can many times be the best
solution to a programming problem. In fact, it is hard to imagine ML
programming without it. One additional note
about JMP: it makes a program non-relocatable. If you later need to
move your whole ML program to a different part of memory, all the
JMP's (and JSR's) need to be checked to see if they are pointing to
addresses which are no longer correct (JMP or JSR into your BASIC
ROM's will still be the same, but not those which are targeted to
addresses within the ML program). This can be an important
consideration if you are going to use an ML subroutine in other
programs where the locations might well differ. Fully relocatable ML
routines can be convenient if you like to program by drawing from a
personal collection of solved problems.
2000 JMP 2005 2003 LDY #3 2005
LDA #5
If you moved this
little program up to 5000, everything would survive intact and work
correctly except the JMP 2005 at address 2000. It would still say to
jump to 2005, but it should say to jump to 5005, after the move. You
have to go through with a disassembly and check for all these
incorrect JMP's. To make your programs more "relocatable," you can
use a special trick with unconditional branching which will move
without needing to be fixed:
2000 LDY
#0
|
|
2002 BEQ
2005
|
(since we just loaded Y with a zero, this
Branch-if-EQual-to-zero instruction will always be true and
will always cause a pseudo-JMP)
|
2004
NOP
|
|
2005 LDA
#5
|
|
This works because we set the Z flag. Then, when BEQ tests the
zero flag, it will pass the test, it will find that flag "up" and
will branch. If you load X, Y, or A with a zero, the zero flag goes
up. Various monitors and assemblers include a
"move it" routine, which will take an ML program and relocate it
somewhere else in memory for you. On the Apple, you can go into the
monitor and type *5000 < 2000.2006M (although you will have to
give the monitor these numbers in hex). The first number is the
target address. The second and third are the start and end of the
program you want to move. On CBM computers,
the built-in monitor (the VIC-20 and the Original 2001 ROM set do
not have a built-in monitor) does not have a Move it command.
However, it is easy to add a "monitor extension" program to the
built-in monitor. Supermon and Micromon are such extensions. The
format for Moveit in Commodore machines is. T 2000 2006 5000 (start
and end of the program to be moved, followed by the target address).
Again, these numbers must be in hex. The T stands for
transfer. The Atari Assembler Editor
Cartridge follows a convention similar to Apple's: M 5000 < 2000,
2006.
IF-THEN
This
familiar and primary computing structure is accomplished in ML with
the combination of CMP-BNE or any other conditional branch: BEQ,
BCC, etc. Sometimes, the IF half isn't even necessary. Here's how it
would look:
2000 LDA 57
(what's in address 57?) 2002 CMP #15 (is it
15?) 2004 BEQ 2013 (IF
it is, branch up to 2013) 2006 LDA #10
(or ELSE, put a 10 into address
57) 2008 STA 57 2010 JMP 2017
(and jump over the THEN part) 2013 LDA #20 (THEN, put a 20
into address 57) 2015 STA
57 2017
(continue with the program ... )
Often, though, your flags are already set by an action,
making the CMP unnecessary. For example, if you want to branch to
2013 if the number in address 57 is zero, just LDA 57 BEQ 2013. This
is because the act of loading the accumulator will affect the status
register flags. You don't need to CMP #0 because the zero flag will
be set if a zero was just loaded into the accumulator. It won't hurt
anything to use a CMP, but you'll find many cases in ML programming
where you can shorten and simplify your coding. As you gain
experience, you will see these patterns and learn how and what
affects the status register flags.
INPUT
This is
a series of GETS, echoed to the screen as they are typed in, which
end when the typist hits the RETURN key. The reason for the echo
(the symbol for each key typed is reproduced on the screen) is that
few people enjoy typing without seeing what they've typed. This also
allows for error correction using cursor control keys or DELETE and
INSERT keys. To handle all of these actions, an INPUT routine must
be fairly complicated. We don't want, for example, the DELETE to
become a character within the string. We want it to immediately act
on the string being entered during the INPUT, to erase a
mistake. Our INPUT routine must be smart
enough to know what to add to the string and what keys are intended
only to modify it. Here is the basis for constructing your own ML
INPUT. It simply receives a character from the keyboard, stores it
in the screen RAM cells, and ends when the RETURN key is pressed.
This version is for Upgrade and 4.0 CBM/PETs and we'll write it as a
subroutine. That simply means that when the 13 (ASCII for carriage
return) is encountered, we'll perform an RTS back to a point just
following the main program address which JSRed to our INPUT
routine:
5000 LDY
#0
|
(Y will act here as an offset for storing the
characters to the screen as they come in)
|
5002 LDA
158
|
(this is the "number of keys in the keyboard
buffer' location. If it's zero, nothing has been typed
yet)
|
5004 BNE
5002
|
(so we go back to 5002)
|
5006 LDA
623
|
(get the character from the keyboard
buffer)
|
5009 CMP
#13
|
(is it a carriage return?)
|
5011 BNE
5014
|
(if not, continue)
|
5013
RTS
|
(otherwise return to the main
program)
|
5014 STA
32768,Y
|
(echo it to the screen)
|
5017
INY
|
|
5018 LDA
#0
|
|
5020 STA
158
|
(reset the "number of keys" counter to
zero)
|
5022 JMP
5002
|
(continue looking for the next
key)
|
This INPUT could be made much larger and more complex.
As it stands, it will contain the string on the screen only. To save
the string, you would need to read it from screen RAM and store it
elsewhere where it will not be erased. Or, you could have it echo to
the screen, but (also using Y as the offset) store it into some safe
location where you are keeping string variables. The routine above
does not make provisions for DELETE or INSERT either. The great
freedom you have with ML is that you can redefine anything you want.
You can softkey: define a key's meaning via software; have
any key perform any task. You might use the $ key to
DELETE. Along with this freedom goes the
responsibility for organizing, writing, and debugging these
routines.
LET
Although
this word is still available on most BASICs, it is a holdover from
the early days of computing. It is supposed to remind you that a
statement like LET NAME =NAME +4 is an assignment of a value to a
variable, not an algebraic equation. The two numbers on either side
of the "equals" sign, in BASIC, are not intended to be equal in the
algebraic sense. Most people write NAME =NAME +4 without using LET.
However, the function of LET applies to ML as well as to BASIC: we
must assign values to variables. In the
Atari, VIC, and Apple, for example, where the address of the screen
RAM can change depending on how much memory is in the computer, etc.
- there has to be a place where we find out the starting address of
screen RAM. Likewise, a program will sometimes require that you
assign meanings to string variables, counters, and the like. This
can be part of the initialization process, the tasks performed
before the real program, your main routine, gets started. Or it can
happen during the execution of the main loop. In either case, there
has to be an ML way to establish, to assign, variables. This also
means that you must have zones of memory set aside to hold these
variables. For strings, you can think of LET
as the establishment of a location in memory. In our INPUT example
above, we might have included an instruction which would have sent
the characters from the keyboard to a table of strings as well as
echoing them to the screen. If so, there would have to be a way of
managing these strings. For a discussion on the two most common ways
of dealing with strings in ML, see Chapter 6 under the subhead
"Dealing With Strings. " In general, you will
probably find that you program in ML using somewhat fewer variables
than in BASIC. There are three reasons for this:
1. You will probably not write many programs in ML such as
data bases where you manipulate hundreds of names, addresses, etc.
It might be somewhat inefficient to create an entire data base
management program, an inventory program for example, in ML. Keeping
track of the variables would be a nightmare. An important benefit of
ML is its speed of execution, but a drawback is that it slows
programming down. So, for an inventory program, you could write the
bulk of the program in BASIC and simply attach ML routines for
sorting and searching tasks within the program.
2. Also, the variables in ML are often handled within a series
of instructions (not held elsewhere as BASIC variables are). FOR I
=1 TO 10: NEXT I becomes LDY #1, INY, CPY #10, BNE. Here, the BASIC
variable is counted for you and stored outside the body of the
program. The ML "variable," though, is counted by the program
itself. ML has no interpreter which handles such things. If you want
a loop, you must construct all of its components yourself.
3. In BASIC, it is tempting to assign values to
variables at the start of the program and then to refer to them
later by their variable names, as in: 10 BALL= 79. Then, any time
you want to PRINT the BALL to the screen, you could say, PRINT
CHR$(BALL). Alternatively, you might define it this way in BASIC: 10
BALL$= "0". In either case, your program will later refer to the
word BALL. In this example we are assuming that the number 79 will
place a ball character on your screen. In ML
we are not free to use variable names except when using a
complicated, advanced assembler. With the Simple Assembler, you will
find it easier just to LDA #79, STA (screen position) each time.
Some people like to put the 79 into their zone of variables (that
arbitrary area of memory set up at the start of a program to hold
tables, counters, and important addresses). They can pull it out of
that zone whenever it's needed. That is somewhat cumbersome, though,
and slower. You would LDA 1015, STA (screen position), assuming you
had put a 79 into this "ball" address earlier.
Obviously a value like BALL will remain the same
throughout a program. A ball will look like a ball in your game,
whatever else happens. So, it's not a true variable, it does not
vary. It is constant. A true variable must be located in your "zone
of variables," your variable table. It cannot be part of the body of
your program itself (as in: LDA #79) because it will change. You
don't know when writing your program what the variable will be. So
you can't use immediate mode addressing because it might not be a
#79. You have to LDA 1015 (or whatever) from within your table of
variables. Elsewhere in the program you have
one or more STA 1015's or INC 1015's or some other manipulation of
this address which keeps updating this variable. In effect, ML makes
you responsible for setting aside areas which are safe to hold
variables. What's more, you have to remember the addresses, and
update the variables in those addresses whenever necessary. This is
why it is so useful to keep a piece of paper next to you when you
are writing ML. The paper lists the start and end addresses of the
zone of variables, the table. You also write down the specific
address of each variable as you write your program.
LIST
This is
done via a disassembler. It will not have line numbers (though,
again, advanced assembler-disassembler packages do have line
numbers). Instead, you will see the address of each instruction in
memory. You can look over your work and debug it by working with the
disassembler, setting BRKs into problem areas, etc. See Appendix
D.
LOAD
The
method of saving and loading an ML program varies from computer to
computer. Normally, you have several options which can include
loading: from within the monitor, from BASIC, or even from an
assembler. When you finish working on a program, or a piece of a
program, on the Simple Assmbler you will be given the starting and
ending addresses of your work. Using these, you can save to tape or
disk in the manner appropriate to your computer. To LOAD, the
simplest way is just to LOAD as if you were bringing in a BASIC
program. Unfortunately, this only works on Commodore machines.
You'll get your ML program, not a BASIC program, so it won't start
at the normal starting address for BASIC unless you wrote and saved
it at that address. You should type NEW after loading it, however,
to reset some pointers in the computer. That will not NEW out the ML
program. To save from within the monitor on
Commodore machines:
.S "PROGRAM NAME",
01,NNNN,NNNN* (for tape) .L "PROGRAM NAME",01
(for tape) .S "0:PROGRAM NAME",08,NNNN,NNNN* (for
disk) .L "0:PROGRAM NAME",08 (for
disk)
*You should add one to the hex
number for the end of your program or the SAVE will clip off the
last byte. If your program exists in RAM from $0300 to $0350, you
save it like this: S "PROGRAM NAME", 01, 0300, 0351.
On the Apple, you must BLOAD from disk. On the Atari,
if you have DOS you can use the "L" command from the DOS menu to
LOAD in an ML program. If you don't, you need to use a short BASIC
program that grabs in the bytes via a series of
GETs:
10
OPEN#1,4,0,"C:" 20 GET#1,NN:GET#1,NN: REM DISCARD THE
HEADER 30 GET#1,LO:GET#1,HI: REM START ADDRESS 40
START =LO+256*HI 50 GET#1,LO:GET#1,Hl: REM ENDING
ADDRESS 60 FIN =LO+256*HI 70 TRAP 100 80
FORI=START TO FIN: GET#1,A: POKEI,A:NEXTI 90 GOTO
30 100 END
Note:
This will not work correctly if the START and FIN addresses overlap
this BASIC program in memory. It would then load in on top of
itself.
NEW
In
Microsoft BASIC, this has the effect of resetting some pointers
which make the machine think you are going to start over again. The
next program line you type in will be put at the
"start-of-a-BASIC-program" area of memory. Some computers, the Atari
for example, even wash memory by filling it with zeros. There is no
special command in ML for NEWing an area of memory, though some
monitors have a "fill memory" option which will fill a block of
memory as big as you want with whatever value you choose.
The reason that NEW is not found in ML is that you do
not always write your programs in the same area of memory (as you do
in BASIC), building up from some predictable address. You might have
a subroutine floating up in high memory, another way down low, your
table of variables just above the second subroutine, and your main
program in the middle. Or you might not. We've been using 2000 as
our starting address for many of the examples in this book and 5000
for subroutines, but this is entirely arbitrary.
To "NEW" in ML, just start assembling over the old program.
Alternatively, you could just turn the power off and then back on
again. This would, however, have the disadvantage of wiping out your
assembler along with your program.
ON GOSUB
In
BASIC, you are expecting to test values from among a group of
numbers: 1,2,3,4,5 .... The value of X must fall within this narrow
range: ON X GOSUB 100, 200, 300 ... (X must be 1 or 2 or 3 here). In
other words, you could not conveniently test for widely separated
values of X (18, 55, 220). Some languages feature an improved form
of ON GOSUB where you can test for any values. If your computer were
testing the temperature of your bathwater:
CASE 80 OF
GOSUB HOT ENDOF 100 OF GOSUB
VERYHOT ENDOF 120 OF GOSUB
INTOLERABLE ENDOF ENDCASE
ML permits you the greater freedom of the CASE
structure. Using CMP, you can perform a multiple branch
test:
2000 LDA
150
|
(get a value, perhaps input from the
keyboard)
|
2002 CMP #
80
|
|
2004 BNE
2009
|
|
2006 JSR
5000
|
(where you would print "hot," following your
example of CASE)
|
2009 CMP #
100
|
|
2011 BNE
2016
|
|
2013 JSR
5020
|
(print "very hot")
|
2016 CMP #
120
|
|
2018 BNE
2023
|
|
2020 JSR
5030
|
(print
"intolerable")
|
Since you are JSRing and then will be RTSing back to within
the multiple branch test above, you will have to be sure that the
subroutines up at 5000 do not change the value of the accumulator.
If the accumulator started out with a value of 80 and, somehow, the
subroutine at 5000 left a 100 in the accumulator, you would print
"hot" and then also print "very hot." One way around this would be
to put a zero into the accumulator before returning from each of the
subroutines (LDA #0). This assumes that none of your tests, none of
your cases, responds to a zero.
ON GOTO
This
is more common in ML than the ON GOSUB structure above. It
eliminates the need to worry about what is in the accumulator when
you return from the subroutines. Instead of RTSing back, you jump
back, following all the branch tests.
2000 LDA
150
|
|
2002 CMP #
80
|
|
2004 BNE
2009
|
|
2006 JMP
5000
|
(print "hot")
|
2009 CMP #
100
|
|
2011 BNE
2016
|
|
2013 JMP
5020
|
(print "very hot")
|
2016 CMP #
120
|
|
2018 BNE
2023
|
|
2020 JMP
5030 |
(print "intolerable")
|
2030
|
(all the subroutines JMP 2023 when they
finish)
| Instead
of RTS, each of the subroutines will JMP back to 2023, which lets
the program continue without accidentally "triggering" one of the
other tests with something left in the accumulator during the
execution of one of the subroutines.
PRINT
You
could print out a message in the following way:
2000 LDY
#0
|
|
2002 LDA
#72
|
(use whatever your computer's screen POKE value
is for the letter "H")
|
2004 STA
32900,Y
|
(an address on the screen)
|
2007
INY
|
|
2008 LDA
#69
|
(the letter "E")
|
2010 STA
32900,Y
|
|
2013
INY
|
|
2014 LDA
#76
|
(the letter "L")
|
2016 STA
32900,Y
|
|
2019
INY
|
|
2020 LDA
#76
|
(the letter "L")
|
2022 STA
32900,Y
|
|
2025
INY
|
|
2026 LDA
#79
|
(the letter "O")
|
2028 STA
32900,Y
|
|
But this is clearly a cumbersome,
memory-eating way to go about it. In fact, it would be absurd to
print out a long message this way. The most common ML method
involves putting message strings into a data table and ending each
message with a zero. Zero is never a printing character in computers
(excepting Atari which cannot use the technique described here). To
print the ASCII number zero, you use 48: LDA #48, STA 32900. So,
zero itself can be used as a delimiter to let the printing routine
know that you've finished the message. In a data table, we first put
in the message "hello". Recall that you should substitute your own
computer's screen POKE code:
1000
72 H 1001 69 E 1002 76 L 1003 76
L 1004 79 O 1005 0
(the delimiter, see Chapter 6) 1006 72 H 1007 73 I
(another message) 1008 0 (another
delimiter)
Such a message table can be as
long as you need; it holds all your messages and they can be used
again and again:
2000 LDY
#0
|
|
2002 LDA
1000,Y
|
|
2005 BEQ
2012
|
(if the zero flag is set, it must mean that
we've reached the delimiter, so we branch out of this printing
routine)
|
2005 STA
39000,Y
|
(put it on the screen)
|
2008
INY
|
|
2009 JMP
2002
|
(go back and get the next letter in the
message)
|
2012
|
(continue with the
program.)
| Had
we wanted to print "HI," the only change necessary would have been
to put 1006 into the LDA at address 2003. To change the location on
the screen that the message starts printing, we could just put some
other address into 2006. The message table, then, is just a mass of
words, separated by zeros, in RAM memory. The
easiest way to print to the screen, especially if your program will
be doing a lot of printing, is to create a subroutine and use some
bytes in zero page (addresses 0 to 255) to hold the address of the
message and the screen location you want to send it to. This is one
reason why hex numbers can be useful. To put an address into zero
page, you will need to put it into two bytes. It's too big to fit
into one byte. With two bytes together forming an address, the 6502
can address any location from $0000 to the top $FFFF. So, if the
message is at decimal location 1000 like "HELLO" above, you should
turn 1000 into a hex number. It's $03E8. Then
you split the hex number in two. The left two digits, $03, are the
MSB (the most significant byte) and the right digits, $E8, make up
the LSB (least significant byte). If you are going to put this
target address into zero page at 56 (decimal):
2000 LDA #232 (LSB,
in decimal) 2002 STA 56 2004
LDA #3 (MSB) 2006 STA 57 2008 JSR 5000
(printout subroutine)
5000 LDY
#0 5002 LDA (56),Y 5004 BEQ 5013
(if zero, return from subroutine) 5006 STA 32900,Y (to
screen) 5009 INY 5010 JMP
5002 5013 RTS
One drawback to the subroutine is that it will always print
any messages to the same place on the screen. That 32900 (or
whatever you use there) is frozen into your subroutine. Solution?
Use another zero page pair of bytes to hold the screen address.
Then, your calling routine sets up the message address, as above,
but also sets up the screen address. The
Atari contains the address of the first byte of the screen addresses
in zero page for you at decimal 88 and 89. You don't need to set up
a screen address byte pair on the Atari. We are using the Apple II's
low resolution screen for the examples in this book, so you will
want to put 0 and 4 into the LSB and MSB respectively. The PET's
screen is always located in a particular place, unlike the Atari,
Apple, VIC, and 64 screen RAM locations which can move, so you can
put a $00 and an $80 into LSB and MSB for PET. The following is in
decimal:
2000 LDA
#232
|
(LSB)
|
2002 STA
56
|
(set up message address)
|
2004 LDA
#3
|
(MSB)
|
2006 STA
57
|
|
2008 LDA #
0
|
(LSB for PET and Apple)
|
2010 STA
58
|
(we'll just use the next two bytes in zero page
above our message address for the screen address)
|
2012 LDA #
4
|
(this is for Apple II; use 128 ($80) for
PET)
|
2014 STA
59
|
|
2016 JSR
5000
|
|
|
|
5000 LDY
#0
|
|
5002 LDA
(56),Y
|
|
5004 BEQ
5013
|
(if zero, return from subroutine)
|
5006 STA
(58),Y
|
(to screen)
|
5009
INY
|
|
5010 JMP
5002
|
|
5013
RTS
|
|
For Atari: 5006 STA (88),Y. You have less flexibility because
you will always be printing your messages to the first line on
screen, using address 88 as your screen storage target. To be able
to put the message anywhere on screen, Atari users will have to use
some other zero page for the screen address, as we did for Apple II
and PET above. Atari users would have to keep track of the "cursor
position" for themselves in that case.
READ
There is
no reason for a reading of data in ML. Variables are not placed into
ML "DATA statements." They are entered into a table when you are
programming. The purpose of READ, in BASIC, is to assign variable
names to raw data or to take a group of data and move it somewhere,
or to manipulate it into an array of variables. These things are
handled by you, not by the computer, in ML programming.
If you need to access a piece of information, you set
up the addresses of the datum and the target address to which you
are moving it. See the "PRINT" routines above. As always, in ML you
are expected to keep track of the locations of your variables. You
keep a map of data locations, vectors, tables, and subroutine
locations. A pad of paper is always next to you as you program in
ML. It seems as if you would need many notes. In practice, an
average program of say 1000 bytes could be mapped out and commented
on, using only one sheet.
REM
You do
this on a pad of paper, too. If you want to comment or make notes
about your program - and it can be a necessary, valuable explanation
of what's going on - you can disassemble some ML code like a BASIC
LISTing. If you have a printer, you can make notes on the printed
disassembly. If you don't have a printer, make notes on your pad to
explain the purpose of each subroutine, the parameters it expects to
get, and the results or changes it causes when it
operates. Complex, large assemblers often
permit comments within the source code. As you program with them,
you can include REMarks by typing a semicolon, or parentheses, or
some other signal to the assembler to ignore the REMarks when it is
assembling your program. In these assemblers, you are working much
closer to the way you work in BASIC. Your remarks remain part of the
source program and can be listed out and
studied. RETURN
RTS
works the same way that RETURN does in BASIC: it takes you back to
just after the JSR (GOSUB) that sent control of the program away
from the main program and into a subroutine. JSR pushes, onto the
stack, the address which immediately follows the JSR itself. That
address then sits on the stack, waiting until the next RTS is
encountered. When an RTS occurs, the address is pulled from the
stack and placed into the program counter. This has the effect of
transferring program control back to the instruction just after the
JSR.
RUN
There are
several ways to start an ML program. If you are taking off into ML
from BASIC, you just use SYS or USR or CALL. They act just like JSR
and will return control to BASIC, just like RETURN would, when there
is an unmatched RTS in the ML program. By unmatched we mean the
first RTS which is not part of a JSR/RTS pair. USR and SYS and CALL
can be used either in immediate mode (directly from the keyboard) or
from within a BASIC program as one of the BASIC commands.
USR is just like SYS and CALL except that you can "send"
values from BASIC to ML by attaching them to the USR ( )
within the parentheses. In Microsoft BASIC (Apple, PET/CBM, etc.),
you must set up the location of your target ML program in special
USR addresses, before exiting BASIC via USR. For example, to "gosub"
to an ML routine located at $0360 (hex), you want to put a $60 (hex)
into address 1 and an 03 into address 2. The 03 is obvious, just
POKE 2,3. Atari goes from BASIC to ML via USR. The USR's argument
may place several parameters on the stack along with the "count,"
the number of parameters which were passed.
The hex 60 means that you would multiply 16 x 6, since the
second column in hex is the "16's" column. So you would POKE 1, 96.
Recall that we always set up ML addresses to be used by "indirect
indexed addressing" (LDA (00),Y) by putting the LSB (least
significant byte) first. To set up 0360, then, you first separate
the hex number into its two bytes, 03 60. Then you translate them
into decimal since we're in BASIC when we use USR: 3 96. Then you
switch them so that they conform to the correct order for ML:
LSB/MSB 96 3. Finally, you POKE them into memory locations 1 and
2. If this seems rather complex, it is. In
practice, Microsoft BASIC users rarely use USR. The number which is
"passed" to ML from within the parentheses is put into the floating
point accumulator. Following this you must JSR to FPINT, a BASIC ROM
routine which converts a floating point value into an integer that
you could work with in ML. As we mentioned, working with floating
point arithmetic in ML is an arcane art. For most applications which
must pass information from BASIC to ML, it is far easier to use
ordinary "integer" numbers and just POKE them into some
predetermined ML variable zone that you've set aside and noted on
your workpad. Then just SYS to your ML routine, which will look into
the set-aside, POKEd area when it needs the values from
BASIC. In Atari BASIC, USR works in a more
simplified and more convenient way. For one thing, the target ML
address is contained within the argument of the USR command: USR
(address). This makes it nearly the exact parallel of BASIC's GOSUB.
What's more, USR passes values from BASIC by putting them on the
stack as a two-byte hex number. USR (address,X) does three things.
1. It sends program control to the ML routine which starts at
"address." 2. It pushes the number X onto the stack where it can be
pulled out with PLA's. 3. Finally, it pushes the total number
of passed values onto the stack. In this case, one value, X, was
passed to ML. All of these actions are useful and make the Atari
version of USR a more sensible way of GOSUBing from BASIC to
ML. If you are not going between BASIC and
ML, you can start (RUN) your ML program from within your "monitor."
The PET/CBM and the Apple have built-in monitor programs in their
ROM chips. On the Atari, a monitor is available as part of a
cartridge. On the "Original" PET/CBM (sometimes called BASIC 2.0),
there is no built-in monitor. A cassette with a program called TIM
(terminal interface monitor) can be LOADed, though, and used in the
same way that the built-in versions are on later models. Neither the
VIC nor the 64 has a built-in monitor. To
enter "monitor mode" (as opposed to the normal BASIC mode), you can
type SYS 1024 or SYS 4 on the PET/CBM. These locations always
contain a zero and, by "landing" on a zero in ML, you cause a BRK to
take place. This displays the registers of your 6502 and prints a
dot on the screen while waiting for your instructions to the
monitor. To enter the monitor on Apple II, type CALL -151 and you
will see an asterisk (instead of PET's period) as your prompt. From
within Atari's Assembler Cartridge, you would type BUG to enter the
equivalent of the Apple and PET monitor. The Atari will print the
word DEBUG and then the cursor will wait for your next
instruction. To RUN an ML program, all five
computers use the abbreviation G to indicate "goto and run" the hex
address which follows the G. Unfortunately, the format of the ML RUN
(G), as always, differs between machines. To run a program which
starts at address $2000:
Apple II,
you type: 2000G (8192
in decimal) PET, VIC,64, you type: G
2000 Atari,
you type: G 2000
One
other difference: the Apple II expects to encounter an unmatched RTS
to end the run and return control to the monitor. Put another way,
it will think that your ML program is a subroutine and 2000G causes
it to JSR to the subroutine at address (in hex.) 2000. The
Commodores and the Atari both look for a BRK instruction (00) to
throw them back into monitor mode.
SAVE
When you
SAVE a BASIC program, the computer handles it automatically. The
starting address and the ending address of your program are
calculated for you. In ML, you must know the start and end yourself
and let the computer know. From the Apple II monitor, you type the
starting and ending address of what you want saved, and then "W" for
write:
2000.2010W (This is only for cassette
and these commands are in hex. These addresses are 8192.8208, in
decimal.)
From BASIC to disk
use:
B SAVE Name, A, L (A=address,
L=length)
On the VIC, 64, and PET, the
format for SAVE is similar, but includes a
filename:
.S "PROGRAM NAME",01,2000,2010 (the
01 is the "device number" of the tape player)
To save to disk, you must change the device number to 08 and
start the filename with the number of the drive you are SAVEing
to:
.S "0: NAME", 08,2000,2010
(Always add one to the "finish" address; the example
above saves from 2000 to 200F.)
With the
Atari Assembler Cartridge, you:
SAVE#C: NAME <
2000,2010 (do this from the EDIT, not DEBUG, mode). The NAME is
not required with cassette.
To write
Atari source code to cassette, type: .SAVE#C. For disk, type
SAVE#D:FILENAME.EXT or use DOS.
STOP
BRK (or
an RTS with no preceding JSR, on the Apple) throws you back into the
monitor mode after running an ML program. This is most often used
for debugging programs because you can set "breakpoints" in the same
way that you would use STOP to examine variables when debugging a
BASIC program.
String
Handling ASC
In BASIC, this will give you the
number of the ASCII code which stands for the character you are
testing. ?ASC("A") will result in a 65 being displayed. There is
never any need for this in ML. If you are manipulating the character
A in ML, you are using ASCII already. In other words, the letter A
is 65 in ML programming. If your computer stores letters and other
symbols in nonstandard ways (such as Commodore character codes for
lowercase, and Atari's ATASCII), you will need to write a special
program to be able to translate to standard ASCII if you are using a
modem or some other peripheral which uses ASCII. See your computer's
manual, the Atari BASIC Reference Manual for example, for
information on your computer's internal character
code.
CHR$
This is
most useful in BASIC to let you use characters which cannot be
represented within normal strings, will not show up on your screen,
or cannot be typed from the keyboard. For example, if you have a
printer attached to your computer, you could "send" CHR$(13) to it,
and it would perform a carriage return. (The correct numbers which
accomplish various things sometimes differ, though decimal 13 - an
ASCII code standard - is nearly universally recognized as carriage
return.) Or, you could send the combination CHR$(27)CHR$(8) and the
printer would backspace. Again, there is no
real use for CHR$ within ML. If you want to specify a carriage
return, just LDA #13. In ML, you are not limited to the character
values which can appear on screen or within strings. Any value can
be dealt with directly.
The following
string manipulation instructions are found in Microsoft
BASIC:
LEFT$
As usual
in ML, you are in charge of manipulating data. Here's one way to
extract a five-character-long "substring" from out of the left side
of a string as in the BASIC statement: LEFT$ (X$,5)
2000 LDY
#5
|
|
2002 LDX
#0
|
(use X as the offset for buffer
storage)
|
2004 LDA
1000,Y
|
(the location of X$)
|
2007 STA
4000,X
|
(the "buffer," or temporary storage area for
the substring)
|
2010
INX
|
|
2011
DEY
|
|
2012 BNE
2004
|
| LEN
In some
cases, you will already know the length of a string in ML. One of
the ways to store and manipulate strings is to know beforehand the
length and address of a string. Then you could use the subroutine
given for LEFT$ above. More commonly, though, you will store your
strings with delimiters (zeros, except in Atari) at the end of each
string. To find out the length of a certain string:
2000 LDY
#0
|
|
2002 LDA
1000,Y
|
(the address of the string you are
testing)
|
2003 BEQ
2009
|
(remember, if you LDA a zero, the zero flag is
set. So you don't really need to use a CMP #0 here to test
whether you've loaded the zero delimiter)
|
2005
INY
|
|
2006 BNE
2002
|
(we are not using a JMP here because we assume
that all your strings are less than 256 characters
long.)
|
2008
BRK
|
(if we still haven't found a zero after 256
INY's, we avoid an endless loop by just BRKing out of the
subroutine)
|
2009
DEY
|
(the LENgth of the string is now in the Y
register)
|
We had to DEY at the end because the final INY picked up the
zero delimiter. So, the true count of the LENgth of the string is
one less than Y shows, and we must DEY one time to make this
adjustment.
MID$
To
extract a substring which starts at the fourth character from within
the string and is five characters long (as in MID$(X$,4,5)
):
2000 LDY
#5
|
(the size of the substring we're
after)
|
2002 LDX
#0
|
(X is the offset for storage of the
substring)
|
2004 LDA
1003,Y
|
(to start at the fourth character from within
the X$ located at 1000, simply add three to that address.
Instead of starting our LDA,Y at 1000, skip to 1003. This is
because the first character is not in position one. Rather, it
is at the zeroth position, at 1000.)
|
2007 STA
4000,X
|
(the temporary buffer to hold the
substring)
|
2010
INX
|
|
2011
DEY
|
|
2012 BNE
2004
|
| RIGHT$
This,
too, is complicated because normally we do not know the LENgth of a
given string. To find RIGHT$(X$,5) if X$ starts at 1000, we should
find the LEN first and then move the substring to our holding zone
(buffer) at 4000:
2000 LDY
#0
|
|
2002 LDX
#0
|
|
2004 LDA
1000,Y
|
|
2007 BEQ
2013
|
(the delimiting zero is found, so we know
LEN)
|
2009
INY
|
|
2010 JMP
2004
|
|
2013
TYA
|
(put LEN into A to subtract substring size from
it)
|
2014
SEC
|
(always set carry before subtraction)
|
2015 SBC
#5
|
(subtract the size of the substring you want to
extract)
|
2017
TAY
|
(,put the offset back into Y, now adjusted to
point to five characters from the end of X$)
|
2018 LDA
1000,Y
|
|
2021 BEQ
2030
|
(we found the delimiter, so end)
|
2023 STA
4000,X
|
|
2026
INX
|
|
2027
DEY
|
|
2028 BNE
2018
|
|
2030
RTS
|
| The above does not
apply to Atari since it cannot use zero as a
delimiter.
SPC
This
formatting instruction is similar to TAB. The difference is that
SPC(10) moves you ten spaces to the right from wherever the cursor
is on screen at the time. TAB(10) moves ten spaces from the
left-hand side of the screen. In other words, TAB always counts over
from the first column on any line; SPC counts from the cursor's
current position. In ML, you would just add
the amount you want to SPC over. If you were printing to the screen
and wanted ten spaces between A and B so it looked like this
(A B), you could write:
2000 LDA #65 (A) 2002 STA
32768 (screen RAM
address) 2005 LDA #66
(B) 2007 STA 32778
(you've added ten to the target
address)
Alternatively, you could add ten
to the Y offset:
2000 LDY #0 2002 LDA
#65 2004 STA 32768,Y 2007 LDY #10
(add ten to Y) 2009
LDA #66 2011 STA 32768,Y
If you are printing out many columns of numbers and need a
subroutine to correctly space your printout, you might want to use a
subroutine which will add ten to the Y offset each time you call the
subroutine:
5000 TYA 5001 CLC 5002
ADC #10 5004 TAY 5005 RTS
This subroutine directly adds ten to the Y register whenever
you JSR 5000. To really do this job, however, you should use a
two-byte register to keep track of the cursor.
TAB
Quite
similar to SPC, except that you don't add the offset from the cursor
position (whatever location you most recently printed). Rather,
TAB(X) moves ten over from the left side of the screen, or, if you
are using a printer, from the left margin on the piece of paper.
There is no particular reason to use TAB in ML. You have much more
direct control in ML over where characters are printed out.
Return to Table
of Contents | Previous
Chapter | Next
Chapter |
|