Fast Access


Macro Framework

Macro Framework

Files:
OPTFN - BASIC
BACKGROUND

Some time ago I wrote a program, which might be of use to other programmers. It is a framework for assembly programs for the BBC/Master. The program started life a few years ago. At that time I was only programming the 6502 in assembly language. Later I switched to other processors. Returning to the BBC, I found myself writing a lot of similar code, only the operands changed a little each time. So I started writing some macro's to make life easier. I have been using the macro's for quite some time and find programming the 6502 much easier, although the code is not optimal.

The Program

The program is called OPTFN and it contains a framework for 6502 assembly programs and a macro library. It is written in BBC BASIC, so it is not very difficult to change to suit your own needs. The framework makes things easier by allocating some space, defining a few constants and checking that the object code still fits in its allocated space. As the user of the program, you simply start writing assembly language, without bothering about passes and P%. The library contains macro's for the most commonly used operations, such as addition, subtraction, copying and comparing of data. Also two useful functions are included for returning the high or low byte of a value. To show how to use the library, suppose we want to add the constant 10 to a word (16 bits) at &70 and store the result in &72. To generate code for this simply type:

OPT FNaddi(&70,10,&72,2)

The macro will generate

CLC
LDA &70: ADC #10: STA &72
LDA &71: ADC #0 :STA &73

which does exactly what we wanted.

The Framework

To use the framework, fill in the variables starting from line @@@@. The variable 'codelen%' defines the maximum length of the code. 'Phystart%' defines where the code will be placed during assembly. Normally this is set correctly and you do not need to change it. 'Logstart%' defines the address where the code will load (set this to &8000, when assembling ROM images). If this address is different from 'phystart%', the framework will assume you want remote assembly and set pass% accordingly. 'Srcfname$' is the name of the source program (containing the assembly source). 'Dstfname$' is the name of the object code. The last variable you need to set is 'execaddr%'. It is defined below the assembly source, just before the end of PROCasm. This variable defines the execution address of the object code. After initialising the variables, allocating some space, etc, the framework assembles the source code three times (the reason for this is explained below) and prints either 'Code too long', indicating the code needs more space than 'codelen%' allows, or some statistics and two lines, which you can copy to save the source program and/or the object code. Note that the framework will not save the source or the code by itself, instead it allows the user to decide whether or not he wants to save anything.

The Macro Library

All macro functions use parameters to pass information about the operands. To avoid confusion, I used consistent names for a certain type of parameter. See the list below:

adr% means a memory address
jmp% destination address of a branch / jump
src%  
src1%  
src2% memory address of a source parameter
destn% memory address of the destination
len% length of the sources and destination in bytes (max 4 bytes)
nshft% number of shifts
val% value of a constant
cond% three dig it constant to define the branch condition
A% constant value for the A register
X% constant value for the X register
Y% constant value for the Y register

Macro
FNrmb(len%) Reserve memory bytes.
This macro does not generate code, it just reserves len% bytes in memory, typically used as variable space.

Macro
FNpush(adr%,len%) Push data from memory onto the stack.
FNpull(adr%,len%) Pull data from stack to the stack.
These macro's generate sequences of LDA ... :PHA or PLA:STA ... instructions to move a variable to or from the stack. X and Y are preserved.

Macro
FNinc(adr%,len%) Increment a variable. FNdec(adr%,len%) Decrement a variable.
These macro's generate code to increment / decrement a variable at address adr% with length len%. Note that decrementing is quite complicated, so the macro will generate almost twice as much code as the incrementing macro. X and Y are preserved.

Macro
FNshl(adr%,len%,nshft%) Shift a variable to the left.
FNshr(adr%,len%,nshft%) Shift a variable to the right.
These macro's shift a variable at address adr% with length len% nshift% times to the left or the right. Every shift is written as a sequence of ASL/ROL or LSR/ROR instructions, so do not try to shift too many times or you will be using a lot of memory. A, X and Y registers are preserved.

Macro
FNldxy(adr%) Load X/Y registers from memory.
FNldxyi(val%) Load X/Y registers immediate.
FNstxy(adr%) Store X/Y registers to memory.
The registers X and Y are often used to transfer pointers from one routine to another.
These macro's allow you to load and save these registers easily. All macro's assume Y as high byte and X as low byte, as usual. A register is preserved.

Macro
FNosbyte(A%,X%,Y%) Perform an osbyte call.
Osbyte is an important way to communicate with the OS. A special problem occurs here: Not every call needs three parameters. This has been solved by not generating code if the value for X% and/or Y% is negative. This means that the call OPT FNosbyte(0,-1,100) will generate code for the Y register, but not for the X register.

Macro
FNmove(src%,destn%,len%) Move data from memory to memory.
FNmovei(val%,destn%,len%) Move immediate value to memory.
Moving variables around in memory is often used. OPT FNmove copies the variable at address src% with length len% to the address destn%, FNmovei initialises a variable at destn% and length len% with the constant val%. Overlapping is checked, but it is not recommended to move large quantities of memory. The X and Y registers are preserved.

Macro
FNadd(src1%,src2%,destn%,len%) Add two variables.
FNaddi(src%,val%,destn%,len%) Add an immediate value to a variable.
FNsub(src1%,src2%,destn%,len%) Subtract two variables.
FNsubi(src%,val%,destn%,len%) Subtract am immediate value.
These macro's are also quite often used. Basically these macro's generate code to perform "src1% +/- src2% -> destn%", src2% may be replaced with a constant value. The destination address may be either one of the source addresses (or both !). Watch out for strange results if the destination partly overlaps either source. The X and Y registers are preserved.

Macro FNcmp(src1%,src2%,len%,cond%,jmp%) Compare memory and branch.
FNcmpi(src%,val%,len%,cond%,jmp%) Compare memory & value and branch.
These macro's generate code to compare two variables with each other, or a variable with a constant. In both cases cond% defines when a jump should be made. See the list below:

cond% Jump if
001 src1 > src2 (unsigned)
010 src1 = src2
100 src1 < src2 (unsigned)

Different entries may be combined, so a value of 101 means jump if src1 <>src2. Registers X and Y are preserved.

Macro
FNlbcc(jmp%) Long Branch Carry Clear.
FNlbcs(jmp%) Long Branch Carry Set.
FNlbeq(jmp%) Long Branch EQual.
FNlbne(jmp%) Long Branch Not Equal.
FNlbmi(jmp%) Long Branch Minus.
FNlbpl(jmp%) Long Branch PLus.
FNlbvc(jmp%) Long Branch oVerflow Clear.
FNlbvs(jmp%) Long Branch oVerflow Set.

These macro's do exactly the same as the corresponding branch instructions would do. However, as soon as the destination gets out of range for a normal branch, a conditional JUMP is generated.

Function
FNlow(val%) Return low byte of value.
FNhi(val%) Return high byte of value.

As the 6502 only manages to move bytes around, you often have to split words into two bytes. Normally you have to use "value MOD 256" or "value DIV 256", which can be very tedious. These functions replace this and "FNlow(value)" or "FNhi(value)" will generate the same result.

Technical Details Of The Framework

The main problem within the framework is why one has to assemble three times. To illustrate this, have a look at the following piece of assembler:

.start
CMP #0
OPT FNlbne(notnul) :\.....(1)
OPT FNldxyi(nulmsg)
JSR prnmesg
RTS
.nulmsg
EQUS "It might be possible that ?temp is zero, but I am not sure."
EQUB 0
.notnulmsg
EQUS "After careful examination I found the value of ?temp to be nonzero."
EQUB 0
.notnul :\.....(2)
OPT FNldxyi(notnulmsg)
JSR prnmesg
RTS
.prnmesg
OPT FNstxy(&70)
LDY #0
.prnmesg1
LDA (&70),Y
BEQ endmesg
JSR oswrch
INY
BNE prnmesg1
INC &71
JMP prnmesg1
.endmesg
JSR osnewl
RTS

During pass one, at (1), the computer does not know the address of notnul, so it produces an BNE instruction. The destination address gets known at (2) during the first pass. The computer uses this information during pass two. At (1) it calculates the distance and finds it too long for a normal branch. To avoid the out of range error, it generates "BEQ over:JMP notnul:.over", which is 5 bytes instead of 2. Because of this, the entire program moves 3 bytes, so notnul moves 3 bytes as well. Now the computer needs another pass to correct this.

Technical Details Of The Macro Library

The main problem in the library forms the use of labels within a macro. To make sure this goes exactly as it is supposed to work, you have to make all variables used in the macro, including the labels self, LOCAL. Secondly, you have to assemble the macro twice, because the value ofthe label does not stay preserved from one pass to the next. I used the following construction with success:

DEF FNmacro :REM we define a macro
LOCAL spass%, OO%, PP%, label :REM make everything local
PP% = P%: OO% = O% :REM store P% and O%
label = PP% :REM initialise label (see below)
FOR spass% = (pass% AND 4) TO (pass% OR 2) STEP ((pass% AND 3) OR 2
P%=PP%:O%=OO% :REM restore P% and O%
[OPT spass%
CMP #10
BNE label :\ Forward branch
ADC #5 .label
] NEXT spass%
=pass% :REM end of the macro

As you can see the label is initialised to P% before the loop. This seems unneccessary, but without this statement, I get out of range errors on remote assembly, which I cannot explain. Maybe another reader can solve this mystery.

Extensions To The Framework Program

The first thing which comes to mind when thinking about extending the framework is the inclusion of more OS constants. Another extension might be the possibility of multifile assembly, although that is a lot more complicated.

Extensions To The Macro Library

It is easy to add new macro's to the library, since the entire program is written in BASIC. When adding macro's always remember you will probably use the macro quite often, so if you manage to write a macro which generates less code, all your programs will be shorter. If you plan to add a lot of new macro's, you will probably run out of memory. Then you will have to move the macro's to SWR or to disc. Most of the macro's do not generate optimal code. For example look at the result of the macro call OPT FNsubi(&900,25,&900,2). This produces:

SEC
LDA &900:SBC #25:STA &900
LDA &901:SBC #0 :STA &901

Instead, the macro should produce:

SEC
LDA &900:SBC #25:STA &900:BCS endmacro
DEC &901 .endmacro

This can be solved by making the macro smarter (have also a look at FNaddi).Things get trickier if you want optimal code between two macro calls. Consider these calls:

OPT FNmovei(0,&2000,1)
OPT FNmovei(0,&70,2)

This generates

LDA #0:STA &2000
LDA #0:STA &70
STA &71

Clearly, the second LDA #0 can be eliminated. Almost the same problem occurs if you initialise pointers:

OPT FNmovei(&3000,srcptr,2)
OPT FNmovei(&7000,dstptr,2)

To solve these kind of problems you will probably need to save the produced macro expansion to disc and feed this file to another program, which optimises the assembler sources. Then you can assemble the optimised program using BASIC.