Bootstrapping Scroll Runner
What you'll learn:
- Set up ACME assembler and VICE emulator toolchain.
- Understand essential 6510 CPU architecture and C64 memory map.
- Convert the BASIC launcher into an all-assembly entry routine.
- Lay out the project memory map and zero-page workspace for Scroll Runner.
- Load existing map/asset data into assembly-friendly buffers.
Lesson 1 – Bootstrapping Scroll Runner
You’ve completed the BASIC course and built Scroll Runner in BASIC V2. Now we’re taking it to the next level: pure assembly. This lesson sets up your development environment, covers essential assembly concepts, and gets you started writing 6510 machine code. By the end, you’ll have Scroll Runner running entirely in assembly with no BASIC overhead.
[📷 suggested: Project tree screenshot showing
/code-samples/commodore-64/assembly/scroll-runner/
withsrc/
andassets/
folders]
Prerequisites & Setup
Before writing assembly, you need the right tools and foundational knowledge. This section covers everything in ~20 minutes.
Toolchain Setup (5 minutes)
1. Install ACME Assembler
ACME converts assembly source code (.asm
files) into runnable C64 programs (.prg
files).
macOS/Linux:
# Using Homebrew (macOS)
brew install acme
# Or download from: https://github.com/meonwax/acme
# Build: cd acme/src && make && sudo make install
Windows:
Download ACME from https://github.com/meonwax/acme/releases
Extract to C:\acme\
Add C:\acme to your PATH
Docker (recommended for consistency):
# Use the provided dev container
cd /path/to/Code198x
docker-compose up -d
docker-compose exec workspace bash
# ACME is pre-installed
Verify installation:
acme --version
# Should output: ACME 0.97 or similar
2. Install VICE Emulator
VICE is the C64 emulator with built-in monitor/debugger.
- Download: https://vice-emu.sourceforge.io/
- macOS: Install via Homebrew:
brew install vice
- Linux:
sudo apt-get install vice
orsudo dnf install vice
- Windows: Download installer from VICE website
Verify installation:
x64sc --version
# Should show VICE version info
3. Test Your Toolchain
Create a simple test program:
# Create test.asm
echo '!to "test.prg",cbm' > test.asm
echo '* = $0801' >> test.asm
echo '!byte $0c,$08,$0a,$00,$9e' >> test.asm
echo '!byte $32,$30,$36,$31,0,0,0' >> test.asm
echo '* = $080d' >> test.asm
echo 'lda #0' >> test.asm
echo 'sta $d020' >> test.asm
echo 'sta $d021' >> test.asm
echo 'rts' >> test.asm
# Assemble it
acme -f cbm -o test.prg test.asm
# Run it
x64sc test.prg
If the emulator shows a black screen, your toolchain works! Press Alt+Q to quit VICE.
Memory Map Essentials (5 minutes)
The C64’s 64KB address space is divided into specific regions. You must know where things live:
Key Memory Regions:
$0000-$00FF Zero Page (fast access, use for pointers/counters)
$0100-$01FF Stack (hardware managed, grows down from $01FF)
$0200-$03FF BASIC/KERNAL workspace (avoid or reuse carefully)
$0400-$07FF Screen RAM (1000 bytes, 40×25 characters)
$0800-$0FFF Free RAM (your program starts here traditionally)
$1000-$3FFF Free RAM (map data, buffers, code)
$4000-$7FFF Free RAM (more space for assets/code)
$8000-$9FFF Free RAM or ROM (bankable)
$A000-$BFFF BASIC ROM (or free RAM if banked out)
$C000-$CFFF Free RAM or ROM (bankable)
$D000-$DFFF I/O (VIC-II, SID, CIA chips)
$E000-$FFFF KERNAL ROM (essential routines)
Color RAM: $D800-$DBE7 (not part of main address space, special)
Critical I/O Registers:
$D000-$D02E VIC-II (video chip) - sprites, scrolling, raster
$D400-$D41C SID (sound chip) - music and sound effects
$DC00-$DC0F CIA #1 - keyboard, joystick
$DD00-$DD0F CIA #2 - VIC bank, serial port
$D020 Border color
$D021 Background color
Zero Page is Gold
The zero page ($00-$FF) is special: operations here run faster and some addressing modes only work with zero page. Use it for:
- Pointers (2 bytes each):
$FB-$FC
,$FD-$FE
- Loop counters:
$FF
- Frequently accessed flags:
$FA
Safe Zero-Page Addresses for Your Code:
$FB-$FE
: Reliable for pointers$02-$8F
: Usually safe but check KERNAL usage- Avoid
$90-$FF
: KERNAL uses these
6510 CPU Crash Course (5 minutes)
The 6510 is an 8-bit CPU derived from the 6502. You need to know:
Registers (3 bytes total):
- A (Accumulator): Main working register for math/logic
- X (Index): Used for loops, array indexing, stack operations
- Y (Index): Similar to X, used for loops and indexing
- SP (Stack Pointer): Managed by hardware, points into $0100-$01FF
- PC (Program Counter): Current instruction address (16-bit)
Status Flags (processor status register):
N V - B D I Z C
│ │ │ │ │ │ └─ Carry (set by addition/subtraction/comparisons)
│ │ │ │ │ └─── Zero (set when result is zero)
│ │ │ │ └───── IRQ Disable (when set, IRQs are blocked)
│ │ │ └─────── Decimal Mode (BCD arithmetic, rarely used)
│ │ └───────── Break (set by BRK instruction)
│ └───────────── Overflow (signed math overflow)
└─────────────── Negative (bit 7 of result)
Common Instructions:
LDA #$00 ; Load A with immediate value $00
STA $D020 ; Store A to address $D020
LDX #$08 ; Load X with 8
INX ; Increment X
DEY ; Decrement Y
CMP #$10 ; Compare A with $10 (sets flags)
BEQ label ; Branch if Equal (Z flag set)
BNE label ; Branch if Not Equal (Z flag clear)
JMP label ; Unconditional jump
JSR routine ; Jump to subroutine (pushes return address)
RTS ; Return from subroutine
Addressing Modes (how you specify operands):
LDA #$42 ; Immediate: literal value $42
LDA $FB ; Zero Page: read from address $FB (fast!)
LDA $1000 ; Absolute: read from address $1000
LDA $1000,X ; Absolute,X: read from $1000 + X
LDA $1000,Y ; Absolute,Y: read from $1000 + Y
LDA ($FB),Y ; Indirect Indexed: read from [($FB)+Y]
That’s enough to read assembly! You’ll learn more by doing.
Assets from BASIC Week 8 (3 minutes)
Scroll Runner needs map data and sprite definitions. If you completed Week 8, you should have:
- Map data: Tile/column definitions
- Sprite data: Character graphics
- HUD tiles: Score/life display graphics
If you have Week 8 assets:
- Export them as binary files (
.bin
) - We’ll load them in this lesson
If you don’t have assets:
- Don’t worry! We’ll create minimal placeholder data
- You can upgrade them later
Quick export from BASIC (if needed):
10 REM EXPORT MAP DATA
20 OPEN 1,8,2,"MAP-DATA.BIN,S,W"
30 FOR I=0 TO 255
40 PRINT#1,CHR$(MAP(I));
50 NEXT I
60 CLOSE 1
Ready to Code!
You now have:
- ✅ ACME assembler installed and tested
- ✅ VICE emulator running
- ✅ Understanding of C64 memory map
- ✅ Basic 6510 CPU knowledge
- ✅ Assets ready (or placeholders prepared)
Let’s build Scroll Runner in assembly!
The One-Minute Tour
- Create a dedicated assembly project folder mirroring the BASIC assets from Week 8.
- Replace the BASIC
SYS
stub with an assembly boot routine that initialises memory and jumps into the main loop. - Reserve RAM for map data, sprites, HUD buffers, and zero-page mailboxes that future lessons will use.
Project Skeleton
Start inside code-samples/commodore-64/assembly
. Create a new project folder that will hold the Act I build:
mkdir -p scroll-runner/src scroll-runner/assets
cp ../basic/week-8/scroll-runner/map-data.bin scroll-runner/assets/
cp ../basic/week-8/scroll-runner/sprites.bin scroll-runner/assets/
Tip: The exact filenames may differ from your repository. Copy the assets you exported during Week 8 (map columns, HUD tiles, sprite definitions) so the assembly version can reuse them without BASIC
DATA
statements.
Create scroll-runner/src/constants.inc
to centralise the memory map and zero-page allocations:
; constants.inc — shared across every module
; Runtime addresses
ENTRY = $080d
MAP_BUFFER = $4000 ; 8 KB (two 4 KB banks for scroll data)
SPRITE_DATA = $6000 ; 2 KB sprite definitions
HUD_BUFFER = $6800 ; off-screen HUD workspace
CODE_BANK = $8000 ; main code lives here and above
; Sizes
MAP_BUFFER_END = MAP_BUFFER + $2000
SPRITE_DATA_END = SPRITE_DATA + $0800
HUD_BUFFER_END = HUD_BUFFER + $0400
; Zero-page map (documented to avoid clashes)
ZP_PTR_LO = $fb
ZP_PTR_HI = $fc
ZP_STATE = $fd
ZP_FRAME_LO = $fe
ZP_FRAME_HI = $ff
Then create scroll-runner/src/main.asm
with the following scaffold. Note how it imports the shared constants and places assets directly at their runtime addresses to avoid brittle copy loops:
!to "scroll-runner.prg",cbm
* = $0801
!src "constants.inc"
; -----------------------------------------------------------------------------
; BASIC header: SYS into ENTRY
; -----------------------------------------------------------------------------
!byte $0c,$08,$0a,$00,$9e
!byte $32,$30,$36,$31,0,0,0
; -----------------------------------------------------------------------------
; Boot code begins at ENTRY
; -----------------------------------------------------------------------------
* = ENTRY
Start:
sei ; set up before IRQs run
cld
ldx #$ff
txs
jsr InitState
; Assets are already in place via !binary includes below
cli ; Lesson 2 will install a proper IRQ loop
jsr WaitVBlank ; simple guard so we don’t spin too fast yet
jmp MainLoop
; -----------------------------------------------------------------------------
; Initial state setup
; -----------------------------------------------------------------------------
InitState:
lda #0
sta ZP_STATE
sta ZP_PTR_LO
sta ZP_PTR_HI
sta ZP_FRAME_LO
sta ZP_FRAME_HI
rts
; -----------------------------------------------------------------------------
; Minimal frame wait (placeholder until Lesson 2)
; -----------------------------------------------------------------------------
WaitVBlank:
lda $d012 ; current raster line
WaitLoop:
cmp $d012
beq WaitLoop
rts
; -----------------------------------------------------------------------------
; Main loop placeholder – replaced in Lesson 2
; -----------------------------------------------------------------------------
MainLoop:
jmp MainLoop
; -----------------------------------------------------------------------------
; Assert memory layout at build time
; -----------------------------------------------------------------------------
!if MAP_BUFFER_END > SPRITE_DATA !error "Map buffer overlaps sprite data"
!if SPRITE_DATA_END > HUD_BUFFER !error "Sprite data overlaps HUD buffer"
!if HUD_BUFFER_END > CODE_BANK !error "HUD buffer overlaps code bank"
; -----------------------------------------------------------------------------
; Asset binaries placed directly into RAM
; -----------------------------------------------------------------------------
* = MAP_BUFFER
MapBinary:
!binary "../assets/map-data.bin"
MapBinaryEnd:
* = SPRITE_DATA
SpriteBinary:
!binary "../assets/sprites.bin"
SpriteBinaryEnd:
* = HUD_BUFFER
HUDBinary:
!fill HUD_BUFFER_END-HUD_BUFFER,0
Building now ensures the assets occupy the addresses the rest of the lessons expect without runtime copies, and the guard clauses raise errors if the files ever outgrow their slots.
This assembly file does three critical things:
- Bootstraps without BASIC. The only BASIC code left is the two-line
SYS
stub. Once it runs, the 6510 executes your assembly boot routine directly. - Declares the memory map up front. Future lessons will use these constants for the game loop, renderer, and HUD. Adjust addresses if you prefer a different layout, but keep them centralised.
- Keeps assets in place. ACME’s
!binary
directive drops the map and sprite files directly at their runtime addresses, so no runtime copy loops are needed.
Tip: ACME’s
!binary
offsets are resolved at assemble time. If your binaries are larger than expected, list their lengths in a comment and assert with!if
to prevent silent overflow.
Build Script
Add a Makefile (or extend the repository’s existing one) so you can build Act I with a single command:
scroll-runner.prg: src/main.asm src/constants.inc assets/map-data.bin assets/sprites.bin
acme -f cbm -o $@ src/main.asm
Run the build and confirm the PRG launches:
cd code-samples/commodore-64/assembly/scroll-runner
make
x64sc scroll-runner.prg
On launch you will see a blank screen—the main loop simply spins. That is expected. Lesson 2 will replace MainLoop
with the deterministic scheduler.
[🎥 suggested: short clip showing VICE booting the PRG and remaining idle, confirming the launcher works]
Experiment Section
- Try swapping
MAP_BUFFER
andSPRITE_DATA
addresses to match your preferred layout. Updateconstants.inc
and ensure the overlap asserts still pass. - If you intend to stream assets from disk instead of bundling them, replace the
!binary
sections with a loader (JSR $FFD5
) and add matching guards to confirm the transfer succeeds. - Add extra zero-page entries for timers (
ZP_FRAME_LO/HI
) or input flags—Lesson 3 will use these, so reserve them now if you plan to track more state. - Split the asset includes into separate modules (
renderer.asm
,audio.asm
) and use ACME’s!src
directive. Keeping the project modular will matter once Acts II–IV add more systems.
Concept Expansion
Declaring the memory map explicitly is the difference between tinkering and engineering. Every future routine can reference the same constants for buffers and mailboxes, eliminating “magic numbers” from the code. Likewise, booting directly into assembly means you no longer pay the BASIC interpreter’s overhead or risk BASIC resetting your memory after a crash.
Game Integration
Record the key addresses in your developer notes so every subsystem agrees:
ENTRY = $080d
MAP_BUFFER = $4000 (8 KB)
SPRITE_DATA = $6000 (2 KB)
HUD_BUFFER = $6800 (1 KB)
CODE_BANK = $8000 onwards
Paste this table into the top of new source files or into a shared constants.asm
. Keeping the map visible prevents pointer conflicts when Acts II–IV allocate more RAM.
From the Vault
- Memory Banks — refresher on banking out BASIC/KERNAL ROM.
- Scroll Runner Design Notes — the Week 8 planning document.
Quick Reference
!src "constants.inc" ; shared memory map + zero-page
; Assembly entry stub (replaces BASIC loop)
!byte $0c,$08,$0a,$00,$9e
!byte $32,$30,$36,$31,0,0,0
; Simple VBlank wait placeholder
WaitVBlank:
lda $d012
.wait
cmp $d012
beq .wait
rts
; Assets resident at runtime addresses
* = MAP_BUFFER
!binary "../assets/map-data.bin"
* = SPRITE_DATA
!binary "../assets/sprites.bin"
* = HUD_BUFFER
!fill HUD_BUFFER_END-HUD_BUFFER,0
What You’ve Learnt
- Booted Scroll Runner directly into assembly, removing BASIC from the runtime equation.
- Established a clear memory map and zero-page layout that future lessons will reuse.
- Included map and sprite assets at build time so the assembly version can access them instantly.
Next lesson: Build the deterministic assembly game loop that will drive every subsystem.