Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor
A MEGA65 program does not have to be written entirely in one language. BASIC 65 is excellent at the parts of a program that change often and run rarely: menus, setup, file handling, scoring, game logic that runs once a frame. Machine language is excellent at the parts that run constantly or must be exact: drawing, collision checks, sorting, decompression, interrupt handlers. Writing each part in the language that suits it gives a program that is both quick to develop and quick to run.
The hard part is the boundary between them. This chapter covers everything that happens at that boundary:
Chapter 6 introduced several of the BASIC 65 features used here: BANK, SYS, RREG, BLOAD and POINTER. This chapter puts them to work and ends with a complete program that uses each of them.
Throughout, the machine-language routines are cooperative in the sense of Chapter 8: they are called by SYS, they run with the SYS mapping, and they restore everything they change before returning with RTS.
On the C64 the usual answer was $C000, a 4KB block BASIC
never touches. The MEGA65 has no single equivalent, but it has several
good places, each with a different trade-off.
Where code can live depends on what SYS can reach. SYS builds a fixed memory map for each call, and only part of it follows the bank chosen with BANK or a long address (MEGA65 Book, pp. 14-18, 14-43):
| CPU address | During SYS |
|---|---|
$0000-$1FFF |
Always bank 0 |
$2000-$7FFF |
The selected bank, 0 to 5 |
$8000-$BFFF |
Always bank 0 |
$C000-$FFFF |
ROM and I/O, in every bank |
A long address does the same job as BANK: SYS $47000
calls $7000 in bank 4. SYS cannot reach attic RAM at all.
With that in mind, the main places for machine code are:
| Location | Size | Called with | Notes |
|---|---|---|---|
Bank 0, $1600-$1EFF |
2.25KB | SYS $1600 from any bank |
The Book's recommended home for routines; also the only range usable
with SYS TO ... I1 in ROMs 920414 to 920418 |
| After the BASIC program in bank 0 | Varies | SYS with its address |
Moves if the program is edited; ideal for single-file programs |
Banks 4 and 5, $x2000-$x7FFF |
24KB per bank | SYS $42000, or BANK and SYS |
Reserve banks 4 and 5 with MEM; shared with BASIC's graphics |
| Anywhere in the first 384KB | Any | SYS TO with an O offset |
Code must expect to run at its mapped address |
| Attic RAM | 8MB | Not directly | Storage only: copy into chip RAM to run |
The area from $1600 to $1EFF lies below the
start of BASIC text, and during SYS it is always visible, whatever bank
BASIC has selected. The Book recommends it for machine-code subroutines
called from BASIC (MEGA65 Book, pp. 14-18, 14-19). In ROMs 920414 to
920418 it was also the only range usable with SYS TO's BASIC-active
mode, described later; from 920419 that mode works anywhere SYS can
reach (MEGA65 ROM change log). The
MEGA65 team's own unit-test module for BASIC programs loads here too (MEGA65
Developer Guide).
It is a shared resource rather than a reserved one. Resident tools such as on-device assemblers may claim part of it, so a program that will be used alongside other software should keep its footprint small or use another bank.
Machine code can be appended to the end of a BASIC program in bank 0 and saved with it as one file. This is a common way to build a single-file program, and the next section shows how. The only limitation is that the code moves if the BASIC program above it changes length, so it suits finished programs better than ones still being edited.
For anything larger, banks 4 and 5 offer 24KB each in the
$2000-$7FFF window that SYS maps from the selected bank.
Banks 1 to 3 are not available: bank 1 holds BASIC's arrays and strings,
and banks 2 and 3 hold the ROM (MEGA65 Book, memory chapter). Banks 4
and 5 are reserved with MEM as described in
Chapter 6 so BASIC's graphics commands leave them alone. Code at
$42000 is called with SYS $42000, or with
BANK 4 : SYS $2000. Code at $40000-$41FFF or
$48000-$4FFFF cannot be called with plain SYS, because
those offsets fall outside the bank's window; $8000-$BFFF
during SYS is always bank 0.
Bank 0 itself can hold code above the BASIC program up to
$BFFF, provided it stays clear of the end of the program
text and BASIC's work areas (MEGA65 Book, p. B-256).
Code cannot run from attic RAM through SYS, but attic RAM is a good place to keep many routines or overlays that are copied into chip RAM when needed. A single EDMA command moves an overlay into place faster than BASIC could PEEK a few bytes.
A program written entirely or mostly in machine language should still load and run like a BASIC program, so the user can type RUN and nothing else. The standard way is to begin the file with a one-line BASIC program whose only job is to call the machine code that follows it. This is called a BASIC header, or a BASIC stub.
The line we want is:
10 BANK0:SYS$2012
The BANK 0 makes the SYS address refer to bank 0
whatever BANK setting the user left behind. Early MEGA65 ROMs required
it before a SYS like this, and the Book recommends it as best practice
on all versions (MEGA65 Book, p. 14-18). $2012 is the
address immediately after the header, worked out below. The line has no
spaces, because spaces typed in a line are stored too, one byte each,
and would move the address.
Using the program format from Chapter 6, the line becomes:
| Address | Bytes | Meaning |
|---|---|---|
$2001 |
$10 $20 |
Link: the next line starts at $2010 |
$2003 |
$0A $00 |
Line number 10 |
$2005 |
$FE $02 |
BANK token |
$2007 |
$30 $3A |
"0:" |
$2009 |
$9E |
SYS token |
$200A |
$24 $32 $30 $31 $32 |
"$2012" |
$200F |
$00 |
End of line |
$2010 |
$00 $00 |
End of program |
$2012 |
First byte of machine code |
In assembler form:
* = $2001
.word next ; link to the next line
.word 10 ; line number
.byte $fe, $02 ; BANK
.text "0:"
.byte $9e ; SYS
.text "$2012" ; entry point, as text
.byte 0 ; end of line
next .word 0 ; end of program
start ; machine code begins here, at $2012
The address in the SYS is text, so the assembler cannot fill it in
automatically unless it can convert a number to hex digits; check your
assembler's documentation, which may also offer a ready-made header
macro. Writing the
address in hex rather than decimal makes the arithmetic easy either way:
four digits, always five characters with the $.
The assembler writes a PRG file whose first two bytes are the load
address, $2001. On the MEGA65 that file loads and lists
like any BASIC program:
LOAD "MYPROG"
LIST
10 BANK0:SYS$2012
RUN
The header must be built for $2001. LOAD and DLOAD
always put a BASIC program at $2001, whatever load address
the file has, so a header built for another address, such as the C64's
$0801, would point its SYS at the wrong place (MEGA65 ROM
source, LOAD). C64 programs are run in GO64 mode instead.
When the machine code returns with RTS, BASIC carries on with the rest of line 10 and then finds the end of the program. Further BASIC lines can be added between the header and the code, as the worked example at the end of this chapter does, provided the SYS address is updated to match.
A BASIC program that uses machine-language helpers needs a way to put them in memory before calling them. There are three common methods.
| Method | Best for | Drawback |
|---|---|---|
| BLOAD from disk | Routines of any size, data, graphics | A second file to keep with the program |
| DATA statements and POKE | Small routines in a self-contained listing | Slow to load, bulky, easy to mistype |
| Appended to the program | Finished single-file programs | Code moves if the BASIC part changes |
BLOAD loads a file into memory without disturbing the BASIC program in memory. With B and P it loads to a given bank and address; without P it uses the load address stored in the file:
10 BLOAD "SPRITES.BIN", B4, P8192 : REM TO $42000
20 BLOAD "HELPERS" : REM TO ITS OWN ADDRESS
Because BLOAD does not restart the program, it can be called from inside a running program, which is the usual way to load overlays or level data. A missing file stops the program with an error unless TRAP is set, as in Chapter 6; check DS for other drive problems.
BOOT combines BLOAD and SYS: it loads a PRG file and starts executing at its load address. With no file name it runs AUTOBOOT.C65, the MEGA65's auto-start file (MEGA65 Book, p. B-31):
BOOT "HELPERS" : REM LOAD AND RUN
BLOAD normally treats the first two bytes of a file as its load
address. The R option reads every byte as data instead, and a
,S suffix on the name lets BLOAD read a sequential file,
which is how data written by other programs or tools can be loaded
(MEGA65 Book, p. B-29):
BLOAD "USER DATA,S", P($1600), R : REM RAW SEQ FILE TO $1600
BLOAD "TABLES", P($8000000), R : REM INTO ATTIC RAM
An address above $FFFF puts BLOAD into flat-address
mode, which reaches attic RAM; the older form with a 16-bit P address
and a B bank cannot.
For a short routine in a printed listing, DATA statements remain the most portable method: the whole program is one listing that can be typed in.
10 FOR I=0 TO 8
20 READ B : POKE $1600+I, B : C=C+B
30 NEXT
40 IF C<>1396 THEN PRINT "DATA ERROR" : STOP
50 SYS $1600
60 END
100 DATA 162,0,238,32,208,202,208,250,96
The routine cycles the border colour 256 times in 9 bytes:
LDX #0, INC $D020, DEX,
BNE back to the INC, RTS. The checksum in line
40 catches typing mistakes before they crash the machine, a practice
worth keeping for any DATA loader.
When the BASIC header technique is extended with more BASIC lines, the machine code sits after the last line in the same file. A single LOAD brings in both parts, and the program needs no other files. The cost is that the SYS address must be updated whenever the BASIC part changes length, so this method suits programs built by an assembler from source, where the address is computed automatically.
A program with many routines can load them all once into attic RAM, then copy each into its working area with EDMA as needed. Because EDMA is so fast, swapping an overlay takes less time than a single screen refresh.
SYS is the bridge from BASIC into machine language. Its full form loads the registers before the call:
SYS address, a, x, y, z, s
Every argument after the address is optional and is placed in the
matching register: A, X, Y, Z and the status register. Each must be 0 to
255. Arguments can be skipped with empty commas, so
SYS $1600,,5 sets only X.
There are four ways to pass information to a routine, suited to different amounts and kinds of data.
| Method | Capacity | Good for |
|---|---|---|
| SYS registers | 4 bytes | Small numbers, flags, a 28-bit address |
| Parameter block | Any size | Several values, words, settings |
| POINTER | Any variable or array | Large data shared in place |
| String descriptors | Any string | Text |
With plain SYS, a register you don't give a value isn't set to zero:
it gets whatever was left in its save location, usually the result of
the previous SYS, and the same goes for the status register. SYS TO sets
every register you leave out to zero (MEGA65 ROM source, SYS). Pass
every register your routine depends on. During the call, SYS
switches BASIC's ROM out of $2000-$BFFF and tells the
KERNAL's interrupt handler to skip BASIC's interrupt-driven features,
such as PLAY music and MOVSPR sprite movement. They resume when the
routine returns with RTS (MEGA65 Book, pp. B-256, 14-44). A routine
called often while music plays can therefore make the music stutter.
SYS TO is a second form of SYS with named arguments: A,
X, Y, Z and S for
registers, plus two extra abilities (MEGA65 Book, pp. B-256, 14-18,
14-19):
| Argument | Effect |
|---|---|
A(n) ... S(n) |
Load a register; hexadecimal values go in parentheses, as in
X($FF) |
I1 |
Keep BASIC active during the call, so PLAY and MOVSPR continue. In
ROMs 920414 to 920418 the routine had to lie in
$1600-$1EFF; from 920419 it can be anywhere |
O(offset) |
Add offset × 256 to addresses $2000-$7FFF, so code
anywhere in the first 384KB can run |
I and O cannot be used together, because both rely on the MAP register. Some examples:
SYS TO $1600, X($FF), A(5) : REM REGISTERS BY NAME
SYS TO $1600, I1 : REM MUSIC KEEPS PLAYING
SYS TO $6100, O($520) : REM RUNS CODE AT $58100
In the last line, $520 × 256 is $52000,
which maps $2000-$7FFF to $54000-$59FFF; the
routine at $58100 runs at CPU address $6100.
Code run this way must be written for the address it runs at, since its
JMP and JSR targets are CPU addresses, or use only relative
branches.
Register passing is the fastest and simplest. A routine that fills a row of the screen with a character might take the character in A and the row in X:
10 BANK 0
20 SYS $1600, 160, 12 : REM FILL ROW 12 WITH SOLID BLOCKS
The routine sees A = 160 and X = 12 at its first instruction, exactly as if another machine-language routine had loaded them.
When four bytes are not enough, BASIC can POKE parameters into a small block of memory the routine knows about, then call it. The block is best placed at the start of the routine's own reserved area, so the address is fixed and documented:
10 PB=$1600 : REM PARAMETER BLOCK
20 POKE PB, X AND 255 : POKE PB+1, X/256
30 POKE PB+2, Y : POKE PB+3, COL
40 SYS $1610 : REM ROUTINE FOLLOWS THE BLOCK
This also works in reverse: the routine writes results into the block and BASIC PEEKs them back.
For large amounts of data, copying is wasteful. POINTER gives the
routine the address of a variable or array element, and the routine
works on the data where it lies. Because POINTER returns a flat address,
split it across the registers and let the routine use
[zp],Z addressing:
10 DIM D&(999)
20 P=POINTER(D&(0))
30 SYS $1600, MOD(P,256), INT(P/256) AND 255, INT(P/65536) AND 255, INT(P/16777216)
The low byte uses MOD because an array's address is at least
$10000, and AND accepts only values up to 65,535. The
routine stores A, X, Y and Z into four bytes of the base page,
then reads or writes [zp],Z. Byte arrays are the easiest
type to share this way, because each element is exactly one byte and the
elements are stored one after another.
POINTER applied to a string variable returns the address of its descriptor, not the text itself. Following the Commodore convention, the descriptor holds the length in one byte followed by the address of the text, and the text of a BASIC 65 string is in bank 1. A routine therefore reads the length and address from the descriptor, then reads the characters from bank 1 with a flat pointer.
The Book documents this layout as part of POINTER, so it can be relied on: three bytes, the length followed by the low and high bytes of the text's address as an offset in bank 1. The text itself can move whenever garbage collection runs, so a routine should take the address from the descriptor at the moment it is called rather than keep it (MEGA65 Book, p. B-193). Chapter 6 shows the same descriptor read from BASIC.
The same methods work in the other direction. Which to use depends again on how much comes back.
When a routine returns with RTS, BASIC saves the registers as they were at that moment. RREG copies them into BASIC variables:
RREG a, x, y, z, s
The names after RREG are ordinary BASIC variables, not the registers themselves, so any names will do. Using names such as RA, RX and RY avoids overwriting variables called A, X or Y that the program uses for other things. Like SYS, RREG lets you skip positions with empty commas.
A routine that returns a 16-bit result in X and Y, low byte first, can be read like this:
10 SYS $1600
20 RREG , RX, RY
30 R = RX + 256*RY
The status register comes back as the fifth RREG value, which gives a routine a convenient way to report errors. The carry flag, bit 0, is the usual choice, as the Hypervisor does: set it for success and clear it for failure, or the other way round, as long as routine and caller agree.
10 SYS $1600
20 RREG ,,,, RS
30 IF (RS AND 1)=0 THEN PRINT "FAILED" : STOP
Avoid the tempting name ST: it is BASIC's reserved I/O status variable, and assigning to it is an error.
For more than a few bytes, the routine writes into memory and BASIC reads it. A parameter block works in both directions, and BASIC reads it with PEEK. For bulk results, BASIC can pass the POINTER address of a byte array, and the routine fills the array in place. The results then appear in the array with no PEEKs at all:
10 DIM R&(255)
20 P=POINTER(R&(0))
30 SYS $1600, MOD(P,256), INT(P/256) AND 255, INT(P/65536) AND 255, INT(P/16777216)
40 FOR I=0 TO 255 : PRINT R&(I); : NEXT
Writing directly into floating-point or integer variables is possible too, but it means encoding values in BASIC's internal formats. Byte arrays avoid that: the value the routine stores is exactly the value BASIC reads.
SYS is a statement: it does something but cannot appear inside an expression. USR is a function, so a machine-language routine called through it can take part in a calculation:
Y = 10 + USR(X) * 2
When BASIC evaluates USR, it evaluates the argument, then jumps
through the USR vector to a machine-language routine, and uses whatever
the routine leaves behind as the function's value. The vector is the two
bytes at $02F8, low byte first, read in the system bank;
the Book's POKE entry sets it to a routine at $1800 (MEGA65
Book, p. B-194):
10 BANK 128
20 POKE $02F8, 0, 24 : REM USR VECTOR = $1800
POKE writes successive bytes from a list, so one statement sets both
halves of the vector; WPOKE $02F8,$1800 does the same.
BASIC 65's USR accepts string arguments as well as numbers. The
MEGA65 team's own unit-test module uses this: after it is loaded at
$1600 and initialised with SYS, calls such as
A=USR("P") send commands to a testing host, with the first
character choosing the operation (MEGA65
Developer Guide). That pattern, one vector serving many commands
distinguished by the argument, is a neat way to give BASIC a small
family of new functions without patching the interpreter.
USR's convenience comes at a price. The argument arrives in BASIC's floating-point accumulator, not in a register, and the result must be left there too. A routine must therefore call BASIC's own conversion routines to turn the argument into an integer and to turn its result back into floating point, and it must remove a string argument from BASIC's temporary string area when it is done. Those routines are internal to BASIC 65, and their addresses can change between ROM versions.
For that reason this book recommends SYS with RREG for most work. Reserve USR for cases where calling machine code inside an expression genuinely simplifies the BASIC program, and keep the conversion calls in one place so they are easy to update if the ROM changes.
This example brings the chapter's techniques together. A machine-language routine adds up every element of a BASIC byte array and returns the 24-bit total. BASIC passes the array's address with POINTER and the registers, passes the element count in a parameter block, and reads the result from the same block. The routine reports success through the carry flag.
| Address | Contents |
|---|---|
$1600-$1601 |
count: number of elements, set by BASIC |
$1602-$1604 |
result: 24-bit sum, set by the routine |
$1610 |
Entry point of the routine |
$1700-$17FF |
The routine's private base page |
The private base page is a 45GS02 technique worth noting. Rather than guess which zero-page locations BASIC 65 leaves free, the routine points B at a page of its own, uses it freely, and sets B back to zero before returning.
* = $1600
count .word 0 ; set by BASIC
result .byte 0, 0, 0 ; returned to BASIC
* = $1610
ptr = $00 ; offset in the private base page
workpg = $17 ; private base page at $1700
sum pha ; save A: low byte of the array address
lda #workpg
tab ; base page = $1700
pla
sta ptr ; store the flat address of the array
stx ptr+1
sty ptr+2
stz ptr+3 ; STZ stores the Z register
ldz #$00
lda #$00
sta result
sta result+1
sta result+2
loop lda count ; finished when count reaches zero
ora count+1
beq done
clc
lda [ptr],z ; next element of the array
adc result
sta result
bcc next
inc result+1
bne next
inc result+2
next inw ptr ; advance the pointer
lda count ; count = count - 1
bne nodec
dec count+1
nodec dec count
bra loop
done lda #$00
tab ; restore base page 0 for BASIC
sec ; report success
rts
INW advances only the low 16 bits of the pointer, so the routine assumes the array does not cross a 64KB boundary. That holds for arrays of normal size; a routine meant for very large arrays would advance all four bytes, for instance with LDQ, INQ and STQ, saving Z around them since it is part of Q.
10 BANK 0
20 BLOAD "SUMBYTES", B0, P5632 : REM LOAD TO $1600
30 DIM D&(999)
40 FOR I=0 TO 999 : D&(I)=INT(RND(1)*256) : NEXT
50 P=POINTER(D&(0))
60 POKE $1600, 1000 AND 255 : POKE $1601, INT(1000/256)
70 SYS $1610, MOD(P,256), INT(P/256) AND 255, INT(P/65536) AND 255, INT(P/16777216)
80 RREG ,,,, RS
90 IF (RS AND 1)=0 THEN PRINT "FAILED" : STOP
100 S=PEEK($1602)+256*PEEK($1603)+65536*PEEK($1604)
110 PRINT "SUM:"; S
120 T=0 : FOR I=0 TO 999 : T=T+D&(I) : NEXT
130 PRINT "CHECK:"; T
140 BANK 128
Lines 120 and 130 compute the same total in BASIC so the two can be compared, and the difference in speed is plain to see. The machine-language routine finishes before the screen has time to redraw, while the BASIC loop takes a noticeable moment even at 40.5 MHz.
| Technique | Where |
|---|---|
| Code placed in bank 0 below BASIC | $1600, called with SYS from any bank |
| BLOAD to a fixed address | Line 20 |
| Parameters in registers | The flat address in A, X, Y and Z, line 70 |
| Parameter block | count and result, lines 60 and 100 |
| POINTER and flat addressing | Line 50 and [ptr],z in the routine |
| Status through RREG | Lines 80 and 90 |
| Private base page | TAB in the routine |
$0000-$1FFF and
$8000-$BFFF are always bank 0, $2000-$7FFF
comes from the selected bank, and $C000-$FFFF is ROM and
I/O. Attic RAM is out of reach.$1600-$1EFF is the
Book's recommended home for routines; code appended to the program suits
single-file releases; banks 4 and 5 hold up to 24KB each in their
$2000-$7FFF window; attic RAM stores overlays.10 BANK0:SYS$2012 at
$2001 makes machine code load and run like a BASIC program;
LOAD always puts a BASIC program at $2001.SYS TO ... I1 keeps them running, and
SYS TO ... O() reaches code anywhere in the first
384KB.$02F8. USR is
useful inside expressions, but it depends on BASIC's internal conversion
routines, so SYS with RREG is the safer default.Chapter 10 is the complete reference to the 45GS02 instruction set.