Mocking the Silicon
With the memory map known, every chip needed to respond just right — or the firmware would loop forever, waiting for hardware that didn't exist.
The Infinite Init Loop
Knowing where the chips live is only half the battle. The firmware doesn't just poke addresses and move on — it expects answers. Every peripheral initialization follows the same pattern: write a command, read back status, check specific bits, retry if wrong. And "retry" in firmware-speak means "loop forever until the hardware responds correctly."
After the memory map was established, the emulator hit a wall. The CPU was stuck at PC=0xFFF8A0AE — a subroutine that initialized the ES5506 DOC and ES5510 DSP in an endless loop. It would upload 160 instructions of microcode to the DSP, configure all 32 DOC voices, check status registers, find them wrong, and start over. Every time.
We needed to build mock implementations of four chips. Not full emulations — just enough to make the firmware believe it was talking to real silicon.
The ES5510 DSP Mock
The ES5510 was the most complex mock. Using MAME's es5510.cpp as reference, we identified the host interface protocol. The DSP exposes its internals through four offset ranges, each triggering a different operation:
// ES5510 host interface offsets (relative to base FC3000)0x80 Read-select GPR // latch a GPR for reading0xA0 Write GPR // write value to selected GPR0xC0 Write instruction // write to instruction memory0xE0 Write both // write GPR + instruction simultaneously The firmware uploads the DSP program by writing all 160 instruction words through offset 0xC0, then initializes the GPRs (General Purpose Registers) through 0xA0. After the upload, it reads back select GPRs through offset 0x80 to verify the microcode was stored correctly.
Our mock needed three things: a GPR latch array (32 entries), an instruction memory array (160 entries), and the read/write select logic that routes operations based on the offset.
// ES5510 mock - simplified from the emulator sourcetypedef struct { uint32_t gpr[32]; // General Purpose Registers uint64_t instr[160]; // Instruction memoryint gpr_latch_idx; // Currently selected GPRint instr_idx; // Current instruction write index
} es5510_mock_t; void es5510_write(es5510_mock_t *dsp, uint32_t offset, uint32_t data) { switch (offset & 0xE0) { case0x80: // Read-select: latch GPR index dsp->gpr_latch_idx = data & 0x1F; break; case0xA0: // Write GPR dsp->gpr[dsp->gpr_latch_idx] = data; break; case0xC0: // Write instruction dsp->instr[dsp->instr_idx++] = data; break; case0xE0: // Write both GPR + instruction dsp->gpr[dsp->gpr_latch_idx] = data; dsp->instr[dsp->instr_idx++] = data; break; }
} uint32_t es5510_read(es5510_mock_t *dsp, uint32_t offset) { if ((offset & 0xE0) == 0x80) return dsp->gpr[dsp->gpr_latch_idx]; return 0;
}With this mock in place, the DSP microcode upload completed successfully. The firmware wrote all 160 instructions, read back the verification GPRs, and was satisfied. One chip down, three to go.
The SCC Event Register Puzzle
The next wall was at address FC2079. The firmware was polling it with this sequence:
[FFF8A1C0] MOVE.B ($00FC2079), D0 ; read SCC event register[FFF8A1C6]ANDI.B #$1F, D0; mask to lower 5 bits[FFF8A1CA]BEQ.S $FFF8A1D2; if ALL ZERO: continue[FFF8A1CC] BRA.S $FFF8A1C0 ; else: loop forever; Firmware wants (event_reg & 0x1F) == 0x00; ALL event bits must be CLEAR = "all events handled" The AND #$1F, BEQ pattern is the key: the firmware tests the lower 5 bits of the event register and branches only if all of them are zero. This is the SCC (Serial Communication Controller) event register — each bit represents a pending event (TX buffer empty, RX data ready, etc.). The firmware wants all events cleared, meaning "nothing is pending, the channel is idle."
Our initial mock returned 0x02 (TXB — Transmit Buffer empty), thinking that was a "good" status. Wrong. TXB set means "there's a pending TX event you haven't acknowledged." The firmware saw a pending event and kept looping, waiting for us to clear it.
The fix was embarrassingly simple:
BEFORE: read FC2079 -> 0x02; TXB set = "event pending"result: AND #$1F = 0x02, BEQ fails -> infinite loopAFTER: read FC2079 -> 0x00; all clear = "nothing pending"result: AND #$1F = 0x00, BEQ succeeds -> boot continues Return 0x00 = "all events handled, channel is idle." One byte, one bit, days of debugging.
The 68681 DUART Odd-Byte Fix
The Motorola 68681 DUART is an 8-bit peripheral on a 16-bit bus. On the 68000, an 8-bit device connected to data lines D0–D7 appears at odd byte addresses only. Register 0 is at offset 0x01, register 1 at 0x03, register 2 at 0x05, and so on. The formula to convert a bus address to a DUART register index is:
// DUART register mapping on 16-bit bus// Bus address FC48xx -> DUART register (xx - 0x01) / 2register = (address - base - 1) / 2; // Examples:FC4801 -> reg 0x00 = MR1A/MR2A (Mode Register A)FC4803 -> reg 0x01 = SRA/CSRA (Status/Clock Select A)FC4813 -> reg 0x09 = SRB (Status Register B)FC4817 -> reg 0x0B = THRB (TX Holding Register B) We got this division wrong initially. Off by one. Address FC4813 was being mapped to register 0x0A instead of 0x09. Register 0x0A is the reserved/test register. Register 0x09 is SRB — the Status Register for Channel B, where the firmware checks the RXRDY (Receive Ready) bit.
With the wrong mapping, the firmware read a test register that never reported RXRDY, and sat in a polling loop waiting for serial data that would never arrive. Fixing the address calculation let SRB respond correctly, and the DUART handshake moved forward.
The DBRA Fast-Forward Hack
While debugging the init loops, we noticed the emulator spending enormous amounts of time in tight delay loops. The 68000 idiom for "waste N cycles" is:
; Classic 68000 delay loop MOVE.W #$FFFF, D0 ; D0 = 65535DBRA D0, * ; decrement D0, branch to self until D0 = -1; opcode: 51C8 FFFEDBRA D0, * (Decrement and Branch) with a branch offset of $FFFE (which means "branch back to yourself") executes 65,536 iterations, doing nothing but burning CPU cycles. The ASR-10 firmware uses these delay loops everywhere — after every register write, waiting for analog circuits to settle, between communication retries.
In an emulator, there's no analog circuit to wait for. These loops were eating millions of emulated cycles for nothing. So we added a pattern detector:
// DBRA fast-forward: detect opcode 51C8 FFFE and skip aheadif (opcode == 0x51C8 && displacement == 0xFFFE) { // This is DBRA Dn, * (branch to self)// Instead of executing 65536 iterations, just set Dn = -1 cpu->d[reg] = 0xFFFFFFFF; cpu->pc += 4; // skip past the DBRA// Saved: ~600,000 emulated cycles per occurrence
} The emulator detects the 51C8 FFFE pattern (DBRA to self), sets the counter register to -1 (the exit condition), and skips past the instruction. Boot time dropped from minutes to seconds.
The ROM Patch
Even with all four mocks in place, the firmware was still stuck. The CPU kept executing BSR $FFF8A0AE — the DOC/DSP initialization subroutine — and never returned. The subroutine was completing its work (uploading microcode, configuring voices), but then entering a verification loop that checked hardware state we couldn't yet replicate correctly.
After days of debugging the verification logic — trying different status register values, timing variations, interrupt flag combinations — we made a pragmatic decision: patch the ROM.
; Original instruction at the BSR call site:[FFF89BD6]BSR $FFF8A0AE; 6100 04D8; calls DOC/DSP init -- never returns; Patched:[FFF89BD6]NOP; 4E71[FFF89BD8]NOP; 4E71; skip init, continue to DUART handshake We replaced the 4-byte BSR instruction (6100 04D8) with two NOP instructions (4E71 4E71). Same size, no side effects, and the firmware sailed past the DOC/DSP initialization and into the next phase: the DUART handshake with the front panel.
ROM patching is a compromise. It means the DSP effects engine won't be initialized, and the 32 DOC voices won't be configured. But it unblocked the boot process so we could reach the display output code. We'll circle back and fix the verification logic properly once we understand the full DSP protocol. First, let's see what the firmware has to say.
Checkpoint
After building mocks for four chips, fixing a one-bit status register error, correcting an off-by-one address mapping, optimizing away millions of delay cycles, and surgically patching one stubborn subroutine, the ASR-10 firmware was finally past its hardware initialization phase.
The CPU was now executing code we hadn't seen before — the DUART communication protocol that talks to the front panel. Bytes were flowing to Channel B. Something was about to appear on the display.
Next: the firmware sends a handshake to the front panel, and for the first time, bytes flow that decode to readable text. Read the conclusion in ENSONIQ ASR-10.
