C64 KickAss Workshop Part 1
Hello, World!
Assembly language is the closest you can get to the machine — every instruction directly controls the 6502 CPU. It’s more work than BASIC, but the payoff is total control and blazing speed. The classic first step: get something on screen.
We’ll use Kick Assembler (KickAss), a cross-assembler that runs on your modern machine and outputs C64-compatible .prg files. The Kick Assembler Reference Manual covers everything the assembler can do.
Objective
Write a KickAssembler program that clears the screen, sets custom colours, and displays a message of your choosing.
Requirements
- The program must use
:BasicUpstartto create an auto-run BASIC loader - The program must clear the screen
- The program must set a custom border and/or background colour
- The program must display a text message on screen
Bonus
- Display your message in a custom text colour
- Position your message at a specific row and column on screen
- Display multiple lines of text
Getting Started
What You Need
- Java (version 5.0 or later) — Kick Assembler runs on Java
- Kick Assembler — download from theweb.dk/KickAssembler
- A text editor — any editor works (VS Code, Sublime Text, Notepad++, etc.)
- VICE emulator — see Part 0 of the BASIC workshop for setup instructions
Assemble and Run
Save your code as a .asm file (e.g., hello.asm), then assemble:
java -jar KickAss.jar hello.asm
This produces hello.prg. Open it in VICE by dragging it onto the emulator window, or use File → Autostart disk/tape image.
Alternatively, for Linux, here’s a script to assemble and immediately test using VICE:
#!/bin/sh
java -jar /path/to/KickAss.jar "$1" -showmem && x64sc -autostart "${1%.asm}.prg"
Tip: Save the script as “kickassrun.sh” (and ensure you chmod +x kickassrun.sh so you can actually run it!), and remember to set the KickAss.jar and x64sc paths as needed. Then you can run it with ./kickassrun.sh hello.asm.
Resources / Reference
Program Structure
A C64 KickAss program has two sections: a BASIC loader and your machine code. The BASIC loader is a tiny program that tells the C64 to jump to your code when you type RUN.
Use this template for all programs in this workshop:
.pc = $0801 "Basic Upstart" // BASIC loader goes here
:BasicUpstart(start)
.pc = $0810 "Program"
start:
// Your code goes here
Line by line:
.pc = $0801— Sets where the next bytes go in memory.$0801is where the C64 stores BASIC programs.:BasicUpstart(start)— A Kick Assembler macro that generates a one-line BASIC program (10 SYS 2064). When the C64 runs this, it jumps straight to yourstartlabel..pc = $0810— Sets the location for your actual machine code.$0810is right after the BASIC loader in memory.start:— A label marking where your program begins. Labels end with:when declared.
The two .pc lines are needed because the BASIC loader and your code live at different addresses — think of them as two sections in the same file. Every program you write will start with this exact template.
CPU Registers
The 6502 CPU has three main registers — think of them as built-in variables:
- A (Accumulator): The main register for loading, storing, and arithmetic
- X (X Index): Used as a counter or offset for indexed addressing
- Y (Y Index): Another counter/index register, similar to X
Each holds one byte (0–255).
Number Formats
Kick Assembler supports three number formats:
| Prefix | Format | Example |
|---|---|---|
| (none) | Decimal | lda #42 |
$ |
Hexadecimal | lda #$2a |
% |
Binary | lda #%00101010 |
Hex is used heavily in C64 programming because hardware addresses are conventionally written in hex (e.g., screen memory at $0400, border colour at $d020).
LDA / STA
Load a value into the Accumulator and store it to memory.
lda #BLACK // Load the VALUE "BLACK" (0) into A
sta $d020 // Store A to the ADDRESS $d020
The # is critical: # means “this exact value” (immediate mode). Without #, the CPU reads FROM that memory address instead.
lda #5 // A = 5 (the number five)
lda $0400 // A = whatever byte is stored at address $0400
LDX / INX / DEX
Load, increment, and decrement the X register — essential for loops and counting.
ldx #0 // X = 0
inx // X = 1
inx // X = 2
dex // X = 1
ldy, iny, and dey are the equivalent instructions for the Y register. dex and dey are commonly used to count down in loops — when the counter reaches zero, the zero flag is set, so you can use bne to keep looping.
Labels, JMP, and BEQ
Labels mark positions in code. jmp jumps unconditionally. beq jumps only if the last result was zero.
loop:
inc $d020 // Increment border colour
jmp loop // Jump back — infinite loop!
beq (Branch if EQual to zero) checks the CPU’s zero flag, which is set automatically when any operation produces a zero result:
lda message,x // Load a byte
beq done // If it was zero, jump to 'done'
bne (Branch if Not Equal to zero) is the opposite — jumps when the result is NOT zero.
Halting: Always end your program with a jump to itself so the CPU doesn’t run into garbage memory:
done:
jmp done // Loop forever (halt)
The shorthand jmp * does the same thing — * means “the current address.”
Multi labels let you reuse the same label name. They start with ! and use + or - to refer to the next or previous instance:
ldx #100
!loop: inc $d020
dex
bne !loop- // Jump to the PREVIOUS !loop
JSR
Jump to SubRoutine — call a routine and return when done.
The C64’s KERNAL ROM provides useful built-in routines:
jsr $e544 // Call KERNAL: clear the screen
When the routine finishes (with rts — return from subroutine), execution continues after the jsr.
.text and .byte
Embed data directly in your program.
.text stores a string as C64 screen code bytes — ready to write directly to screen memory.
.byte stores raw byte values.
message:
.text "hello" // 5 bytes of screen codes
.byte 0 // A zero byte (end-of-string marker)
data:
.byte 1,2,3,4,5 // 5 raw bytes
A zero byte at the end is a common convention for marking where a string ends — your loop can check for it with beq.
Use lowercase in .text strings. KickAssembler maps lowercase source letters to the screen codes for A–Z. Uppercase source letters map to shifted/graphics characters instead.
Indexed Addressing
Use X (or Y) as an offset when loading or storing. This lets you process strings and arrays:
ldx #2
lda message,x // Load byte at (message address + 2)
sta $0400,x // Store to screen memory position 2
If message contains "hello", then with X=2, lda message,x loads 'l' (third character — counting starts at 0).
Printing Text to Screen
Combine indexed addressing with a loop to copy a string to screen memory one character at a time:
ldx #0 // Start at the first character
!loop:
lda message,x // Load next character from our string
beq done // If it's zero, we've reached the end
sta $0400,x // Write it to screen memory
inx // Move to the next character
jmp !loop- // Repeat
done:
jmp done // Halt
message:
.text "hello world"
.byte 0 // End-of-string marker
This is the core pattern: load a character, check if it’s the zero terminator, store it to screen memory, advance to the next position, and repeat. Everything else — clearing the screen, setting colours, choosing where on screen your text appears — builds on top of this loop.
Screen Memory
The C64 screen is 40 columns × 25 rows. Two memory regions control what you see:
- Screen memory (
$0400): Which character is displayed at each position - Colour memory (
$d800): The foreground colour of each character (0–15)
To calculate the address for a specific row and column:
$0400 + (row × 40) + column (screen) or $d800 + (row × 40) + column (colour)
Kick Assembler can do this maths at assemble time with .var:
.var screenPos = $0400 + 12 * 40 + 13 // Row 12, column 13
.var colourPos = $d800 + 12 * 40 + 13
Comments
Kick Assembler uses C-style comments:
// This is a line comment
lda #0 // Comment after code
/* This is a
block comment */
Colour Constants
Kick Assembler has built-in C64 colour constants you can use by name:
| Constant | Value | Constant | Value |
|---|---|---|---|
| BLACK | 0 | ORANGE | 8 |
| WHITE | 1 | BROWN | 9 |
| RED | 2 | LIGHT_RED | 10 |
| CYAN | 3 | DARK_GRAY | 11 |
| PURPLE | 4 | GRAY | 12 |
| GREEN | 5 | LIGHT_GREEN | 13 |
| BLUE | 6 | LIGHT_BLUE | 14 |
| YELLOW | 7 | LIGHT_GRAY | 15 |
These values work for border ($d020), background ($d021), and colour memory ($d800).
Example:
lda #BLACK
sta $d020 // Set border to black
lda #BLUE
sta $d021 // Set background to blue
Quick Reference
| Address | Purpose |
|---|---|
$0400 |
Screen memory start (40 columns × 25 rows) |
$d800 |
Colour memory start (foreground colour per character, 0–15) |
$d020 |
Border colour register (0–15) |
$d021 |
Background colour register (0–15) |
$e544 |
KERNAL routine: clear screen |
| Instruction | What It Does |
|---|---|
lda #value |
Load a value into A |
sta address |
Store A to a memory address |
ldx #value |
Load a value into X |
inx / dex |
Increment / decrement X by 1 |
iny / dey |
Increment / decrement Y by 1 |
jmp label |
Jump to a label (unconditional) |
beq label |
Branch to label if last result was zero |
bne label |
Branch to label if last result was NOT zero |
jsr address |
Call a subroutine |
Tips for Success
- Remember the
#inlda #value— without it, the CPU reads from a memory address instead of using the value directly, which is the most common beginner mistake - End your program with
jmpto itself so the CPU doesn’t run into garbage memory - If your text is invisible, check that text and background aren’t the same colour
- Use lowercase in
.textstrings — KickAssembler maps lowercase to letter screen codes (A–Z), while uppercase maps to shifted/graphics characters - Assemble often — fix errors one at a time rather than writing everything and debugging all at once
- Kick Assembler error messages include line numbers — read them carefully