7.2 KiB
System documentation: memory model and CPU
Memory address space layout
The 6502 CPU can directly address 64 kilobyte of memory. Most of the 64 kilobyte address space can be used by IL65 programs.
type | memory area | note |
---|---|---|
ZeroPage | $00 - $ff |
contains many sensitive system variables |
Hardware stack | $100 - $1ff |
is used by the CPU and should normally not be accessed directly |
Free RAM or ROM | $0200 - $ffff |
free to use memory area, often a mix of RAM and ROM |
A few memory addresses are reserved and cannot be used, because they have a special hardware function, or are reserved for internal use in the code generated by the compiler:
reserved address | in use for |
---|---|
$00 |
data direction (hw) |
$01 |
bank select (hw) |
$02 |
IL65 scratch var |
$03 |
IL65 scratch var |
$fb-$fc |
IL65 scratch var |
$fd-$fe |
IL65 scratch var |
$fffa-$fffb |
NMI vector (hw) |
$fffc-$fffd |
RESET vector (hw) |
$fffe-$ffff |
IRQ vector (hw) |
A particular 6502/6510 based machine such as the Commodore-64 will have many other special addresses as well:
- ROMs installed in the machine (BASIC, kernal and character roms)
- memory-mapped I/O registers (for the video and sound chip for example)
- RAM areas used for screen graphics and sprite data
Directly Usable CPU Registers
The following 6502 CPU hardware registers are directly usable in program code (and are reserved symbols):
A
,X
,Y
the A, X and Y cpu registers (8 bits)AX
,AY
,XY
surrogate 16-bit registers: LSB-order (lo/hi) combined register pairsSC
status register's Carry flagSI
status register's Interrupt Disable flag
The other status bits of the status register are not directly accessible, but can be acted upon via conditional statements. The stack pointer and program counter registers are not accessible.
ZeroPage ("ZP")
The ZP addresses $02
- $ff
can be regarded as 254 other 'registers', because
they take less clock cycles to access and need fewer instruction bytes than access to other memory locations.
Theoretically you can use all of them in a program but there are a few limitations:
- several addresses (
$02
,$03
,$fb - $fc
,$fd - $fe
) are reserved for internal use as scratch registers by IL65 - most other addresses often are in use by the machine's operating system or kernal, and overwriting them can crash the machine. The program must take over the entire system to be able to safely use all ZP addresses. This means it can no longer use most BASIC and kernal routines.
- it's often more convenient to let IL65 allocate the particular addresses for you and just use symbolic names in the code.
For the Commodore-64 here is a list of free-to-use ZP addresses even when its BASIC and KERNAL are active:
$02
; $03
; $04
; $05
; $06
; $2a
; $52
;
$f7
- $f8
; $f9
- $fa
; $fb
- $fc
; $fd
- $fe
The six reserved addresses mentioned above are subtracted from this set, leaving you with just five 1-byte and two 2-byte usable ZP 'registers'. IL65 knows about all this: it will use the above ZP addresses to place its ZP variables in, until they're all used up. You can instruct it to output a program that takes over the entire machine, in which case (almost) all of the ZP addresses are suddenly available for variables. IL65 can also generate a special routine that saves and restores the ZP to let the program run and return safely back to the system afterwards - you don't have to take care of that yourself.
IRQs and the ZeroPage:
The normal IRQ routine in the C-64's kernal will read and write several addresses in the ZP
(such as the system's software jiffy clock which sits in $a0 - $a2
):
$a0 - $a2
; $91
; $c0
; $c5
; $cb
; $f5 - $f6
These addresses will never be used by the compiler for ZP variables, so variables will not interfere with the IRQ routine and vice versa. This is tru for the normal zp mode but also for the mode where the whole ZP has been taken over. So the normal IRQ vector is still running when the program is entered!
ZeroPage handling in programs
The global %zp
directive can be used to specify the way the compiler
will treat the ZP for the program. The default is compatible
, where most
of the ZP is considered a no-go zone by the compiler.
compatible
: only use the few 'free' addresses in the ZP, and don't change anything else. This allows full use of BASIC and KERNAL ROM routines including default IRQs during normal system operation.full
: claim the whole ZP for variables for the program, overwriting everything, except the few addresses mentioned above that are used by the system's IRQ routine. Even though the default IRQ routine is still active, it is impossible to use most BASIC and KERNAL ROM routines. This includes many floating point operations and several utility routines that do I/O, such asprint_string
. It is also not possible to cleanly exit the program, other than resetting the machine. This option makes programs smaller and faster because many more variables can be stored in the ZP, which is more efficient.full-restore
: likefull
, but makes a backup copy of the original values at program start. These are restored (except for the software jiffy clock in$a0 - $a2
) when the program exits, and allows it to exit back to the BASIC prompt.
Subroutine Calling Conventions
Subroutine arguments and results are passed via registers (and sometimes implicitly
via certain memory locations).
@todo support call non-register args (variable parameter passing)
In IL65 the "caller saves" principle applies to registers used in a subroutine. This means the code that calls a subroutine or performs some function that clobber certain registers (A, X or Y), is responsible for storing and restoring the original values if that is required.
You should assume that the 3 hardware registers A, X and Y are volatile and their contents cannot be depended upon, unless you make sure otherwise.
Normally, the registers are NOT preserved when calling a subroutine or when a certian operations are performed. Most calls will be simply a few instructions to load the values in the registers and then a JSR or JMP.
By using the %saveregisters
directive in a block, you can tell the
compiler to preserve all registers. This does generate a lot of extra code that puts
original values on the stack and gets them off the stack again once the subroutine is done.
In this case however you don't have to worry about A, X and Y losing their original values
and you can essentially treat them as three local variables instead of scratch data.
You can also use a !
on a single subroutine call to preserve register values, instead of
setting this behavior for the entire block.