Penguin Books
Creative Assembler How To Write Arcade
Games
As one of Acornsoft's games programmers, Jonathan Griffiths
was responsible for such widely popular programs as Snapper and JCB Digger for
the BBC Microcomputer Model B and Acorn Electron He has prepared a cassette
program, which can be used in conjunction with this book, available from
Acornsoft Limited, c/o Vector Marketing Ltd, Denington Industrial Estate,
Wellingborough, Northants
NN8 2RL.
Acknowledgements
Thanks are due to David Johnson-Davies for providing
an earlier version of the first few chapters, Jim Dobson and Jeremy Bennett for
helping in the writing of this book, and to Philippa Bush and Sharron Fellows
for editing it Also, thanks are due to Orlando
M. Pilchard (Q.C.), Chris Jordan, Paul Hudson,Peter
Cockerell, Dominic Verity, Jeremy San, Robert Macmillan, Paul Fellows, John
Collins, Simon Hughes and Mark Holmes for proof reading
This
book was written and prepared on a BBC Microcomputer Model B using the VIEW word
processor
This book is dedicated to all the staff at
Acornsoft
CREATIVE ASSEMBLER How To Write Arcade
Games
Jonathan Griffiths
The Penguin Acorn Computer Library is a joint venture,
produced by Acomsoft Limited (in association with Pilot Productions Limited),
and published by Penguin Books Limited
Penguin Books Ltd, Harmondsworth, Middlesex,
England
Penguin Books, 40 West 23rd Street, New York, New York,
10010, U.S A.
Penguin Books Australia Ltd, Ringwood, Victoria,
Australia
Penguin Books Canada Ltd, 2801 John Street, Markham,
Ontario, Canada L3R 1~
Penguin Books (N.Z.) Ltd, 182-190 Wairau Road, Auckland 10,
New Zealand
First published 1984
Copyright © Acornsoft Limited, 1984
All rights reserved
Set in Palatino by Repro Graphics, 61 Cromwell Road,
Southampton
Colour origination by RCS Graphics Ltd, 39-40 Springfield
Mills, Farsley, Pudsey, Leeds, and MRM Graphics, 61 Station Road, Winslow,
Bucks
Made and printed in Spain by Printer industria grafica s.a.,
Sant Vicenc dels Horts, Barcelona
D.L.B. 1517O-1984
Line illustrations by Rob Shone
Original photography by Nick Wright
Except in the United States of America, this book is sold
subject to the condition that it shall not, by way of trade or otherwise, be
lent, re-sold, hired out, or otherwise circulated without the publisher's
consent in any form of binding or cover other than that in which it
is
published and without a similar condition including this
condition being imposed on the subsequent purchaser
INTRODUCTION
The
BASIC assembler which is available on the BBC Microcomputer and Acorn Electron
is a very powerful tool for programmers. It provides a comprehensible interface
between the programmer and the machine code language which the 6502 processor
itself uses. Hence the programmer is able to control the machine more directly
using assembler.
The
main reason why people write programs in assembler rather than BASIC is probably
because of the speed difference between the two. Assembler instructions can be
executed extremely quickly, a program written in BASIC will take between 10 and
100 times as long. Hence assembler is particularly useful for games' programmers
since it enables them to move missiles and creatures across the screen quickly
and smoothly. If BASIC was used to calculate the new coordinates of each object
and draw them at those positions then movement would tend to occur in jerky
leaps.
However,
speed is not the only factor to be taken into consideration. Assembler
programming gives you more power to solve a problem than BASIC does. All
high-level languages require programs to have a certain structure and this puts
constraints on programs written in that language.
Sceptics
may advise against using assembler on the grounds that it is too complex. It is
true that operations such as multiplication and division which are easy to
perform in BASIC are not as straightforward in assembler. For what might be
considered a trivial task, for example multiplying a number by three, several
assembler instructions are required instead of just a single BASIC one. A
further problem is that there are no FOR ... NEXT or REPEAT
UNTIL
loops in assembler; if you require a loop you must set one up yourself. The same
applies to floating point arithmetic assembler only supports integer
calculations.
My
advice is that you ignore these sceptics. It isn't difficult to learn to program
in assembler. The programs look much less like English than BASIC I ones
do but nevertheless to someone who knows the language they are easy to
understand. Like learning to do anything else, all that is required is a certain
amount of knowledge and a lot of practice. This book has been written to provide
the knowledge - the rest is up to you.
The
book is divided into three sections, each of which has a different task to
perform. The first part aims to introduce the more useful assembler instructions
available for the 6502 processor, giving simple examples of how they can be
used. The second part introduces some of the more complex programming techniques
which are aimed in particular at people writing large assembler programs. The
third part is aimed mainly at the games' programmer. It provides many useful
routines and finally shows how these can all be linked together to produce a
complete game.
DATA
STORAGE
Before
you can start writing programs in assembler you need to know a few things about
how data is stored inside the computer and how that data can be accessed and
changed. This chapter looks at the ways in which you can enter numbers from the
keyboard and the notation which the computer uses to store these values in its
memorv.
~.1
Rexadecimal notation
To
most people it seems natural to use base ten when dealing with numbers. We have
ten digits; 0,1,... 8,9, and can use these to represent numbers as large as we
please by making the value of a digit depend on which column it is in. Thus,
when we consider the number 171 the first '1' represents 100, and the second '1'
represents just one. Moving a digit one column to the left multiplies its value
by ten; this is why our system is called base 10 or decimal.
When
entering numbers into a computer you can still use base 10 if you wish, but
another base - base 16 - is also available. For reasons which should become
clear as you read through this chapter, base 16 (or hexadecimal) is far more
suitable for working with computers. Hence it is advisable at this stage to
spend some time becoming familiar with this number system.
In
base 16 we need 16 different symbols to represent the 16 different 'hexadecimal
digits'. For convenience we retain the symbols 0 to 9, and use the letters A to
F to represent the values of ten to fifteen.
Hexadecimal 0
1 2 3 4 5 6 7 8 9 A B C D E F
Decimal
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Another
difference between base 10 and base 16 is what happens to a digit or hexadecimal
digit when it is shifted one column to the left. Whereas we have seen that in
base 10 this multiplies the value of the digit by ten, in base 16 it multiplies
the value by sixteen. Hence 10 in hexadecimal represents the value
sixteen.
Having
two bases in which we can work can lead to confusion. Consider, for example, the
number 10; as we have seen this can represent either of the values ten or
sixteen depending on whether it is being interpreted as a decimal or hexadecimal
number. We need a method of specifying whether a number is decimal or
hexadecimal. Normally we do this by prefixing hexadecimal numbers with an
ampersand (&), e.g.
&B1
The
'B' has the value 16*11 because it is in the second column to the left, and the
'1' represents 1 unit; the number therefore has the decimal value
176
+1 = 177.
&123
The
'1' is in the third column to the left, so it hasthe value 16*16*1, the '2' has
the value 16*2 and the '3' has the value 3. Adding these together produces
256
+ 32 + 3 = 291
There
is no real need to leam how to convert between hexadecimal and decimal because
the computer can do it for you, as shown below.
Converting
hexadecimal to decimal
To
print out the decimal value of a hexadecimal number, such as &123,
type
PRINT
&123
The
answer, 291, is printed out.
Converting
decimal to hexadecimal
To
print, in hexadecimal, the value of a decimal number, type
PRINT ~&123
The
answer, 7B, is printed out. The number printed will be in hexadecimal notation,
but note that the computer doesn't use the symbol '&' when it is printing
hexadecimal numbers. In this case it is obvious that the answer is a hexadecimal
number but for an answer such as 79 you would need to know which base you
requested the computer to use to be able to interpret the result
correctly.
The
symbol twiddle or, more accurately, ti(de) ~ means 'print in hexadecimal'; thus
writing
PRINT~123
will
print 123.
- Binary notation and
bits
Although
the computer can accept numbers in either decimal or hexadecimal notation, it
uses neither of these two systems for storing the numbers in its memory. The
computer's memory consists of electronic circuits that can be put into one of
two different states. The two states are normally represented as 0 and 1, but
they are often referred to by different terms as listed below:
0 1
zero one
Low high
clear set
off on
false true
The
circuits are said to be in a 'bistable state', i.e. they are always in one of
two possible states. When the digits 0 and 1 are used to refer to these two
states they are termed 'binary digits', or 'bits' for brevity.
With
two bits, e.g. M and N, four different states can be
represented:
M N
0 0
- 1
- 0
1 1
With
a 'nibble', which is four bits, 16 different values can be represented (16 =
2^4). This means that a hexadecimal digit can be represented by a four-bit
binary number. The hexadecimal digits and their binary equivalents are shown in
the following table:
Decimal Hexadecimal Binary
0 0 0000
1 1 0001
2 2 0010
3 3 0011
4 4 0100
5 5 0101
6 6 0110
7 7 0111
8 8 1000
9 9 1001
10 A 1010
11 B 1011
12 C 1100
13 D 1101
14 E 1110
15 F 1111
Any
hexadecimal number can be converted into its binary representation by the simple
procedure of converting each hexadecimal digit into the corresponding four bits,
for example
Hexadecimal
&19
/ \
/ \
/ \
Binary
0001 1001
Thus
the binary equivalent of &19 is 00011001, (or leaving out the leading zeros
which are irrelevant,
11001).
1.3 Memory
locations and bytes
The
computer's memory is made up of a number of 'locations', each capable of holding
a value. The size of each memory location is normally referred to as a 'byte'.
Each byte can hold an eight-bit number, which means that it can store any one of
256 (2^8) different values; 0... 255.
We
have seen already that each hexadecimal digit requires four bits to specify it.
A byte, since it contains eight bits, can therefore represent any hexadecimal
number between 0 and &FF.
The
bits in a byte are usually numbered for convenience, as
follows:
bit
number 7 6 5 4 3 2 1 0
byte
0 0 0 1 1 0 0 1
Bit
0 is often referred to as the 'low-order bit' or 'least-significant bit', and
bit 7 as the 'high-order bit' or most-significant bit'.
1.4 More
about memory locations
Somehow
it must be possible to distinguish between one location and another. Houses in a
town are distinguished by each of them having a unique address. Even when the
occupants of a house change, the address of the house remains the same.
Similarly, each location in a computer has a unique 'address' consisting of a
number which remains unchanged even when the contents of the memory location are
altered. Thus we can speak of the 'contents of location 100' as being the number
found in the location whose address is 100. The memory locations start from
address 0 and could look something like this:
Decimal
value of the number being stored
27 35 6 91
Address 0
1 2 3
An
address can be one or two bytes long. This means that addresses can cover the
range 0 to &FFFF. For a detailed look at which part of the memory each
address corresponds to see the memory maps in Appendix A.
Examining
memory locations
We
can look at the contents of some memory locations in the computer using the
query (?) operator. The reference '?X' means use the value of X as the address
of the location under consideration. Hence the reference '?&FFEE' means that
we are concerned with the location whose address is &FFEE. To look at this
location type
PRINT
?&FFEE
This
prints out the value found at the location specified, which in this case should
be the number 108. Any memory location can be examined in this way and all of
them will contain a number between 0 and 255.
It
is often convenient to look at several memory locations in a row; for example,
to list the contents of the 32 memory locations from &70 upwards,
type
FOR
N = 0 TO 31 : PRINT ?(N+&70); :NEXT N
An
alternative way of writing this is
FOR
N=0 TO 31 : PRINT N?&70; : NEXT N
This
method is tidier than the other and gives
identical
results; i.e. for each of the values of N between 0 and 31, N is added to the
number &70 to give the address of the location whose contents are to be
printed out. This should result in the contents of 32 memory locations being
listed on the screen.
Changing
memory locations
It
is possible to change the number stored at a particular memory location by
assigning a new value to it. As an example try changing the contents of &70.
First, print the contents of this address; the value there will be whatever was
in the memory when the computer was switched on since the computer does not use
this location for storing any of the variables it is working with. To change the
contents to 7, type
?&70=7
To
verify the change, type
PRINT
?&70
Try
setting the contents of this memory location to other numbers. Setting the
contents to a number greater than 255 or &FF will result in the number
entered modulo 256 being stored there, for example
?
&70=600
PRINT
?&70
This
will print out
(600
MOD 256)
A
word of warning: Before you change the contents of any other memory locations be
sure that vou know what you are doing. Although it is quite safe to look at
almost any memory location in the computer, care must be exercised when changing
any of them. The example given here uses a specific location which is not used
by the computer; if you change any other location you may lose any program you
have in memory or confuse the computer to such an extent that it proves
necessary to reset it by pressing BREAK to make it accept any further
commands.
1.5 Negative
numbers - two's complement
Although
the values stored in the memory locations are between 0 and 255 these can be
used to represent both positive and negative numbers. To do this two's
complement representation is used. To represent a number using this system we
first have to consider what its positive counterpart is in binary notation. For
example to find out how -5 would be stored consider the number
+5
= 00000101
We
then find the complement of this, i.e. change each 0 into a 1 and each I into a
0, e.g.
complement
of +5 = 1111010
Finally
we add one: 11111010
+
1
11111011
This
gives us the two's complement representation of -5.
We
can now try adding together +5 and -5 to see if they give us
0.
00000101
11111011
(1) 00000000
Ignoring
the 1 which has overflowed gives us the result, zero, which we were
expecting.
Note
that when representing numbers using two's complement notation a single byte can
represent any number between -128 and +127. The left-hand bit is 1 if the number
is negative and 0 otherwise. Zero is classed as a non-negative
number.
1.6 Storing
text
If
locations can only hold numbers between 0 and
255,
how is text stored in the computer's memory?
The
answer is that numbers are used to represent thedifferent characters. Hence text
is stored simply as a sequence of numbers in successive memory locations. The
computer does not become confused about whether a number is representing an
actual number or a character since the context will always make it clear how it
should be interpreted.
The
unique number corresponding to each character is given by its ASCII code
(American Standard Code for Information Interchange). To find the ASCII code of
a given character the ASC function can be used, for example
type
PRINT
ASC “A”
and
the number 65 will be printed out. This means that the character 'A' is
represented internally by the number 65. If you try repeating this process for B
C D you will notice that there is a certain regularity. The same is true for a
b c~... and the sequence 1 2 3 4
A
full table of the ASCII codes used to represent all the characters is given in
Appendix A.
2
CARRYING OUT INSTRUCTIONS
The
previous chapter showed how characters and numbers are represented in the
computer's memory, i.e. how the computer deals with data storage. However, data
on its own is useless to a computer, it also needs instructions telling it what
to do with the data. This chapter looks at how the computer handles instructions
and explains some of the simpler assembler instructions which it can use. The
instructions listed refer only to the 6502 processor so if you have any other
sort of processor connected to your machine, e.g. a Z-80 second processor, this
will have to be disabled before attempting any of the routines given in this and
subsequent chapters.
2.1The
CPU
The
Central Processing Unit or CPU is the computer's brain. It is the most active
part of the computer; although areas of memory can remain unchanged for hours on
end when a computer is being used, the CPU is working all the time the machine
is switched on. The CPU's job is to read a sequence of instructions from memory
and carry out the operations specified by those instructions.
The
instructions which the CPU acts on are just values stored in memory locations.
The CPU takes a byte and interprets it as an instruction, e.g. &18 will be
interpreted to mean 'clear carry flag'; this will be explained later in
this chapter. It then performs the operation as instructed and goes on to
collect the next byte.
The
CPU, the computer's brain.
The
first byte of all instructions is the operation code, or 'op-code'. Some
instructions, such as the example above, consist of just the op-code; other
instructions require data on which they must operate. These instructions
therefore consist of two or three bytes, the first one being the op-code and the
other one or two consisting of data. For example the value &E6 is translated
into the instruction 'increase the contents of the memory location with the
following one-byte address by one'. Hence the CPU then takes the next byte from
the memory and interprets this, not as an instruction, but as the address of the
location whose contents are to be incremented. It then adds one to the number
stored in that location. Having executed this instruction, the CPU then goes on
to the next byte which is taken to represent the next instruction it must
perform.
2.2Machine
code V assembler
The
above few paragraphs should have given you the idea that everything the CPU acts
upon is a number between 0 and &FF (255 decimal); each number being
interpreted by the CPU as an instruction or some data which an instruction must
use. The list of numbers which are being used are referred to as machine code.
It is possible for us to talk to the computer in its own language, i.e. program
in machine code, but this would mean that we would have to know which
instructions all the op-codes stand for. Programming in assembler alleviates the
need for learning all these translations. In assembler each op-code is
represented by a three letter mnemonic, e.g. CLC is used instead of &18 to
give the instruction 'clear carry'. The computer then converts all the mnenomics
into the corresponding op-codes.
This
process is carried out by an assembler and hence is known as 'assembling'. The
program that the assembler takes as its input is known as the source code, and
the machine code output is referred to as the object code.
2.3
The accumulator and the carry flag
The
accumulator is just a temporary location inside the CPU which plays a part in
many of the operations performed by the CPU. For example, to add two numbers
together you have to load the first number into the accumulator from the memory,
add in the second number from memory, and then store the result somewhere. To do
this the following assembler instructions will be needed:
Mnemonic
Description Symbol
LDA load
accumulator from A=M
memory
STA store
accumulator in M=A
memory
ADC add
memory to A=A+M+C
accumulator
with carry
CLC clear
carry C=0
The
carry is needed to allow numbers greater than one byte (255 or &FF) to be
generated. When an eight-bit value is added to another eight-bit value the
result could be too great to be represented by eight bits, e.g. 140 +160 = 300
(>255).
In
order to allow for this, the CPU will use the carry as the ninth bit of the
accumulator, and thus the carry will contain the extra bit. In the above
example, when the numbers 140 and 160 are added together and the result stored
in a memorv location, this location will contain the value 44 (300 MOD 256). By
using the carry flag you will have a record of whether the result of the
addition was actually the value 44 or if it was 300. Hence, to avoid confusion,
clear the carry before performing any additions.
2.4
Writing an assembler program
Enter
the following assembler program:
10
DIM P% 100
20[
30 LDA
&80
40 CLC
50 ADC
&81
60 STA
&82
70 RTS
80]
90
END
The
meaning of each line in this assembler program is as follows:
10
The DIM statement is not an assembler mnemonic; it is a BASIC instruction to
tell the assembler where to put the assembled machine code by DIMensioning off
an area of memory for it. The DIM statement is followed by a number (not in
brackets) and the statement reserves this number of bytes for the machine code
which will be generated. As a rough guide to the amount of room needed count the
number of assembler instructions used, treble it and reserve at least this
number of bytes.
The
BASIC variable P% is used by the assembler as a location counter to specify the
next free address. Hence the statement sets P0/o to the lowest
address of the reserved block of memory and then as each byte of machine code is
generated, P0/o increases by one byte so that it always points to the
next free location.
20 The
'[' symbol is an 'assembler delimiter' which has to be used immediately before
the first assembler statement to tell the BASIC interpreter that the following
statements will be in assembler rather than BASIC.
30 Load
the accumulator with the contents of the memory location whose address is
&80. (The contents of the memory location are not
changed.)
40 Clear
the carry flag.
50 Add
the contents of location &81 to the accumulator with the carry. (Location
&81 is not changed by this operation.)
60 Store
the contents of the accumulator to location &82. (The accumulator is not
changed by this operation.)
70 The
RTS instruction will usually be the last instruction of any program; it causes a
return to BASIC from the machine-code program. The mnenomic stands for 'return
from subroutine'.
80 The
']' symbol is an assembler delimiter which has to be used after the last
assembler instruction to tell the interpreter that the following statements will
be in BASIC.
90 The
END statement is not an assembler mnemonic; it just denotes the end of the
program.
Now
type RUN and the assembler program will be assembled; the assembled code being
inserted directly in memory at the address specified by
P0/o.
An
'assembler listing' will be printed out to show the machine code the assembler
has generated to the left of the corresponding assembler
mnemonics:
>RUN
OE5D
OE5D A5 80 LDA &80
OE5F 18 CLC
0E60 65 81 ADC &81
0E62 85 82 STA &82
0E64 60 RTS
operand
mnemonic
statement
instruction
data/address
instruction op code
location
counter statement
The
program has been assembled in memory starting at &0E5D, immediately after
the program text. This address may be different when you enter the example
program if you have inserted extra spaces into the program or if you have filing
systems other than cassette in your machine, but that will not affect any other
part of the listing. All the numbers in the listing are in hexadecimal; thus
&18 is the op-code for the CLC instruction, and &A5 is the op-code for
LDA when the number being loaded is not given directly but is obtained by
looking in the memory location whose one-byte address is given. Hence this LDA
instruction consists of two bytes; the first byte is the op-code, and the second
byte is the address; &80 in this case.
Another
method of finding out where the machine code is, is to find out where 'TOP' is
by typing
PRINT
~TOP
This
value gives the address of the memory location immediately after the program
text. Since the machine code follows on straight after the text this address is
the one corresponding to the first instruction, &A5. Thus the machine code
is stored in memory as follows:
A5
80 18 65 81 85 82 60
^
TOP
When
'RUN' was typed this assembled the assembler program and put the machine code
produced into the computer's memory, however it did not execute the program. The
method for doing this is described below.
2.5
Executing a machine-code program
To
execute the machine-code program at TOP, type
CALL
TOP
Nothing
obvious will happen except for the '>' prompt being printed again on the
screen. This indicates that the computer has finished executing the program and
hence the contents of locations &80 and &81 will have been added
together and the results placed in &82.
You
can verify this by setting the contents of &80 and &81 to certain values
by typing, for example
?&80=7
: ?&81=9
If
you wish you can also set the contents of &82 to 0. Now
type
CALL
TOP
and
then look at the contents of &82 by typing
PRINT
?&82
The
result is 16 (in decimal); the computer has just added 7 and 9 and obtained
16
2.6
Adding two-byte numbers
Try
executing the program for different numbers in
&80
and &81. You might like to try the following:
?&80=140
: ?&81=160 CALL TOP
We
saw earlier in this chapter that if an addition generates a number greater than
255 then the result stored in the memory location specified will be that number
modulo 256. Hence the result in this case will be 44 rather than 300. Here is
the calculation in hexadecimal:
160 &A0
140 &8C
300 &12C
Only
two hex digits can fit in one byte, so the '1' of &12C is lost, and only the
&2C is retained. Luckily the '1' carry is retained for us in the carry flag
as was mentioned earlier, though we didn't see then how to use this. The example
below shows how the two numbers can be treated as being two-byte numbers and
added together using the carry to produce a two-byte number which is the
complete answer. This method can be extended to any number of bytes since the
carry flag makes it a simple matter to add together two numbers as large as we
please. Modify the program already in memory by retyping lines 50 to 120, if you
wish (leaving out the comments to the right of the assembler text). Here is the
modified program:
10
DIM P% 100
20[
30
LDA &80 Low byte of one number
40
CLC Clear carry flag
50
ADC &82 low byte of other number
60 STA &84 low
byte of result
70 LDA &81 high
byte of one number
80 ADC &83 high
byte of other number
90 STA &85 high
byte of result
100
RTS
110]
123
END
Assemble
the program:
>RUN
OE6E
OE6E AS 80 LDA &80
0E70 18 CLC
0E71 65 82 ADC &82
0E73 85 84 STA &84
0E75 AS 81 LDA &81
0E77 65 83 ADC &83
0E79 85 85 STA &85
OE7B 60 RTS
Now
set up the two numbers as follows:
?&81=&8C
: ?&81=&00
?&82=&A0
: ?&83=&00
Finally,
execute the program by typing
CALL
TOP
and
look at the result, printing it in hexadecimal this time for
convenience:
PRINT
~?&84, ~?&85
The
low byte of the result is &2C, as was obtained
before
using the one-byte addition program, but this time the high byte of the result,
&1, has been correctly obtained. The carry generated by the first addition
was added into the second addition, giving
0+
0 + carry = 1
Try
some other two-byte additions using the new program.
2.7
Subtraction
The
subtract instruction is just like the add instruction, except that there is a
'borrow' if the carry flag is zero. Therefore to perform a single-byte
subtraction the carry flag should first be set with the SEC
instruction.
Mnemonic
SEC set
carry flag C=1
SBC subtract
memory from A=A-M-(1-C)
A
with carry
Example
10
DIM P% 100
20[
30
LDA &80 Low byte of first number
40 SEC Initialise
carry flag
50
SBC &82 Low byte of other number
60
STA &84 Low byte of result
70
LDA &81 Now do high bytes
80 SBC &83
90
STA &85
100
RTS Return
110]
120
END
Note
that the above program is very similar in structure to the addition example in
section 2.6.
2.8
Comments
There
are two methods of putting comments in assembler programs. The first of these,
which is used in previous examples, is to put the comment after an assembler
instruction, separated from it by one or more spaces, e.g.
60
STA&84 low byte of result
Alternatively
a statement may start with a backslash (\), in which case the remainder of that
statement is ignored, e.g.
65
Now for the high bytes
Note
that a colon (:) will end the comment and start a new assembler statement, for
example line 60 could be replaced by
60
Low byte of result STA &84
2.9
Printing a character
The
computer contains routines for the basic operations of printing a character to
the VDU, and reading a character from the keyboard, and these routines
can
be
called from assembler programs.
Name Address Function
OSWRCH &FFEE Puts
character in accumulator
to
output (VDU)
OSRDCH &FFEO Reads
from input (keyboard)
into
accumulator
In
each case all the other registers are preserved. The names of these routines are
acronyms for ‘operating system write character' and 'operating system read
character' respectively. These routines are executed with the instruction JSR
(jump to subroutine).
A
detailed description of how the JSR instruction works will be left until the
following chapter.
The
following program outputs the contents of memory location &80 as a character
to the VDU, using a call to the subroutine OSWRCH:
10
DIM P% 100
20
oswrch=&FFEE
30[
40
LDA &80
50
JSR oswrch
60
RTS
70]
80
END
The
variable 'oswrch' is used for the address of the OSWRCH routine. Assemble the
program, and then set the contents of &80 to &21 by
typing
?&80=&21
Then
execute the program using
CALL
TOP
and
an exclamation mark will be printed out before returning to the computer's
prompt character, because &21 is the code for an exclamation mark. An
alternative method of setting the contents of location &80 to &21 is
therefore
?&80=ASC"!"
Try
executing the program with different values in &80, with values chosen from
the table of ASCII values in Appendix A.
2.10
immdiate addressing
In
the previous example the instruction
LDA
&80
loaded
the accumulator from the location whose address is &80, this is known as
'absolute' addressing. The location was then set to contain &21, the code
for an exclamation mark. If at the time the program was written it was known
that an exclamation mark was to be printed in would be more convenient to
specify this in the program as the actual data to be loaded into the
accumulator. Fortunately an 'immediate' addressing mode is provided which
achieves just this. Change the instruction to
LDA
#&21
where
the '#' (hash) symbol specifies to the assembler that immediate addressing is
required. Assemble the program again, and note that the instruction op-code for
'LDA #&21' is &A9, not &A5 as it was previously for the absolute
addressing. The op-code of the instruction specifies to the CPU whether the
following byte is the actual data loaded, or the address of the location
containing the data.
2.11
Using addresses
So
far when a value has been saved, a numerical address has been used to define
where it is to be stored, e.g.
STA
&80
A
better method of giving an address is to use a variable name,
e.g.
STA
addr
In
this case 'addr' must be specified at the beginning of the program,
e.g.
addr
= &80
This
method is better than the previous one since it makes the program easier to
understand, i.e. an address can be given a relevant name, e.g. 'xlowbyte' or
'yhighbyte'. In addition, changing the location of a value becomes easier, since
only the initial specification need be altered rather than every occurence of
that value throughout the program.
The
locations used must be chosen carefully to avoid corrupting operating system or
BASIC workspace. The memory map in Appendix A should help to show which
locations can be used in different circumstances. Also there are some locations
which are always free when using BASIC; these are &70 to
&8F.
3
JUMPS,BRANCHES AND LOOPS
When
an assembler program has been assembled and is being executed, the address of
the next instruction to be executed is kept in a register called the 'program
counter'. All the programs met so far have been executed in the order that the
instructions were written, so the program counter has just steadily increased
until it reached the last instruction. This chapter introduces the jump and
branch instructions which can make the program counter jump over
instructions or move back to previous ones to execute them again. These
instructions make it possible to implement loops and perform different
instructions depending on the outcome of previous ones.
3.1
Jumps
Ordinary
jumps
Mnemonic Description
JMP jump
to instruction whose address is given
The
JMP instruction is followed by the address of the instruction to be executed
next, e.g.
JMP
&E48 or JUMP addr
Instead
of describing the address by a number, we can use a 'label' to indicate to the
assembler where we want to go. In the assembler, labels are variables prefixed
with a full stop (.).
10
oswrch =&FFEE
20
DIM P% 100
30[
40.enter
50 LDA
#ASC”*”
60..Loop
70 JSR
oswrch
80 JMP
Loop
90]
100
END
When
the program is assembled the address corresponding to the label '.loop' will be
inserted in the machine code. When the code is executed the value of the program
counter will be set to this address and the CPU will collect its next
instruction from the location with that address and will continue executing from
there.
The
label '.enter' at the start of the program has been included so that this label
can be called in order to execute the program. This is a better way of executing
a program than calling TOP since TOP doesn't always point to the first machine
code instruction. This is true for the above example since TOP will point to the
assignment statement ‘oswrch=&FFEE'.
The
program will output an asterisk (*), and then jump back to the previous
instruction. The program has become stuck in an endless loop! Compare this
program with the following BASIC program:
10
A
20
VDU A
30
GOTO 20
To
get out of the BASIC loop you press ESCAPE. This will not automatically halt
machine code programs, however. To exit from a machine code loop without losing
the program you must press BREAK, and then type 'OLD' to retrieve the original
program.
Jumps
to subroutines
Mnemonic Description
JSR jump
to subroutine
Examples
of this instruction have been used previously. Like the JMP instruction it is
followed by a two-byte address, e.g.
JSR
oswrch
In
this case the address of the instruction directly following the JSR instruction
in the code is noted, and then the value of the program counter is set to the
address of 'oswrch'. The CPU will go to this address for its next instruction
and start executing the code from there until it meets an RTS. This will set the
program counter to the address which was noted earlier so that the CPU can then
continue executing the code following the JSR instruction. Subroutines jumped to
can either be part of the assembler program or, as in this example, sub-routines
which exist in the operating system memory.
3.2 The
zero and negative flags
There
are several flags in the CPU which can be set or cleared depending on the
outcome of certain instructions. The carry flag was introduced in the previous
chapter, this is set or cleared as the result of an ADC (add with carry)
instruction. Another very useful one is the zero flag, called Z. This is set if
the result of the previous operation gave zero, and is cleared otherwise,
e.g.
LDA
&8O
would
set the zero flag if the contents of &80 were zero.
Similarly
the negative flag, N, is set if the result of the previous operation was
negative in two's complement notation, i.e. if the top bit was set,
e.g.
LDA
&80
would
set the negative flag if the number stored in location &80 was greater than
127 (01111111).
The
conditions of all the flags are stored in a byte called the status register (P),
and each flag is represented by one bit: e.g. the top bit of the status register
is set if N=1 and the bottom bit is set if C=I.
3.3 Conditional
branches
Conditional
branches enable the program to act on the outcome of an operation. There are
eight different branch instructions, six of which are
introduced.
Mnemonic Description Status
BEQ branch
if equal to zero (ie Z=1)
BNE branch
if not equal to zero (ie Z=0)
BCC branch
if carry clear (ie C=0)
BCS branch
if carry set (ie C=1)
BPL branch
if plus (ie N=0)
BMI branch
if minus (ie N=1)
The
conditional branch instructions test the state of the various condition flags,
e.g. the zero flag and negative flag. If the condition is not satisfied then it
carries on executing, but if the condition is satisfied then the computer goes
to the place indicated by the byte following the branch op-code. This byte is
stored as a relative address, thus if you say
BCS
notzero
the
assembler works out the difference (in bytes) between the current instruction
and the place where the label '.notzero' is, and puts this value after the
op-code. This means that the value of this byte is used, in conjunction with the
address of the current instruction, to tell the CPU where to go
next.
Because
only a single byte is allowed in this relative addressing mode, the branch
instructions can only point to one of 255 nearby bytes. The two's complement
representation of numbers is used to give the offset relative to the current
address. Branches which point forwards are restricted to 0-127 bytes beyond the
current location. The value of the byte following the op-code for these is then
0-127. Branches which point backwards to places at lower addresses in memory
require a negative value to be added to the current location. These use the
numbers 128-255 to represent the values -128 to -1.
The
JMP instruction does not use relative addressing; it is followed by two bytes
which specify the absolute address which will be the destination. Hence the
branch instruction is shorter than the jump instruction, the jump being three
bytes long (op-code and two-byte address) and the branch being two bytes long
(op-code and one-byte offset). This difference is automatically looked after by
the assembler.
The
following simple program will print an
exclamation
mark if 'character' contains zero, and a star if it does not. The comments to
the right of the assembler statements may be omitted when you enter the
program.
10 DIM
P% 100
20 character=&80
30 oswrch
= &FFEE
40[
50.enter
60 LDA
character
70 BEQ
exclamation If zero print '!’
80 LDA
#ASC"*" Star
90 JSR
oswrch Print it
100 RTS Return
1
10.exc Lamat ion
120 LDA
#ASC"!" ExcLamation mark
130 JSR
oswrch Print it
140 RTS Return
150]
160
END
Note
that the above program can be made shorter, by replacing the
instructions
JSR
oswrch
RTS
with
the single instruction
JMP
oswrch
Replacing
JSR and RTS instructions by a JMP to a subroutine reduces the size of both a
source program and the object code it produces, and hence increases execution
speed.
Now
assemble the program by typing RUN. You should get the message:
No
such variabLe at Line 70
This
is because the assembler processes the mnemonic instructions in the order in
which they are listed in the program. Therefore when it encounters 'BEQ
exclamation' it has not yet found the label exclamation' so it cannot work out
the offset which is required in the following byte. This is known as the
forward-reference problem, and is easily overcome using the method of two-pass
assembly which is explained below.
3.4 Two-pass
assembly
When
a program contains forward references it needs to be assembled twice. During the
first pass of the assembler the addresses of all the labels are noted so that
during the second pass the offsets of the branch instructions can be included.
And the assembler must be told not to worry when, during the first pass, it
comes across errors of the sort indicated above.
This
can be done using the OPT statement, an assembler directive which has a single
parameter for which the following values are possible:
OPT
0 No error messages, and no listing
OPT
1 No error messages, and listing
OPT
2 Error messages reported, and no listing
OPT
3 Error messages reported, and listing (Default)
Thus
to suppress messages and a listing on the first pass, and to restore them on the
second pass, we need to use OPT 0 and OPT 3 respectively. This can be effected
by placing the directive inside a FOR NEXT loop, which goes from 0 to 3 in steps
of 3. Then
The
value of the control variable is used as the parameter of the OPT statement. So,
to alter the program which was given above, simply enter these
lines:
10
DIM code 100
23
FOR pass = 0 TO 3 STEP 3
26
P% = code
30[
OPT pass
145
NEXT pass
This
time the error message will not be produced and the correct offset will be
calculated for the branch instruction.
Note
lines 10 and 26, which replace the old 'DIM P% 100' statement. P% must be reset
to the starting value each time that the code is assembled.
Now
execute the program by typing
CALL
enter
and
verify that the program behaves as it should for different values in
&80.
- XandYregisters
The
CPU contains two registers, called the X and Y registers, in addition to the
accumulator. As with the accumulator, there are instructions to load and store
the X and Y registers:
Mnemonic
Description Symbol
LDX load
X register from memory X=M
LDY load
Y register from memory Y=M
STX store
X register to memory M=X
STY store
Y register to memory M=Y
However,
unlike the accumulator, the X and Y registers cannot be used as one of the
operands in arithmetic instructions; they have their own special uses which will
be outlined later.
The
X and Y registers are particularly useful as the control variables in iterative
loops, because four special instructions exist which will either increment (add
1 to) or decrement (subtract 1 from) their values.
Mnemonic Description Symbol
INX increment
X register X""X+1
INY increment
Y register Y=Y+1
DEX decrement
X register X""'X-1
DEY decrement
Y register Y=Y-1
Note
that these instructions do not affect the carry flag: incrementing &FF will
give &00 without changing the carry bit. The zero and negative flags are,
however, affected by these instructions.
- Iterative
loops
The
iterative loop enables the same set of instructions to be executed a fixed
number of times, e.g.
10
DIM P% 100
20
oswrch = &FFEE
30E
40.enter
50
LDX #8 InitiaLise X
60
LDA #ASC"*" Code for star
70.
Loop
80
JSR oswrch Output star
90
DEX Count it
100
BNE Loop ALL done?
110
RTS
120 END
Assemble
the program by typing RUN. This program prints out a star, decrements the X
register, and then branches back if the result after decrementing the X register
is not zero. Consider what value X will have on successive trips around the loop
and predict how many stars will be printed out; then execute the program with
'CALL enter' and see if your prediction was correct. (If you were wrong, try
thinking about the case where X was initially set to 1 instead of 8 in line
50.)
How
many stars are printed if you change the instruction on line 50 to 'LDX
#0'?
3.7 Comparing
values
In
the previous example the condition X=0 was used to terminate the loop. Sometimes
we might want to count up from 0 and terminate on some other specified value.
The compare instruction can be used to compare the contents of a register with a
value in memory; if the two are the same, the zero flag will be set. If they are
not the same, the zero flag will be cleared. The compare instruction also
affects the carry flag by setting it to 1 if the register is greater than or
equal to the value in memory, and 0 otherwise.
Mnemonic
Description Symbol
CMP
compare accumulator with A-M
memory
CPX compare
X register with X-M
memory
CPY compare
Y register with Y-M
memorv
Note
that the compare instruction does not affect its two operands, it just changes
the flags as a result of the comparison.
The
next example again prints eight stars, but this time it uses X as a counter to
count upwards from 0 to 8.
10
DIM P% 100
20
oswrch=&FFEE
30[
40.enter
50 LDX
#0 Start at zero
60..Loop
70 LDA #ASC”*” Code
for star
80 JSR oswrch Output
star
90 INX Next X
100 CPX #8 ALL
done?
110 BNE Loop If
not then repeat
120 RTS Else
return
130]
140
END
In
this program X takes the values 0, 1, 2, 3, 4, 5, 6, and 7. The last time around
the loop X is incremented to 8, and the loop terminates. Try drawing a flowchart
for this program.
3.8 Using
the control variable
In
the previous two examples X was simply used as a counter, and so it made no
difference whether we counted up or down. However, it is often useful to use the
value of the control variable in the program. For example, we could print out
the character in the X register each time around the loop. The order in which we
want the characters would then determine whether we count up or down. We
therefore need a way of transferring the value in the X register to the
accumulator so that it can be printed out by the OSWRCH routine. One way would
be to execute:
STX
tempaddr
LDA
tempaddr
where
'tempaddr' is not being used for any other purpose. However, there is a more
convenient way, using one of four new instructions:
Mnemonic
Description Symbol
TAX transfer
accumulator to X register X=A
TAY transfer
accumulator to Y register Y=A
TXA transfer
X register to accumulator A=X
TYA transfer
Y register to accumulator A=Y
Note
that the transfer instructions only affect the register being transferred
to.
The
following example prints out the alphabet by making X cover the range A to
Z.
10
DIM P% 100
20
oswrch = &FFEE
30[
40.enter
50 LDX
#ASC"A" Start with the Letter A
60.Loop
70 TXA
Put it in the accumuLator
80 JSR oswrch Print it
90 INX Next one
100 CPX
# (ASC"Z”+1) Finished ?
110 BNE
Loop If so - continue
120 RTS
ELse return
130]
140
END
All
these examples could have used Y as the control variable instead of X in exactly
the same way.
3.9 Conditional
assembly
Assembler
source text can contain tests, and assemble different statements depending on
the outcome of these tests. This is especially useful where
slightly
Different
versions of a program are needed for many different purposes. Rather than
creating a different source file for each different version, a single variable
can determine the changes using conditional assembly, e.g
10
DIM CODE%100
20
char=&70
30
oswrch=&F FEE
40
osrdch=&FFEO
50
beLL=7 Bleep = VDU 9
60
prompt=ASC":”
70
INPUT"bell",beLL$ Input 'bell$'
80
bellflag=INSTR("Yy",bell$) bellflag' is true if
90
FOR pass=0 TO 3 STEP 3 bell$ = 'y' or 'Y'
100
P%=CODE%
1
10[ OPTpass
120.enter
130]
140
IF bellflag THEN [OPT pass:LDA #bell :JSR oswrch:]
150[OPT
pass
160 LDA
#prompt Load A with ':' prompt
170 JSR
oswrch Print from A
180 JSR
osrdch Read in a character
190 STA
char store it at char and
200 JSR
oswrch ,print it out
210 RTS
220]
230
NEXTpass
When
this program is run it asks if you want the computer to bleep or not and sets
'bellflag' accordingly. Then when the machine code is executed it inputs a
character from the keyboard, bleeping if 'bellflag' is set to remind you that an
input is required, and prints out a character corresponding to the first key
pressed. This character is also saved at the address 'char'.
4LOGICAL
OPERATIONS, SHIFTS AND ROTATES
We
have seen previously that each byte or memory location is made up of eight bits,
each of which can be set to the value 0 or 1. Although the operations we have
considered so far have treated the whole byte as the smallest quantity being
dealt with, many operations in the computer's instruction set are best
considered as operations which act on eight separate bits. Some of these perform
such important tasks as changing the case of characters, or multiplying and
dividing.
4.1 Logical
operations
Logical
operations are performed between the individual bits of two operands; one of the
operands is always the accumulator and the other is a memory location or
immediate value. In this section three such