1
0
mirror of https://github.com/RevCurtisP/C02.git synced 2024-11-30 23:50:04 +00:00
C02/doc/design.txt

544 lines
15 KiB
Plaintext
Raw Normal View History

C02 Design Considerations
Variable Storage
================
Zero Page
---------
The 6502 has a Zero Page addressing mode. This differs from the absolute
addressing mode in that the address operand is only one byte instead of
two and the instructions take less cycles to execute.
Also, some systems have very limited RAM, requiring all variables to be
stored in zero page. One example is the Atari 2600, which maps the 128
bytes of RAM in the RIOT chip to the range $80-$8F and mirrors it at
$180-$18F (for the machine stack).
To allow the use of zero page variables, the compiler recognizes the
"zeropage" declaration modifier and the "#pragma zeropage" directive,
the latter of which specifies a base address for allocating zero page
variables.
I may add a -Z command line option to allow the specification of the
zero page variable base address.
ROM Based Code
--------------
For compiled programs that will reside in ROM, such as an EPROM or a
cartridge, variables will need to reside in a separate memory area than
the program.
The compiler normally allocates all variables directly after the generated
code, const variables first and regular variables afterward. In addition,
the regular variables are all all allocated as zero bytes in the assembled
object code, which can unnecesarilly inflate the size of the generated
binary file.
The compiler allows the location of non-const variables to be specified
using the "#pragma rambase" directive.
I may add a -R command line option to allow the specification of the RAM
variable base address.
Read/Write Variables
--------------------
Some systems used different memory addresses for reading and writing to
RAM. One example is Atari 2600 catridges with RAM. The SARA Superchip
contained 128 bytes of RAM, which were written at addresses $F000 through
$F01F and read from addresses $F080 through $F0FF, while CBS RAM+ had 256
bytes which were written from $F000 though $F0FF and read from $F100
through $F1FF.
To allow for this, the compiler recognizes an additional directive,
"#pragma writebase" directive. In this case the "#pragma rambase"
will specify the base address for reads. When a write base address is
specified, the compiler will allocate all variables using the read base
address and generate an offset, which will be included in all variable
assignments. Thus the code
#pragma rambase $F100
#pragma writebase $F000
char b,c;
c = b;
would generate the assembly code
LDA B
STA C-256
B EQU $F100
C EQU $F101
I may add a -W command line option to allow the specification of the write
base address.
Variable Types
==============
Pointers
--------
The 6502 differs from nearly all other microprocessors by not having
a 16 bit index register, instead using indirect indexed mode in
conjunction with zero page.
On other processors, it would be trivial to dereference a pointer
stored anywhere in memory. For example, on the 6800:
LDX P ;N = *P
LDAA IX
There is no direct equivalent on the 6502, but by using indirect
indexed mode, any zero page variable pair can be used as a pointer:
LDY #0 ;N = *P
LDA (P),Y
Additionally, accessing elements of a dereferenced zero page pointer
is just as trivial:
LDY I ;N = *P[I]
LDA (P),Y
expr ;N - *P[expr]
TAY
LDA (P),Y
However, trying to use a 16-bit value stored outside of zero-page
would take extra code and require using a a zero page byte pair:
LDY P ;N = *P
STY ZP
LDY P+1
STY ZP+1
LDY #0
LDA (ZP),Y
This violates two principles of the C02 design philosophy: that
each token correspond to one or two machine instructions and
that the compiler is completely agnostic of the system configuration.
Therefore, if pointers are implemented, they will have to be in
zero page.
Implementation:
Declaration of a pointer should use two bytes of zero page storage,
the address of which would be taken from the free zero page space
specified by the #pragma zeropage directive.
P EQU $80 ;char *p
Q EQU $82 ;char *q
Any pointers declared in a header file would added to the variable
table but not allocated, so would be defined in the accompanying
assembly file:
char *dst; //stddef.h02
char dstlo,dsthi;
DST EQU $30 ;stddef.a02
DSTLO EQU $30
DSTHI EQU $31
Since they are 16-bit values, raw pointers cannot be passed into
used directly in expressions. But could be used in standalone
assignments:
LDX Q ;p = q
STX P
LDY Q+1 ;X is used for LSB and Y as MSB to match
STY P+1 ;the address passing convention for functions
Likewise if a 16-bit integer variable type was added to C02,
then one could be assigned to the other using the exact same
assembly code.
2019-03-23 01:22:24 +00:00
A dereferenced pointer can be used anywhere an array references
is allowed and would be subject to the same restrictions.
A raw pointer used as an argument to a function will be passed
using the same convention as an address:
LDY P+1 ;func(p)
LDX P
JSR FUNC
JSR FUNC ;p = func()
STY P+1
STX P
Since pointers are zero page the address of operator on a pointer
will generate a char value:
LDA #P ;&p
This will allow passing of pointers for use inside of functions
using indexed mode:
LDA #P ;inc(&p)
JSR FUNC
TAX ;void inc()
INC ($00,X)
RTS
The memio module makes extensive use of pointer addresses as
function arguments.
Structs
-------
For the declarations
STRUCT RECORD { CHAR NAME[8]; CHAR INDEX; CHAR DATA[128]; };
STRUCT RECORD REC;
references to the members of the struct REC will generate the code:
2019-03-23 01:22:24 +00:00
XXX REC+$09 ;REC.INDEX
LDX I ;REC.DATA[I]
2019-03-23 01:22:24 +00:00
XXX REC+$0A,X
Using the address of operator on a struct member generates assembly
code with parentherical expressions, which are not recognized by all
assemblers:
LDY #>(REC+$0A) ;FUNC(&REC.DATA)
LDX #<(REC+$0A)
JSR FUNC
The compiler could optimize the generation of code for references
to the first member of a struct, producing
2019-03-23 01:22:24 +00:00
XXX REC ;REC.NAME
instead of
2019-03-23 01:22:24 +00:00
XXX REC+$00 ;REC.NAME
2019-03-23 01:22:24 +00:00
but the machine code produced by the assembler should be identical
in either case.
Expression Evaluation
=====================
Array Indexes
-------------
Array indexing normally uses the X register and indexed addressing mode:
LDX I ;R[I]
2019-03-23 01:22:24 +00:00
XXX R,X
If the index is a constant or literal, absolute addressing is used:
LDA R+1 ;R[1]
2019-03-23 01:22:24 +00:00
XXX S+0 ;S[0]
Specifying a register as the index also uses indexed addressing mode:
TAX ;R[A]
2019-03-23 01:22:24 +00:00
XXX R,X
XXX R,Y ;R[Y]
XXX R,X ;R[X]
Allowing for an expression as the index in the first term of an
expression uses only one extra byte of code and two machine cycles:
expression ;R[expr]
TAX
LDAA R,X
while in any other termm is uses an extra three extra bytes of code
and ten machine cycles:
PHA ;R[expr]
expr code ;code to evaluate the expression
TAX
PLA
2019-03-23 01:22:24 +00:00
XXX R,X
compared to the extra four to six bytes and six to eight machine cycles
used by the equivalent C02 code required to achieve the same result:
expr code ;Z = expr
STA Z
LDX Z ;R[Z]
2019-03-23 01:22:24 +00:00
XXX R,X
Function Calls
--------------
A function call in the first term of an expression requires additional
processing by the compiler, since the accumulator holds the return
value upon completion of the function call:
JSR FUNC ;R = func()
STA R
Allowing a function call in susbsequent terms, however, requires
extensive stack manipulation:
whereas the equivalent C02 code generates much simpler machine code:
Shift Operators
----------------
In standard C, the shift operators use the number of bits to shift as
the operand. Since the 6502 shift and rotate instructions only shift
one bit at a time, this would require the use of an index register and
six to seven bytes of code
expr code ;expr
.LOOP LDY B ;>> B
LSR
DEY
BNE .LOOP
whereas a library function would require five bytes of code:
SHIFTR: LSR ;A=Value to Shift
DEY ;Y=Bits to Shift
BNE SHIFTR
RTS
and each function call would use five bytes of code
expr code ;shiftr(expr, B)
LDY B
JSR SHIFTR
Following the philosophy that a operator should correspond to a single
machine instruction, in C02 shifting is implemented as post-operators
using the various available addressing modes:
ASL S ;S<<
LDX I ;T[I]>>
LSR T,X
ASL ;A<<
Post-Operators and Pre-Operators
--------------------------------
Parsing for post-operators in a standalone expression is trivial, since
that is the only time the relevant characters will only follow the operand.
Implementing pre-operators on standalone expressions would be redundant
since their would be no difference in the generated code.
Parsing for post-operators and/or preoperators within an expression or
evaluation, however, would complicate the detection of operators and
comparators.
In addition, the code generated from a post-operator or pre-operator
withing an expression:
DEC I ;R[--I]
LDX I
2019-03-23 01:22:24 +00:00
XXX R,X
LDX I ;R[I++]
2019-03-23 01:22:24 +00:00
XXX R,X
INC I
Is indentical to the code generated when using a standalone post-operator;
DEC I ;I--
LDX I ;R[I]
2019-03-23 01:22:24 +00:00
XXX R,X
LDX I ;R[I]
2019-03-23 01:22:24 +00:00
XXX R,X
INC I ;I++
Assignments
===========
Assignment to a variable generates an STA instruction, using either absolute
or indexed addressing mode:
expr code ;R = expr
STA R
expr code ;R[2] = expr
STA R+2
expr code ;R[I] = expr
LDX I
STA R,I
while assignment to an index register generates a transfer instruction:
expr code ;Y = expr
TAY
expr code ;X = expr
TAX
and assignment to the Accumulator is simply a syntactical convenience:
expr code ;A = expr
Specific to C02 is the implied assignment, which also generates an STA:
STA S ;S
Allowing an expression as an array index in an assignment is problematic
on an NMOS 6502 since the index expression will be evaluated prior to the
expression being assigned to the array element and neither index register
may be directly pushed or pulled from the stack.
The addition of the PLX instruction to the 65C02 would allows this to be
done using only two extra bytes of code for each variable assignment:
indexp oode ;R[indexp] = expr
PHA
expr code
PLX
STA R,X
expr oode ;R[expr], S[exps] = func()
PHA
exps code
PHA
JSR FUNC
PLX
STY S,X
PLX
STA R,X
however, this would only work with the A and Y variables of a plural
assignment.
A workaround for the NMOS 6502 would require an extra five bytes of code
indexp oode ;R[indexp] = expr
PHA
expr code
TAY
PLA
TAX
TYA
STA R,X
and the use of the Y register would limit it to the only the A variable
of a plural assigment, whereas the equivalent C02 code would use an
extra four to six bytes of code
indexp code ;I = indexp;
STA I
expr code ;R[I] = expr
LDX I
STA R,X
and works with all three variables of a plural assignment.
Conditionals
============
2019-03-23 01:22:24 +00:00
Conditionals are separate and distinct from expressions, due to the fact
that the comparison operators all set status flags which affect the various
branch instructions:
CMP DATA Normal Inverted
Reg < Data BCC BCS
Reg = Data BEQ BNE
Reg ≥ Data BCS BCC
Reg ≠ Data BNE BEQ
2019-03-23 01:22:24 +00:00
The remaining operators can be implemented with CLC and SBC, but this will
change the value in the accumulator.
CLC:SBC DATA Normal Inverted
Reg ≤ Data BCC BCS
Reg > Data BCS BCC
2019-03-23 01:22:24 +00:00
Or they could be implemented using multiple branch instructions. This would
leave the value of the left side expression in the accumulator, but use one
more byte of code and require and extra label.
CMP DATA Normal Inverted
Reg ≤ Data BEQ exec:BCC exec BNE exec:BCS exec
Reg > Data BEQ skip:BCS exec BNE skip:BCC exec
When compiling a comparison, the generated code will usually, but not
always, branch when the condition is false, skipping to the end of the
block of code following the comparison. In addition, the logical not
operator will invert the comparison.
By arranging the eight standard comparisons, along with evaluation of
a term as true when non-zero, in this order:
0 1 2 3 4 5 6 7
!0 = < ≤ ≥ > ≠ 0
the comparison can be inverted with a simple exclusive-or. For this
reason, The ! operator is logical rather than bitwise and affects
the result of the comparison rather than an individual expression
or term within the expression.
Standalone Expressions
----------------------
Flags Operators
---------------
Logical Operators
-----------------
Parsing the logical operators && and || is trivial if the preceding
condition is a comparison or flag operation, since any expression
evaluation is complete before the parse encounters the initial & or |
2019-03-23 01:22:24 +00:00
character. For a standalone evaluation of an expression as true or
false, however, the expression evaluator will mistake the initial
character of the && or || as a bitwise operator. Differentiating
the two would require changing to a look-ahead parser.
2019-03-23 01:22:24 +00:00
One solution is to enclose require parentheses around each comparison
when using logical operators, which is allowable in C syntax, but
it's just as easy, and arguably cleaner looking, to use the words
2019-03-23 01:22:24 +00:00
"and" and "or" instead. This is allowable in standard C by using
the #define directive to alias "and" to "&&" and "or" to "||".
The most efficient way to implement logical operators is to use shortcut
evaluations.
Under the normal circumstances, where the generated code branches when
the condition is false, && can be implemented by evaluation the next
comparison only in the event of the first condition being true.
For ||, however, the following comparison will be evaluated only if the
first comparison was false.
LDA I ;IF (I<J
CMP J
BCS ENDIF
LDA M ;&& M<>N)
CMP N
BEQ ENDIF
LDA I ;IF (I<J
CMP J
BCC block
LDA M ;|| M<>N)
CMP N
BEQ ENDIF
block
When chaining multiple && and/or || operators, the shortcut evaluation
2019-03-23 01:22:24 +00:00
effectively make them right-associative.