Home / Commodore 64 / Assembly / Lesson 1
Lesson 1 of 8

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.
13% Complete

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/ with src/ and assets/ 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 or sudo 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:

  1. Map data: Tile/column definitions
  2. Sprite data: Character graphics
  3. 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:

  1. 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.
  2. 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.
  3. 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 and SPRITE_DATA addresses to match your preferred layout. Update constants.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


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.