Bottom     Previous     Contents

Chapter Four
Structured Assembler

Chapters Two and Three discussed some of the ideas involved in structured programming as applied to BBC BASIC. If you followed the main points of the argument for the use of structured programming you will realise that its objectives, namely to produce programs with a clear flow of control structure, apply to any computer language including assembler! This is not such a widely held belief as you might expect, indeed some programmers claim that it isn't possible to write well structured programs in a language as 'primitive' as BASIC, let alone assembler! The question of whether modular programming and stepwise refinement are at all useful in assembler is less controversial. Nearly every assembly language programmer quickly learns the value of subroutines and, in a language where each instruction achieves so little, stepwise refinement is an ideal way of building up large programs without getting into a position where 'the trees obscure the view of the wood'. In short, all of the programming methods that you should apply to BBC BASIC should also be applied to assembly language and any other computer language that you might use.
The first part of this chapter looks in detail at the natural structuring elements in assembler and at ways of constructing modules from subroutines. These ideas apply to 6502 assembler on any machine, not just the BBC Micro. To make things more specific, the second half of the chapter examines more advanced ways of using the BBC Micro's assembler to convert 6502 assembly language into machine code. The BBC Micro's assembler may appear at first sight to be a very simple assembler but, because it works in such close contact with BBC BASIC, it can be used in ways that are jess than obvious. In particular, it is possible to introduce better ways of defining data constants, conditional assembly and macros without any difficulty.

The natural structure of assembler

It is very important that an assembly language program has a simple flow of control. Each assembly language instruction performs so little work that it can take many instructions to complete a reasonable task and even more to complete an entire program. Thus assembly language programs are often large and intrinsically difficult to understand. A spaghetti assembly language program is a nightmare worse than anything that can be produced in BASIC but it is a sad fact that most assembly language programs are more or less spaghetti! It is almost as if otherwise skilful and careful programmers throw every thought of good structure away as soon as assembly language is contemplated. The attitude seems to be that writing a program in assembly language is difficult enough without worrying about anything else. Of course, it is only difficult if you don't use the familiar programming methods!
The problem in applying structured programming to assembler s;eems to be that the natural structuring elements of IF . . . THEN . . . ELSE, etc., are just not present. This is not surprising as IF . . . THEN . . . ELSE is a natural structuring element of BBC BASIC there are even some versions of BASIC in which it is not present! Before it is possible to write structured assembler it is necessary to identify its natural structure. As in the case of BASIC there are only two categories of structure selection and looping.

Assembler selection

The fundamental commands of 6502 selection, or conditional execution, are the conditional branches. The usual situation is that a calculation, some test, or the simple loading or storing of a register is used to set the condition codes in the status register. Following this a conditional branch (for example, BCC Branch Carry Clear) can be used to divert the flow of control depending on the setting of the condition code bits. You should be able to see that this is very similar to the

IF condition THEN GOTO x

statement of BASIC. The 'condition' corresponds to the operations that set the condition code bits and the 'THEN GOTO x' part corresponds to the conditional branch instruction. The correspondence is not exact because more than one condition code bit may be set at a time and exactly what is being tested for also depends on the particular conditional branch that is used. For example, the BASIC instruction:

IF A= 0 THEN GOTO x

where x is a line number, is equivalent to:

CMP #0
BEQ x

where x is a label in 6502 assembler but the instruction CMP #0 not only sets or clears the Z flag it a]so sets or clears the N and C flags. Thus:

CMP #0
BMI x

is equivalent to:

IF A<0 THEN GOTO x

In other words, the condition that causes the branch to be taken cannot be determined solely from the operations that set the condition codes, it also depends on which conditional branch is used. Thus the form of all assembly language conditionals is:

(1) one or more operations that set the condition codes followed by:
(2) a conditional branch that branches depending on the state of one of the condition code bits.
This still leaves the question of how the transfer of control should be used to select which part of the program is to be executed? In assembler the fundamental structuring element for conditional execution is the 'conditional skip'.

. . .
operations that set the condition codes
B** SKIP
. . .
list of assembly language instructions
. . .
.SKIP rest of program
. . .

where B** is a particular conditional branch that transfers control to .SKIP when the desired condition is true. (All the conditional branch instructions apart from BEQ and BNE are of the form BfS or BfC where 'f' is the name of the condition code bit or flag to be tested for Set or Clear.) Notice that the 'list of assembly language instructions' is only carried out when the condition is false, otherwise it is 'skipped'. As an example of a conditional skip consider:

CPY #0
BEQ SKIP
DEY
.SKIP rest of program

Which will decrement the Y register only if it is not already zero. In other words, the DEY is 'skipped' if Y is zero. You should be able to see that this conditional skip is similar to:

IF Y=0 THEN GOTO x
Y=Y-1
x rest of program

in BASIC where x is a line number.
The conditional skip is the fundamental structuring element for all assembly language conditional execution. This might seem rather surprising as in BASIC the IF. . .THEN. . .ELSE at least makes it possible to select between two alternatives. In assembler even the choice between two sections of program is built up from a choice to skip one of them! For example to execute 'list I' when 'condition' is true and 'list 2' if it is false you would use:

. . .
operations that set the condition codes
B** LIST1
. . .
list 2
. . .
JMP REST
.LIST1 . . .
list1
. . .
.REST rest of program

which should be compared to the example of how IF. . .THEN. . .ELSE can be constructed from IF. . .THEN GOTO given in Chapter Two. A particular example may help to clear up any difficulties. The following assembly language:

CPY #0
BMI MINUS
DEY
JMP REST
.MINUS INY
.REST rest of program

will decrement Y if it is positive or zero, and increment it if it is negative. Thus, it is equivalent to the BASIC:

IF Y>=0 THEN Y=Y-1 ELSE Y=Y+1

Once you have seen how the conditional skip can be used to select between two alternatives it is easy to use it to select between any number of alternatives. Fortunately situations that need a great many choices happen very rarely in assembly language programming because such problems are best dealt with using BASIC.

Assembly language loops

The subject of assembly language loops is identical to that of BASIC loops except of course that there are no explicit FOR . . . NEXT and REPEAT . . . UNTIL commands. All assembly language loops have to be made up from scratch using a jump instruction to form an infinite loop and a conditional branch to form the exit point. For example:

LDY #0
.LOOP INY
CPY #l0
BEQ EXIT
JMP LOOP
.EXIT rest of program

is a conditional loop with its exit point at the end and so it ts the assembly language equivalent of the until loop, Y ou may notice the slight inefficiency in havjng the conditional branch immediately followed by the JMP instruction and indeed the assembly language until loop is more normally implemented as:

LDY #0
.LOOP INY
CPY #10
BNE LOOP
rest of progranm

All of the classification of loops and guidelines for using them given in Chapter Two apply to assembly language loops. In particular try to:

(1) use only one exit point per loop,
(2) use only until and while loops,
(3) make the initial and final values of enumeration loops clear.

Modular assembly language

As well as using the natural structuring elements to put together an assembly language program it is very important to use modular stepwise refinement. The process of stepwise refinement is exactly the same for assembler as it is for BASIC; at each step the program is extended by filling in the details of dummy modules used in previous steps and introducing new dummy modules to defer things to the next step, This is easy once you have identified a method of constructing modules in assembler. Unfortunately there is no convenient way of forming modules analogous to the BBC BASIC procedure with its parameters and LOCAL variables. Assembler has only the JSR (Jump to Subroutine) and RTS (ReTurn from Subroutine) instructions and these are more like the BASIC GOSUB and RETURN than PROC and ENDPROC. In fact, using assembly language subroutines is exactly like writing modular programs in traditional BASIC using COS UB and RETURN. All the variables and registers in an assembly language program are global, in the sense that they can be used by any part of the program with the danger of creating accidental side effects and difficult-to-find bugs. There is very little that can be done about this problem and it is something that makes the use of assembly language different from BBC BASIC. It is possible to keep the 6502's registers local to a particular subroutine by pushing them on the stack at the start of the subroutine and restoring them by pulling them off the stack at the end of the subroutine, but this is tedious and time-consuming. Equally the problem of passing data into and out of a subroutine is difficult to solve. There are many schemes that can be used. Some, for example, involve pushing data that is to be passed to a subroutine onto the stack before calling the subroutine. Similarly, data that is to be passed out of the subroutine is pushed onto the stack to be retrieved by the calling program. But, once again, such schemes are tedious and time-consuming. It is conventional for small quantities of data to be passed into and out of assembly language subroutines using the registers. Larger quantifies of data are either passed through known and fixed areas of memory or the address of the area of memory is passed in the X and Y registers. Whatever method is used it is important that it is made clear where the subroutine gets its data from, how it returns results and which memory locations it uses. Many examples of parameter passing in assembly language can be found in the applications programs in subsequent chapters.
Even though there are many disadvantages to the assembly language subroutine, it is no worse than the traditional BASIC subroutine and in both cases modular programming is still better than attempting to write large programs in one chunk. However, it is worth being aware of the increased danger of accidental side effects. Whenever an assembly language program, which was well behaved at earlier stages of stepwise refinement, suddenly starts to act in an apparently illogical manner you can be almost sure that the trouble stems from one subroutine altering variables used by another.

The role of comments

At the end of Chapter Three the idea of a well structured BBC BASIC program being almost self-documenting was introduced as a reason for not worrying too much about including hundreds of REM statements. You may think that a well structured assembly language program would share this property of self-documentation, but this is not so. While the flow of control and the subroutine nesting structure of assembler should be as clear as a well structured BBC BASIC program, the intent of the program isn't. BASIC is made up of almost self-explanatory statements such as PRINT A and IF A=1 THEN PRINT "ONE" and, as long as the flow of control is clear, it is usually not difficult to understand the program by analysing what is happening in each statement. The situation is assembly language is completely different as each instruction performs a very small part of the larger task. Without knowing what the larger task is the single instruction often seems unconnected with anything going on in the program. Thus in assembler it is important to include comments that inform the reader of the overall idea of the action of each part of the program. Notice that this doesn't mean that you have to include comments on each instruction. Indeed, the most obvious comments in a program are the most irritating! For example, the comments in the following program section add nothing (except irritation):

LDA NUM1 \ load the A register with NUM1
CLC \ clear the carry flag
ADC TEST \ add TEST to the A register
and so on

Whereas the single comment \A=NUM1+TEST would have said it all!

Using the assembler

All that has been said so far applies to using 6502 assembler on any machine and much of it applies to using any assembler! In this and subsequent sections we look more specifically at the way that the BBC Micro's built-in assembler can be used in some advanced ways, but first it is worth briefly going over how it works.
To leave BBC BASIC and use the assembter all you have to do is enclose any assembly language statements in square brackets. The rule is that anything within square brackets will be treated as assembly language, anything outside square brackets is pure BASIC. (Notice that this implies that you cannot use BASIC statements within the square brackets that enclose a ptece of assembly language, but sec later). This is a very easy way of switching between BASIC and assembler but it is important to understand the very different ways that BASIC and assembler are processed. When the BASIC interpreter encounters a line of BASIC it carries out the instruction, but when the assembler meets a line of assembly language it doesn't carry out the instruction; it simply translates it to machine code. Of course, to be of any use this machine code must be carried out sooner or later but this is nothing to do with the assembler!
When the assembler translates the assembly language it has to store it somewhere until it is needed. There are a number of places where the machine code can be stored and the most useful is in a byte array. A byte array is created by a special form of the DIM statement:

DIM variable maximum_index

which creates an area in memory consisting of 'maximum index' + 1 bytes and stores the address of its first memory location in 'variable'. For example:

DIM CODE% 20

gives a byte array with 21 elements (i.e. element 0 to element 20) and stores the address of the first element in the integer variable CODE%. Notice that CODE% is a perfectly standard BASIC variable and can be used in expressions, etc., as usual. As the address of the first element of the array is stored in CODE% you can use the indirection operator '?' to store and retrieve data within the array. That is:

CODE%?I=X

will store X in the Ith element of the byte array and:

X=CODE%?I

will store the Ith element of the byte array in X. Combined with the indirection operators a byte array can be used for many things other than just storing machine code.
The second component involved in the storage of machine code by the assembler is one of the resident integer variables. The resident integer variables are, unlike all other BASIC variables, present whether you use them or not and are always stored in the same memory locations. The resident integer variables are named @%, A% to Z% and the value stored in P% is used by assembler as the address where the next item of machine code will be stored. In addition to this, every time the assembler stores an item of machine code it adds one to P% so making sure that each item of machine code is stored in a new memory location. By setting P% to a particular address before using the assembler you can determine where the assembler stores the machine code that it produces. Thus, to make the assembler store the machine code that it produces in a byte array all that you have to do is set P% equal to the start address of the array before entering the assembler. For example:

10 DIM CODE% 20
20 P%=CODE%
30[
40 LDA #65
50 JSR &FFE3
60 RTS
70 ]

Line 10 creates a byte array with 21 elements and places the address of the first element in the variable CODE%. Line 20 stores this start address in P% so that the assembler will start storing machine code in the byte array.
Lines 30 to 70 form a short assembly language program that prints the letter A on the screen and then returns to BASIC. However, if you run the program you will only see a listing of the machine code program looking something like:

1951 A9 41 LDA #65
1953 20 E3 FF JSR &FFE3
1956 60 RTS

If you recall that the assembler only converts 6502 assembly language to machine code this isn't at all surprising. If you look at the first line of the output then the first number, 195 I, is the address where the first item of machine code, &A9, will be stored. The second item, &41, will automatically be stored at 1952 and so on to 1956. To carry out the machine code you have to use the BASIC CALL command:

CALL 'address'

This transfers control from BASIC to a machine code routine starting at 'address'. Notice that CALL must be carried out by BASIC so the standard sequence in writing and using a machine code program is:

(1) in BASIC set up space for machine code, etc.,
(2) in assembler translate assembly language to machine code,
(3) in BASIC use CALL to transfer control to the machine code.

If you don't want to use the machine code immediately then there is nothing to stop you from saving it on tape (using *SAVE) and then loading it (using *LOAD) at a later date and possibly into a different program. The only thing that you have to ensure is that the machine code is loaded back into the same area of memory that it was originally assembled to. This problem is taken up in Chapters Eight and Nine.
To use the machine code in the last example all you have to do is add line

80 CALL CODE%

Notice that CODE% still holds the start address of the byte array, and hence of the machine code, whereas P% at this point holds the address of the memory location where the next byte of machine code would be stored. Now running the whole program not only results in the conversion of the assembly language to machine code but also in the execution of the machine code.

Labels and variables

So far the subject of labels has been completely ignored. In fact the BBC assembler has a very interesting way of handling labels. Although you cannot use BASIC commands within the assembler you can use BASIC expressions! For example, instead of writing LDA #65 to load the ASCII code for "A" into the A register, you can use LDA #ASC(" A"). In the same way you can use an expression in the address field of an instruction. So you could write JMP 2*3+6, which would cause a jump to address 12, or an expression like JMP CALL%+ 10, which would cause a jump to the address given by adding 10 to the contents of the variable CALL%. There are many other ways in which this powerful idea can be applied and one of the main problems is seeing which of the many ways is useful! The key point to remember is:

You can use any valid BASIC expression, including defined variables, anywhere that the assembler would expect data or an address as part of an instruction.

The example 'print A' program given in the last section can now be rewritten in a more readable form as:

10 DIM CODE%
20 P% CODE%
30 OUT%=&FFE3
40 [
50 LDA #ASC("A")
60 JSR OUT%
70 RTS
80 ]
90 CALL CODE%

Apart from the use of ASC(" A") already described, notice the use of OUT%, defined in line 30, gives the address to which the JSR will transfer control. In general, if you have any fixed subroutines or memory locations within an assembly language program it is better to define variables with the correct values and appropriate names within BASIC and then use these variables within the program. If you follow this simple rule your programs will be easier to understand and easier to modify.
If you are familiar with any other assemblers you will realise that the command JSR OUT% is using a normal BASIC variable as if it was a label. This is not unusual. After all a label is simply a variable that is used to hold an address. What is unusual is the fact that BBC assembler labels are also BASIC variables and this is one of the reasons that this simple assembler is so powerful!
The fact that assembler labels and BASIC variables are one and the same thing also applies to labels defined within an assembly language program. Using a label preceded by a full stop causes the assembler to store the current value of P% in it. For example, in the program:

10 DIM CODE%
20 P%=CODE%
30 OUT%=&FFE3
40 [
50 LDA #ASC("A")
60 .LOOP% JSR OUT%
70 JMP LOOP%
80 ]
90 CALL CODE%

the variable/label LOOP% defined in line 60 is to contain the address of the JSR OUT% instruction and this is used in line 70 to jump back to this instruction repeatedly. (Of course, when the machine code is used by line 90 the result is that the infinite loop fills the screen with letter As.) It is important to notice that although LOOP% was set to a particular value by the assembler, it is still a BASIC variable and, once back inside BASIC, it can be used just like any other variable. So you could add 85 PRINT LOOP% to find out the address of the JSR instruction in fine 60.

Two-pass assembly

The only outstanding problem with using labels is related to the forward jump problem described in Chapter Two. Consider for example:

l0 DIM CODE%
20 P%=CODE%
30 [
40 LDA #0
50 BEQ EXIT%,
60 LDA #0
70 .EXIT RTS
80 ]

The program itself doesn't do anything useful, it loads the A register with zero then jumps to EXIT% if the result of the load was zero (it always is!). However, the BBC assembler gives an error message if you run it. It is not difficult to see the reason for this error message. When the assembler reaches line 50 it needs to know the value of EXIT% but unfortunately EXIT% has not been defined and will not be defined until line 70. This is called the forward reference problem and the traditional solution is to use a two-pass assembler. A two-pass assembler, as its name suggests, takes two 'looks' at the program. The first time through it picks up all the definitions of the labels and ignores any errors. The second time through it uses the values of the labels defined in the first pass to assemble the program correctly any errors at this stage are real!
The situation with the BBC assembler seems hopeless it's a simple one-pass assembler. However this is where its close association with BBC BASIC comes to the rescue for the first but not the last time. There is an assembler instruction called OPT which can be used to suppress error messages and program listings in the following way:

n OPT n
0 ignore errors and don't produce a listing
1 ignore errors but produce a listing
2 report errors but don't produce a listing
3 report errors and produce a listing

(The default value of OPT is OPT 3.) The definition of OPT 0 should enable you to understand how the two-pass method works. First the assembler is run over the program using OPT 0 to define all the variables used as labels. As these variables are BASIC variables they still exist and their values are unaltered after the assembler has finished. Then the assembler is run again using OPT 3, safe in the knowledge that all the labels are defined. The only problem is how to run the assembler a second time? We could write the entire assembly language program out twice but this would soon put a stop to any serious applications! The answer is, of course, to use a BASIC FOR loop to pass the assembler over the program twice! This may sound uninteresting, especially if you have been using it for some time as directed by the User Guide, but it is our first example of using BASIC to modify the way that the assembler works. The two-pass version of the forward reference example given above is:

10 DIM CODE%
20 FOR PASS=0 TO 3 STEP 3
30 P%=CODE%
40 [OPT PASS
50 LDA #0
60 BEQ EXIT%
70 LDA #0
80 .EXIT RTS
90 ]
100 NEXT PASS

Even this simple example exhibits one subtlety. You must be very careful to set P% to the value stored in CODE% within the loop, otherwise the second pass over the program will store its machine code after the incorrect machine code produced by the first pass. Also notice the way that the assembler must be left before trying to execute the NEXT PASS instruction. Apart from very simple programs, this two-pass method of using the BBC assembler is the norm and for this reason it is all too easy to miss its implications for using BASIC with the assembler in other ways.

Conditional assembly

We now have all the information necessary to see how to turn the BBC assembler into a 'conditional assembler'. An example is the best way to explain exactly what a conditional assembler is. Suppose you had written a communications program that worked at two different speeds. In any given application you would only want the fast or the slow version of the program so you could use something like:

IF FAST=1 THEN [LDA #60:] ELSE [LDA #30:]

where FAST is a normal BASIC variable that can be set elsewhere in the program. Depending on the value of FAST either the first instruction will be assembled into the program or the second. The only thing that you have to be careful about is to remember to leave the assembler to execute the IF statement and then remember to return to it afterwards. Notice that the IF statement isn't part of the machine code that the assembler produces, it simply affects what machine code the assembler produces.
As another example, it is often a good idea to use regular jumps to a subroutine that prints the values stored in certain variables while you are debugging an assembly language program. Once you have the program working the obvious thing to do is to take out the debugging aids but this does cause something of a problem if you then go on to find another bug! The best solution is, of course, conditional assembly.

10 TEST=1
. . .
150 [ LDA #&40
160 STA IRQC%
165 ]: IF TEST=1 THEN [ JSR DEBUG%:]
166 [
170 LDA AUXC%
180 ORA #&C0
. . .

This may look a little complicated but it is easy to follow. Line 165 first returns to BASIC to carry out the IF statement which will either re-enter the assembler to assemble JSR DEBUG% or just pass on to line 166 depending on the value in TEST. While debugging the program TEST would be set to I, otherwise it would be set to 0.
By now you should be able to see how to use IF . . . THEN and IF . . . THEN . . . ELSE to conditionally assemble any list of instructions into a program and so all that is left is to decide when the facility is useful and use it!

Macros

Now that we have seen how BASIC can be used to alter the way that the assembler works, the principles behind a macro are easy. Consider the fairly common problem of carrying out a shift or rotate instruction a few times. Normally this would be done by writing the command as many times as needed. For example, four arithmetic shift lefts would be:

100 ASL A
110 ASL A
120 ASL A
130 ASL A

Using the same idea as for the two-pass assembler we could generate four ASL A instructions by placing a single ASL A instruction inside a FOR loop, but this time not resetting the value of P% each time through. To take this idea one stage further we could write a PROC with an appropriate name and a parameter that would repeat the instruction a given number of times. That is:

1000 DEF PROCASL(N)
1010 LOCAL I
1020 FOR I=1 TO N
1030 [ ASL A:]
1040 NEXT I
1050 ENDPROC

and in the main program the four ASL A instructions would be produced by:

100 ]:PROCASL(4):[

which would first leave the assembler, then call the PROC and then, after generating the required machine code, re-enter the assembler. The FROG itself is fairly straightforward apart from the need to define I as LOCAL just in case it is used anywhere else.
Once you have seen this idea in operation it 'takes off to produce all sorts of useful macros. For example, you can use the parameters passed to the PROC within BASIC to control the assembler, i.e. as in the FOR loop or in conditional assembly, or you can use them within the assembler as part of expressions. You could write a general addition macro along the lines:

1000 DEF PROCADD(Nl,N2,ANS)
1010 [ CLC
1020 LDA N1
1030 ADC N2
1040 STA ANS
1050 ]
1060 ENDPROC

To add two number stored at memory location DATA1 and DATA2 and store the answer in DATA3 you would use:

PROCADD(DATA1,DATA2,DATA3)

which would generate the correct machine code at whatever position it was used within the main program.
If you have a standard assembly language operation then the advantages of turning it into a macro are as great as turning it into a subroutine (using JSR . . . RTS) and in this sense macros are just a useful as part of modular programming as subroutines. However, as use of the macro generates the necessary machine code every time it is used there are none of the inefficiencies incurred with jumping to and from a subroutine.

Data definition

Macros can help with the very common problem of initialisation of assembly language variables. The recommended method of doing this is to leave the assembler and use the indirection operators to store the data values directly into memory. For example, if you want to initialise two variables, NUM I and NUM2, each consisting of a single byte to 50 and 100 respectively then you would use:

]
?P%=50
NUM1=P%
P%=P%+1
?P%=100
NUM2=P%
P%=P%+1
[

If you look at the way that these two variables are constructed you will be able to see that each one involves three stages:

(1) The '?' indirection operator is used to store the value in the next free memory location.
(2) The address in P% is stored in the variable's name for further use in the program.
(3) The address in P% is incremented to point to the next free location,

This is a workable but not very clear method of defining data.
Fortunately it is not difficult to define three macros (using functions) that make data definition very easy. For example the macro:

DEF FNequb(VA%)
?P%r(VA% MOD 256)
P%=P%+1
=P%-1

will store a single byte value in the next free memory location and return its address, The MOD 256 in the second line simply ensures that the constant is in the range 0 to 255. Using this macro, the previous definitions of NUM1 and NUM2 can be written:

]
NUM1=FNequb(50)
NUM2=FNequb(100)
[

which is a great improvement! (The function's name stands for EQUal to Byte. )
In the same way the macro:

DEF FNequw(VA%)
?P%=(VA% MOD 256)
P%?1=(VA% DIV 256)
P%=P%+2
=P%-2

will store a two byte value in the next free memory location and return its address. (The function's name stands for EQUal to Word.) The macro:

DEF FNequs(S$)
$P%=S$
P%=P%+LEN(S$)+1
=P%-LEN(S$)-1

will store all of the characters in the string SS starting at the first free memory location and returns the address of the first character. (The function's name stands for EQUal to String.) Notice that this macro does not automatically append a carriage return to the string. To construct a string complete with carriage return use:

]
HELLO=FNequs(" HELLO FOLKS")
CA=FNequb(&0D)

Some further examples of the use of these macros are given in the assembly language program in later chapters. These macros serve the same function as the EQUB, EQUW, and EQUS assembler commands found in Electron BASIC and BASIC II.

Problems with macros - OPT, BASIC and local variables

There are three complications that occur when using macros. The first is due to the default setting of OPT being used whenever the assembler is entered unless explicitly changed by an OPT statement. Thus, if you leave the assembler using ']' and then enter it again with a macro using nothing but '[', the default of OPT 3 will cause any undefined labels on the first pass to cause an error and stop the assembly. The best solution would be to use some method of finding out what the last setting of OPT was and restore it when re-entering the assembler. However, this is not possible without PEEKing system locations that might change in later versions of BASIC. The only practical solution is always to re-enter the assembler with [OPT PASS'.
The second problem is that there is no way of suppressing errors caused by variables not being defined while in BASIC. For example, if a macro uses a variable that is not defined until later as a parameter in the assembly, the program will stop and report an undefined variable error on the first pass. The only solution to this problem is to ensure that all the variables used in the BASIC part of a macro are defined before the macro is called on the first pass.
The final problem is caused by local variables being set to zero on each pass. If a macro needs a label then the best way to ensure that no other part of the program uses the same label is to name it in a LOCAL statement. This causes no trouble if the label is defined on the first pass before it is used but the usual two pass mechanism of using the first pass to define labels that are then used in the second pass doesn't work! This is because the value that a local variable acquires on the first pass is lost when the LOCAL statement is encountered on the second pass! In other words, local variables can only be used to implement backward jumps. If you need to use a forward jump within a macro the best method is to use a constant rather than a label in the address field of a conditional branch. Thus BEQ 6 will jump forward six memory locations. These restrictions make the wnting of foolproof macros a little more difficult than you might imagine but then most useful macros are surprisingly simple.

Assembly language examples

No examples of large assembly language programs have been given in this chapter to illustrate structured assembly language programming. This is not an oversight, however, as Chapters Eight, Nine and Ten all include some long assembly language programs.


Next     Top