Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor
The MEGA65 is an 8-bit machine with a 28-bit address space, and almost everything interesting about programming it follows from that mismatch. A CPU that can only see 64KB at a time must reach 256MB of potential address space, three generations of video chip must share one set of registers, and a modern operating layer must sit underneath a Commodore-style ROM without getting in the way.
BASIC 65 hides most of this. You can write substantial programs without ever thinking about where your variables live or which bank a bitmap occupies. But the moment you want to go faster, use more memory, mix BASIC with machine language, or understand why a POKE did nothing, you need a working model of the machine underneath.
This chapter builds that model. It covers:
Later chapters return to each of these in depth. Chapter 7 covers the 45GS02 instruction set, Chapter 8 covers machine-language techniques that depend on banking and DMA, and Chapter 12 covers the VIC-IV in detail. Here the aim is the big picture: enough to reason about the machine as a whole.
A note on notation. Addresses the CPU sees directly are written as four hex digits, such as $D020. Full 28-bit addresses are written with seven hex digits, such as $FF80000. Where it helps, a 28-bit address may be split with a dot, as $FF8.0000, separating the megabyte from the offset within it.
The 45GS02 is the latest member of a family that runs 6502 → 65CE02 → 4510 → 45GS02. Each step kept everything below it, so standard 6502 code runs unchanged, and the C65's 4510 code runs unchanged too.
The 65CE02 contributed the features most visible to a 6502 programmer: a third index register, a relocatable zero page, a 16-bit stack pointer, 16-bit branches, and word increment and shift instructions. The 4510 added the MAP instruction for memory management. The 45GS02 adds 32-bit pointers, 32-bit arithmetic using a virtual register called Q, and the hooks that let the Hypervisor take control.
| Register | Width | Purpose |
|---|---|---|
| A | 8 bits | Accumulator |
| X | 8 bits | Index register |
| Y | 8 bits | Index register |
| Z | 8 bits | Third index register, used by the (zp),Z and [zp],Z modes |
| B | 8 bits | Base page: the high byte of the page used as zero page |
| SP | 8 or 16 bits | Stack pointer; 16 bits when the E flag is clear |
| PC | 16 bits | Program counter |
| P | 8 bits | Status flags: N V E B D I Z C |
| Q | 32 bits | Virtual register formed from A, X, Y and Z, with A as the low byte and Z as the high byte |
The B register deserves a word of warning. On a 6502, zero page is always $0000-$00FF. On the 45GS02 it is whichever page B points to. B is zero after reset, so existing code behaves as expected, but a routine that changes B and forgets to restore it will break everything that uses zero page afterwards, including the KERNAL.
The E flag occupies the bit that is unused on a 6502. When E is set, the stack behaves exactly as on a 6502, confined to page 1. When E is clear, the stack pointer is 16 bits wide and the stack can live anywhere in the 64KB the CPU currently sees.
The 45GS02 can run at four speeds:
| Speed | Purpose |
|---|---|
| 1 MHz | C64 compatibility: 6502 cycle counts per instruction, though not exact access timing |
| 2 MHz | C128 fast mode compatibility |
| 3.5 MHz | C65 fast mode compatibility |
| 40.5 MHz | Full MEGA65 speed |
MEGA65 mode starts at full speed. C64 mode starts at 1 MHz so that games, demos and loaders that count cycles behave as they would on a real C64. From C64 mode, POKE 0,65 switches to full speed, which is often the quickest way to make an old BASIC program dramatically faster.
At 40.5 MHz, instruction cycle counts differ from the 6502's, so code that relies on exact timing must run at 1 MHz. Chapter 7 discusses timing, and the MEGA65 Book's instruction-set appendix gives the cycle counts. The practical consequence is simple: at full speed, code that was too slow in BASIC on a C64 is often fast enough on a MEGA65 without any machine language at all.
Every byte the MEGA65 can reach has a 28-bit address, giving a 256MB address space. Only a small part of it is populated, and the CPU never sees more than 64KB of it at once. The rest of this chapter is largely about how pieces of the 28-bit space are brought into that 64KB window.
The C65 had a 20-bit, 1MB address space, divided into sixteen 64KB
banks numbered 0 to 15. The MEGA65 keeps that arrangement for its first
megabyte, so C65 software and BASIC 65's BANK command still make sense.
Bank 0 is $00000-$0FFFF, bank 1 is
$10000-$1FFFF, and so on.
| 28-bit address | Size | Contents |
|---|---|---|
$0000000-$001FFFF |
128KB | Chip RAM, banks 0 and 1: the C65's original main memory |
$0020000-$003FFFF |
128KB | Chip RAM holding the system ROM, normally write-protected |
$0040000-$005FFFF |
128KB | Chip RAM, banks 4 and 5 |
$4000000 upward |
varies | Cartridge port |
$8000000-$87FFFFF |
8MB | Attic RAM |
$FF80000-$FF87FFF |
32KB | Colour RAM |
$FFD0000-$FFD3FFF |
16KB | I/O, one 4KB block per personality |
$FFF8000-$FFFBFFF |
16KB | Hypervisor |
Two details trip people up. First, the first 2KB of colour RAM also
appears at $001F800-$001FFFF, where the C65 kept its colour
RAM. Writes to that range go to colour RAM as well as the chip RAM
underneath, and reads come from colour RAM, so it can't be used as
ordinary chip RAM (mega65-core, gs4510.vhdl). Second,
the ROM is not a separate chip: it is loaded into chip RAM at boot and
then protected. Software that disables the protection can patch the ROM,
or reclaim that 128KB as ordinary RAM.
The 16-bit addresses a program uses are translated into 28-bit addresses through several layers. In C64 mode the result is exactly the C64's memory map, controlled by the CPU port at $01:
| CPU address | Default contents in C64 mode |
|---|---|
$0000-$9FFF |
RAM |
$A000-$BFFF |
C64 BASIC ROM |
$C000-$CFFF |
RAM |
$D000-$DFFF |
I/O |
$E000-$FFFF |
C64 KERNAL ROM |
In MEGA65 mode there is no single fixed layout. The KERNAL and BASIC 65 keep parts of the ROM mapped into the window and change the map as they run, reaching program text, variables and strings in other banks behind the scenes. Chapter 6 shows how. Any machine-language program is free to rearrange the view entirely.
The MEGA65 has four kinds of memory, and they are not interchangeable. Each differs in speed, size and, most importantly, in what can see it.
| Memory | Size | CPU speed | VIC-IV can see it | Typical use |
|---|---|---|---|---|
| Chip RAM | 384KB | Full speed | Yes | Programs, screens, bitmaps, sprites, character sets |
| Attic RAM | 8MB | Slower | No | Bulk data, buffers, overlays, assets loaded once |
| Colour RAM | 32KB | Full speed | Colour data only | Colour and attribute information for text screens |
| ROM | 128KB in chip RAM | Full speed | Yes | KERNAL, BASIC 65, editor, DOS, character sets |
Chip RAM lives inside the FPGA. The Book explains the name: it is the memory the CPU can reach at full speed (MEGA65 Book, memory chapter). Anything the VIC-IV must display, such as a screen, a bitmap, a character set or sprite data, has to be in chip RAM. Because it is internal it also runs at full CPU speed with no wait states.
Attic RAM is 8MB of external memory on the main board. It is much larger than chip RAM but slower, and the VIC-IV cannot see it at all. The name suggests its role: a place to store things until they are needed. A game might keep all its levels in attic RAM and copy the current one into chip RAM with a DMA job.
The CPU can reach attic RAM in two ways: through a 32-bit pointer
using the [zp],Z addressing mode, or by mapping it into the
64KB window with MAP. For large transfers DMA is usually the better
choice. Release boards, the development kit and Xemu have attic RAM, but
the Nexys FPGA boards that can run the MEGA65 core do not, so software
meant for every MEGA65 variant should check for it rather than assume it
(MEGA65 Book, memory map appendix).
On the C64, colour RAM is a separate 1KB of 4-bit memory at
$D800. On the MEGA65 it is 8 bits wide and much larger, at
$FF80000. The first 1KB still appears at $D800
for compatibility, and the first 2KB is mirrored into bank 1 as
described above.
The extra width matters. In the VIC-IV's enhanced text modes each character cell uses two bytes of colour RAM, holding attributes such as blink, reverse, underline and alternate palette selection as well as the colour itself. Chapter 12 covers these modes.
The MEGA65 ROM is a 128KB image loaded from the SD card into chip RAM
at $20000-$3FFFF when the machine starts. It contains the
KERNAL, BASIC 65, the screen editor, the DOS and the character sets.
Because it is really RAM, a different ROM version can be used simply by
placing a different file on the SD card, and the C64-mode KERNAL and
BASIC are part of the same image.
Three separate mechanisms decide what appears in the CPU's 64KB window, and a fourth lets the CPU bypass the window altogether. They come from the C64, the C65 and the MEGA65, and all of them are active at once. MAP takes precedence over every other banking mechanism (MEGA65 Book, p. K-7), but only for the 8KB blocks it actually maps. For every block MAP leaves unmapped, the older mechanisms decide:
| Order | Mechanism | Origin | Applies to |
|---|---|---|---|
| 1 | MAP instruction | 4510 | Each 8KB block it maps, with any offset |
| 2 | $D030 ROM bits and the CPU port at
$01 |
C65 / VIC-III and C64 | Fixed ROM and I/O areas in unmapped blocks; if either brings ROM into a region, ROM appears there |
| 3 | Nothing mapped | Chip RAM in bank 0 |
Flat 32-bit addressing sits outside this table: it ignores all three and goes straight to a 28-bit address.
The interaction matters most in the $A000-$BFFF and
$E000-$FFFF regions, where both $D030 and
$01 can bring in ROM: if either is active and MAP does not
map the block, the region shows ROM (MEGA65 Book, p. 14-39).
Location $01 works exactly as on the C64. Its low three
bits, LORAM, HIRAM and CHAREN, choose whether BASIC ROM, KERNAL ROM, I/O
and the character ROM appear in the window. The familiar values still
apply: $37 is the default, $35 leaves only I/O
visible above RAM, and $34 gives RAM everywhere. This
mechanism matters most in C64 mode, but it remains active underneath
MEGA65 mode too.
$D030 ROM bitsThe C65 added a register in the VIC-III, at $D030, that
switches pieces of ROM into fixed places in the window. They show
parts of ROM bank 2, which holds the C64-mode BASIC and KERNAL along
with other ROM code; the MEGA65's main KERNAL, in bank 3, is reached
through MAP instead. These regions are read-only, and MAP overrides them for any
block it maps (MEGA65 Book, pp. K-6, K-7).
| Bit | Name | Effect when set |
|---|---|---|
| 0 | CRAM2K | Shows the second kilobyte of colour RAM at
$DC00-$DFFF |
| 3 | ROM8 | $28000-$29FFF at $8000-$9FFF |
| 4 | ROMA | $2A000-$2BFFF (C64 BASIC) at
$A000-$BFFF |
| 5 | ROMC | $2C000-$2CFFF at $C000-$CFFF |
| 6 | CROM9 | Meant to select between the C64 and C65 character sets; not implemented on the MEGA65 |
| 7 | ROME | $2E000-$2FFFF (C64 KERNAL) at
$E000-$FFFF |
Bits 1 and 2 are not banking bits: bit 1 enables external video
sync (genlock), and bit 2 chooses whether colours 0-15 come from the
palette ROM or palette RAM.
The ROM-banking form of $D030 exists only in the VIC-III
and VIC-IV personalities, described in the next section; in the C64
personality, $D030 is the C128's 2 MHz register instead.
CRAM2K deserves a warning: while it is set, the CIA registers at
$DC00 and $DD00 are replaced by colour RAM, so
the keyboard can't be scanned and CIA interrupts can't be read or
acknowledged, though the timers keep running (mega65-core,
iomapper.vhdl).
MAP is the general-purpose mechanism. It divides the 64KB window into
eight 8KB blocks and lets each block be redirected by adding an offset
to its address. The lower four blocks, $0000-$7FFF, share
one offset; the upper four, $8000-$FFFF, share another.
MAP takes its arguments in the four registers:
| Register | Bits 7-4 | Bits 3-0 |
|---|---|---|
| A | Lower offset, bits 15-12 | Lower offset, bits 11-8 |
| X | Enable blocks 3-0 ($6000, $4000,
$2000, $0000) |
Lower offset, bits 19-16 |
| Y | Upper offset, bits 15-12 | Upper offset, bits 11-8 |
| Z | Enable blocks 7-4 ($E000, $C000,
$A000, $8000) |
Upper offset, bits 19-16 |
The offset is added to the CPU address to form the physical address.
To show bank 4 memory at $44000-$45FFF in the window at
$4000-$5FFF, the offset is
$44000 - $4000 = $40000, and only block 2 needs to be
enabled:
lda #$00 ; lower offset bits 15-8
ldx #$44 ; enable block 2 ($4000), offset bits 19-16 = 4
ldy #$00 ; upper offset bits 15-8
ldz #$00 ; no upper blocks mapped
map
eom
Two things about MAP are easy to get wrong. First, it sets both
halves at once. The example above also unmaps anything the upper half
was showing, including the KERNAL, so a real program must supply
suitable values for Y and Z as well. Second, MAP blocks interrupts until
the next EOM. EOM shares the 6502's NOP opcode, $EA, so the
pair is always written together.
The C65's MAP could only reach the first megabyte. The MEGA65 extends
it with a megabyte selector. A MAP with X set to $0F loads
A into bits 27-20 of the lower offset, and a MAP with Z set to
$0F loads Y into bits 27-20 of the upper offset. A second
MAP then sets the offset within that megabyte as usual. This is how
attic RAM or the I/O personalities can be mapped into the window.
The simplest way to reach any byte is not to map it at all. The
45GS02's [zp],Z addressing mode takes a four-byte pointer
from zero page and uses it as a 28-bit address, ignoring every banking
mechanism above:
ptr = $fb
lda #$00
sta ptr
sta ptr+1
lda #$f8
sta ptr+2
lda #$0f
sta ptr+3 ; ptr = $0FF80000, start of colour RAM
ldz #$00
lda [ptr],z ; read the first colour RAM byte
The assembler emits this as an EOM prefix before the ordinary
(zp),Z instruction. It costs a couple of cycles more than a
16-bit indirect access, which is very little for the ability to reach
any address without disturbing the map. For scattered accesses this is
usually the best choice; for bulk copies, DMA is faster.
The $D000-$DFFF I/O area looks different depending on
which machine the MEGA65 is pretending to be. This is called its I/O
personality. A C64 program expects to find a VIC-II there and nothing
else; a MEGA65 program wants the full VIC-IV register set, the DMA
controller, the math unit and much more. Rather than compromise, the
MEGA65 keeps a separate 4KB I/O view for each case and shows the CPU
whichever one is selected.
| Personality | 28-bit address | Visible at $D000 |
|---|---|---|
| C64 | $FFD0000 |
VIC-II, SID, CIAs, colour RAM: what a C64 program expects |
| C65 | $FFD1000 |
Adds the VIC-III registers, including $D030, and the
C65 DMA controller |
| MEGA65 with Ethernet | $FFD2000 |
MEGA65 registers, with the Ethernet buffers in place of part of the I/O area |
| MEGA65 | $FFD3000 |
Full VIC-IV register set and all MEGA65 devices |
The split exists for compatibility. Many C64 programs write to I/O addresses that are unused on a real C64, sometimes deliberately and sometimes by accident. If those addresses held live VIC-IV registers, such programs would scramble the display. In the C64 personality those addresses do what they did on a C64, which is usually nothing.
The personality is changed by writing a two-byte "knock" sequence to
$D02F, a register that does nothing on a real VIC-II. The
same register exists in every personality, so the knock always
works:
| Personality wanted | Bytes to write to $D02F |
|---|---|
| VIC-III (C65) | $A5, then $96 |
| VIC-IV (MEGA65) | $47, then $53 |
| MEGA65 with Ethernet | $45, then $54 |
| VIC-II (C64) | Any other value, such as $00 |
To unlock the VIC-IV from machine language:
lda #$47
sta $d02f
lda #$53
sta $d02f
The VIC-IV bytes are the ASCII codes for "GS", and the Ethernet bytes spell "ET". MEGA65 mode starts with the VIC-IV personality already selected, so most MEGA65 programs never need the knock. It matters when you write code that starts in C64 mode, or when you need to be certain of the state after other software has run.
The personality affects only what the CPU sees at
$D000-$DFFF. It does not change the video hardware itself,
and a flat 32-bit pointer can reach any personality's registers at its
28-bit address regardless of which one is selected.
Underneath the KERNAL runs a small, privileged program called the
Hypervisor, often known by the name of its implementation, Hyppo. It is
the first code to run at power-on, and it remains resident afterwards in
its own 16KB of memory, which the CPU maps in at
$8000-$BFFF only while in hypervisor mode and which
ordinary programs cannot reach (MEGA65 Book, p. K-3). Only the serial
monitor and debugger interface can still read it, through its 28-bit
address.
The idea comes from virtual machines on larger computers. The Hypervisor runs in a special CPU mode with access to everything, while the KERNAL, BASIC and user programs run in a normal mode that cannot see it. When something needs the Hypervisor's help, the CPU switches into hypervisor mode, the Hypervisor does its work, and control returns as though nothing had happened.
A program asks for a Hypervisor service by writing to one of the trap
registers at $D640-$D67F. The write switches the CPU into
hypervisor mode and jumps to the matching handler. The service number
must be in A, and the trap is followed by a CLV instruction, because the
CPU may or may not execute the byte after the trap when the service
returns. On return, the carry flag is set if the call succeeded; if it
is clear, A holds an error code (MEGA65 Book, pp. N-4, N-5).
For example, this asks the Hypervisor for its version number using
the general DOS trap at $D640:
lda #$00 ; function: get version
sta $d640 ; trap into the Hypervisor
clv
bcc failed ; carry clear means the call failed
The trap registers exist only in the MEGA65 I/O personalities, the
normal one and the Ethernet one (mega65-core, gs4510.vhdl). Chapter
11 lists every Hypervisor function, and Chapter 14 shows how to use the
file services to read and write SD card files directly.
Because the Hypervisor saves and restores the complete machine state, a trap is safe to use from almost anywhere, including from inside a program that has remapped memory heavily. That same property is what makes the Freezer possible.
DMAgic is the MEGA65's DMA controller: hardware that copies and fills blocks of memory far faster than the CPU can, at 40.5MB per second for fills and 20.25MB per second for copies (MEGA65 Book, p. P-3). It was designed for the C65; on the MEGA65 it is built into the CPU, and it reaches the full 28-bit address space.
DMAgic does not take commands through registers alone. Instead the program builds a small data structure in memory, called a DMA list, describing the job, then tells DMAgic where the list is. Writing the final byte of the list's address starts the job, and the CPU pauses until it finishes.
| Register | Purpose |
|---|---|
$D700 |
List address, bits 7-0. Writing here starts a job in the original C65 list format |
$D701 |
List address, bits 15-8 |
$D702 |
List address, bits 22-16: the bank holding the list |
$D703 |
Bit 0 selects the F018B list format instead of F018A |
$D704 |
List address, bits 27-20: the megabyte holding the list |
$D705 |
List address, bits 7-0. Writing here starts an enhanced job |
An enhanced job is the normal choice on the MEGA65. Its list begins
with option bytes, ended by $00, that extend what the job
can do. The most useful options are:
| Option | Meaning |
|---|---|
$0A |
The list that follows uses the F018A format |
$0B |
The list that follows uses the F018B format |
$80 nn |
Source address is in megabyte nn |
$81 nn |
Destination address is in megabyte nn |
$00 |
End of options |
The megabyte options are what let a DMA job reach attic RAM at
$8000000 or colour RAM at $FF80000.
After the options comes the job itself. The low two bits of the
command byte choose the operation: %00 copy,
%01 mix, %10 swap, %11 fill. Only
copy and fill are implemented on the MEGA65; the swap and mix codes are
defined by the F018 format but do nothing useful (MEGA65 Book, pp. P-6,
P-24). A copy needs a source; a fill uses the low byte of the source
address as the fill value.
This example clears an 80 by 25 text screen at $0800 in
bank 0 by filling its 2,000 bytes with spaces:
lda #$00
sta $d702 ; list is in bank 0
sta $d704 ; list is in megabyte 0
lda #>list
sta $d701
lda #<list
sta $d705 ; start the enhanced job
rts
list .byte $0a ; option: F018A format
.byte $00 ; end of options
.byte $03 ; command: fill
.word 2000 ; byte count
.word $0020 ; source: fill value $20 in the low byte
.byte $00 ; source bank
.word $0800 ; destination address
.byte $00 ; destination bank
.word $0000 ; modulo, unused here
The same job in a loop of STA instructions would take thousands of cycles. DMAgic does it in a small fraction of that time, and BASIC 65 exposes the controller directly through its EDMA command. Chapter 8 covers chained jobs, modulo and skip options, and copying to and from attic RAM.
The 6502 family has no multiply or divide instructions, and generations of programmers have written shift-and-add routines to make up for it. The MEGA65 instead provides a hardware multiplier and divider in the I/O area. You write two numbers into input registers and read the results back from output registers.
Both units work on unsigned 32-bit inputs and share the same pair of inputs, so a single setup gives you both the product and the quotient.
| Register | Size | Contents |
|---|---|---|
$D770-$D773 |
32 bits | MULTINA: first input |
$D774-$D777 |
32 bits | MULTINB: second input |
$D778-$D77F |
64 bits | MULTOUT: MULTINA × MULTINB |
$D768-$D76B |
32 bits | DIVOUT fraction: fractional part of MULTINA ÷ MULTINB |
$D76C-$D76F |
32 bits | DIVOUT whole: integer part of MULTINA ÷ MULTINB |
$D70F |
8 bits | Status: bit 7 set while the divider is busy. Bit 6 (MULBUSY) is defined but currently never set, because the multiplier finishes in one cycle. Programs should still test it before reading MULTOUT, in case a future or slower implementation needs it |
All values are little-endian, low byte first, as everywhere else on the 6502 family.
The multiplier is effectively instantaneous: by the time the CPU can read MULTOUT after writing an input, the product is ready. This multiplies two 16-bit values:
lda #<1000
sta $d770
lda #>1000
sta $d771
lda #<300
sta $d774
lda #>300
sta $d775
lda #$00
sta $d772 ; clear the upper input bytes
sta $d773
sta $d776
sta $d777
lda $d778 ; product 300000 is now in $d778-$d77b
Division takes a few cycles longer. Wait for bit 7 of
$D70F to clear before reading DIVOUT:
wait bit $d70f
bmi wait ; bit 7 set: divider still busy
lda $d76c ; integer part of the quotient
The fractional part makes the divider useful for fixed-point arithmetic, such as computing line slopes or scaling factors in graphics routines, without any floating-point code. If you need signed results, handle the sign yourself before and after, just as you would with a software routine.
BASIC 65 programs can reach the accelerator with POKE and PEEK, but the real benefit is in machine language, where it replaces some of the most frequently written subroutines in 6502 programming with a handful of loads and stores.
The MEGA65 is easiest to understand as layers, built up from the C64 through the C65 to the MEGA65, and all still active:
$D030 and the C64 port at $01 decide
unmapped blocks; MAP takes precedence for the blocks it maps. Flat
32-bit addressing bypasses all three.$D000-$DFFF, selected by a knock sequence written to
$D02F.$D640-$D67F, and
runs the Freezer.With this model in mind, the rest of the book can go into detail. Chapter 6 looks at how BASIC 65 uses this memory, and Chapters 7 to 10 turn to machine language on the 45GS02.