Programming the MEGA65 — Chapter 5: MEGA65 Architecture

Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor

Introduction

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 Processor

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.

Registers

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.

Speed

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.

The 28-Bit Address Space

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.

The memory map

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.

What the CPU sees

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.

Kinds of Memory

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

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

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).

Colour RAM

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.

ROM

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.

Banking

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).

The C64 CPU port

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.

The $D030 ROM bits

The 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).

The MAP instruction

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.

Flat 32-bit addressing

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.

I/O Personalities

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 knock

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.

The Hypervisor

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.

What it does

Hypervisor traps

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

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.

The registers

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.

A DMA list

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 Math Accelerator

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.

Multiplying

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

Dividing

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.

Summary

The MEGA65 is easiest to understand as layers, built up from the C64 through the C65 to the MEGA65, and all still active:

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.