16 KiB
Programming in c65gm
c65gm is a high-level language for 6502 assembly programming that combines modern programming constructs with direct hardware access. Originally designed for Commodore 64 development, it compiles to efficient 6502 assembly code.
Table of Contents
- Getting Started
- Variables and Types
- Expressions and Operators
- Control Flow
- Functions
- Memory Operations
- Code Blocks
- Preprocessor
- Writing Libraries
- Common Patterns
Getting Started
Your First C64 Program
Here's a simple complete program that changes the screen border color:
#INCLUDE <c64start.c65>
#INCLUDE <c64defs.c65>
GOTO start
FUNC main
BYTE borderColor @ $D020
borderColor = color_green
FEND
LABEL start
main()
Creating a C64 Executable
To create a runnable C64 program, always start with:
#INCLUDE <c64start.c65>
This creates a valid C64 executable with a BASIC loader (0 SYS 2064) and sets up the proper memory layout.
Program Structure
A typical c65gm program for C64 follows this pattern:
#INCLUDE <c64start.c65>
#INCLUDE <c64defs.c65> // Optional: standard C64 definitions
GOTO start // Jump over your function definitions
// Variables
BYTE counter = 0
WORD screen = $0400
// Functions - must be declared before we use them.
FUNC processData
// Do something
FEND
FUNC initialize
counter = 10
processData()
FEND
// Entry point - execution starts here
LABEL start
initialize()
Important: The GOTO start jumps over your function definitions. Without it, the CPU would try to execute directly into your function code, causing a crash. Always put GOTO start before your functions, and LABEL start before your actual program entry point.
Variables and Types
BYTE Variables
BYTE variables store 8-bit values (0-255).
BYTE count // Uninitialized
BYTE speed = 5 // Initialized to 5
BYTE screen @ $D020 // Memory-mapped to specific address
BYTE CONST MAX_SPEED = 10 // Constant (recommended over #DEFINE)
WORD Variables
WORD variables store 16-bit values (0-65535).
WORD counter // Uninitialized
WORD address = $C000 // Initialized to hex value
WORD message = "Hello" // String literal (pointer to text)
WORD screenPtr @ $FB // Zero-page pointer
WORD CONST SCREEN_RAM = $0400 // Constant
Memory-Mapped Variables
Variables can be placed at specific addresses using @:
BYTE borderColor @ $D020 // VIC-II border color
WORD irqVector @ $0314 // IRQ vector
WORD zpPointer @ $FB // Zero-page variable (fast access)
Constants
Use CONST keyword for named constants (preferred over preprocessor defines):
BYTE CONST MAX_ITEMS = 100
WORD CONST SCREEN_START = $0400
WORD CONST CHAR_ROM = $D000
// Usage
BYTE itemCount = MAX_ITEMS
screen = SCREEN_START
Expressions and Operators
Number Formats
value = 123 // Decimal
value = $FF // Hexadecimal ($ prefix)
value = %11111111 // Binary (% prefix)
Arithmetic Operators
result = 10 + 5 // Addition
result = 100 - 5 // Subtraction
Bitwise Operators
mask = value & $FF // Bitwise AND
flags = flags | $01 // Bitwise OR
toggle = value ^ $FF // Bitwise XOR
Increment and Decrement
counter++ // Increment by 1
counter-- // Decrement by 1
index++
lives--
Critical: No Operator Precedence
Expressions evaluate strictly left to right! There is no operator precedence.
result = 2+3*4 // Evaluates as (2+3)*4 = 20, NOT 2+(3*4) = 14
value = 100-20+5 // Evaluates as (100-20)+5 = 85
For complex expressions, use temporary variables:
// Instead of: result = (b - c) + a
temp = b - c
result = a + temp
Control Flow
IF Statements
Basic conditional execution:
IF count == 10
result = 1
ENDIF
IF value > threshold
process()
ELSE
skip()
ENDIF
Supported comparison operators: = == <> != > < >= <=
Single-parameter IF treats 0 as false, non-zero as true:
IF running
// Executes if running != 0
update()
ENDIF
WHILE Loops
Loop while condition is true:
WHILE counter < 100
counter++
process(counter)
WEND
WHILE running
gameLoop()
WEND
WHILE x != y
x++
WEND
FOR Loops
Loop with automatic counter:
FOR i = 0 TO 10
screen = i
NEXT
FOR x = 0 TO 255 STEP 2
result = result + x
NEXT
FOR counter = start TO finish
process(counter)
NEXT
BREAK
Exit a loop early:
FOR i = 0 TO 100
IF i == 50
BREAK
ENDIF
process(i)
NEXT
WHILE running
IF error
BREAK
ENDIF
update()
WEND
Functions
Declaring Functions
FUNC initialize
BYTE temp = 0
screen = temp
FEND
FUNC setColor(color, brightness)
borderColor = color
backgroundColor = brightness
FEND
Calling Functions
initialize()
setColor(1, 14)
process(xpos, ypos, spriteData)
Parameter Passing Modes
Parameters can have different access modes:
in:- Read-only (default if not specified)out:- Write-only (for returning values)io:- Read-write
FUNC add(in:a, in:b, out:result)
result = a + b
FEND
FUNC swap(io:x, io:y)
BYTE temp = x
x = y
y = temp
FEND
Local Variables
Variables declared inside functions are local:
FUNC calculate(value)
BYTE local = 10 // Only exists inside this function
WORD temp // Local temporary
temp = value + local
FEND
Curly Brace Syntax
Alternative parameter syntax using curly braces:
FUNC process({BYTE value} out:{BYTE result})
result = value + 1
FEND
Returning from Functions
FUNC checkValue(value)
IF value == 0
EXIT // Return early
ENDIF
process(value)
FEND
Memory Operations
PEEK - Reading Memory
Read a byte from memory:
value = PEEK $D020 // Read from absolute address
char = PEEK screenPtr[index] // Read with offset
byte = PEEK pointer // Read from pointer
Important: For indexed access, the address must be a WORD variable in zero page.
WORD buffer @ $FB // Zero-page pointer
value = PEEK buffer[10] // Read buffer+10
POKE - Writing Memory
Write a byte to memory:
POKE $D020 WITH 0 // Write to absolute address
POKE screenPtr[index] WITH char // Write with offset
POKE pointer WITH value // Write to pointer
PEEKW - Reading 16-bit Words
Read a 16-bit value from memory:
WORD address
address = PEEKW $FFFC // Read reset vector
WORD buffer @ $FB
value = PEEKW buffer[10] // Read word at buffer+10
POKEW - Writing 16-bit Words
Write a 16-bit value to memory:
POKEW $0314 WITH irqHandler // Set IRQ vector
POKEW dataPtr[0] WITH address // Write word with offset
POINTER - Setting Pointers
Set a pointer to an address:
POINTER screenPtr TO $0400
POINTER bufferPtr TO dataBuffer
POINTER funcPtr TO myFunction
Code Blocks
ASM Blocks
Inline assembly for direct hardware control:
ASM
lda #$00
sta $d020
jsr $ffd2
ENDASM
Referencing Variables in ASM
Global variables can be referenced directly:
BYTE temp = 5
ASM
lda temp
clc
adc #10
sta temp
ENDASM
Local variables (inside FUNC) need pipe delimiters |varname|:
FUNC calculate(value)
BYTE local = 10
ASM
lda |local| // Reference local variable
clc
adc value // Global parameter
sta |local|
ENDASM
FEND
SCRIPT Blocks
Generate assembly code at compile time using Starlark (Python-like):
// Generate a table of squares
SCRIPT
print("squares:")
for i in range(256):
print(" !8 %d" % (i * i % 256))
ENDSCRIPT
Sine Table Example
SCRIPT
import math
print("sintable:")
for i in range(256):
angle = (i * 2.0 * math.pi) / 256.0
sine = math.sin(angle)
value = int((sine + 1.0) * 127.5)
print(" !8 %d" % value)
ENDSCRIPT
Referencing Variables
Use |varname| syntax in generated assembly:
BYTE tableSize = 64
SCRIPT
print("lookup:")
for i in range(64):
print(" !8 %d" % (i * 2))
print(" // Size stored in |tableSize|")
ENDSCRIPT
Preprocessor
Comments
// Single-line comment
BYTE counter = 0 // End-of-line comment
In ASM blocks, use assembly syntax:
ASM
lda #$00 ; Assembly comment
ENDASM
In SCRIPT blocks, use Python syntax:
SCRIPT
# Python-style comment
print("hello")
ENDSCRIPT
Include Files
#INCLUDE mylib.c65 // Relative to current file
#INCLUDE lib/string.c65 // Subdirectory
#INCLUDE <stdlib.c65> // Search in C65LIBPATH
Include guards prevent multiple inclusion:
#IFNDEF __MY_LIBRARY
#DEFINE __MY_LIBRARY = 1
// Library code here
#IFEND
Define Macros
Text substitution macros:
#DEFINE MAX_SPEED = 10
#DEFINE SCREEN = $$0400 // $$ escapes to literal $
speed = MAX_SPEED // Replaced with 10
Special characters in defines:
#DEFINE SPACE = $20 // Space character (hex 20)
#DEFINE NEWLINE = $0D // Carriage return
#DEFINE HEXADDR = $$D020 // Literal "$D020" text
Note: Prefer BYTE CONST and WORD CONST over #DEFINE for constants.
Conditional Compilation
#IFDEF DEBUG
BYTE debugFlag = 1
#IFEND
#IFNDEF __LIB_INCLUDED
#DEFINE __LIB_INCLUDED = 1
// Include library code
#IFEND
Pragma Directives
Control compiler behavior:
#PRAGMA _P_USE_LONG_JUMP 1 // Use JMP instead of branches
#PRAGMA _P_USE_IMMUTABLE_CODE 1 // No self-modifying code (for ROM)
#PRAGMA _P_USE_CBM_STRINGS 1 // Use PETSCII encoding
Debug Directives
#PRINT Compiling main module // Print during compilation
#HALT // Stop compilation
Writing Libraries
When creating a library file to be included by other programs, use this structure:
#IFNDEF __MY_LIBRARY
#DEFINE __MY_LIBRARY = 1
GOTO lib_mylib_skip // Jump over library code
// Library variables
WORD lib_mylib_buffer
// Library functions
FUNC lib_mylib_initialize
lib_mylib_buffer = $C000
FEND
FUNC lib_mylib_process(value)
// Do something
FEND
// Skip label - execution continues here after GOTO
LABEL lib_mylib_skip
#IFEND
Key points for libraries:
- Include guard - Use
#IFNDEFto prevent multiple inclusion - GOTO skip - Jump over all library code immediately
- LABEL skip - Place at the end so GOTO jumps past everything
- Naming convention - Prefix all names with
lib_yourlib_to avoid conflicts
This ensures when someone does #INCLUDE <mylib.c65>, the library functions are defined but not executed.
Common Patterns
Complete Working Example
Here's a complete C64 program showing all the pieces together:
#INCLUDE <c64start.c65>
#INCLUDE <c64defs.c65>
GOTO start
// Variables
BYTE frameCount = 0
WORD screenPtr @ $FB
// Functions
FUNC initialize
BYTE borderColor @ $D020
borderColor = color_black
POINTER screenPtr TO $0400
FEND
FUNC updateScreen
BYTE color
color = frameCount & $0F
POKE screenPtr[0] WITH color
frameCount++
FEND
// Entry point
LABEL start
initialize()
WHILE 1
updateScreen()
WEND
Screen Manipulation
WORD CONST SCREEN = $0400
WORD CONST COLOR_RAM = $D800
BYTE CONST SCREEN_WIDTH = 40
BYTE CONST SCREEN_HEIGHT = 25
FUNC clearScreen
WORD screenPtr @ $FB
POINTER screenPtr TO SCREEN
WORD remaining = 1000
WHILE remaining > 0
POKE screenPtr[0] WITH 32 // Space character
screenPtr++
remaining--
WEND
FEND
String Handling
// Print null-terminated string
FUNC printString(textPtr)
BYTE char
char = PEEK textPtr[0]
WHILE char != 0
ASM
lda |char|
jsr $FFD2 // CHROUT
ENDASM
textPtr++
char = PEEK textPtr[0]
WEND
FEND
Sprite Manipulation
WORD CONST VIC2 = $D000
BYTE spriteX @ VIC2+0
BYTE spriteY @ VIC2+1
BYTE spriteEnable @ VIC2+21
FUNC enableSprite(spriteNum)
BYTE mask
mask = 1
FOR i = 0 TO spriteNum
mask = mask * 2
NEXT
spriteEnable = spriteEnable | mask
FEND
Zero Page Optimization
For frequently accessed pointers, use zero page:
WORD fastPtr @ $FB // Zero page = fast indexed access
FUNC processBuffer(buffer, size)
POINTER fastPtr TO buffer
WHILE size > 0
BYTE value
value = PEEK fastPtr[0]
// Process value
fastPtr++
size--
WEND
FEND
Interrupt Handlers
WORD CONST IRQ_VECTOR = $0314
WORD oldIRQ
FUNC installIRQ
ASM
sei // Disable interrupts
ENDASM
oldIRQ = PEEKW IRQ_VECTOR
POKEW IRQ_VECTOR WITH myIRQ
ASM
cli // Enable interrupts
ENDASM
FEND
LABEL myIRQ
// IRQ handler code
ASM
// Save registers
pha
txa
pha
tya
pha
// Do IRQ work
inc $d020
// Restore and return
pla
tay
pla
tax
pla
rti
ENDASM
Lookup Tables
Generate tables at compile time:
ASM
colorTable:
ENDASM
SCRIPT
colors = [0, 1, 15, 12, 11, 9, 2, 8]
for c in colors:
print(" !8 %d" % c)
ENDSCRIPT
Delay Loops
FUNC delay(frames)
BYTE raster @ $D012
BYTE oldRaster
WHILE frames > 0
oldRaster = raster
WHILE raster == oldRaster
// Wait for raster to change
WEND
frames--
WEND
FEND
Bit Manipulation
// Set bit
flags = flags | %00000001 // Set bit 0
// Clear bit
flags = flags & %11111110 // Clear bit 0
// Toggle bit
flags = flags ^ %00000001 // Toggle bit 0
// Test bit
IF flags & %00000001
// Bit 0 is set
ENDIF
Best Practices
1. Use Constants for Magic Numbers
// Bad
POKE $D020 WITH 5
// Good
BYTE CONST COLOR_GREEN = 5
BYTE borderColor @ $D020
borderColor = COLOR_GREEN
2. Zero Page for Performance
Place frequently accessed pointers in zero page ($00-$FF):
WORD screenPtr @ $FB // Fast indexed access
WORD tempPtr @ $FD
3. Watch Expression Evaluation Order
Remember: left-to-right evaluation, no precedence!
// Be careful with expressions
result = 2 + 3 * 4 // = 20, not 14
// Use temps for clarity
temp = 3 * 4
result = 2 + temp // Now = 14
4. Include Guards
Always use include guards in library files:
#IFNDEF __MY_LIB
#DEFINE __MY_LIB = 1
// Library code
#IFEND
5. Comment Your Code
// Explain what and why, not how
FUNC calculateTrajectory(velocity, angle)
// Use fixed-point math (8.8 format)
// because we don't have floating point
WORD xVel
WORD yVel
// ...
FEND
6. Use Meaningful Names
// Bad
BYTE x
BYTE y
// Good
BYTE playerXPos
BYTE playerYPos
Further Resources
- See
commands.mdfor complete command reference - See
syntax.mdfor detailed syntax rules - Check the
lib/directory for example library code - Set
C65LIBPATHenvironment variable to specify library search paths
Quick Reference Card
// Variables
BYTE varName = 10
WORD address = $C000
BYTE CONST MAX = 100
// Control Flow
IF x == 10
// code
ENDIF
WHILE x < 100
x++
WEND
FOR i = 0 TO 10
// code
NEXT
// Functions
FUNC myFunc(in:param1, out:result)
result = param1 + 1
FEND
// Memory
value = PEEK $D020
POKE $D020 WITH 5
address = PEEKW $FFFC
POKEW $0314 WITH handler
// Operators
+ - * / // Arithmetic
& | ^ // Bitwise
++ -- // Increment/Decrement
== != < > <= >= // Comparison
// Blocks
ASM
// assembly code
ENDASM
SCRIPT
# Python code
ENDSCRIPT