Skip to content
Game 1 Unit 1 of 128 1 hr learning time

Ship on Screen

Display a hardware sprite using VIC-II registers — your first machine code program on the Commodore 64.

1% of Starfield

A white spaceship against black. Nothing moves, nothing makes sound — it just sits there.

That’s all you need. This single image proves you can talk to the VIC-II chip, the C64’s graphics engine. Every sprite, every animation, every game you’ll ever write on this machine starts with the same two instructions you’ll learn here: LDA (load a value) and STA (store it somewhere). One sprite, five registers, and the satisfaction of knowing you put it there with machine code.

The BASIC Stub

Every C64 program needs a way to start. The first two bytes of any .prg file tell the C64 where to load the program in memory. After that, we need a BASIC line that launches our machine code:

; BASIC stub: 10 SYS 2061
; These bytes encode a single BASIC line that jumps
; to our machine code at address $080D (decimal 2061)
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

The magic bytes encode 10 SYS 2061 — a BASIC line that calls the machine code starting at address $080D (decimal 2061). When you type RUN, BASIC executes this line and jumps to your code.

You don’t need to memorise these bytes. They’re the same for every program that starts at $080D.

LDA and STA

The 6510 processor works with three registers: A (the accumulator), X, and Y. For now, you only need A.

Two instructions do most of the work:

  • LDA #valueLoaD the Accumulator with a value
  • STA addressSTore the Accumulator to a memory address

The # symbol means “this exact value” (called immediate mode). Without it, the number would be treated as a memory address to read from.

        lda #$01        ; Put the value 1 into A
        sta $d027       ; Write A to address $D027 (sprite 0 colour)

That’s it. Load a value, store it somewhere. The entire VIC-II chip — sprites, colours, screen layout — is controlled by storing values to specific memory addresses. This is called memory-mapped I/O: the hardware responds to writes at certain addresses as if they were physical switches.

Clearing the Screen

Before showing our ship, we clear away the BASIC startup text. This short loop fills screen memory with space characters:

        ; Clear the screen (fill with spaces)
        ldx #$00
-       lda #$20            ; Space character
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

Screen memory lives at $0400$07E7 (1,000 bytes — one per character cell on the 40×25 screen). The loop uses the X register as a counter: LDX loads it, INX increments it, and BNE loops back until X wraps from 255 to 0. We’ll explore loops properly in Unit 2. For now, just know this gives us a clean black canvas.

VIC-II Sprite Registers

The VIC-II chip controls all graphics on the C64. To display a sprite, it needs four pieces of information:

        ; Tell VIC-II where sprite 0's graphic data lives
        ; Block number = address / 64.  $2000 / 64 = 128.
        lda #128
        sta $07f8           ; Sprite 0 data pointer

        ; Position sprite 0 near centre-bottom of screen
        lda #172            ; X position
        sta $d000           ; Sprite 0 X position
        lda #220            ; Y position
        sta $d001           ; Sprite 0 Y position

        ; Set sprite 0 colour to white
        lda #$01
        sta $d027           ; Sprite 0 colour

        ; Enable sprite 0
        lda #%00000001      ; Bit 0 = sprite 0
        sta $d015           ; Sprite enable register
  1. Where’s the graphic? — Register $07F8 holds a block number pointing to the sprite’s pixel data. Block 128 means address $2000 (128 × 64 = 8192 = $2000).

  2. Where on screen? — Registers $D000 (X) and $D001 (Y) set the sprite’s position. X runs left-to-right, Y runs top-to-bottom.

  3. What colour? — Register $D027 sets sprite 0’s colour. The C64 has 16 colours (0–15). White is 1.

  4. Is it switched on? — Register $D015 is the sprite enable register. Each bit controls one sprite. Bit 0 = sprite 0, bit 1 = sprite 1, and so on up to 7. Set the bit to 1 to make the sprite visible.

$D015 — Sprite Enable 7 Spr 7 0 6 Spr 6 0 5 Spr 5 0 4 Spr 4 0 3 Spr 3 0 2 Spr 2 0 1 Spr 1 0 0 Spr 0 1
Setting bit 0 enables sprite 0 — the only sprite we use in this unit.

Sprite Data

A C64 sprite is 24 pixels wide and 21 pixels tall. Each row is stored as 3 bytes (24 bits), making 63 bytes per sprite. With one padding byte, that’s 64 bytes — which is why sprite data must sit at 64-byte boundaries in memory.

Each bit represents one pixel. Bit 7 (leftmost) of the first byte is the leftmost pixel in that row. A 1 bit draws the pixel in the sprite’s colour; a 0 bit is transparent.

$00 $18 $00 $00 $3C $00 $00 $3C $00 $00 $7E $00 $00 $7E $00 $00 $FF $00 $00 $FF $00 $01 $FF $80 $03 $FF $C0 $07 $FF $E0 $07 $FF $E0 $07 $E7 $E0 $03 $C3 $C0 $01 $FF $80 $00 $FF $00 $00 $FF $00 $00 $DB $00 $00 $DB $00 $00 $66 $00 $00 $24 $00 $00 $00 $00
The ship sprite — each filled cell is a 1 bit, each dark cell is a 0. The blue lines mark byte boundaries.

The hex values on the left correspond to the three !byte values in each row of the code below:

; Sprite data: 24 pixels wide x 21 rows
; Each row is 3 bytes. Bit 7 of each byte = leftmost pixel.
; Total: 63 bytes (+ 1 padding byte = 64-byte alignment)

*= $2000
        ;                       Pixels
        !byte $00,$18,$00   ;        ##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$7e,$00   ;      ######
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $03,$ff,$c0   ;   ############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$e7,$e0   ;  ###..####..###
        !byte $03,$c3,$c0   ;   ##....##....##
        !byte $01,$ff,$80   ;    ##########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$24,$00   ;       #..#
        !byte $00,$00,$00   ;

This data draws a pointed ship with flared wings and engine exhausts.

$D000 $D02E VIC-II registers Sprites, colours, screen $2040 $2000 $203F Sprite data 64 bytes (block 128) $0900 $080D $08FF Program code $0801 $080C BASIC stub SYS 2061 $0800 $07F8 $07FF Sprite pointers Block numbers $07E8 $0400 $07E7 Screen RAM 40×25 characters
Key memory regions used by the ship-on-screen program. Addresses are not to scale.

The Complete Code

; Starfield - Unit 1: Ship on Screen
; Assemble with: acme -f cbm -o starfield.prg starfield.asm

; ------------------------------------------------
; BASIC stub — launches machine code with SYS 2061
; ------------------------------------------------
*= $0801
!byte $0c,$08,$0a,$00,$9e,$32,$30,$36,$31,$00,$00,$00

; ------------------------------------------------
; Main program
; ------------------------------------------------
*= $080d
        ; Black screen
        lda #$00
        sta $d020           ; Border colour
        sta $d021           ; Background colour

        ; Clear the screen (fill with spaces)
        ldx #$00
-       lda #$20            ; Space character
        sta $0400,x
        sta $0500,x
        sta $0600,x
        sta $0700,x
        inx
        bne -

        ; Tell VIC-II where sprite 0's graphic data lives
        ; Block number = address / 64.  $2000 / 64 = 128.
        lda #128
        sta $07f8           ; Sprite 0 data pointer

        ; Position sprite 0 near centre-bottom of screen
        lda #172            ; X position
        sta $d000           ; Sprite 0 X position
        lda #220            ; Y position
        sta $d001           ; Sprite 0 Y position

        ; Set sprite 0 colour to white
        lda #$01
        sta $d027           ; Sprite 0 colour

        ; Enable sprite 0
        lda #%00000001      ; Bit 0 = sprite 0
        sta $d015           ; Sprite enable register

        ; Loop forever
-       jmp -

; ------------------------------------------------
; Sprite data at $2000 (block 128)
; 24 pixels wide x 21 rows = 63 bytes
; Each row: 3 bytes, bit 7 = leftmost pixel
; ------------------------------------------------
*= $2000
        ;                       Pixels
        !byte $00,$18,$00   ;        ##
        !byte $00,$3c,$00   ;       ####
        !byte $00,$3c,$00   ;       ####
        !byte $00,$7e,$00   ;      ######
        !byte $00,$7e,$00   ;      ######
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $01,$ff,$80   ;    ##########
        !byte $03,$ff,$c0   ;   ############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$ff,$e0   ;  ##############
        !byte $07,$e7,$e0   ;  ###..####..###
        !byte $03,$c3,$c0   ;   ##....##....##
        !byte $01,$ff,$80   ;    ##########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$ff,$00   ;     ########
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$db,$00   ;     ##.##.##
        !byte $00,$66,$00   ;      ##..##
        !byte $00,$24,$00   ;       #..#
        !byte $00,$00,$00   ;

If It Doesn’t Work

  • No sprite? Check $D015 — it must have bit 0 set. A value of $00 means all sprites are off.
  • Wrong position? $D000 is X (horizontal), $D001 is Y (vertical). Easy to swap them.
  • Wrong colour? Sprite colour registers go from $D027 (sprite 0) to $D02E (sprite 7). Values 0–15.
  • Garbled sprite? The pointer at $07F8 must match where you placed the data. Block 128 = address $2000. If you move the data, update the pointer.
  • Screen not black? Make sure you’re writing to both $D020 (border) and $D021 (background).

Try This: Change the Colour

Find this line:

        lda #$01            ; White
        sta $d027

Change $01 to any value from 0 to 15:

ValueColour
$00Black
$01White
$02Red
$03Cyan
$05Green
$07Yellow
$0aLight red
$0eLight blue

Reassemble and reload. Your ship changes colour instantly.

Try This: Move the Ship

Change the X and Y position values:

        lda #172            ; Try 24, 160, or 255
        sta $d000
        lda #220            ; Try 50, 150, or 230
        sta $d001

Notice that X only goes up to 255 — but the screen is 320 pixels wide. For positions beyond 255, you’ll need an extra register ($D010). We’ll tackle that in a later unit.

Try This: Design Your Own Sprite

Click pixels to draw, then copy the output straight into your code. The byte values update as you draw.

Output
Click to draw, copy the bytes into your sprite data section

What You’ve Learnt

  • LDA and STA — Load a value into the accumulator, store it to a memory address. These two instructions control the entire machine.
  • Memory-mapped I/O — The VIC-II chip responds to writes at addresses $D000$D02E. No function calls, no APIs — just poke values into memory.
  • Sprite enable register ($D015) — Each bit enables one of eight hardware sprites. Bit 0 = sprite 0.
  • Sprite pointer ($07F8) — Tells the VIC-II where in memory to find the sprite’s pixel data. Value × 64 = address.
  • Sprite data format — 24 × 21 pixels, 3 bytes per row, 63 bytes total. One bit per pixel.
  • The BASIC stub — A tiny BASIC program that launches your machine code with SYS.

What’s Next

The ship sits still. In Unit 2, you’ll read the joystick and make it move — left, right, up, down — using the CIA chip and your first conditional branches.