Skip to content
Game 1 Unit 6 of 64 1 hr learning time

The Blitter Introduction

Why 8 sprites isn't enough. DMA graphics engine. BOBs for cars.

9% of Signal

What You’re Building

The Blitter.

Hardware sprites are wonderful—automatic, fast, zero CPU cost. But the Amiga only has 8 of them. A Frogger game needs dozens of moving objects: five lanes of cars, five lanes of logs and turtles, plus the frog. Eight sprites won’t cut it.

The solution? The Blitter—a DMA coprocessor that copies rectangular blocks of memory at blazing speed.

By the end of this unit:

  • One car drives across the road
  • Drawn using Blitter, not sprites
  • You understand BOBs vs sprites
  • Foundation for many moving objects

Unit 6 Screenshot

Why Not Just More Sprites?

The Amiga’s 8 hardware sprites have limitations:

  • Only 16 pixels wide (though they can be any height)
  • 3 colours each (plus transparent)
  • Multiplexing is tricky (reusing sprites mid-screen)

For a game with many similar objects moving horizontally, the Blitter is often easier.

What Is the Blitter?

The Block Image Transferrer is a DMA coprocessor. It copies rectangular regions of memory without CPU involvement. While the Blitter works, the CPU can do other things.

Key capabilities:

  • Copy graphics from one place to another
  • Combine multiple sources using logic operations
  • Shift data for pixel-precise positioning
  • Fill areas with solid colour
  • Draw lines (yes, hardware line drawing!)

We’ll start with simple copying.

Setting Up Bitplanes

Unlike our sprite-only previous units, we now need screen memory:

SCREEN_W        equ 40          ; 320 pixels = 40 bytes per line
SCREEN_H        equ 256         ; PAL lines
PLANE_SIZE      equ SCREEN_W*SCREEN_H   ; 10240 bytes

; In chip BSS section
screen_plane:   ds.b    PLANE_SIZE

The Copper points at this memory, and the Blitter draws into it.

Blitter Registers

The Blitter has four channels: A, B, C, and D (destination). For a simple copy, we use A (source) and D (destination):

RegisterPurpose
BLTCON0Control: which channels, what operation
BLTCON1Control: shift amount, special modes
BLTAPTHSource A pointer (high word)
BLTDPTHDestination D pointer (high word)
BLTAMODSource A modulo (bytes to skip per row)
BLTDMODDestination D modulo
BLTSIZESize and trigger—writing here starts the blit

A Simple Copy

To copy our car graphics to the screen:

; Draw Car with Blitter
; A→D copy: source graphics to screen

draw_car:
            bsr     wait_blit

            ; Calculate screen destination
            move.w  car_x,d0
            move.w  #CAR_ROW,d1
            bsr     calc_screen_addr        ; A0 = destination

            ; Set up channels
            move.l  #car_gfx,BLTAPTH(a5)    ; A = source graphics
            move.l  a0,BLTDPTH(a5)          ; D = screen

            ; Masks: no masking needed (full words)
            move.w  #$ffff,BLTAFWM(a5)
            move.w  #$ffff,BLTALWM(a5)

            ; Modulos
            move.w  #0,BLTAMOD(a5)                      ; Source: no gaps
            move.w  #SCREEN_W-CAR_WIDTH*2,BLTDMOD(a5)   ; Dest: screen stride

            ; BLTCON0: A→D copy, D=A minterm
            ; $09f0 = use A+D channels, minterm F0 (copy A)
            move.w  #$09f0,BLTCON0(a5)
            move.w  #0,BLTCON1(a5)

            ; Size: 12 lines × 2 words wide
            move.w  #(CAR_HEIGHT<<6)|CAR_WIDTH,BLTSIZE(a5)

            rts

Understanding BLTCON0

The value $09f0 breaks down as:

$09f0 = %0000 1001 1111 0000
              │  │ ││││
              │  │ └┴┴┴─ Minterm: $F0 = D = A
              │  └────── USEA: Use channel A
              └───────── USED: Use channel D (always for output)

The minterm is a 4-bit pattern that specifies how to combine A, B, and C. $F0 means “D = A”—just copy A to D.

Understanding BLTSIZE

move.w  #(CAR_HEIGHT<<6)|CAR_WIDTH,BLTSIZE(a5)

BLTSIZE encodes both dimensions:

  • Bits 15-6: Height in lines (max 1024)
  • Bits 5-0: Width in words (max 64 words = 1024 pixels)

Writing to BLTSIZE triggers the blit immediately.

Waiting for the Blitter

The Blitter runs asynchronously. Before starting a new blit or reading results, we must wait:

wait_blit:
    btst    #6,DMACONR(a5)      ; Test BBUSY flag
    bne.s   wait_blit           ; Loop while busy
    rts

Bit 6 of DMACONR is the “blitter busy” flag.

Erasing the Car

Before drawing at the new position, we erase at the old position. We use the clear_screen approach which uses the Blitter with minterm 0:

; Clear Screen with Blitter
; D channel only, minterm 0 = all zeros

clear_screen:
            bsr     wait_blit           ; Must wait before setting up

            ; Destination pointer
            move.l  #screen_plane,BLTDPTH(a5)

            ; Modulo: 0 = continuous (no gaps between rows)
            move.w  #0,BLTDMOD(a5)

            ; BLTCON0: D channel only, minterm 0 (clear)
            ; $0100 = use D, minterms all 0
            move.w  #$0100,BLTCON0(a5)
            move.w  #0,BLTCON1(a5)

            ; Size: trigger the blit!
            ; Format: [height << 6] | [width_in_words]
            ; 256 lines × 20 words = full PAL screen
            move.w  #(256<<6)|20,BLTSIZE(a5)

            rts

Minterm $00 (via BLTCON0 = $0100) means “D = 0”—clear the destination.

The Animation Loop

Each frame:

  1. Wait for VBlank
  2. Erase car at old position
  3. Update car position
  4. Draw car at new position
mainloop:
    bsr     wait_vblank
    bsr     update_frog
    bsr     update_sprite

    bsr     erase_car       ; Clear old
    bsr     move_car        ; Update position
    bsr     draw_car        ; Draw new

    bra     mainloop

BOBs vs Sprites

What we’ve created is called a BOB (Blitter Object):

FeatureHardware SpriteBOB
Count8 maximumUnlimited*
Width16 pixelsAny size
Colours3 (+transparent)As many as bitplanes allow
CPU costZeroLow (Blitter DMA)
ErasingAutomaticMust be done manually
Behind playfieldEasyRequires masking

*Limited by Blitter time per frame

Enabling Blitter DMA

We added blitter DMA to our DMACON setup:

move.w  #$87e0,DMACON(a5)   ; Master + blit + copper + bitplane + sprite

Bit 6 (BLTEN) enables Blitter DMA.

Key Takeaways

  • 8 sprites isn’t enough for games with many objects
  • The Blitter copies rectangular blocks via DMA
  • BOBs are objects drawn by the Blitter
  • BLTCON0 controls which channels and what operation
  • BLTSIZE encodes size and triggers the blit
  • Always wait for the Blitter before starting another blit

The Code

;══════════════════════════════════════════════════════════════════════════════
; SIGNAL - A Frogger-style game for the Commodore Amiga
; Unit 6: The Blitter Introduction
;
; Hardware sprites are limited to 8. A Frogger game needs dozens of moving
; objects: cars, trucks, logs, turtles. The solution? The Blitter—a DMA
; coprocessor that copies rectangular blocks of memory at high speed.
;
; This unit introduces the Blitter with a simple demonstration: one car
; driving across the road, drawn using the Blitter instead of sprites.
;══════════════════════════════════════════════════════════════════════════════

;══════════════════════════════════════════════════════════════════════════════
; CONSTANTS
;══════════════════════════════════════════════════════════════════════════════

; Screen setup
SCREEN_W        equ 40          ; 320 pixels = 40 bytes per line
SCREEN_H        equ 256         ; PAL lines
PLANE_SIZE      equ SCREEN_W*SCREEN_H   ; 10240 bytes per bitplane

; Grid constants (from Unit 5)
GRID_COLS       equ 20
GRID_ROWS       equ 13
CELL_SIZE       equ 16
GRID_ORIGIN_X   equ 48
GRID_ORIGIN_Y   equ 44
START_GRID_X    equ 9
START_GRID_Y    equ 12

; Animation
HOP_FRAMES      equ 8
PIXELS_PER_FRAME equ 2
STATE_IDLE      equ 0
STATE_HOPPING   equ 1
DIR_UP          equ 0
DIR_DOWN        equ 1
DIR_LEFT        equ 2
DIR_RIGHT       equ 3
FROG_HEIGHT     equ 16

; Car constants
CAR_WIDTH       equ 2           ; 32 pixels = 2 words
CAR_HEIGHT      equ 12          ; 12 lines tall
CAR_SPEED       equ 2           ; Pixels per frame
CAR_ROW         equ 10          ; Road lane (row 10 = scanline 204)

; Zone colours
COLOUR_BLACK    equ $0000
COLOUR_HOME     equ $0282
COLOUR_HOME_LIT equ $03a3
COLOUR_WATER1   equ $0148
COLOUR_WATER2   equ $026a
COLOUR_MEDIAN   equ $0383
COLOUR_ROAD1    equ $0333
COLOUR_ROAD2    equ $0444
COLOUR_START    equ $0262
COLOUR_START_LIT equ $0373

; Sprite and playfield colours
COLOUR_FROG     equ $00f0
COLOUR_EYES     equ $0ff0
COLOUR_OUTLINE  equ $0000
COLOUR_CAR      equ $0f00       ; Red car on bitplane

;══════════════════════════════════════════════════════════════════════════════
; HARDWARE REGISTERS
;══════════════════════════════════════════════════════════════════════════════

CUSTOM      equ $dff000

DMACONR     equ $002
VPOSR       equ $004
JOY1DAT     equ $00c

; Blitter registers
BLTCON0     equ $040
BLTCON1     equ $042
BLTAFWM     equ $044
BLTALWM     equ $046
BLTCPTH     equ $048
BLTBPTH     equ $04c
BLTAPTH     equ $050
BLTDPTH     equ $054
BLTSIZE     equ $058
BLTCMOD     equ $060
BLTBMOD     equ $062
BLTAMOD     equ $064
BLTDMOD     equ $066

COP1LC      equ $080
COPJMP1     equ $088
DMACON      equ $096
INTENA      equ $09a
INTREQ      equ $09c
COLOR00     equ $180
SPR0PTH     equ $120
SPR0PTL     equ $122

;══════════════════════════════════════════════════════════════════════════════
; CODE SECTION
;══════════════════════════════════════════════════════════════════════════════

            section code,code_c

start:
            lea     CUSTOM,a5

            ; --- System takeover ---
            move.w  #$7fff,INTENA(a5)
            move.w  #$7fff,INTREQ(a5)
            move.w  #$7fff,DMACON(a5)

            ; --- Clear screen ---
            bsr     clear_screen

            ; --- Set up bitplane pointer in copper list ---
            lea     screen_plane,a0
            move.l  a0,d0
            swap    d0
            lea     bplpth_val,a1
            move.w  d0,(a1)
            swap    d0
            lea     bplptl_val,a1
            move.w  d0,(a1)

            ; --- Initialise frog ---
            move.w  #START_GRID_X,frog_grid_x
            move.w  #START_GRID_Y,frog_grid_y
            move.w  #STATE_IDLE,frog_state
            clr.w   joy_prev
            bsr     grid_to_pixels

            ; --- Set sprite pointer ---
            lea     frog_data,a0
            move.l  a0,d0
            swap    d0
            lea     sprpth_val,a1
            move.w  d0,(a1)
            swap    d0
            lea     sprptl_val,a1
            move.w  d0,(a1)

            bsr     update_sprite

            ; --- Initialise car ---
            clr.w   car_x

            ; --- Install copper list ---
            lea     copperlist,a0
            move.l  a0,COP1LC(a5)
            move.w  d0,COPJMP1(a5)

            ; --- Enable DMA (including Blitter) ---
            move.w  #$87e0,DMACON(a5)   ; Master + blit + copper + bitplane + sprite

;══════════════════════════════════════════════════════════════════════════════
; MAIN LOOP
;══════════════════════════════════════════════════════════════════════════════

mainloop:
            bsr     wait_vblank

            ; --- Update frog ---
            bsr     update_frog
            bsr     update_sprite

            ; --- Update car ---
            bsr     erase_car           ; Erase at old position
            bsr     move_car            ; Update position
            bsr     draw_car            ; Draw at new position

            bra     mainloop

;══════════════════════════════════════════════════════════════════════════════
; BLITTER ROUTINES
;══════════════════════════════════════════════════════════════════════════════

;------------------------------------------------------------------------------
; WAIT_BLIT - Wait for Blitter to finish
;------------------------------------------------------------------------------
wait_blit:
            btst    #6,DMACONR(a5)      ; Test BBUSY flag
            bne.s   wait_blit           ; Loop while busy
            rts

;------------------------------------------------------------------------------
; CLEAR_SCREEN - Clear the screen buffer using Blitter
;------------------------------------------------------------------------------
clear_screen:
            bsr.s   wait_blit

            ; Clear entire plane: D channel only, minterm = 0
            move.l  #screen_plane,BLTDPTH(a5)
            move.w  #0,BLTDMOD(a5)      ; No modulo (continuous)
            move.w  #$0100,BLTCON0(a5)  ; D only, minterm 0 (clear)
            move.w  #0,BLTCON1(a5)
            move.w  #(SCREEN_H<<6)|SCREEN_W/2,BLTSIZE(a5)   ; Trigger!

            rts

;------------------------------------------------------------------------------
; ERASE_CAR - Clear car at current position
;------------------------------------------------------------------------------
erase_car:
            bsr.s   wait_blit

            ; Calculate screen address for car position
            move.w  car_x,d0
            move.w  #CAR_ROW,d1
            bsr     calc_screen_addr    ; A0 = destination

            ; Erase: D channel only, minterm 0
            move.l  a0,BLTDPTH(a5)
            move.w  #SCREEN_W-CAR_WIDTH*2,BLTDMOD(a5)
            move.w  #$0100,BLTCON0(a5)
            move.w  #0,BLTCON1(a5)
            move.w  #(CAR_HEIGHT<<6)|CAR_WIDTH,BLTSIZE(a5)

            rts

;------------------------------------------------------------------------------
; DRAW_CAR - Draw car at current position using Blitter
;------------------------------------------------------------------------------
draw_car:
            bsr.s   wait_blit

            ; Calculate screen address
            move.w  car_x,d0
            move.w  #CAR_ROW,d1
            bsr     calc_screen_addr    ; A0 = destination

            ; Copy A→D: source graphics to screen
            move.l  #car_gfx,BLTAPTH(a5)
            move.l  a0,BLTDPTH(a5)

            move.w  #$ffff,BLTAFWM(a5)  ; No masking
            move.w  #$ffff,BLTALWM(a5)
            move.w  #0,BLTAMOD(a5)      ; Source: no gaps
            move.w  #SCREEN_W-CAR_WIDTH*2,BLTDMOD(a5)    ; Dest: screen width
            move.w  #$09f0,BLTCON0(a5)  ; A→D, D=A
            move.w  #0,BLTCON1(a5)
            move.w  #(CAR_HEIGHT<<6)|CAR_WIDTH,BLTSIZE(a5)  ; Trigger!

            rts

;------------------------------------------------------------------------------
; CALC_SCREEN_ADDR - Calculate screen address from grid position
;------------------------------------------------------------------------------
; Input: D0 = pixel X, D1 = row number
; Output: A0 = screen address

calc_screen_addr:
            lea     screen_plane,a0

            ; Calculate Y offset: row × CELL_SIZE × SCREEN_W
            move.w  d1,d2
            mulu    #CELL_SIZE,d2       ; D2 = row in pixels (from top of grid)
            add.w   #GRID_ORIGIN_Y,d2   ; Add top margin
            mulu    #SCREEN_W,d2        ; D2 = byte offset for Y
            add.l   d2,a0

            ; Calculate X offset: pixel_x / 8 (convert to bytes)
            move.w  d0,d2
            lsr.w   #3,d2               ; D2 = X in bytes
            ext.l   d2
            add.l   d2,a0

            rts

;------------------------------------------------------------------------------
; MOVE_CAR - Update car position
;------------------------------------------------------------------------------
move_car:
            add.w   #CAR_SPEED,car_x
            cmp.w   #320,car_x
            blt.s   .no_wrap
            clr.w   car_x
.no_wrap:
            rts

;══════════════════════════════════════════════════════════════════════════════
; FROG ROUTINES (from Unit 5)
;══════════════════════════════════════════════════════════════════════════════

update_frog:
            tst.w   frog_state
            beq     .idle
            bra     .hopping

.idle:
            bsr     read_joystick_edge
            tst.w   d0
            beq     .done

            btst    #8,d0
            beq.s   .not_up
            move.w  frog_grid_y,d1
            tst.w   d1
            beq.s   .not_up
            move.w  #DIR_UP,frog_dir
            bra.s   .start_hop
.not_up:
            btst    #0,d0
            beq.s   .not_down
            move.w  frog_grid_y,d1
            cmp.w   #GRID_ROWS-1,d1
            beq.s   .not_down
            move.w  #DIR_DOWN,frog_dir
            bra.s   .start_hop
.not_down:
            btst    #9,d0
            beq.s   .not_left
            move.w  frog_grid_x,d1
            tst.w   d1
            beq.s   .not_left
            move.w  #DIR_LEFT,frog_dir
            bra.s   .start_hop
.not_left:
            btst    #1,d0
            beq     .done
            move.w  frog_grid_x,d1
            cmp.w   #GRID_COLS-1,d1
            beq     .done
            move.w  #DIR_RIGHT,frog_dir

.start_hop:
            move.w  #STATE_HOPPING,frog_state
            clr.w   frog_anim_frame
            bra.s   .done

.hopping:
            addq.w  #1,frog_anim_frame
            move.w  frog_dir,d0

            cmp.w   #DIR_UP,d0
            bne.s   .hop_not_up
            sub.w   #PIXELS_PER_FRAME,frog_pixel_y
            bra.s   .check_done
.hop_not_up:
            cmp.w   #DIR_DOWN,d0
            bne.s   .hop_not_down
            add.w   #PIXELS_PER_FRAME,frog_pixel_y
            bra.s   .check_done
.hop_not_down:
            cmp.w   #DIR_LEFT,d0
            bne.s   .hop_not_left
            sub.w   #PIXELS_PER_FRAME,frog_pixel_x
            bra.s   .check_done
.hop_not_left:
            add.w   #PIXELS_PER_FRAME,frog_pixel_x

.check_done:
            cmp.w   #HOP_FRAMES,frog_anim_frame
            blt.s   .done

            move.w  frog_dir,d0
            cmp.w   #DIR_UP,d0
            bne.s   .end_not_up
            subq.w  #1,frog_grid_y
            bra.s   .snap
.end_not_up:
            cmp.w   #DIR_DOWN,d0
            bne.s   .end_not_down
            addq.w  #1,frog_grid_y
            bra.s   .snap
.end_not_down:
            cmp.w   #DIR_LEFT,d0
            bne.s   .end_not_left
            subq.w  #1,frog_grid_x
            bra.s   .snap
.end_not_left:
            addq.w  #1,frog_grid_x

.snap:
            bsr     grid_to_pixels
            move.w  #STATE_IDLE,frog_state

.done:
            rts

grid_to_pixels:
            move.w  frog_grid_x,d0
            mulu    #CELL_SIZE,d0
            add.w   #GRID_ORIGIN_X,d0
            move.w  d0,frog_pixel_x

            move.w  frog_grid_y,d0
            mulu    #CELL_SIZE,d0
            add.w   #GRID_ORIGIN_Y,d0
            move.w  d0,frog_pixel_y
            rts

read_joystick_edge:
            move.w  JOY1DAT(a5),d0
            move.w  d0,d1
            lsr.w   #1,d1
            eor.w   d1,d0

            move.w  joy_prev,d1
            not.w   d1
            and.w   d0,d1
            move.w  d0,joy_prev

            move.w  d1,d0
            rts

;══════════════════════════════════════════════════════════════════════════════
; UTILITY ROUTINES
;══════════════════════════════════════════════════════════════════════════════

wait_vblank:
            move.l  #$1ff00,d1
.wait:
            move.l  VPOSR(a5),d0
            and.l   d1,d0
            bne.s   .wait
            rts

update_sprite:
            lea     frog_data,a0
            move.w  frog_pixel_y,d0
            move.w  frog_pixel_x,d1

            move.w  d0,d2
            lsl.w   #8,d2
            lsr.w   #1,d1
            or.b    d1,d2
            move.w  d2,(a0)

            add.w   #FROG_HEIGHT,d0
            lsl.w   #8,d0
            move.w  d0,2(a0)
            rts

;══════════════════════════════════════════════════════════════════════════════
; VARIABLES
;══════════════════════════════════════════════════════════════════════════════

frog_grid_x:    dc.w    9
frog_grid_y:    dc.w    12
frog_pixel_x:   dc.w    0
frog_pixel_y:   dc.w    0
frog_state:     dc.w    0
frog_dir:       dc.w    0
frog_anim_frame: dc.w   0
joy_prev:       dc.w    0

car_x:          dc.w    0

;══════════════════════════════════════════════════════════════════════════════
; COPPER LIST
;══════════════════════════════════════════════════════════════════════════════

copperlist:
            dc.w    COLOR00,COLOUR_BLACK

            ; --- Bitplane control: 1 plane, colour on ---
            dc.w    $0100,$1200         ; BPLCON0: 1 plane
            dc.w    $0102,$0000         ; BPLCON1: no scroll
            dc.w    $0104,$0000         ; BPLCON2: default priority
            dc.w    $0108,$0000         ; BPL1MOD
            dc.w    $010a,$0000         ; BPL2MOD

            ; --- Bitplane pointer ---
            dc.w    $00e0               ; BPL1PTH
bplpth_val: dc.w    $0000
            dc.w    $00e2               ; BPL1PTL
bplptl_val: dc.w    $0000

            ; --- Playfield colours ---
            dc.w    $0180,$0000         ; Colour 0: transparent (shows Copper bg)
            dc.w    $0182,COLOUR_CAR    ; Colour 1: red (car from bitplane)

            ; --- Sprite palette ---
            dc.w    $01a2,COLOUR_FROG
            dc.w    $01a4,COLOUR_EYES
            dc.w    $01a6,COLOUR_OUTLINE

            ; --- Sprite 0 pointer ---
            dc.w    SPR0PTH
sprpth_val: dc.w    $0000
            dc.w    SPR0PTL
sprptl_val: dc.w    $0000

            ; ROW 1: HOME ZONE
            dc.w    $2c07,$fffe
            dc.w    COLOR00,COLOUR_HOME
            dc.w    $3407,$fffe
            dc.w    COLOR00,COLOUR_HOME_LIT
            dc.w    $3807,$fffe
            dc.w    COLOR00,COLOUR_HOME

            ; ROWS 2-6: WATER ZONE
            dc.w    $3c07,$fffe
            dc.w    COLOR00,COLOUR_WATER1
            dc.w    $4c07,$fffe
            dc.w    COLOR00,COLOUR_WATER2
            dc.w    $5c07,$fffe
            dc.w    COLOR00,COLOUR_WATER1
            dc.w    $6c07,$fffe
            dc.w    COLOR00,COLOUR_WATER2
            dc.w    $7c07,$fffe
            dc.w    COLOR00,COLOUR_WATER1

            ; ROW 7: MEDIAN
            dc.w    $8c07,$fffe
            dc.w    COLOR00,COLOUR_MEDIAN

            ; ROWS 8-12: ROAD ZONE
            dc.w    $9c07,$fffe
            dc.w    COLOR00,COLOUR_ROAD1
            dc.w    $ac07,$fffe
            dc.w    COLOR00,COLOUR_ROAD2
            dc.w    $bc07,$fffe
            dc.w    COLOR00,COLOUR_ROAD1
            dc.w    $cc07,$fffe
            dc.w    COLOR00,COLOUR_ROAD2
            dc.w    $dc07,$fffe
            dc.w    COLOR00,COLOUR_ROAD1

            ; ROW 13: START ZONE
            dc.w    $ec07,$fffe
            dc.w    COLOR00,COLOUR_START
            dc.w    $f407,$fffe
            dc.w    COLOR00,COLOUR_START_LIT
            dc.w    $f807,$fffe
            dc.w    COLOR00,COLOUR_START

            ; BOTTOM BORDER
            dc.w    $fc07,$fffe
            dc.w    COLOR00,COLOUR_BLACK

            dc.w    $ffff,$fffe

;══════════════════════════════════════════════════════════════════════════════
; GRAPHICS DATA
;══════════════════════════════════════════════════════════════════════════════

            even
car_gfx:
            ; 32×12 pixel car (2 words wide × 12 lines)
            dc.w    $0ff0,$0ff0         ; ..####....####..
            dc.w    $3ffc,$3ffc         ; ##########..####
            dc.w    $7ffe,$7ffe         ; ##############..
            dc.w    $ffff,$ffff         ; ################
            dc.w    $ffff,$ffff         ; ################
            dc.w    $ffff,$ffff         ; ################
            dc.w    $ffff,$ffff         ; ################
            dc.w    $ffff,$ffff         ; ################
            dc.w    $ffff,$ffff         ; ################
            dc.w    $7ffe,$7ffe         ; ##############..
            dc.w    $3c3c,$3c3c         ; Wheels
            dc.w    $0000,$0000

;══════════════════════════════════════════════════════════════════════════════
; SPRITE DATA
;══════════════════════════════════════════════════════════════════════════════

            even
frog_data:
            dc.w    $ec50,$fc00

            dc.w    $0000,$0000
            dc.w    $07e0,$0000
            dc.w    $1ff8,$0420
            dc.w    $3ffc,$0a50
            dc.w    $7ffe,$1248
            dc.w    $7ffe,$1008
            dc.w    $ffff,$2004
            dc.w    $ffff,$0000
            dc.w    $ffff,$0000
            dc.w    $7ffe,$2004
            dc.w    $7ffe,$1008
            dc.w    $3ffc,$0810
            dc.w    $1ff8,$0420
            dc.w    $07e0,$0000
            dc.w    $0000,$0000
            dc.w    $0000,$0000

            dc.w    $0000,$0000

;══════════════════════════════════════════════════════════════════════════════
; SCREEN BUFFER (Chip RAM)
;══════════════════════════════════════════════════════════════════════════════

            section chipbss,bss_c

            even
screen_plane:
            ds.b    PLANE_SIZE

Build It

vasmm68k_mot -Fhunkexe -kick1hunks -o signal signal.asm

What’s Next

We have one car. In Unit 7, we’ll add collision detection—the frog needs to die when it hits the car!

What Changed

Unit 5 → Unit 6
+269-114
11 ;══════════════════════════════════════════════════════════════════════════════
22 ; SIGNAL - A Frogger-style game for the Commodore Amiga
3-; Unit 5: Grid-Based Movement
3+; Unit 6: The Blitter Introduction
44 ;
5-; Real Frogger doesn't glide—it hops. Each press moves the frog exactly one
6-; grid cell, with smooth animation over 8 frames. This unit implements that
7-; distinctive movement feel using a simple state machine.
5+; Hardware sprites are limited to 8. A Frogger game needs dozens of moving
6+; objects: cars, trucks, logs, turtles. The solution? The Blitter—a DMA
7+; coprocessor that copies rectangular blocks of memory at high speed.
8+;
9+; This unit introduces the Blitter with a simple demonstration: one car
10+; driving across the road, drawn using the Blitter instead of sprites.
811 ;══════════════════════════════════════════════════════════════════════════════
912
1013 ;══════════════════════════════════════════════════════════════════════════════
11-; GRID CONSTANTS
14+; CONSTANTS
1215 ;══════════════════════════════════════════════════════════════════════════════
13-
14-; Grid dimensions
15-GRID_COLS equ 20 ; 20 columns (320 ÷ 16)
16-GRID_ROWS equ 13 ; 13 rows (matching our playfield)
17-CELL_SIZE equ 16 ; Each cell is 16×16 pixels
18-
19-; Grid origin (pixel coordinates of top-left cell)
20-GRID_ORIGIN_X equ 48 ; Left margin
21-GRID_ORIGIN_Y equ 44 ; Top of playfield
22-
23-; Starting position (grid coordinates)
24-START_GRID_X equ 9 ; Middle column (0-19)
25-START_GRID_Y equ 12 ; Bottom row (0-12)
2616
27-; Animation timing
28-HOP_FRAMES equ 8 ; Animation lasts 8 frames
29-PIXELS_PER_FRAME equ 2 ; 2 pixels per frame × 8 frames = 16 pixels
17+; Screen setup
18+SCREEN_W equ 40 ; 320 pixels = 40 bytes per line
19+SCREEN_H equ 256 ; PAL lines
20+PLANE_SIZE equ SCREEN_W*SCREEN_H ; 10240 bytes per bitplane
3021
31-; Movement states
32-STATE_IDLE equ 0 ; Waiting for input
33-STATE_HOPPING equ 1 ; Currently animating a hop
22+; Grid constants (from Unit 5)
23+GRID_COLS equ 20
24+GRID_ROWS equ 13
25+CELL_SIZE equ 16
26+GRID_ORIGIN_X equ 48
27+GRID_ORIGIN_Y equ 44
28+START_GRID_X equ 9
29+START_GRID_Y equ 12
3430
35-; Direction codes
31+; Animation
32+HOP_FRAMES equ 8
33+PIXELS_PER_FRAME equ 2
34+STATE_IDLE equ 0
35+STATE_HOPPING equ 1
3636 DIR_UP equ 0
3737 DIR_DOWN equ 1
3838 DIR_LEFT equ 2
3939 DIR_RIGHT equ 3
40-
4140 FROG_HEIGHT equ 16
41+
42+; Car constants
43+CAR_WIDTH equ 2 ; 32 pixels = 2 words
44+CAR_HEIGHT equ 12 ; 12 lines tall
45+CAR_SPEED equ 2 ; Pixels per frame
46+CAR_ROW equ 10 ; Road lane (row 10 = scanline 204)
4247
4348 ; Zone colours
4449 COLOUR_BLACK equ $0000
...
5257 COLOUR_START equ $0262
5358 COLOUR_START_LIT equ $0373
5459
55-; Sprite palette
60+; Sprite and playfield colours
5661 COLOUR_FROG equ $00f0
5762 COLOUR_EYES equ $0ff0
5863 COLOUR_OUTLINE equ $0000
64+COLOUR_CAR equ $0f00 ; Red car on bitplane
5965
6066 ;══════════════════════════════════════════════════════════════════════════════
6167 ; HARDWARE REGISTERS
...
6470 CUSTOM equ $dff000
6571
6672 DMACONR equ $002
67-DMACON equ $096
68-INTENA equ $09a
69-INTREQ equ $09c
7073 VPOSR equ $004
7174 JOY1DAT equ $00c
75+
76+; Blitter registers
77+BLTCON0 equ $040
78+BLTCON1 equ $042
79+BLTAFWM equ $044
80+BLTALWM equ $046
81+BLTCPTH equ $048
82+BLTBPTH equ $04c
83+BLTAPTH equ $050
84+BLTDPTH equ $054
85+BLTSIZE equ $058
86+BLTCMOD equ $060
87+BLTBMOD equ $062
88+BLTAMOD equ $064
89+BLTDMOD equ $066
90+
7291 COP1LC equ $080
7392 COPJMP1 equ $088
93+DMACON equ $096
94+INTENA equ $09a
95+INTREQ equ $09c
7496 COLOR00 equ $180
7597 SPR0PTH equ $120
7698 SPR0PTL equ $122
...
89111 move.w #$7fff,INTREQ(a5)
90112 move.w #$7fff,DMACON(a5)
91113
92- ; --- Initialise game state ---
114+ ; --- Clear screen ---
115+ bsr clear_screen
116+
117+ ; --- Set up bitplane pointer in copper list ---
118+ lea screen_plane,a0
119+ move.l a0,d0
120+ swap d0
121+ lea bplpth_val,a1
122+ move.w d0,(a1)
123+ swap d0
124+ lea bplptl_val,a1
125+ move.w d0,(a1)
126+
127+ ; --- Initialise frog ---
93128 move.w #START_GRID_X,frog_grid_x
94129 move.w #START_GRID_Y,frog_grid_y
95130 move.w #STATE_IDLE,frog_state
96- move.w #0,frog_anim_frame
97131 clr.w joy_prev
98-
99- ; --- Calculate initial pixel position ---
100132 bsr grid_to_pixels
101133
102- ; --- Set sprite pointer in copper list ---
134+ ; --- Set sprite pointer ---
103135 lea frog_data,a0
104136 move.l a0,d0
105137 swap d0
...
109141 lea sprptl_val,a1
110142 move.w d0,(a1)
111143
112- ; --- Update sprite control words ---
113144 bsr update_sprite
145+
146+ ; --- Initialise car ---
147+ clr.w car_x
114148
115149 ; --- Install copper list ---
116150 lea copperlist,a0
117151 move.l a0,COP1LC(a5)
118152 move.w d0,COPJMP1(a5)
119153
120- ; --- Enable DMA ---
121- move.w #$83a0,DMACON(a5)
154+ ; --- Enable DMA (including Blitter) ---
155+ move.w #$87e0,DMACON(a5) ; Master + blit + copper + bitplane + sprite
122156
123157 ;══════════════════════════════════════════════════════════════════════════════
124158 ; MAIN LOOP
...
126160
127161 mainloop:
128162 bsr wait_vblank
163+
164+ ; --- Update frog ---
129165 bsr update_frog
130166 bsr update_sprite
131- bra.s mainloop
167+
168+ ; --- Update car ---
169+ bsr erase_car ; Erase at old position
170+ bsr move_car ; Update position
171+ bsr draw_car ; Draw at new position
172+
173+ bra mainloop
132174
133175 ;══════════════════════════════════════════════════════════════════════════════
134-; UPDATE_FROG - Main frog logic
176+; BLITTER ROUTINES
135177 ;══════════════════════════════════════════════════════════════════════════════
136-; State machine:
137-; IDLE: Check for joystick input, start hop if valid
138-; HOPPING: Animate position, return to IDLE when complete
178+
179+;------------------------------------------------------------------------------
180+; WAIT_BLIT - Wait for Blitter to finish
181+;------------------------------------------------------------------------------
182+wait_blit:
183+ btst #6,DMACONR(a5) ; Test BBUSY flag
184+ bne.s wait_blit ; Loop while busy
185+ rts
186+
187+;------------------------------------------------------------------------------
188+; CLEAR_SCREEN - Clear the screen buffer using Blitter
189+;------------------------------------------------------------------------------
190+clear_screen:
191+ bsr.s wait_blit
192+
193+ ; Clear entire plane: D channel only, minterm = 0
194+ move.l #screen_plane,BLTDPTH(a5)
195+ move.w #0,BLTDMOD(a5) ; No modulo (continuous)
196+ move.w #$0100,BLTCON0(a5) ; D only, minterm 0 (clear)
197+ move.w #0,BLTCON1(a5)
198+ move.w #(SCREEN_H<<6)|SCREEN_W/2,BLTSIZE(a5) ; Trigger!
199+
200+ rts
201+
202+;------------------------------------------------------------------------------
203+; ERASE_CAR - Clear car at current position
204+;------------------------------------------------------------------------------
205+erase_car:
206+ bsr.s wait_blit
207+
208+ ; Calculate screen address for car position
209+ move.w car_x,d0
210+ move.w #CAR_ROW,d1
211+ bsr calc_screen_addr ; A0 = destination
212+
213+ ; Erase: D channel only, minterm 0
214+ move.l a0,BLTDPTH(a5)
215+ move.w #SCREEN_W-CAR_WIDTH*2,BLTDMOD(a5)
216+ move.w #$0100,BLTCON0(a5)
217+ move.w #0,BLTCON1(a5)
218+ move.w #(CAR_HEIGHT<<6)|CAR_WIDTH,BLTSIZE(a5)
219+
220+ rts
221+
222+;------------------------------------------------------------------------------
223+; DRAW_CAR - Draw car at current position using Blitter
224+;------------------------------------------------------------------------------
225+draw_car:
226+ bsr.s wait_blit
227+
228+ ; Calculate screen address
229+ move.w car_x,d0
230+ move.w #CAR_ROW,d1
231+ bsr calc_screen_addr ; A0 = destination
232+
233+ ; Copy A→D: source graphics to screen
234+ move.l #car_gfx,BLTAPTH(a5)
235+ move.l a0,BLTDPTH(a5)
236+
237+ move.w #$ffff,BLTAFWM(a5) ; No masking
238+ move.w #$ffff,BLTALWM(a5)
239+ move.w #0,BLTAMOD(a5) ; Source: no gaps
240+ move.w #SCREEN_W-CAR_WIDTH*2,BLTDMOD(a5) ; Dest: screen width
241+ move.w #$09f0,BLTCON0(a5) ; A→D, D=A
242+ move.w #0,BLTCON1(a5)
243+ move.w #(CAR_HEIGHT<<6)|CAR_WIDTH,BLTSIZE(a5) ; Trigger!
244+
245+ rts
246+
247+;------------------------------------------------------------------------------
248+; CALC_SCREEN_ADDR - Calculate screen address from grid position
249+;------------------------------------------------------------------------------
250+; Input: D0 = pixel X, D1 = row number
251+; Output: A0 = screen address
252+
253+calc_screen_addr:
254+ lea screen_plane,a0
255+
256+ ; Calculate Y offset: row × CELL_SIZE × SCREEN_W
257+ move.w d1,d2
258+ mulu #CELL_SIZE,d2 ; D2 = row in pixels (from top of grid)
259+ add.w #GRID_ORIGIN_Y,d2 ; Add top margin
260+ mulu #SCREEN_W,d2 ; D2 = byte offset for Y
261+ add.l d2,a0
262+
263+ ; Calculate X offset: pixel_x / 8 (convert to bytes)
264+ move.w d0,d2
265+ lsr.w #3,d2 ; D2 = X in bytes
266+ ext.l d2
267+ add.l d2,a0
268+
269+ rts
270+
271+;------------------------------------------------------------------------------
272+; MOVE_CAR - Update car position
273+;------------------------------------------------------------------------------
274+move_car:
275+ add.w #CAR_SPEED,car_x
276+ cmp.w #320,car_x
277+ blt.s .no_wrap
278+ clr.w car_x
279+.no_wrap:
280+ rts
281+
282+;══════════════════════════════════════════════════════════════════════════════
283+; FROG ROUTINES (from Unit 5)
284+;══════════════════════════════════════════════════════════════════════════════
139285
140286 update_frog:
141287 tst.w frog_state
...
143289 bra .hopping
144290
145291 .idle:
146- ; --- Read joystick with edge detection ---
147292 bsr read_joystick_edge
148293 tst.w d0
149- beq .done ; No new input
294+ beq .done
150295
151- ; --- Check each direction ---
152- btst #8,d0 ; Up?
296+ btst #8,d0
153297 beq.s .not_up
154298 move.w frog_grid_y,d1
155299 tst.w d1
156- beq.s .not_up ; Already at top
300+ beq.s .not_up
157301 move.w #DIR_UP,frog_dir
158302 bra.s .start_hop
159303 .not_up:
160- btst #0,d0 ; Down?
304+ btst #0,d0
161305 beq.s .not_down
162306 move.w frog_grid_y,d1
163307 cmp.w #GRID_ROWS-1,d1
164- beq.s .not_down ; Already at bottom
308+ beq.s .not_down
165309 move.w #DIR_DOWN,frog_dir
166310 bra.s .start_hop
167311 .not_down:
168- btst #9,d0 ; Left?
312+ btst #9,d0
169313 beq.s .not_left
170314 move.w frog_grid_x,d1
171315 tst.w d1
172- beq.s .not_left ; Already at left
316+ beq.s .not_left
173317 move.w #DIR_LEFT,frog_dir
174318 bra.s .start_hop
175319 .not_left:
176- btst #1,d0 ; Right?
320+ btst #1,d0
177321 beq .done
178322 move.w frog_grid_x,d1
179323 cmp.w #GRID_COLS-1,d1
180- beq .done ; Already at right
324+ beq .done
181325 move.w #DIR_RIGHT,frog_dir
182- ; Fall through to start_hop
183326
184327 .start_hop:
185- ; --- Begin hopping animation ---
186328 move.w #STATE_HOPPING,frog_state
187- move.w #0,frog_anim_frame
329+ clr.w frog_anim_frame
188330 bra.s .done
189331
190332 .hopping:
191- ; --- Advance animation frame ---
192333 addq.w #1,frog_anim_frame
193-
194- ; --- Move pixel position based on direction ---
195334 move.w frog_dir,d0
196335
197336 cmp.w #DIR_UP,d0
198337 bne.s .hop_not_up
199338 sub.w #PIXELS_PER_FRAME,frog_pixel_y
200- bra.s .check_hop_done
339+ bra.s .check_done
201340 .hop_not_up:
202341 cmp.w #DIR_DOWN,d0
203342 bne.s .hop_not_down
204343 add.w #PIXELS_PER_FRAME,frog_pixel_y
205- bra.s .check_hop_done
344+ bra.s .check_done
206345 .hop_not_down:
207346 cmp.w #DIR_LEFT,d0
208347 bne.s .hop_not_left
209348 sub.w #PIXELS_PER_FRAME,frog_pixel_x
210- bra.s .check_hop_done
349+ bra.s .check_done
211350 .hop_not_left:
212- ; Must be RIGHT
213351 add.w #PIXELS_PER_FRAME,frog_pixel_x
214352
215-.check_hop_done:
216- ; --- Check if animation complete ---
353+.check_done:
217354 cmp.w #HOP_FRAMES,frog_anim_frame
218355 blt.s .done
219356
220- ; --- Hop complete: update grid position ---
221357 move.w frog_dir,d0
222-
223358 cmp.w #DIR_UP,d0
224359 bne.s .end_not_up
225360 subq.w #1,frog_grid_y
226- bra.s .snap_to_grid
361+ bra.s .snap
227362 .end_not_up:
228363 cmp.w #DIR_DOWN,d0
229364 bne.s .end_not_down
230365 addq.w #1,frog_grid_y
231- bra.s .snap_to_grid
366+ bra.s .snap
232367 .end_not_down:
233368 cmp.w #DIR_LEFT,d0
234369 bne.s .end_not_left
235370 subq.w #1,frog_grid_x
236- bra.s .snap_to_grid
371+ bra.s .snap
237372 .end_not_left:
238- ; Must be RIGHT
239373 addq.w #1,frog_grid_x
240374
241-.snap_to_grid:
242- ; --- Snap pixel position to grid (prevents drift) ---
375+.snap:
243376 bsr grid_to_pixels
244-
245- ; --- Return to idle state ---
246377 move.w #STATE_IDLE,frog_state
247378
248379 .done:
249380 rts
250-
251-;══════════════════════════════════════════════════════════════════════════════
252-; GRID_TO_PIXELS - Convert grid coordinates to pixel coordinates
253-;══════════════════════════════════════════════════════════════════════════════
254-; Input: frog_grid_x, frog_grid_y
255-; Output: frog_pixel_x, frog_pixel_y
256381
257382 grid_to_pixels:
258383 move.w frog_grid_x,d0
...
264389 mulu #CELL_SIZE,d0
265390 add.w #GRID_ORIGIN_Y,d0
266391 move.w d0,frog_pixel_y
267-
268392 rts
269-
270-;══════════════════════════════════════════════════════════════════════════════
271-; READ_JOYSTICK_EDGE - Read joystick with edge detection
272-;══════════════════════════════════════════════════════════════════════════════
273-; Returns only newly-pressed directions (not held directions)
274-; Output: D0 = direction bits (only set on transition from not-pressed)
275393
276394 read_joystick_edge:
277- ; --- Read and decode joystick ---
278395 move.w JOY1DAT(a5),d0
279396 move.w d0,d1
280397 lsr.w #1,d1
281- eor.w d1,d0 ; D0 = decoded current state
398+ eor.w d1,d0
282399
283- ; --- Edge detection: current AND NOT previous ---
284400 move.w joy_prev,d1
285- not.w d1 ; D1 = NOT previous
286- and.w d0,d1 ; D1 = newly pressed only
287-
288- ; --- Save current state for next frame ---
401+ not.w d1
402+ and.w d0,d1
289403 move.w d0,joy_prev
290404
291- move.w d1,d0 ; Return newly pressed in D0
405+ move.w d1,d0
292406 rts
293407
294408 ;══════════════════════════════════════════════════════════════════════════════
295-; SUBROUTINES
409+; UTILITY ROUTINES
296410 ;══════════════════════════════════════════════════════════════════════════════
297411
298412 wait_vblank:
...
317431 add.w #FROG_HEIGHT,d0
318432 lsl.w #8,d0
319433 move.w d0,2(a0)
320-
321434 rts
322435
323436 ;══════════════════════════════════════════════════════════════════════════════
324437 ; VARIABLES
325438 ;══════════════════════════════════════════════════════════════════════════════
326-
327-; Grid position (which cell the frog occupies)
328-frog_grid_x: dc.w 9 ; Column (0-19)
329-frog_grid_y: dc.w 12 ; Row (0-12)
330-
331-; Pixel position (for rendering and animation)
332-frog_pixel_x: dc.w 0 ; Screen X
333-frog_pixel_y: dc.w 0 ; Screen Y
334439
335-; Movement state machine
336-frog_state: dc.w 0 ; 0=idle, 1=hopping
337-frog_dir: dc.w 0 ; Direction of current hop
338-frog_anim_frame: dc.w 0 ; Current frame of hop animation (0-7)
440+frog_grid_x: dc.w 9
441+frog_grid_y: dc.w 12
442+frog_pixel_x: dc.w 0
443+frog_pixel_y: dc.w 0
444+frog_state: dc.w 0
445+frog_dir: dc.w 0
446+frog_anim_frame: dc.w 0
447+joy_prev: dc.w 0
339448
340-; Input state
341-joy_prev: dc.w 0 ; Previous joystick state (for edge detection)
449+car_x: dc.w 0
342450
343451 ;══════════════════════════════════════════════════════════════════════════════
344452 ; COPPER LIST
...
346454
347455 copperlist:
348456 dc.w COLOR00,COLOUR_BLACK
457+
458+ ; --- Bitplane control: 1 plane, colour on ---
459+ dc.w $0100,$1200 ; BPLCON0: 1 plane
460+ dc.w $0102,$0000 ; BPLCON1: no scroll
461+ dc.w $0104,$0000 ; BPLCON2: default priority
462+ dc.w $0108,$0000 ; BPL1MOD
463+ dc.w $010a,$0000 ; BPL2MOD
464+
465+ ; --- Bitplane pointer ---
466+ dc.w $00e0 ; BPL1PTH
467+bplpth_val: dc.w $0000
468+ dc.w $00e2 ; BPL1PTL
469+bplptl_val: dc.w $0000
470+
471+ ; --- Playfield colours ---
472+ dc.w $0180,$0000 ; Colour 0: transparent (shows Copper bg)
473+ dc.w $0182,COLOUR_CAR ; Colour 1: red (car from bitplane)
349474
350475 ; --- Sprite palette ---
351476 dc.w $01a2,COLOUR_FROG
...
407532 dc.w COLOR00,COLOUR_BLACK
408533
409534 dc.w $ffff,$fffe
535+
536+;══════════════════════════════════════════════════════════════════════════════
537+; GRAPHICS DATA
538+;══════════════════════════════════════════════════════════════════════════════
539+
540+ even
541+car_gfx:
542+ ; 32×12 pixel car (2 words wide × 12 lines)
543+ dc.w $0ff0,$0ff0 ; ..####....####..
544+ dc.w $3ffc,$3ffc ; ##########..####
545+ dc.w $7ffe,$7ffe ; ##############..
546+ dc.w $ffff,$ffff ; ################
547+ dc.w $ffff,$ffff ; ################
548+ dc.w $ffff,$ffff ; ################
549+ dc.w $ffff,$ffff ; ################
550+ dc.w $ffff,$ffff ; ################
551+ dc.w $ffff,$ffff ; ################
552+ dc.w $7ffe,$7ffe ; ##############..
553+ dc.w $3c3c,$3c3c ; Wheels
554+ dc.w $0000,$0000
410555
411556 ;══════════════════════════════════════════════════════════════════════════════
412557 ; SPRITE DATA
...
414559
415560 even
416561 frog_data:
417- dc.w $ec50,$fc00 ; Control words
562+ dc.w $ec50,$fc00
418563
419564 dc.w $0000,$0000
420565 dc.w $07e0,$0000
...
434579 dc.w $0000,$0000
435580
436581 dc.w $0000,$0000
582+
583+;══════════════════════════════════════════════════════════════════════════════
584+; SCREEN BUFFER (Chip RAM)
585+;══════════════════════════════════════════════════════════════════════════════
586+
587+ section chipbss,bss_c
588+
589+ even
590+screen_plane:
591+ ds.b PLANE_SIZE
437592