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 examine how sounds are triggered from the game code, and look into the MC6800 assembly code, specifically focusing on the boot sound routine as an example.

How Sounds are Triggered

In the first Defender article, we looked at the Defender hardware architecture. We mentioned that the main CPU (Motorola MC6809) runs the game code, and that the Defender Sound Board consisted of a Motorola MC6802 and a few other chips, running the audio code.

These two processors run in parallel. So how do they communicate? Well, 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. The interrupt will cause the MC6802 to immediately jump to a specific address in its program code and execute a bit of code that we call the interrupt handler, which will then start playing a new sound.

In detail, the steps of that process are:

The ROM is mapped to address $F800, and it's 2048 bytes long. So the ROM address space is $F800 - $FFFF.

The last 8 bytes at the end of the sound ROM contains the addresses of the interrupt handlers:

  ; address   interrupt handlers
    FFF8 |    dc.w $FCB6   ; Interrupt Request (IRQ) = FCB6
    FFFA |    dc.w $F801   ; Non-Maskable Interrupt (NMI) = F801
    FFFC |    dc.w $FD2F   ; Software Interrupt
    FFFE |    dc.w $F801   ; Reset handler = F801

So, when a sound is requested from the main MC6809 CPU, we get an interrupt request (IRQ), and the MC6802 CPU of the sound board jumps to $FCB6.

The interrupt handler at $FCB6 looks like this:

  ; FCB6: INTERRUPT HANDLER
  
          lds     #$007F  ; SP = 7f
          ldaa    $0402   ; synth algo is in $0402 (PIA)
                          ; C0 : boot sound (1100 0000)
          cli             ; enable interrupts
  
  	...               ; Here the handler uses the value from $0402
	                  ; and some other state values to select an
			  ; address to jump to. In the case of the boot
			  ; sound, we end up down here at $FD06:
  
  ; FD06: PLAY BOOT SOUND

          suba    #$1C    ; a -= 1c (boot sound, a = 2)
          jsr     $F82A   ; play boot sound

Now we have seen a bit of how sounds are selected and triggered using interrupts. Next, we will look at how boot sound is implemented.

Defender Boot Sound

The boot sound is very unique and iconic, and it's not at all obvious how it is generated. All the more so because it consists of only 98 bytes of machine code:


It also accesses data in the ROM from addresse $FD76 and 12 B forward. The 12 B of data are:

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

The disassembled code for the boot sound looks like this:

;-------------------------------------------------------------------
; DEFENDER BOOT SOUND
; Disassembled and annotated by The Nameless Algorithm 2015
;

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:           ; boot sound, a = 2
    tab          ; b = a
    asla         ; a *= 8
    asla         ; .
    asla         ; .
    aba          ; a = b+a (boot 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

It looks bigger in assembly code, doesn't it?

So what does this code output? The only values it outputs to the DAC ($0400) are the extreme 8-bit values, $FF and its complement, $00. If we alternate between $FF and $00, we get a square wave with a maximum DC offset, which is probably removed with analog circuitry after the DAC. However, the interesting part is in how the time between the two values is modified every cycle.

Or to use synthesis terms, the square wave pulse-width is constantly modulated.

Boot Sound: Pulse-Width Modulation

The waveform isn't much to look at, but when viewed in the spectral domain, we see the complexity that is generated from this simple DSP code:

Boot Sound: Spectrum

Let's remind ourselves that this is generated from 98 bytes of machine code. This is just one synth algorithm, and indeed, at least 10 different algorithms can be identified in the disassembled ROM.

Here is an overview over the 10 different sound algorithms and their locations in ROM:


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.