Close

Act 3: Carnac the Magnificent

A project log for ProFile Emulator

FPGA-based ProFile drive emulator for the Apple ][, ///, and Lisa

jorj-bauerJorj Bauer • 09/27/2026 at 18:30•0 Comments

Buckle up, this one gets deep into the weeds.

I've got the schematic in hand, close as I can tell. And I need to figure out how I'm going to identify what is wrong with this card. It won't boot my hacked-together ProFile emulator card: why? Is it a problem in my hardware implementation? I know the interface card wouldn't boot a genuine ProFile 10, so there's something wrong there - how do I find it, or know when it has been found and fixed?


With the ProFile itself this would be easier: use the known-good ProFile and debug away. But without one I'm fighting two simultaneous problems. If I want to solve this system, I'm going to have to know that when the Apple does , the ProFile should do , which means that the card did - and then find cases that violate those conditions, reverse engineer what failure on the card would cause that situation, and finally attempt some repairs.

My schematic tells me how everything is connected, but not what the card is thinking.

So I thought: I should start with the ROM. A disassembly of it will at least expose how the machine is supposed to use the drive; and from that, maybe I can build a reproducible test for any single part of the functionality. So here we go: 2048 bytes of code -

$C800                  STR      '(C) APPLE COMPUTER INC. 1984'
$C81C LC81C            PHP
$C81D                  SEI
$C81E                  JSR      LC829
$C821                  BCC      LC826
$C823                  PLP
$C824                  SEC
$C825                  RTS
$C826 LC826            PLP
$C827                  CLC
$C828                  RTS
$C829 LC829            STA      $3F
$C82B LC82B            LDA      #$00
$C82D                  STA      $3E
$C82F                  LDA      $C082,X
$C832                  LSR      A
$C833                  BCC      LC838
$C835                  LDA      #$28
$C837                  RTS
$C838 LC838            LDY      $42
...

 ... and so on, for a good while. It's data, to be sure, but we need it to be information.

Some of those entry points are known - ProDOS uses this as a standard block drive device, so we can annotate those entry points and generate a call flow graph to try to understand what it's actually doing and why:

$C800                  STR      '(C) APPLE COMPUTER INC. 1984'
$C81C DRIVER           PHP
$C81D                  SEI
$C81E                  JSR      DRV_DISPATCH
$C821                  BCC      RET_OK
$C823                  PLP
$C824                  SEC
$C825                  RTS
$C826 RET_OK           PLP
$C827                  CLC
$C828                  RTS
$C829 DRV_DISPATCH     STA      $3F
$C82B LC82B            LDA      #$00
$C82D                  STA      $3E
$C82F                  LDA      CARD_STAT,X
$C832                  LSR      A
$C833                  BCC      LC838
$C835                  LDA      #$28
$C837                  RTS
$C838 LC838            LDY      CMD
...

Nicer to read, but still difficult to interpret. Maybe the call flow will help:

Okay, maybe that's something, but it's clearly not enough. 

The ROM dump gives us useful information about command behavior and timing. It corroborates things we already know (block $FFFFFF is informational, block $FFFFFE is the hard drive's scratch RAM). So it's possible to gin up a test like this - 

   Apple //e                         DB-25            ProFile Teensy emulator
 +-----------------+    slot 5     +--------+   (stands in for the drive)
 | 6502 + card ROM |==============>|  card  |========> logs every byte, every
 | 341-0299-B      |  $C0D0-$C0D3  | 820-   |          handshake, and can be
 | probe ML at     |<==============| 5006-B |<======== told to assert or wait
 | $0300 / MLI     |               +--------+          for a specific state
 +-----------------+

Testing was slow - boot the computer, watch the Teensy emulator, and do a PR#5 - the Apple should try to read blocks, and it does - `00 FF FF FF 0A 03` is "READ $FFFFFF" (10 retries, spare threshold 3). Then what I see after is `02 AA` - the drive enters command mode with 02, and then the card NAKs it with AA. What I should see is `02 55`. Hmm... what does that routine look like?

  $CB80  LDA #$03 / STA $06F8         ; progress marker = phase 1  (loop top)
  $CB85  LDA #$01 / JSR $CDAC         ; (1) COMMAND handshake, expect $01
  $CB88  BCC $CB9F                    ;     carry clear = completed
  $CB9F  BNE $CB80                    ;     A != 0 (NAK) -> retry; A==0 (ACK) ->
  on
  $CBA1  JSR $CE4A                    ; (2) send the 6-byte frame 00 FF FF FF 0A
  03
  $CBA4  BCS $CC21                    ;     (frame error)
  $CBA6  LDA #$0A / STA $06F8         ; progress marker = second confirm
  $CBAB  LDA #$02 / JSR $CDAC         ; (3) CONFIRM handshake, expect $02   <-- 
  here
  $CBB0  BCS $CB8C                    ;     carry set  = timeout -> error path
  $CBB2  BNE $CB80                    ;     A != 0     = NAK    -> RETRY from 
  top
  $CBB4  JSR $CE7D                    ;     ACK'd: read 4 status/header bytes...

 Well, that looks correct. We can see the confirm attempt at step (3), with a carry check after JSR $CDAC:

  $CDAC  PHA                          ; save expected ($02)
  $CDAD  JSR $CE16                    ; wait for /PBSY ready (timeout -> carry 
  set)
  $CDB2  LDA ($3E),Y  x3              ; read $C505,$C507,$C504 soft switches:
                                      ;   set READ direction, release the '374, 
  assert /PCMD
  $CDC3  LDA $C082,X / BPL $CDE0      ; poll status bit7 until the byte is ready
  $CDE0  LDA #$00 / STA $3E / PLA     ; restore expected ($02)
  $CDE5  CMP $C081,X                  ; <== compare $02 against the byte on the 
  read node
  $CDE8  BNE $CDF2                    ;     mismatch -> NAK path
         ; --- match path (not taken on the fault) ---
  $CDEA  LDA #$55 / JSR $CEB8         ;     ACK: send $55, return A=0 (success)
         ; --- NAK path (taken) ---
  $CDF2  LDA #$02 / ORA $0778 / STA $0778   ; set error bit 1 ($CDAC mismatch) 
  in the screen hole
  $CDFA  ORA $04B8,Y / STA $04B8,Y          ; paint it into the status hole too
  $CE02  LDA #$AA / JSR $CEB8         ; <== EMIT $AA on the wire  (JSR $CEB8 = 
  STA $C080,X)
  $CE07  ...                          ; return with A != 0 / carry set  (= NAK 
  to the caller)

So the card should only NAK if the byte on the wire (02) isn't $02 (at $CBAB).

If the drive believes it was successful, and we see success on the wire, and the code is checking for the exact success condition we are seeing, why are we seeing a failure? Something must be wrong on the wire. Maybe it's timing of what we're reading (this isn't a real ProFile drive, after all); or maybe we're close to finding something wrong in the card's logic chips somewhere.

We'll cut it in half again: if we drive a constant value onto the bus from the emulator (say, $02), and then read it back from the computer side, then we have no timing issue to contest with. Can we read a value as put on the wire?

Well, $C0D1 (as long as the card is in slot 5) is the data-in read register. We just need a little magic ritual to read it. Johnny Carson did it with a sealed envelope he'd hold to his head as Carnac the Magnificent: "$20.20." Rips it open and reads: "What will a gallon of gas cost by next year." He's got the answer and has to understand the question, which is pretty much where we are, with a slightly more technical ritual. First we have to understand how this card should work so we can build the ritual to find the question.

The ProFile card doesn't just magically put the bus in $C0D1 when you read it. It has no idea when you're reading that memory location. We're talking about a dance between four chips on the card: a '374, '244, '259, and '279.

The 374 is the data latch that holds our value outbound, what we write to the bus. It's paired with a '244 that reads data back from the bus. The '374 drives the bus only while it is enabled, and the '259 holds the one bit that decides that - flip it one way and the '374 is writing such that the '244 can only see the '374's value; and the other way the '244 can see what the drive is actually saying.

To get to that point we also need to describe a couple other chips that decode the address lines. This is the whole data flow:

which means that our ritual to read has to be: 

This is what we want to see:

] CALL -151
* C0D3: 00
* C503
C503- xx
* C0D0: FF
* C0D1
C0D1- FF
* C507
C507- xx
* C0D1
C0D1- A5
* C503
C503- xx
* C0D1
C0D1- FF

The xx are inconsequential reads of soft switches which are passed through as the switching signals to the card. It's really the reads to C0D1 that we expect: in this sequence, we're driving $A5 on the bus from the emulator, so we should read FF / A5 / FF (as written). Which is indeed what we saw. Drive a $02 from the emulator, we read FF / 02 / FF.

And that, my friends, is the sanitized version. It didn't go nearly that smoothly. The first time, I read back 03 instead of FF. PD2 through PD7 were being dragged low, repeatably, though it wandered with how the card was seated. That's not logic or code; it's a copper failure somewhere.

The 74LS38 was stuck low. This is an open-collector NAND gate in the data-read decode path, sitting on the data-in lines. The IC was fried; I replaced it, and started reading the FF that I expected. And yet the card still NAKed; this was not the only failure.

At this point the ritual passes as written; FF / A5 / FF as expected. The 374 lets go when told to, the 244 can read properly, and I can switch back and forth between the two.

Which is the worst possible answer, because it means the new failure isn't in the copper. It's dynamic. The same switch that flips perfectly when I single-step it from the monitor is not landing when the ROM fires it in the middle of a live transaction, with real bus timing, a real /IOSEL window and a real PH1 edge. The suspects become the stateful parts: the '279 lock, the 74LS109 flip-flops, and the RC networks that shape the strobe. At this point I have little choice: what I really need is to know what's on some of the pins, live, while the machine's running.

But that's my last resort. The last thing I want to do is blow out the card or //e logic board due to a stray popped-off clip during debugging. So I started with a looong continuity campaign: beeping every net against my as-documented circuit map. And I found something interesting: with the ROM out of its socket, pin 18 (the 2716's /CE) had no continuity to the '279 that drives it. The ROM had been running on luck, or fumes, or capacitive coupling, who knows. I'd read the ROM fully and correctly, twice, and missed it.

Now there's a choice to make. The ROM is in a socket, which is great for rescuing the part; but the socket is soldered on to the PCB and would require some surgery to replace. But maybe we don't have to: the reason it's failed is almost certainly fatigue on one of the socket holes - a flat-pinned leaf-contact socket operates by spring force and, with a fatigued leaf, won't hold true against the IC's leg. But a wider piece of metal might work fine. A modern, machine pin socket has exactly this sort of profile and, for the cost of rising the ROM up one IC higher, gives us back the continuity that was lost. At least for now. Also sobering: every dynamic observation before that moment had been made on a card whose ROM was only mostly enabled, so all of it was suspect.

And yet... it still NAKs. Two problems found and fixed, and we're still seeing exactly the same failure we started with.

In retrospect, the ritual lost me some time here. I was mostly testing with $A5 driven, which has bit 0 set, which left me blind to the next domino in our chain.

So I built a bigger hammer. Serial console into the //e, the emulator driven from a script, and a new emulator knob that answers the confirm with any byte I choose. Then sweep it, in the real boot path. The result was strange and, briefly,
thrilling: the card ACKed the confirm only when the emulator answered $01, three times out of three, and NAKed $00, $02, $03, $41 and $81. The identical 


LDA #$02 / JSR $CDAC 

sequence, copied into RAM and executed from there, ACKed $02 exactly like the source says it should. Same compare, same drive, same card; the only variable was where the instructions were fetched from. Conclusion, written
down with great confidence: the 2716 is delivering a wrong byte under live timing. Maybe the ROM is sick and needs to be replaced. Replacements can be found, that's not a problem, but I have none in stock. So, after ordering a few replacements for a permanent solution, I dug deeper.

Spoiler: the ROM was fine.

The next morning an in-situ dump of the ROM at live bus timing came back byte-perfect. What the sweep had actually found was a trap in the ROM itself: when a confirm mismatches, $CDAC's error path re-handshakes expecting $01, the phase-one byte. So a sweep that answers $01 gets "accepted" because it matched the error path's retry, not the confirm. I had measured the failure mode and called it success.

Two things narrowed the gap.

The first was the 74LS109. This was new stock from 2021 that had seemingly died: on close measurement I found about 70 ohms to ground on multiple input pins in both polarities, where a healthy input reads one-way. Its FF1 is the card's parity-error latch, and a leaky FF1 behaves exactly the way this card had: statically clean, then set by anything, drifting between good and bad with handling and heat, probably degrading progressively. But somehow the original 1981 chip's inputs measured healthy, so it went back in. With a 100 pF cap from its preset pin to ground soldered at the socket as glitch insurance.

The second wasn't on the card at all. All the manhandling of my emulator board eventually snapped the BSY wire between the Teensy and the DB-25. The card's status bit 7 stopped tracking BSY, so the ROM's response-wait loop timed out. The Teensy could prove it was driving the pin, by reading back its own pad, while the card never saw it.

And finally, grudgingly, the //e read some blocks from the emulator and promptly crashed. Because the image I was using - the original 2021 artifact - is indeed not even bootable. It has its own corruption issues, and I suspect it was taken out of service specifically because the contents were somewhat corrupt. The filesystem looks intact enough but a few hours of side work showed a corrupted boot sector and some parts of the catalog hierarchy that were internally inconsistent.

Just look at the shape of all that. The card had two faults of its own: the '38 and the open /CE leg, where the NAK survived both repairs. Another part I replaced that didn't need it, I'd also broken it of my own testing; and the value sweep test that felt conclusive had confidently implicated an innocent chip. Many theories along the way were 100% reasonable. And wrong. Including the assumption that this hard drive image should boot cleanly when everything is working.


Which brings us to these very valuable reads - after getting to this point, I'd amassed a lot of behavioral analysis of the ROM and could build full flow diagrams of how the ROM behaves. Which becomes really important in the next entry...

Discussions