Skip to content

6502 assembly

The MOS 6502 (and its variants) powered some of the most iconic home computers and consoles ever made — the Commodore 64, the NES, the Atari 8-bit family, the Apple II, and the BBC Micro. If you want to squeeze every last cycle out of these machines, assembly is where you go.

The IDE supports several 6502 assemblers. Which one you get depends on the platform preset you choose:

Assembler Typical platforms Notes
KickAss C64, C128 Powerful macro system; .seg and .pc for memory layout
DASM Atari 2600, NES Widely used in console tutorials
ACME C64, general Clean, portable syntax; !byte, !word pseudo-ops
ca65 (cc65) C64, Atari 8-bit, Apple II, NES Part of the cc65 suite; pairs with ld65 linker

Don't worry too much about which assembler you're using to start — the core 6502 instructions are identical across all of them. The differences are mainly in pseudo-ops and macro syntax.

Load an example first

In the IDE, use the Examples dropdown to load the platform's hello-world assembly template. It includes the correct startup stub and memory layout for you, so you can run something immediately before writing a single line yourself.

The 6502 in a nutshell

The 6502 has three registers you'll use constantly:

Register Size Purpose
A (Accumulator) 8-bit Arithmetic and logic happen here
X 8-bit Index register — great for loops and table lookups
Y 8-bit Index register — pairs with indirect addressing

There's also a stack pointer (SP), program counter (PC), and a processor status register (P) with flags like Zero (Z), Carry (C), and Negative (N). That's it. The power of 6502 programming comes from how cleverly you use these three registers together.

Your first program

Here's the smallest useful 6502 program for the C64 — change the border colour:

; C64 — set border colour to red
; The border colour register is at $D020

*= $C000        ; Load at $C000 (call with SYS 49152 from BASIC)

start:
    lda #2          ; Red = colour index 2 in C64 palette
    sta $D020       ; Store to border colour register
    sta $D021       ; Also set background colour
    rts             ; Return to BASIC

In the IDE's C64 BASIC window, type SYS 49152 and press Enter — your border turns red. Three instructions, direct hardware access.

The instructions you'll use most

Moving data

lda #42         ; Load the value 42 into A  (# = immediate value)
lda $D020       ; Load the byte at address $D020 into A
sta $D020       ; Store A into address $D020
ldx #0          ; Load 0 into X
ldy #10         ; Load 10 into Y
tax             ; Copy A into X
tay             ; Copy A into Y
txa             ; Copy X into A

Arithmetic

clc             ; Always clear Carry before addition
adc #5          ; A = A + 5  (add with carry)
sec             ; Always set Carry before subtraction
sbc #3          ; A = A - 3  (subtract with borrow)
inc $50         ; Increment the byte at zero-page address $50
inx             ; X = X + 1
iny             ; Y = Y + 1
dex             ; X = X - 1
dey             ; Y = Y - 1

Comparisons and branches

cmp #10         ; Compare A with 10 (sets flags, does not change A)
beq equal       ; Branch if equal (Z flag set)
bne not_equal   ; Branch if not equal
bcc less        ; Branch if Carry Clear (unsigned less-than after cmp)
bcs greater_eq  ; Branch if Carry Set (unsigned >=)
bmi negative    ; Branch if Minus (result was negative)
bpl positive    ; Branch if Plus

Loops

A counted loop with X is one of the most common 6502 patterns:

        ldx #10         ; Loop 10 times
loop:   ; ... your code here ...
        dex
        bne loop        ; Branch back while X is non-zero

Subroutines

        jsr my_routine  ; Jump to subroutine (pushes return address on stack)
        ; execution continues here after rts

my_routine:
        ; ... do something ...
        rts             ; Return

Addressing modes

Addressing modes determine where an instruction's data comes from:

Mode Example Meaning
Immediate lda #42 The literal value 42
Zero page lda $50 Byte at address $0050 (fast — 1 fewer byte and cycle)
Absolute lda $D020 Byte at address $D020
Absolute,X lda $D800,x Byte at $D800 + X
Absolute,Y lda $D800,y Byte at $D800 + Y
Zero page,X lda $50,x Byte at $0050 + X
Indirect,Y lda ($50),y Use address stored at $50/$51, then add Y
Implied clc No operand needed

Zero page (addresses $00–$FF) is special — instructions using it are one byte shorter and one cycle faster than their absolute equivalents. Put your most-used variables there.

Indirect,Y ((zp),y) is the workhorse for rendering and data processing — store a pointer in two zero-page bytes and walk through memory with Y.

The stack

The 6502 has a 256-byte hardware stack at $0100–$01FF. You push and pull bytes:

pha     ; Push A onto stack
pla     ; Pull top of stack into A
php     ; Push processor status register
plp     ; Pull processor status register

jsr and rts use the stack automatically. Keep subroutine nesting reasonable — deep nesting on deeply recursive code will overflow the 256-byte limit.

Platform memory maps

The 6502 can address 64 KB ($0000–$FFFF), but what lives where differs completely per machine:

Range Contents
$0000–$00FF Zero page (fast variables)
$0100–$01FF Stack
$0400–$07FF Screen character RAM (default)
$0800–$9FFF BASIC RAM
$C000–$CFFF BASIC ROM (bank-switchable)
$D000–$D3FF VIC-II chip
$D400–$D7FF SID chip
$DC00–$DCFF CIA 1
$DD00–$DDFF CIA 2
$E000–$FFFF Kernal ROM
Range Contents
$0000–$00FF Zero page
$0100–$01FF Stack
$1000–$1DFF Main RAM (only ~3.5 KB without expansion)
$9000–$93FF VIC chip registers
$9400–$97FF Colour RAM
$A000–$BFFF BASIC ROM
$C000–$FFFF Kernal ROM
Range Contents
$0000–$00FF Zero page
$0100–$01FF Stack
$0200–$07FF RAM
$2000–$2007 PPU registers
$4000–$4017 APU and controller I/O
$8000–$FFFF PRG ROM (cartridge — your code)
Range Contents
$0000–$00FF Zero page
$0100–$01FF Stack
$0200–$02FF OS page 2
$D000–$D7FF GTIA / ANTIC / POKEY / PIA
$E000–$FFFF OS ROM
Range Contents
$0000–$00FF Zero page (OS uses part of it)
$0100–$01FF Stack
$0400–$07FF OS workspace (avoid!)
$0E00–$7FFF User RAM
$C000–$FFFF OS ROM

Interrupts and vectors

The 6502 has three hardware vectors at the very top of memory:

Vector Address Purpose
NMI $FFFA/$FFFB Non-Maskable Interrupt (can't be ignored)
RESET $FFFC/$FFFD Power-on / reset start address
IRQ/BRK $FFFE/$FFFF Maskable interrupt / BRK instruction

On most machines the OS owns these and you hook in indirectly (e.g. C64's $0314/$0315 IRQ vector). On bare-metal targets like the Atari 2600 and NES, your cartridge ROM supplies the actual vectors.

Full minimal examples

C64 — clear screen (KickAss syntax)

.const SCREEN = $0400
.const SPACE  = $20
.const COLOUR = $D800
.const WHITE  = 1

*= $C000

start:
    ldx #0
clear:
    lda #SPACE
    sta SCREEN,x
    lda #WHITE
    sta COLOUR,x
    inx
    bne clear           // Clears first 256 chars; repeat for pages 1-3
    rts

Atari 2600 — colour-cycling background (DASM)

    processor 6502
    include "vcs.h"
    org $F000

Start:
    sei
    cld
    ldx #$FF
    txs
    lda #0
    sta VSYNC
    sta VBLANK

MainLoop:
    lda #2              ; VSYNC on
    sta VSYNC
    sta WSYNC
    sta WSYNC
    sta WSYNC
    lda #0
    sta VSYNC

    ldx #37             ; VBLANK — 37 lines
VBloop:
    sta WSYNC
    dex
    bne VBloop
    lda #0
    sta VBLANK

    ldx #192            ; 192 visible scanlines
ScanLoop:
    sta WSYNC
    txa
    sta COLUBK          ; Use X as colour — rainbow effect!
    dex
    bne ScanLoop

    ldx #30             ; Overscan
OVloop:
    sta WSYNC
    dex
    bne OVloop

    jmp MainLoop

    org $FFFC
    .word Start         ; RESET vector
    .word Start         ; IRQ vector

Common mistakes

Forgetting clc before adc — the Carry flag left over from a previous operation will corrupt your result. Always clear it first.

Forgetting sec before sbc — same in reverse. Always set Carry before subtracting.

16-bit values — the 6502 only does 8-bit arithmetic. For 16-bit, carry the high byte manually:

; 16-bit add: result = value + 500
clc
lda value_lo
adc #<500           ; Low byte of 500
sta result_lo
lda value_hi
adc #>500           ; High byte of 500, plus carry from low byte
sta result_hi

Clobbering registers in subroutines — save and restore what you use:

my_sub:
    pha             ; Save A
    txa
    pha             ; Save X (via A)
    ; ... use A and X freely ...
    pla
    tax             ; Restore X
    pla             ; Restore A
    rts

Commenting hardware addressessta $D020 means nothing in six months. Use equates:

BORDER = $D020
; ...
sta BORDER      ; Now it's readable

Pairing with high-level tools

  • cc65 — write C and call assembly, or drop into inline asm for hot routines. See cc65 / C.
  • BASIC SYS — load a small ML routine from BASIC and jump to it with SYS address. See Commodore BASIC V2.

See also