File Op Error

The emulator broke through the INSERT DISK wall and the firmware tried — for the first time — to read the floppy disk. It failed, but it tried.


The FDC Identity Revealed

We had been wrong about FC3000. For weeks we assumed it was the ES5510 DSP — it sat in the right address range, and our ES5510 mock had been absorbing writes to it without complaint. The firmware wrote to it, read from it, and moved on. Everything seemed fine.

Then we found the ASR-10 service manual. Buried in the error code table was the answer: "Errors 40-44: NEC PD 72069 errors". The NEC uPD72069 is a floppy disk controller — a member of the uPD765 family that drove every PC floppy drive from the 1980s onward. The ASR-10 uses a Sony MPF420-1 floppy drive, a 3.5" HD mechanism that connects directly to this chip.

What we thought was a DSP had been a floppy disk controller the entire time. Our ES5510 mock at FC3000 had been silently absorbing FDC commands — Specify, Recalibrate, Sense Interrupt Status — and returning zeros. The firmware saw a controller that never errored, never had a disk ready, and never complained. It simply looped forever, waiting for a disk that would never arrive.

Building the FDC Mock

With the chip identified, we built fdc72069.h based on the uPD765 family protocol. The NEC uPD72069 is register-compatible with the uPD765A but adds a few enhancements for higher-density formats. The ASR-10 maps it into I/O space starting at FC3000, with the registers spread across the address range at specific offsets:

// NEC uPD72069 FDC register map (base FC3000)FC3001 DMA registers // DMA address/count setupFC3025 Status / Control // drive status, motor controlFC302D Main Status Register // RQM, DIO, BUSY flagsFC303F Master Reset // write to reset the FDCFC3181 Command Register // send command bytes hereFC31C1 DMA Buffer // sector data transfer

The Main Status Register at FC302D is the gatekeeper. Bit 7 (RQM — Request for Master) tells the firmware whether the controller is ready to accept or provide data. Bit 6 (DIO) indicates the direction: 0 = host-to-controller (command phase), 1 = controller-to-host (result phase). The firmware polls this register obsessively before every byte transfer.

We loaded the 1.6 MB OS disk image into the mock — the same image we extracted back in the first article. The mock maps it as 80 cylinders, 2 heads, 20 sectors per track, 512 bytes per sector. When the firmware eventually asks to read sector data, the mock will serve it straight from the image file.

The Interrupt System

Here was the problem no one tells you about in emulation tutorials: the firmware runs with all interrupts masked. The Status Register's IPL field is set to 7 — the highest priority level on the 68000 — which blocks every interrupt except NMI. The init routine we NOP'd out back in the peripherals stage normally lowers the IPL after hardware initialization completes. Since we skipped that routine, the CPU was deaf to every interrupt the FDC (or anything else) tried to raise.

Three things had to happen to make interrupts work:

  1. Install the vector table. The boot code copies interrupt vectors from ROM at 0x2000 to RAM at 0x0000. This copy is part of the init sequence we had partially bypassed. We ensured the vector table was properly installed in RAM.
  2. Lower the IPL from 7 to 0 after the boot sequence completes, allowing the CPU to respond to hardware interrupts at any priority level.
  3. Implement the Musashi interrupt acknowledge callback. When the 68000 acknowledges an interrupt, it expects the interrupting device to provide a vector number. We wired the callback to return the correct vector for each device.

The vector table tells the full story of the ASR-10's interrupt architecture:

68302 interrupt vector table
VecIRQDevice Handler--- --- ------------------ -----------703 Timer FFFB9346744 Keyboard Scanner FFFB93B2755 FDC (data ready) FFFB8A10775 FDC (completion) FFFB8A48866 DUART FFFB94C0; Vectors installed by boot code:; ROM[0x2000..0x03FF] -> RAM[0x0000..0x03FF]; 256 vectors x 4 bytes = 1024 bytes

Two vectors for the FDC — 75 for data-ready (the controller has a byte to transfer) and 77 for command completion (a seek or recalibrate finished). The DUART at vector 86 handles serial communication with the front panel. The timer at vector 70 drives the system tick. The keyboard scanner at vector 74 polls the button matrix. Each one a thread in the machine's nervous system, silent until we unmasked them.

The $049D Variable

Every complex system has a variable that controls everything. In the ASR-10, it lives at RAM address $049D — a single byte that acts as a state machine for the entire disk subsystem. The boot sequence, the splash screen, SCSI detection, the INSERT DISK prompt, disk loading — all controlled by the value of this one byte.

We found it by disassembling the message lookup table at FFFB8EC2. The firmware stores 18 entries, each consisting of a 2-byte state code followed by a 4-byte pointer to a display string. The lookup routine walks the table with ADDQ.L #6,A0 increments, comparing the current value of $049D against each entry's code until it finds a match.

message lookup table at FFFB8EC2
Addr Code String Addr Message---------- ----- ----------- --------------------------FFFB8EC20x00 FFFBA210 "DISK OPERATION SUCCESS"FFFB8EC80x01 FFFBA228 "DISK NOT FORMATTED"FFFB8ECE0x02 FFFBA23C "WRONG OS VERSION"FFFB8ED40x03 FFFBA250 "DISK WRITE PROTECTED"FFFB8EDA0x04 FFFBA268 "DISK FULL"FFFB8EE00x05 FFFBA274 "PLEASE INSERT DISK"FFFB8EE60x06 FFFBA288 "FILE NOT FOUND"FFFB8EEC0x07 FFFBA298 "FILE OP ERROR"FFFB8EF20x08 FFFBA2A8 "DISK ERROR"FFFB8EF80x09 FFFBA2B4 "CANCELLED"FFFB8EFE0x0A FFFBA2C0 "LOADING INSTRUMENT"FFFB8F040x0B FFFBA2D4 "SAVING INSTRUMENT"FFFB8F0A0x0C FFFBA2E8 "FORMATTING DISK"FFFB8F100x0D FFFBA2F8 "READING DISK"FFFB8F160xFC FFFBA308 "SEARCHING FOR SCSI DEV"FFFB8F1C0xFD FFFBA320 "SCSI INSTALLED"FFFB8F220xFE FFFBA330 "ENSONIQ ASR-10"FFFB8F280xFF FFFBA340 "LOADING SYSTEM"

Eighteen messages. Eighteen states. The entire disk subsystem's vocabulary, laid out in a flat table with 6-byte entries. The lookup function is elegant in its simplicity — load A0 with the table base, compare the first word against $049D, advance by 6 if no match, repeat until found or end of table.

ValueStateMeaning
0xFESplashShow "ENSONIQ ASR-10" on LCD
0xFDSCSI CheckDisplay "SCSI INSTALLED"
0xFCSCSI ScanDisplay "SEARCHING FOR SCSI DEV"
0x05Wait Disk"PLEASE INSERT DISK" — the loop we were stuck in
0x0DDisk Changed"READING DISK" — try to read the floppy
0xFFLoading"LOADING SYSTEM" — OS load in progress
0x00Success"DISK OPERATION SUCCESS" — boot complete

The state machine is a loop: the firmware writes 0xFE to $049D at the start of each check cycle (show the splash screen), then the display handler overwrites it with 0x05 (prompt for a disk). Without a disk change signal, the value stays at 0x05 and the firmware loops back to the top. With a real floppy drive, inserting a disk triggers a hardware signal that causes the FDC interrupt handler to set $049D to 0x0D, breaking the loop and entering the disk read path.

The Override Trick

We didn't have a working disk-change signal. The FDC mock was new and the interrupt protocol wasn't fully wired. But we had something better: direct access to RAM. The emulator can intercept every write to memory. We added a simple override: whenever the firmware writes 0x05 or 0xFE to address $049D, we force the value to 0x0D instead.

// Override the disk state variable to skip the INSERT DISK loop// Address $049D controls the entire disk subsystem state machinevoid m68k_write_memory_8(unsigned int addr, unsigned int val) { if (addr == 0x049D) { if (val == 0x05 || val == 0xFE) { // 0x05 = "PLEASE INSERT DISK" (waiting loop)// 0xFE = "show splash" (reset to top of cycle)// Force to 0x0D = "disk changed, try to read" val = 0x0D; } } ram[addr] = val; }

Two lines of logic. The firmware writes 0xFE to reset the cycle, we intercept and write 0x0D. The firmware reads back 0x0D, interprets it as "a disk was inserted and changed", and jumps to the disk read code path. It never sees the INSERT DISK prompt. It never enters the waiting loop. It goes straight to reading the disk.

This is the power of emulation-level reverse engineering. You don't need to understand the full disk-change detection circuit, the FDC interrupt timing, or the hardware debouncing logic. You find the variable that matters, and you control it directly.

The Breakthrough

We ran the emulator with the override in place and the FDC mock loaded with the OS disk image. The LCD output told the story:

LCD output -- boot sequence with $049D override
[LCD line 1]ENSONIQ ASR-10[LCD line 2] ...[LCD line 1]SCSI INSTALLED[LCD line 2] ...[LCD line 1]SEARCHING FOR SCSI DEV[LCD line 2] ...[LCD line 1]FILE OP ERROR NUM= 000[LCD line 2] ...[LCD line 1]FILE OP ERROR NUM= 000[LCD line 2]
FILE OP ERROR NUM= 000
40x2 Character LCD -- Virtual Display

FILE OP ERROR NUM= 000.

The firmware passed through the splash screen, SCSI detection, and — for the first time ever — skipped the INSERT DISK loop entirely. It entered the disk read path. It tried to send commands to the FDC. It tried to read sector 0 of the floppy disk. And it failed, because the FDC mock doesn't yet implement the full uPD765 command protocol. But it tried.

Error code 000 maps to a generic file operation failure — the firmware couldn't complete the initial disk read. The read command sequence on the uPD765 family is multi-phase: the host sends a command byte, then parameter bytes (cylinder, head, sector, sector size, end-of-track, gap length, data length), then the controller enters execution phase where it reads the disk and transfers data, then finally a result phase where it reports status. Our mock wasn't handling this sequence correctly, so the firmware saw garbage in the result phase and reported the error.

What's Next

The FDC mock needs to properly implement the uPD765 command protocol. The firmware sends multi-byte commands through the command register at FC3181:

  • Specify (0x03) — set step rate time, head unload time, head load time, DMA mode
  • Recalibrate (0x07) — move the head to track 0
  • Sense Interrupt Status (0x08) — read the result of the last seek/recalibrate
  • Seek (0x0F) — move the head to a specific cylinder
  • Read Data (0x06) — read one or more sectors from the disk

Each command has a command phase (host sends bytes), an optional execution phase (controller reads/writes the disk), and a result phase (controller reports status bytes back to the host). The mock needs a state machine that tracks which phase it's in, how many bytes to expect for each command, and what results to return.

With the correct protocol, the firmware should read sector 0 of the disk, validate the Ensoniq filesystem header ("OS-V350ID"), and transition $049D to 0xFF"LOADING SYSTEM". From there, it will read the remaining OS blocks into RAM and boot the full operating system.


From a misidentified chip to a working FDC mock. From a firmware stuck in an infinite INSERT DISK loop to one that actively tries to read a floppy disk. The error message is not a failure — it's proof that the emulator has reached the disk I/O layer for the first time. Every "FILE OP ERROR" is the firmware saying: I see a disk. I'm trying to read it. Help me understand the protocol.

The next article will be about making that read succeed.

The $049D variable and the message table at FFFB8EC2 are now documented in the emulator's source. Every state transition the firmware makes through this variable is logged, giving us a real-time view of where the boot sequence is and what it expects next.