loading...
All Defender articles | Back to top

Defender ROM Analysis

Defender enemy points values

In the previous article, we disassembled the sound ROM of the Defender arcade machine. In this article, we will look into the MC6800 assembly code, and specifically focus on the boot sound routine.

Interrupt Mechanism

The MC6802 CPU is running a program directly from ROM, using the built-in 128 bytes of RAM for working memory. When the game code running on the MC6809 CPU requests to play a sound on the sound board, it will set a special value determining which sound to play and then send an interrupt to the sound hardware.

In detail, the steps of that process are:

The ROM is mapped to address $F800 (reference?).

The very end of the sound ROM contains the addresses of the interrupt handlers:

    ; interrupt handlers
    dc.w    $FCB6   ; FFF8: interrupt request (IRQ) = FC86
    dc.w    $F801   ; FFFA: non-maskable interrupt (NMI) = F801
    dc.w    $FD2F   ; FFFC: software interrupt
    dc.w    $F801   ; FFFE: reset handler = F801

When a sound is requested from the main MC6809 CPU, we get an interrupt request (IRQ) (reference?), and the sound MC6802 CPU jumps to $FCB6.

The interrupt handler code looks like this:

; FCB6: INTERRUPT HANDLER

    lds     #$007F  ; SP = 7f
    ldaa    $0402   ; synth algo is in $0402 (PIA)
                    ; C0 : intro sound (1100 0000)
    cli             ; enable interrupts
    coma            ; intro sound (0011 1111)
    anda    #$1F    ; a &= 0001 1111 (intro sound, a = 1f)

    ldab    $0008   ; b = 0008 (initially contains 0)
    beq     LFCCD   ; if 0008 = 0, goto LFCCD
    bpl     LFCC9
    jsr     $FA48

LFCC9: deca jsr $FA89 LFCCD: clrb ; b = 0 cmpa #$0E beq LFCD4 ; if a = 0e, goto LFCD4 stab $0006 ; 0006 = b LFCD4: cmpa #$12 beq LFCDA ; if a = 12, goto LFCDA stab $0007 ; 0007 = b LFCDA: ldab $EFFD ; b = EFFD (???) initially 0 cmpb #$7E bne LFCE4 ; if b == 7e, execute subroutine EFFD jsr $EFFD LFCE4: tsta
beq LFD0E ; if a = 0, goto LFD0E deca ; a-- (intro sound, a = 1e) cmpa #$0C bhi LFCF4 ; if a > 0c, goto LFCF4 jsr $FB81 jsr $FBE7 bra LFD0E ; LFCF4: cmpa #$1B bhi LFD06 ; if a > 1b, goto LFD06 suba #$0D asla ldx #$FD58 bsr LFD21 ldx $00,x jsr $00,x ;INFO: index jump bra LFD0E ; LFD06: suba #$1C ; a -= 1c (intro sound, a = 2) jsr $F82A ; play intro sound

ROM Analysis

The disassembled code can be analyzed, and the different sound routines can be identified. The interrupt handler works by setting up some parameters in RAM, and then jumping to a specific subroutine that renders output samples until interrupted again. Here is an overview over the 10 different sound algorithms and their locations in ROM:


The boot sound SYNTH 1 is very unique and iconic, and worth a slightly more thorough analysis. It consists of only 98 bytes of machine code:


It also accesses data in the ROM from addresse $FD76 and 12 B forward. the ROM is mapped at the address $F800, so in the ROM, this data would be located at the offset $FD76 - $F800 = $0576. The 12 B of data are:

40 01 00 10 E1 00 80 FF FF 28 01 00

The disassembled code for 'SYNTH 1' looks like this:

;-------------------------------------------------------------------
; SYNTH 1
;

ptr       = $000F

pw0_init  = $0013 ; pw0 initial value
pw1_init  = $0014 ; pw1 initial value
pw0_mod   = $0015 ; pw0 frequency change
pw1_mod   = $0016 ; pw1 frequency change
pw1_end   = $0017
step_time = $0018 ; time until next frequency mod
unused    = $0019
param0    = $001A
volume    = $001B
pw0       = $001C ; time of pulse down
pw1       = $001D ; time of pulse up

; copy_values( int16 *source, int16 *target, int8 count )
;                      x             $000F         b

LF82A:           ; intro sound, a = 2
    tab          ; b = a
    asla         ; a *= 8
    asla         ; .
    asla         ; .
    aba          ; a = b+a (intro sound, a = 12)
    ldx   #$0013 ; ptr = $0013
    stx   $000F  ;
    ldx   #$FD76 ; x = $FD76 ; init table
    jsr   $FD21  ; x : RESULT = phasor( x: VALUE, a: FREQ )
                 ; - After calling phasor, x is either $FD21 or
                 ;   $FD22, based on the internal state of phasor.
                 ; - Seemingly uses a data table with an oscillator
                 ;   that determines an offset of 0 or 1.
                 ; - Possibly this functions as a way to randomly
                 ;   determine initialization data for SYNTH 1 (only
                 ;   two different initialization states, though)
                 ;
    ldab  #$09   ; b = 9
    jmp   $FB0A  ; copy_values( x: SOURCE, ptr: TARGET, b: COUNT )
                 ; - this initializes the RAM variables used in
                 ;   SYNTH 1 from values stored in ROM
                 ; - Why is this a jmp and not a jsr? PC is
                 ;   not stored on SP, so how does copy_values
                 ;   know where to return to?
		 ; - The values are initialized to:
		 ;   pw0_init  = $40
		 ;   pw1_init  = $01
		 ;   pw0_mod   = $00
		 ;   pw1_mod   = $10
		 ;   pw1_end   = $E1
		 ;   step_time = $00
		 ;   unused    = $80
		 ;   param0    = $FF
		 ;   volume    = $FF

;
LF83F:
    ldaa  $001B  ; DAC = volume (always $FF)
    staa  $0400  ; .

LF844:           ; do
    ldaa  $0013  ;   pw0 = pw0_init
    staa  $001C  ;   .
    ldaa  $0014  ;   pw1 = pw1_init
    staa  $001D  ;   .
LF84C:           ;   do
    ldx   $0018  ;     x = step_time           * output pulse until
LF84E:           ;     do                        x = 0
    ldaa  $001C  ;       a = pw0
    com   $0400  ;       DAC invert            * invert DAC
LF853:           ;       do                    * pw0 busywait
    dex          ;         x -= 1              .
    beq   LF866  ;         if x = 0, goto skip .
    deca         ;         a -= 1              .
    bne   LF853  ;       while a != 0          .
    com   $0400  ;       DAC invert            * invert DAC
    ldaa  $001D  ;       a = pw1               
LF85E:           ;       do                    * pw1 busywait
    dex          ;         x -= 1              .
    beq   LF866  ;         if x = 0, goto skip .
    deca         ;         a -= 1              .
    bne   LF85E  ;       while a != 0          .
    bra   LF84E  ;     while true              * loops forever

LF866:           ;   skip:
    ldaa  $0400  ;     a = DAC                 * a = DAC, invert a
    bmi   LF86C  ;     if a < 0, goto noinv    . if $FF ($FF is
    coma         ;     invert a                .considered negative)
LF86C:           ;   noinv:                    .
    adda  #$00   ;                             .
    staa  $0400  ;     DAC = a                 .
    ldaa  $001C  ;     pw0 += pw0_mod          * increase pulse
    adda  $0015  ;     .                       .  width starting
    staa  $001C  ;     .                       .  values (shorter
    ldaa  $001D  ;     pw1 += pw1_mod          .  widths = higher
    adda  $0016  ;     .                       .  frequency)
    staa  $001D  ;     .                       .
    cmpa  $0017  ;   while pw1 != pw1_end      * stop at specified
    bne   LF84C  ;   .                            frequency
    ldaa  $001A  ;   if param0 = 0, goto done  ???
    beq   LF88B  ;   .
    adda  $0013  ;   pw0_init += param0        ???
    staa  $0013  ;   .
    bne   LF844  ; while pw0_init != 0
LF88B:           ; done:
       rts


;-------------------------------------------------------------------
; copy_values( int16 *source, int16 *target, int8 count )
;                      x             $000F         b
;
; Copies memory block of 'count' bytes from 'source' to 'target' in
; memory.

ptr    = $000D
target = $000F

copy_values:
    psha         ; push a to stack
LFB0B:           ; do
    ldaa  $00,x  ;   a = (*x)
    stx   $000D  ;   ptr = x
    ldx   $000F  ;   (*target++) = a
    staa  $00,x  ;   .
    inx          ;   .
    stx   $000F  ;   .
    ldx   $000D  ;   x = ptr + 1
    inx          ;   .
    decb         ; while(--count > 0)
    bne   LFB0B  ; .
    pula         ; pull a from stack
    rts


;-------------------------------------------------------------------
; result = phasor( int value, int8 freq )
;  x                    x           a
;
; Updates a phase value with the specified frequency and
; increments value when phase rolls over

; static local var
phase = $000E    ; only used in this function in the entire ROM
; local var
tmp   = $000D

LFD21:
    stx   $000D  ; tmp = value
    adda  $000E  ; phase += a
    staa  $000E  ; .
    bcc   LFD2C  ; if phase rolls over, value += 1
    inc   $000D  ; tmp ++
LFD2C:           ; done:
    ldx   $000D  ; result = tmp
    rts

The output signal is a pulse-width modulated square wave with maximum DC offset (which is probably removed after the DAC):

Boot Sound: Pulse-Width Modulation

This simple-looking signal looks more interesting when viewed in the spectral domain:
Boot Sound: Spectrum

References

Motorola 6800:

Before I found DASMx, I tried using Sean Riddle's 6800dasm (mirrored here), but the output of DASMx was easier to convert to code compatible with vasm.