c65gm/language.md

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

  1. Getting Started
  2. Variables and Types
  3. Expressions and Operators
  4. Control Flow
  5. Functions
  6. Memory Operations
  7. Code Blocks
  8. Preprocessor
  9. Writing Libraries
  10. 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:

  1. Include guard - Use #IFNDEF to prevent multiple inclusion
  2. GOTO skip - Jump over all library code immediately
  3. LABEL skip - Place at the end so GOTO jumps past everything
  4. 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.md for complete command reference
  • See syntax.md for detailed syntax rules
  • Check the lib/ directory for example library code
  • Set C65LIBPATH environment 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