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).