Seth Kushniryk

Adapting Mockingboard Code for the Echo Plus

2026/03/15

I’ve been looking at what it takes to make Mockingboard software work with the Echo Plus card. Both are sound cards for the Apple II based on the same AY-3-8910 family of Programmable Sound Generators, driven through 6522 VIA chips. The Echo Plus also has an SSI-263 speech synthesiser, but that’s outwith the scope of this post. For music playback, the two cards are almost identical; the only real difference is how the VIA’s port B pins are wired to the AY chips. I used cybernesto’s mcs-player as a reference — it has Will Harvey’s Music Construction Set player in both Mockingboard and Echo Plus versions, which made the differences easy to isolate.

The hardware difference

The Mockingboard has two 6522 VIAs, each driving one AY-3-8910. VIA1 sits at $Cn00 and VIA2 at $Cn80 (where n is the slot number). Each VIA has its own port B (ORB) for bus control and port A (ORA) for the data bus. DDRB is set to $07 (3 output bits: BC1, BDIR, ~RESET), and both VIAs use the same control values:

Operation ORB value Bits
Latch $07 00000111
Write $06 00000110
Inactive $04 00000100

The Echo Plus has a single 6522 VIA at $Cn00 driving two AY-3-8913 chips. Both AY chips share the VIA’s data bus (ORA), and port B bits 3–4 serve as chip selects. DDRB is $1F (5 output bits). To address each chip, you set the appropriate select bit in ORB:

Operation AY1 (bit 3) AY2 (bit 4)
Latch $0F $17
Write $0E $16
Inactive $0C $14

The lower three bits are the same BC1/BDIR/~RESET protocol on both cards. The difference is that the Mockingboard selects each AY chip by addressing a different VIA, while the Echo Plus selects each AY chip via port B bits on the same VIA.

Since the 6522 only decodes address bits A0–A3, the Echo Plus VIA registers appear at both $Cn00–$Cn0F and $Cn80–$Cn8F. This is why Will Harvey’s MCS player for the Echo Plus (MCS-ECHO+.S) can use the same code structure as the Mockingboard version, writing to “VIA2” at offset $80 — it’s aliased back to the same physical VIA.

The shared-write problem

Because both VIAs use identical control values on the Mockingboard, most programs optimise by writing the same byte to both ORBs:

          LDY #$07
          STY $C400         ; VIA1 ORB
          STY $C480         ; VIA2 ORB — same Y register

On the Echo Plus, there’s only one ORB, but it needs different values for each AY chip — $0F to select AY1, $17 to select AY2. You can’t use the same register for both:

          LDA #$0F
          STA $C400         ; ORB — AY1 selected
          ; ...
          LDA #$17
          STA $C400         ; ORB — AY2 selected

Note that both writes go to the same VIA address ($C400). On the Mockingboard, the writes went to different addresses ($C400 and $C480) but with the same value. On the Echo Plus, they go to the same address but with different values. This is the main complication when adapting existing code.

Detection

Mockingboard detection reads the VIA timer register twice in quick succession. A real 6522’s timer counts down continuously, so two reads differ by a predictable amount. Empty slots return floating bus garbage:

; Check if a VIA timer is present
; Input: SLOT/SLOT+1 = ZP pointer to slot base
;        Y = timer offset ($04 or $84)
; Output: Z set = found, Z clear = not found

CHECK_VIA:
          LDA (SLOT),Y    ; read timer
          STA TEMP
          LDA (SLOT),Y    ; read again
          SEC
          SBC TEMP
          CMP #$F8        ; expect ~$F8 cycles difference
          BEQ @OK
          CMP #$F7        ; allow +/- 1
@OK       RTS

Most Mockingboard detection routines require both VIA1 (offset $04) and VIA2 (offset $84) to pass the timer check. The Echo Plus has only one VIA, so the $84 check either fails (no hardware there) or succeeds (aliased to the same timer at $04). Either way, the detection needs to accept a single VIA as sufficient.

To distinguish the cards, check VIA1 first, then test for VIA2:

; After finding VIA1 at the detected slot:
          LDA #$00
          STA CARD_TYPE   ; 0 = Mockingboard
          LDY #$84
          JSR CHECK_VIA   ; test for second VIA
          BEQ @DONE       ; second VIA present = Mockingboard
          INC CARD_TYPE   ; no second VIA = Echo Plus
@DONE:

A full detection routine that tries slot 4 first, then scans all slots accepting either card:

DETECT_ANY:
          LDA #$00
          STA SLOT
          STA CARD_TYPE

          LDX #$C4          ; try slot 4 first
          STX SLOT+1
          LDY #$04
          JSR CHECK_VIA
          BNE @SCAN         ; no VIA at slot 4
          LDY #$84
          JSR CHECK_VIA
          BEQ @FOUND        ; two VIAs = Mockingboard
          INC CARD_TYPE     ; one VIA = Echo Plus
          SEC
          RTS

@FOUND    SEC
          RTS

@SCAN     LDX #$C7          ; scan from slot 7 down
@NEXT     STX SLOT+1
          LDY #$04
          JSR CHECK_VIA
          BNE @SKIP
          LDY #$84
          JSR CHECK_VIA
          BEQ @FOUND2
          INC CARD_TYPE
          SEC
          RTS
@FOUND2   SEC
          RTS
@SKIP     DEX
          CPX #$C0
          BNE @NEXT
          CLC               ; nothing found
          RTS

Setting up AY control values

After detection, load the correct values into working variables. I use a table with one row per card type:

;                   L1  W1  I1  L2  W2  I2  DDRB
AY_PARAMS:
          HEX 07060407060407  ; Mockingboard
          HEX 0F0E0C1716141F  ; Echo Plus

PARAM_SIZE = 7

SETUP_AY:
          LDA CARD_TYPE
          BEQ @MB
          LDX #PARAM_SIZE
          BNE @COPY
@MB       LDX #$00
@COPY     LDY #$00
@LOOP     LDA AY_PARAMS,X
          STA AY_LATCH_1,Y
          INX
          INY
          CPY #PARAM_SIZE
          BNE @LOOP
          RTS

This copies seven bytes into sequential working variables:

AY_LATCH_1  DS 1     ; AY1 latch value
AY_WRITE_1  DS 1     ; AY1 write value
AY_INACT_1  DS 1     ; AY1 inactive value
AY_LATCH_2  DS 1     ; AY2 latch value
AY_WRITE_2  DS 1     ; AY2 write value
AY_INACT_2  DS 1     ; AY2 inactive value
AY_DDRB     DS 1     ; DDRB value

Writing AY registers

The AY protocol is: put the register number on the data bus (ORA), pulse latch then inactive on ORB, put the value on ORA, pulse write then inactive. Here’s a card-independent bulk update for both AY chips:

; VIA address constants (n = slot number):
;   AY1_ORB  = $Cn00   AY2_ORB  = $Cn80
;   AY1_ORA  = $Cn01   AY2_ORA  = $Cn81
;   AY1_DDRB = $Cn02   AY2_DDRB = $Cn82
;   AY1_DDRA = $Cn03   AY2_DDRA = $Cn83
;   VIA_T1CL = $Cn04   VIA_ACR  = $Cn0B
;   VIA_T1CH = $Cn05   VIA_IFR  = $Cn0D
;                      VIA_IER  = $Cn0E
;
; On the Mockingboard, $Cn00 and $Cn80 are separate VIAs.
; On the Echo Plus, $Cn80 aliases to $Cn00 (one VIA).

INITMOCK:
          TXA
          PHA
          TYA
          PHA
          LDA #$FF
          STA AY1_DDRA           ; all port A bits output
          STA AY2_DDRA           ; (alias on Echo Plus)
          LDA AY_DDRB
          STA AY1_DDRB           ; $07 for MB, $1F for E+
          STA AY2_DDRB           ; (alias on Echo Plus)
          LDX #$00
@LOOP:
          ; --- AY chip 1 ---
          STX AY1_ORA            ; register number
          LDA AY_LATCH_1
          STA AY1_ORB            ; latch
          LDA AY_INACT_1
          STA AY1_ORB            ; inactive
          LDA BUFFER,X
          STA AY1_ORA            ; register value
          LDA AY_WRITE_1
          STA AY1_ORB            ; write
          LDA AY_INACT_1
          STA AY1_ORB            ; inactive
          ; --- AY chip 2 ---
          STX AY2_ORA            ; register number
          LDA AY_LATCH_2         ; different chip select
          STA AY2_ORB            ; latch
          LDA AY_INACT_2         ; different chip select
          STA AY2_ORB            ; inactive
          LDA BUFFER+$10,X
          STA AY2_ORA            ; register value
          LDA AY_WRITE_2         ; different chip select
          STA AY2_ORB            ; write
          LDA AY_INACT_2         ; different chip select
          STA AY2_ORB            ; inactive
          ;
          INX
          CPX #$0F
          BNE @LOOP
          PLA
          TAY
          PLA
          TAX
          RTS

This code works for both cards because the VIA2 address ($Cn80) maps to a second physical VIA on the Mockingboard, or aliases to the same VIA on the Echo Plus. The different AY_LATCH/WRITE/INACT variables handle the chip selection correctly either way.

If this runs in an interrupt handler and you need to save cycles, you can patch the immediate operands at init time instead of loading from variables:

; At init:
          LDA AY_LATCH_1
          STA LOOP_AY1_LATCH+1   ; patch LDA #$xx operand
          LDA AY_LATCH_2
          STA LOOP_AY2_LATCH+1   ; etc for write and inactive

; In the loop:
LOOP_AY1_LATCH:
          LDA #$07               ; patched to $0F for Echo+
          STA AY1_ORB

The MCS player side by side

For reference, here are the INITMOCK routines from the two MCS player versions. The Mockingboard version uses absolute addresses hardcoded for slot 4, writes the same ORB values to both VIAs:

; MCS-MB.S (Mockingboard) — github.com/cybernesto/mcs-player
INITMOCK  TYA
          PHA
          LDA #$FF
          STA $C403         ; DDRA
          STA $C483         ; DDRA2
          LDA #$07
          STA $C402         ; DDRB = $07
          STA $C482         ; DDRB2 = $07
          LDY #$00
IM2       STY $C401         ; ORA = register number
          LDA #$07
          STA $C400         ; ORB = latch
          LDA #$04
          STA $C400         ; ORB = inactive
          LDA BUFFER,Y
          STA $C401         ; ORA = register value
          LDA #$06
          STA $C400         ; ORB = write
          LDA #$04
          STA $C400         ; ORB = inactive
          STY $C481         ; ORA2 = register number
          LDA #$07
          STA $C480         ; ORB2 = latch (same value)
          LDA #$04
          STA $C480         ; ORB2 = inactive
          LDA BUFFER+$10,Y
          STA $C481         ; ORA2 = register value
          LDA #$06
          STA $C480         ; ORB2 = write (same value)
          LDA #$04
          STA $C480         ; ORB2 = inactive
          INY
          CPY #$0F
          BNE IM2
          PLA
          TAY
          RTS

The Echo Plus version uses indirect addressing through a zero-page pointer for slot independence. It writes to the same VIA at both offset ranges ($00 and $80, which alias to the same chip), but uses different ORB values to select each AY:

; MCS-ECHO+.S (Echo Plus) — github.com/cybernesto/mcs-player
SLOT      EQU $CE
ORB       EQU $00
ORA       EQU $01
ORB2      EQU $80           ; aliases to ORB
ORA2      EQU $81           ; aliases to ORA

INITMOCK  TXA
          PHA
          TYA
          PHA
          LDA #$FF
          LDY #$03          ; DDRA
          STA (SLOT),Y
          LDY #$83          ; DDRA2 (alias)
          STA (SLOT),Y
          LDA #$1F
          LDY #$02          ; DDRB = $1F
          STA (SLOT),Y
          LDY #$82          ; DDRB2 (alias)
          STA (SLOT),Y
          LDX #$00
IM2       TXA
          LDY #ORA
          STA (SLOT),Y
          LDA #$0F          ; AY1 latch (bit 3)
          LDY #ORB
          STA (SLOT),Y
          LDA #$0C          ; AY1 inactive
          STA (SLOT),Y
          LDA BUFFER,X
          LDY #ORA
          STA (SLOT),Y
          LDA #$0E          ; AY1 write
          LDY #ORB
          STA (SLOT),Y
          LDA #$0C          ; AY1 inactive
          STA (SLOT),Y
          TXA
          LDY #ORA2         ; same ORA via alias
          STA (SLOT),Y
          LDA #$17          ; AY2 latch (bit 4)
          LDY #ORB2         ; same ORB via alias
          STA (SLOT),Y
          LDA #$14          ; AY2 inactive
          STA (SLOT),Y
          LDA BUFFER+$10,X
          LDY #ORA2
          STA (SLOT),Y
          LDA #$16          ; AY2 write
          LDY #ORB2
          STA (SLOT),Y
          LDA #$14          ; AY2 inactive
          STA (SLOT),Y
          INX
          CPX #$0F
          BNE IM2
          PLA
          TAY
          PLA
          TAX
          RTS

The key differences: the Mockingboard addresses two separate VIAs with the same ORB values, while the Echo Plus addresses the same VIA with different ORB values (chip selects). The loop counter moves from Y to X because Y is needed for indirect addressing.

Binary patching existing software

When you don’t have source code, the process is: find all $C4xx (or whatever slot byte) references in the binary, classify each as VIA1 or VIA2, find the AY control immediates near each ORB write, and change them. Search for byte patterns like A9 07 8D 00 C4 (LDA #$07; STA $C400) for the latch, A9 06 for write, A9 04 for inactive. Also look for LDY/STY variants (A0 07 8C 00 C4).

For the VIA address bytes ($C4), no changes are needed if the Echo Plus is in slot 4. If it’s in another slot, change every $C4 high byte. The $Cn80 writes will alias to $Cn00 on the Echo Plus, which is what we want.

The ORB control values need changing. For AY1 writes (to VIA1 at $Cn00): $07→$0F, $06→$0E, $04→$0C. For AY2 writes (to VIA2 at $Cn80): $07→$17, $06→$16, $04→$14. And change DDRB from $07 to $1F.

If the code uses shared writes (same value to both ORBs), you have two options. The correct approach is splitting them into separate values per AY chip, which requires finding space for extra bytes. The quick hack is using “fat” values that set both chip select bits simultaneously:

$0F | $17 = $1F  (latch)
$0E | $16 = $1E  (write)
$0C | $14 = $1C  (inactive)

These set both bit 3 and bit 4, selecting both AY chips at once. The lower three bits are correct for the bus protocol. On a Mockingboard the extra bits are don’t-cares. On the Echo Plus, addressing both chips simultaneously should be harmless for register writes since you’re writing the same values to both anyway in a shared-write context. It’s not strictly correct, but it avoids restructuring code.

For detection patches, find the VIA2 timer check (bytes A0 84 20 xx xx F0 xx — LDY #$84, JSR check, BEQ found) and replace with a JMP to the success handler padded with NOPs (4C xx xx EA EA EA EA). Or better, keep the check but branch to success either way, and store the result as a card type flag.

Summary

Mockingboard Echo Plus
VIAs 2 1
PSG chips 2x AY-3-8910 2x AY-3-8913 (register-compatible)
AY chip selection separate VIAs port B chip selects
DDRB $07 $1F
AY1 ORB latch/write/inact $07 / $06 / $04 $0F / $0E / $0C
AY2 ORB latch/write/inact $07 / $06 / $04 $17 / $16 / $14
$Cn80 addresses second VIA alias to $Cn00
AY registers identical identical
Music data / timer / interrupts identical identical

The only code changes needed are DDRB and the ORB bus control values. Everything else is the same. The main complication is that Mockingboard code uses the same ORB values for both AY chips (since each has its own VIA), while the Echo Plus needs different values (chip select bits on a shared VIA).

Categories: Computer Retro Projects