Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor
Chapter 4 treated BASIC 65 as a language: how to structure programs, debug them and make them run well. This chapter treats it as a program, which is what it really is. BASIC 65 is a large piece of machine language in ROM that reads your program from memory, keeps your variables in memory, and calls the KERNAL to do its input and output.
Knowing how it does this pays off in several ways. You can read a program's tokens directly when a listing is damaged or a file will not load. You can find a variable's value from machine language and pass data between BASIC and ML without PEEKs and POKEs through a single byte. You can place machine code, data tables and graphics where BASIC will not overwrite them. And you can use the parts of BASIC 65 that reach beyond the 64KB window, which are some of the most useful in the language.
This chapter covers:
The details here follow the MEGA65 ROM as distributed with current systems. BASIC 65 has changed considerably across ROM versions, and where a feature arrived in a particular version, that is noted. A program that depends on these internals should check the ROM version before relying on them.
On the C64, BASIC squeezes everything into one area of about 38KB: the program first, then variables, then arrays, with strings growing down from the top. A long program leaves little room for data, and a large array leaves little room for the program.
BASIC 65 spreads the same structures across two banks, so program text and data no longer compete:
| What | Where | Notes |
|---|---|---|
| Program text | Bank 0, $2001-$F6FF |
About 54KB for the program alone |
| Scalar variables | Bank 0 | Simple variables such as A, B%,
C$ (the descriptor, not the text) |
| Arrays | Bank 1 | All array elements |
| String text | Bank 1 | The characters of every string, for both scalars and arrays |
| Text screen | Bank 0 | 80 by 25 screen at $0800 |
| Bitmap graphics | Banks 4 and 5 | Used by the graphics commands covered in Chapter 12 |
| Attic RAM | $8000000 upward |
Not used by BASIC 65; free for programs |
The arrangement has practical consequences. A program can be up to about 54KB long regardless of how much data it uses, and arrays and strings share most of bank 1 between them. The split also explains why a pointer to a variable is not enough on its own: you also need to know which bank it is in. The POINTER function, described below, solves this by returning a full flat address.
Banks 4 and 5 belong to the graphics system, but a program that does not use all of them can claim parts for its own data or machine code. The MEM command, added in ROM 920273, reserves memory in those banks:
MEM bank4mask, bank5mask
Each argument is a byte whose eight bits stand for the eight 8KB
blocks of that bank. A set bit reserves the block so the graphics
commands will not use it. For example, MEM 1,0 reserves
$40000-$41FFF.
Attic RAM needs no reservation, since BASIC 65 does not touch it. It is the natural home for large tables, level data, or anything loaded once and copied into chip RAM as needed. Bear in mind that some resident utilities do use attic RAM, and that early development boards lack it entirely.
FRE reports more than free space on the MEGA65. FRE(0)
and FRE(1) give the free bytes in banks 0 and 1,
FRE(4) and FRE(5), from ROM 920415, return
the reservation bitfields for banks 4 and 5 in the same format as MEM
(MEGA65 ROM change log), and
FRE(-1) returns the ROM version (MEGA65 Book, p. B-117).
This program reserves 16KB of bank 4 and confirms it:
10 MEM 3,0 : REM RESERVE $40000-$43FFF
20 PRINT FRE(0);"BYTES FREE FOR PROGRAM"
30 PRINT FRE(1);"BYTES FREE FOR VARIABLES"
40 PRINT "BANK 4 RESERVED BLOCKS: ";HEX$(FRE(4))
50 PRINT "ROM VERSION";FRE(-1)
Line 40 shows at least 0003 (HEX$ always gives four
digits), the two blocks reserved in
line 10; other bits may be set if the system is using parts of bank 4.
In 80 by 50 text mode, for example, the first 8KB of bank 4 always holds
screen data, whether or not MEM reserved it (MEGA65 Book, p. B-164).
FRE(-1) is the simplest way for a program to check that it
is running on a ROM new enough for the features it uses.
A BASIC 65 program is stored in bank 0 from $2001 as a
chain of lines, in the same format Commodore has used since the PET.
Each line is laid out like this:
| Bytes | Contents |
|---|---|
| 2 | Link: the address of the next line, low byte first |
| 2 | Line number, low byte first |
| varies | The line's text, with keywords replaced by tokens |
| 1 | $00, marking the end of the line |
The program ends with a link of $0000. BASIC follows the
links to find lines quickly, without scanning every byte, which is why a
corrupted link makes the rest of the program vanish from a listing even
though the bytes are still in memory.
Keywords are not stored as text. When you type a line, BASIC replaces each keyword with a token, a byte with its high bit set. This saves space and saves BASIC from recognising keywords every time a line runs. LIST reverses the process.
The C64's single-byte tokens still mean what they did:
$80 is END and $9E is SYS, for example. BASIC
65 has far more keywords than there are single-byte codes, so most of
its extensions use two bytes: a prefix byte followed by a code.
Extended statements use the prefix $FE, a scheme inherited
from the C128's BASIC 7.0, so BANK is stored as $FE $02 and CURSOR as $FE $41. A
small number of additional functions use a different prefix. Chapter 3
gives each keyword's token alongside its description.
Everything that is not a keyword is stored as PETSCII text, including
numbers. The constant 3000 occupies four bytes, and is
converted to a number each time the line runs. Hexadecimal constants
such as $3000 are stored the same way, as five
characters.
The one-line program 10 BANK0:SYS$3000, typed without
spaces, is stored like this. Spaces typed in a line are stored too, one
byte each, so a version with spaces would be two bytes longer:
$2001: 10 20 link: next line at $2010
$2003: 0a 00 line number 10
$2005: fe 02 BANK
$2007: 30 "0"
$2008: 3a ":"
$2009: 9e SYS
$200a: 24 33 30 30 30 "$3000"
$200f: 00 end of line
$2010: 00 00 end of program
This format matters most when you write machine language that must
load and run like a BASIC program. The usual approach, used by most
MEGA65 assemblers' startup code, is to emit exactly these bytes at
$2001 followed by the machine code. Typing RUN then
executes the SYS. Chapter 9 builds such a header step by step.
Because every line starts with a link to the next, a program can walk its own listing. This one prints each line's number, address and the length of its tokenised text: the line's total size less two bytes of link, two of line number and one terminating zero.
10 BANK 0
20 A=$2001
30 DO
40 N=WPEEK(A) : IF N=0 THEN EXIT
50 PRINT "LINE";WPEEK(A+2);" AT $";HEX$(A);":";N-A-5;"BYTES"
60 A=N
70 LOOP
80 BANK 128
WPEEK reads a 16-bit value low byte first, which matches how both
links and line numbers are stored (MEGA65 Book, p. B-284). Adding a loop
that prints HEX$(PEEK(I)) for each byte of a line shows its
tokens directly, which is a quick way to look up a token that Chapter 3
does not list.
BASIC 65 keeps the classic Commodore approach to variables: each scalar variable has a short entry holding its name and either its value or, for a string, a descriptor pointing to its text. What has changed is where those entries live and how quickly BASIC finds them.
| Suffix | Type | Example | Value |
|---|---|---|---|
| none | Floating point | A, XY |
5-byte Commodore floating-point number |
% |
Integer | B% |
Signed 16-bit integer, -32768 to 32767 |
$ |
String | C$ |
Descriptor: length and address of the text in bank 1 |
& |
Byte | D&(10) |
Unsigned 8-bit value, 0 to 255 |
As on the C64, only the first two characters of a name are
significant, so PX1 and PX2 are the same
variable. A name must also not contain a keyword: TOTAL
and TOP both begin with TO and cannot be used at all. Byte variables are new to BASIC 65 and are most useful in
arrays, where they take a fifth of the space of floating-point
elements.
Two byte arrays are predefined. T@&() is mapped onto
the text screen and C@&() onto colour RAM, so
T@&(X,Y) reads or writes the character at column X, row
Y without any address arithmetic. They were added in ROM 920354.
The C64 finds a variable by searching the variable table from the
start, so a program with many variables slows down noticeably, and
variables created late are found last. Since ROM 920279, BASIC 65 stores
one-letter scalar variables such as A, B% and
C$ at an address computed from the name, so they are found
immediately with no search; from ROM 920286 this covers every one-letter
name of every type (MEGA65 ROM source history).
The practical lesson is simple: give the variables used inside your busiest loops one-letter names. Longer names still work, but they are found the old way.
The POINTER function returns the address of a variable's entry:
10 A%=1234
20 P=POINTER(A%)
30 PRINT HEX$(P)
Since ROM 920154, POINTER returns a full flat address rather than a 16-bit one, so the result can be passed directly to PEEK, POKE or a machine-language routine without working out which bank the variable is in. This is the cleanest way to share data between BASIC and machine code: BASIC calls POINTER, passes the result to the routine, and the routine reads or writes the variable in place.
String text lives in bank 1, growing downward from the top of free memory. Assigning a new value to a string variable does not reuse the old space; it allocates new space and leaves the old text behind as garbage. When free memory runs out, BASIC pauses to collect the garbage, moving live strings together and reclaiming the rest.
On the C64 that pause could last many seconds, because the collector searched every variable for each string it moved. BASIC 65 descends from the C128's BASIC 7.0, which stores a back-pointer with each string so the collector can work in a single pass. Collections still happen, but they are far shorter.
You can check free memory with FRE. FRE(0) reports free
space in bank 0, and FRE(1) reports free space in bank 1,
which is the figure that matters for strings and arrays. Calling
FRE(1) at a quiet moment, such as between levels of a game,
forces a collection when it will not be noticed.
POINTER gives the address of a variable's value. An integer is two bytes, high byte first, unlike most 16-bit values on the MEGA65 (MEGA65 ROM source; the Book's POINTER entry says the opposite); a floating-point variable is five bytes; a string's POINTER is the address of its three-byte descriptor, which holds the length and then the text's address as an offset in bank 1 (MEGA65 Book, p. B-193).
10 BANK 0 : REM SCALARS ARE IN BANK 0
20 A%=1234 : P=POINTER(A%)
30 PRINT "A% AT $";HEX$(P);":";PEEK(P);PEEK(P+1) : REM 4 210
40 N$="MEGA65" : D=POINTER(N$)
50 L=PEEK(D) : S=WPEEK(D+1) : REM LENGTH, TEXT OFFSET
60 BANK 1 : REM STRING TEXT IS IN BANK 1
70 FOR I=0 TO L-1 : PRINT CHR$(PEEK(S+I)); : NEXT : PRINT
80 BANK 128
Line 30 prints 4 and 210, because 1234 is $04D2 stored
high byte first. Lines 40 to 70 rebuild the string from its descriptor. A
string's text can move whenever garbage collection runs, so a program
should call POINTER again rather than keep an old text address. Array
element addresses, in contrast, stay fixed while the program runs.
Arrays are stored element after element, with the leftmost index
changing fastest. DIM A%(3,2) is laid out as (0,0), (1,0),
(2,0), (3,0), (0,1), (1,1) and so on, two bytes each, starting at
POINTER(A%(0,0)) (MEGA65 Book, p. B-193). This program
confirms it by printing each element's offset from the start:
10 DIM A%(3,2)
20 B=POINTER(A%(0,0))
30 FOR Y=0 TO 2 : FOR X=0 TO 3
40 PRINT "(";X;",";Y;")";POINTER(A%(X,Y))-B
50 NEXT X : NEXT Y
The offsets run 0, 2, 4, 6, 8 and so on. This order matters when machine language works through an array: walking memory in order visits A%(0,0), A%(1,0), A%(2,0) before reaching A%(0,1).
The predefined byte arrays T@& and
C@& make the text screen and its colours ordinary BASIC
data. This draws a bar of solid blocks across row 12, each block in a
different colour, without a single POKE:
10 FOR X=0 TO 79
20 T@&(X,12)=160 : REM SCREEN CODE 160: SOLID BLOCK
30 C@&(X,12)=X AND 15 : REM COLOURS 0-15 IN TURN
40 NEXT
Reading works the same way, so T@&(X,Y) tells a game
what character is at a position, which is a simple collision test.
C64 BASIC can only PEEK and POKE the 64KB the CPU sees. BASIC 65 can reach anywhere in the 28-bit address space, through a handful of commands that are worth knowing well.
BANK selects which 64KB bank PEEK, POKE, SYS, BLOAD and BSAVE address
when given a 16-bit address. BANK 0 makes
$3000 mean $03000; BANK 4 makes
it mean $43000.
The default is not bank 0. BASIC starts in a special configuration,
BANK 128, which shows the system memory layout with ROM and
I/O mapped in, much as a C64 program would expect. This is why
POKE 53280,0 changes the border colour without any BANK
command. A common mistake is to write BANK 0 early in a
program to reach RAM and then wonder why a later POKE to
$D020 does nothing: in bank 0, $D020 is just
RAM. Use BANK 128 to return to the system view.
An address above $FFFF given to PEEK or POKE is treated
as a full 28-bit address and ignores BANK entirely:
10 POKE $FF80000,1 : REM FIRST BYTE OF COLOUR RAM
20 PRINT PEEK($8000000) : REM FIRST BYTE OF ATTIC RAM
This is usually clearer than switching banks, and it combines naturally with POINTER, which returns flat addresses.
SYS calls a machine-language routine, which must end with RTS. A
16-bit address is interpreted through BANK; an address above
$FFFF is taken as a flat address and BANK is ignored.
Either way, SYS sets up a fixed mapping for the call, and only part of
it comes from the chosen bank (MEGA65 Book, p. B-256):
| CPU address | During SYS |
|---|---|
$0000-$1FFF |
Always bank 0 |
$2000-$7FFF |
The selected bank, 0 to 5 |
$8000-$BFFF |
Bank 0: BASIC's ROM is switched out for the call |
$C000-$FFFF |
ROM and I/O, as in the system view, so the KERNAL and hardware registers remain available |
The Book explains why: the KERNAL must keep its ROM at
$E000-$FFFF and its variables in $0000-$1FFF
to handle interrupts, and SYS uses the remaining MAP offset to bring the
selected bank into $2000-$7FFF (MEGA65 Book, pp. 14-43,
14-44).
So a routine in bank 4 must lie at $42000-$47FFF to be
called with BANK 4 : SYS $2000, and SYS cannot reach attic
RAM at all. For small routines, the Book recommends bank 0 from $1600 to
$1EFF, which it describes as guaranteed unused by the system; its SYS
entry suggests $1800-$1EFF.
SYS can also load the registers before the call, and RREG reads them back afterwards:
10 BANK 0
20 SYS $3000, 1, 2, 3, 4 : REM A=1, X=2, Y=3, Z=4
30 RREG A, X, Y, Z, S
Chapter 9 uses these to pass parameters between BASIC and machine language.
BLOAD and BSAVE load and save raw memory rather than programs. The B parameter selects the bank and P the start address within it:
BLOAD "LEVEL1", B4, P0
BSAVE "LEVEL1", B4, P0 TO P8192
These are the usual way to load machine code, character sets and graphics data alongside a BASIC program.
EDMA gives BASIC direct access to the DMAgic controller described in Chapter 5. It takes a command, a length, and source and destination addresses, all as flat 28-bit addresses:
EDMA 0, 8192, $8000000, $40000 : REM COPY 8KB FROM ATTIC RAM TO BANK 4
EDMA 3, 2000, 32, $800 : REM FILL 2000 BYTES OF SCREEN WITH SPACES
Command 0 copies and command 3 fills, using the low byte of the source argument as the fill value. Because the hardware does the work, an EDMA copy of thousands of bytes finishes before a FOR loop could move a few dozen. It is the fastest way for BASIC to move data between attic RAM and chip RAM.
SYS has a second form, SYS TO, with named arguments and two extra abilities (MEGA65 Book, p. B-256). Registers are set by name, with hexadecimal values in parentheses:
SYS TO $1800, A(5), X($FF)
The O argument overrides the MAP offset for $2000-$7FFF,
which lets SYS TO run code anywhere in the first 384KB rather than only
in the window above. The value is multiplied by 256 and added to the CPU
address. To run a routine stored at $54000, in bank 5
beyond the reach of ordinary SYS:
SYS TO $2000, O($520) : REM $2000 + $52000 = $54000
The I argument, I(1), keeps BASIC's interrupt-driven
features running during the call, so PLAY music and MOVSPR movement
continue. I and O cannot be combined. SYS TO arrived in ROM 920414. In
920414 to 920418, a routine called with I had to lie between
$1600 and $1EFF; from 920419 it can be
anywhere SYS can reach (MEGA65 ROM change log).
EDMA moves memory far faster than any FOR loop. This program copies
the 80 by 25 screen at $0800 (2048) to attic RAM, clears
it, and puts it back, following the Book's own examples (MEGA65 Book, p.
B-91):
10 EDMA 0, 80*25, 2048, $8000000 : REM COPY SCREEN TO ATTIC RAM
20 EDMA 3, 80*25, 32, 2048 : REM FILL SCREEN WITH SPACES
30 PRINT "PRESS A KEY"
40 GETKEY A$
50 EDMA 0, 80*25, $8000000, 2048 : REM COPY IT BACK
A length of 0 means 65,536 bytes. EDMA uses the F018B job format, in
which bit 4 of the command runs the source backwards and bit 5 the
destination. An overlapping copy to a higher address needs both, so add
48, and give the last byte of each range instead of the first. Adding
only 32 reverses the destination alone, which mirrors the data (MEGA65
ROM source, EDMA; mega65-core,
gs4510.vhdl). This shifts the top row of the screen one
character to the right:
10 EDMA 48, 79, 2126, 2127 : REM BACKWARDS COPY, SAFE FOR OVERLAP
BLOAD and BSAVE switch to flat addressing when given an address above
$FFFF, which lets them reach attic RAM and save more than
64KB at once. The R option reads or writes raw data with no two-byte
load address (MEGA65 Book, pp. B-29, B-35):
10 BLOAD "LEVELS", P($8000000), R : REM INTO ATTIC RAM
20 EDMA 0, 8192, $8000000, $40000 : REM LEVEL 1 TO BANK 4
30 BSAVE "BANK4", P($40000) TO P($42000), R : REM SAVE IT BACK RAW
In the older form, a 16-bit P address with a B bank, BLOAD and BSAVE cannot cross a bank boundary or reach attic RAM; the flat form has neither limit.
When C64 BASIC hits an error, the program stops. BASIC 65 lets a program catch the error, find out what went wrong, and carry on. This turns many fatal mistakes, such as bad input, a missing file or a division by zero, into situations the program can handle itself.
TRAP followed by a line number tells BASIC where to go
when an error occurs. TRAP on its own turns trapping off
again. Inside the handler, three variables describe the error:
| Name | Contents |
|---|---|
ER |
The error number |
EL |
The line number where the error occurred |
ERR$(n) |
The message text for error number n |
The handler ends with one of three forms of RESUME:
| Form | Effect |
|---|---|
RESUME |
Runs the statement that failed again |
RESUME NEXT |
Continues with the statement after the one that failed |
RESUME line |
Continues at the given line |
This program keeps running however the user answers:
10 TRAP 1000
20 INPUT "DIVIDE 100 BY";N
30 PRINT 100/N
40 GOTO 20
1000 PRINT ERR$(ER);" ERROR IN LINE";EL
1010 RESUME 20
Entering 0 produces a division by zero error in line 30; the handler reports it and asks again.
An error inside the handler itself is not trapped, so a handler should be short and simple. Plain RESUME needs particular care: if the cause of the error has not been fixed, the same statement fails again and the program loops forever.
Disk errors are reported separately, because they happen in the drive
rather than in BASIC. A failed OPEN or LOAD may not raise a BASIC error
at all. After any disk operation, the reserved variables DS
and DS$ hold the drive's status number and message:
10 DOPEN#1,"SCORES"
20 IF DS<>0 THEN PRINT DS$:STOP
A status below 20 means success. Checking DS after every important disk operation is the single most effective way to make a BASIC program robust. Chapter 15 lists the drive's status codes.
TRAP and the disk status variables work together. This loader asks for a file name, reports BASIC errors and disk errors in plain words, and asks again rather than stopping:
10 TRAP 900
20 INPUT "FILE TO LOAD";F$
30 BLOAD (F$), P($40000), R
40 IF DS<>0 THEN PRINT "DISK: ";DS$ : GOTO 20
50 PRINT "LOADED" : END
900 PRINT "ERROR";ER;"IN LINE";EL;": ";ERR$(ER)
910 RESUME 20
The file name is a string expression, so it goes in parentheses, as the BLOAD reference requires (MEGA65 Book, p. B-29). A missing file raises a BASIC FILE NOT FOUND error, which the TRAP handler at line 900 reports, as it does for anything else BASIC rejects. Line 40 catches drive errors that still let the load finish.
RESUME with a line number lets a program record how far it got before an error. This uses the pattern from the Book's TRAP entry to find where EXP overflows (MEGA65 Book, p. B-267):
10 TRAP 100
20 FOR I=1 TO 100
30 PRINT EXP(I)
40 NEXT
50 PRINT "STOPPED FOR I =";I
60 END
100 PRINT ERR$(ER) : RESUME 50
The program prints values until EXP exceeds the floating-point range, then reports the overflow and the value of I that caused it, and ends normally.
C64 programmers extended BASIC by redirecting the vectors in page 3, so that new keywords could be recognised and executed. That technique does not carry over cleanly. BASIC 65 has no officially documented extension interface, and its internal routines and work areas have moved between ROM versions. An extension that patches BASIC's internals may stop working after a ROM update.
The robust alternative is to leave BASIC alone and give it fast helpers. Three features from earlier in this chapter make that easy: BANK and SYS to call code anywhere, SYS parameters and RREG to pass small values, and POINTER to pass the address of larger data.
Suppose a machine-language routine at $42000 sorts an
integer array. BASIC loads it once, then passes the array's flat address
in A, X, Y and Z:
10 BLOAD "SORT", B4, P8192
20 DIM A%(99)
30 REM ... FILL THE ARRAY ...
40 P=POINTER(A%(0))
50 BANK 4
60 SYS $2000, MOD(P,256), INT(P/256) AND 255, INT(P/65536) AND 255, INT(P/16777216)
70 BANK 128
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 the four registers into a zero-page pointer and works
through the array with [zp],Z addressing, as shown in
Chapter 5, remembering that each integer element is stored high byte
first. It needs no knowledge of where BASIC keeps anything, because
POINTER has already worked that out, and it will keep working across ROM
versions.
A few rules keep such helpers safe:
BANK 128. After the call,
switch BASIC back to the system view before using POKE on I/O
registers.Chapter 9 develops this pattern fully, including returning results through RREG and through variables, and Chapter 11 documents the ROM entry points that are stable enough to call.
The USR function jumps through a vector at $02F8, which
BASIC sees in the system bank (MEGA65 Book, p. B-285). The smallest
possible USR routine is a single RTS: it leaves BASIC's floating-point
accumulator untouched, so USR returns its argument unchanged. That is
enough to show the mechanism:
10 BANK 0 : POKE $1800, $60 : REM RTS AT $1800
20 BANK 128 : WPOKE $02F8, $1800 : REM POINT THE USR VECTOR AT IT
30 PRINT USR(42) : REM SHOULD PRINT 42
A real USR routine reads its argument from the floating-point accumulator and leaves its result there, using BASIC's internal conversion routines, as Chapter 9 explains.
An ordinary SYS switches BASIC out, so interrupt-driven features
pause for the duration of the call. SYS TO with I(1) keeps
them running, which suits short routines called often while music plays
(MEGA65 Book, p. B-256):
10 PLAY "O4T0IC E G O5C O4G E L"
20 POKE $1800, $EE, $20, $D0, $60 : REM INC $D020 : RTS
30 FOR I=1 TO 50
40 SYS TO $1800, I(1) : REM MUSIC KEEPS PLAYING
50 SLEEP 0.1
60 NEXT
70 PLAY : REM STOP THE MUSIC
The routine sits at $1800, inside the
$1600-$1EFF range that interrupt mode required in ROMs
920414 to 920418, so it works on every ROM that has SYS TO. POKE
accepts several values in a row, so line 20 places a four-byte routine
in one statement.
BOOT loads a program file and starts executing it at its load address, combining BLOAD and SYS. With no arguments it runs a file named AUTOBOOT.C65, the MEGA65's equivalent of an auto-start file (MEGA65 Book, p. B-31):
BOOT "GAME" : REM LOAD AND RUN MACHINE CODE
BOOT : REM RUN AUTOBOOT.C65
$2001, scalar variables in bank 0, and arrays and string
text in bank 1. Banks 4 and 5 serve the graphics system and can be
reserved with MEM. Attic RAM is left free.$FE prefix.FRE(1) reports the space left.BANK 128 returns to the system view.Chapter 7 begins the machine-language part of the book with the 45GS02 itself.