FPGA-based ProFile drive emulator for the Apple ][, ///, and Lisa
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
It's exciting weekend time here at Bauer Labs!
A pile of PCBs has plopped itself upon my bench thusly...
which, after my toaster-oven-reflow treatment, had a few solder bridges that needed removing...
And then a bit of programming...
A tiny bit of case painting (yes, I could have sanded it down a little, but I'm impatient!)...
Fully buttoned up:
I'm in a little bit of disbelief here. This has been a six-year journey of learning about FPGAs to get to the point where I could design and build this thing. I had no idea that I'd be able to support multiple boot images using the stock ProFile interface card; or that I'd be able to mount multiple volumes at the same time from ProDOS. Nor that I'd be able to support full 32MB volumes. Heck, I didn't even know that I'd be able to wrap my head around FPGAs.
But here we are.
The last piece for me to do is publish all the code, which I'll be doing shortly; I just need to clean up the repo (there are hundreds of useless artifacts I'm not going to pollute the world with). Look for a link in the project description to my github when it's done. The schematic, code, and case STL should all be there in the near future...
Obviously, I'm prepping the r2 board. Here's where I am so far; still a few bits to clean up...
The r1 board was large and spaced out intentionally; more room to repair errors. Now it's time to crunch it down.
And while I'm smallifying the board... I'm biggifying the parts. I'd never worked with 0402 size parts before and, while I did manage to get it all working, it was not exactly fun. So we're back to 0603 here; unless I have need to shrink down more, I see no reason to use the little beasties I can barely see, and easily fling around the room accidentally. (The R3 lesson from my r1 board is really what made my mind up here: I dropped a 220 ohm resistor, picked up another resistor that was right there that I thought was the one I dropped, but it was a 10k.)
I've also dropped the parts I never used - the USB-C JTAG driver and all its support electronics are gone. I left in the LEDs because LEDs are cool, but they can be left out in a final build. As can the flash and jtag headers. I'll just need some way to initially program the flash (maybe an SOIC adapter, program them before they're even on the board).
The buck regulator pinouts are corrected; I've changed the footprint for SW1 to match what I've got sitting around; added a few passives to correct missing pull-ups that should make the flash more reliable, and stub terminators where the leads continue to an unpopulated header.
One thing I have to give up here, though: I really like having all the part labels right next to the parts. I've packed a lot of parts in places where I can't necessarily do that... so they're all on the back.
I also want to address a minor noise issue here. I've always been able to hear high frequency noise - like flyback transformers in CRTs. I've lost some of my hearing but that's never faded. Consequently, I was not surprised to hear the buck circuits doing their thing. For those that don't know how they work: high efficiency voltage converters work by rapidly turning on and off the supply, storing energy in magnetic fields and converting it back to electrical supply in a carefully timed dance. The frequency at which it switches on and off is usually very high, so that we can't hear what it's doing - something in the MHz range, well above what any animal can hear. But some parts (and, notably, the parts I picked for r1) do their best to be ultra-efficient; if the demand for current is not high, then it falls back to a much slower speed to save power. In this case, the TLV62568DBVR normally runs at 1.5 MHz and absolutely drops to something I'd estimate at 16kHz. Which is undocumented (the frequency, that is; the behavior is well documented as a power saving measure). There's no way to tell it to not do that, and it is very annoying. So we'll switch to the TPS62A01APDDCR - this is a forced-PWM-mode device that does waste a few extra mA of power, but should stay at its 2.4MHz switching speed so that we don't hear it. (Okay, so that *I* don't hear it.) The cost? Well, I've got a few of the old parts that will now do nothing. Some new inductors and resistors. And overall? It's actually cheaper, by about $1.34/board.
In a couple weeks, we'll see how the r2 PCB fares. And in the mean time, at least I've replaced the pile of boards and adapters with a single bodged-together PCB!
Well here it is, the r1 prototype 2-layer PCB.
I opted to take some risks: 0402 parts (look under "J4 JTAG" - those little black dots are 0402 resistors) and to err on the side of caution in other places (the TQFP-144 FPGA is a compromise: fewer pins, but I avoid BGA).
What you're looking at here is just after the solder mask was squeegeed and most of the important parts have been placed. Notably absent: U5 in the upper-right, although I did fit most of the support parts in case I put it in later; SW1 down bottom-center; and a bunch of LEDs on the left, D1-D8. I picked out some spare LEDs and a random surface mount switch before reflowing the board - but that U5 part was very much "just in case".
Just in case of what? Just in case I couldn't figure out how to program it. There are three paths on this board to programming the flash chip - most obviously J6, the "FLASH SPI" port; then the J4 JTAG port, to have the FPGA itself program the flash; and lastly, a direct serial JTAG connection through USB-C, which is also supplying power to the board.
I was certain that the USB-C serial path would be one I tear out for r2 of the board, once I understood how it all worked. And that turns out to be true - I didn't need it, and it's not moving to the next revision. But it was touch and go for a while.
Now, I"m a toaster-oven-reflow kinda guy. I've never made anything big enough where that's a problem; I don't even have temperature sensors in the oven. I use low-temp solder and, by hand, preheat (soak) the PCB below the melt temperature before cranking it up to melt-temperature and then cooling it back off. I've made hundreds of PCBs this way - all small, about 6" x 3" or smaller. But this relative behemoth, with its large TQFP-144 FPGA, I had no idea how it would go. And the answer would be "poorly."
The board is large enough that it heated unevenly. Even with a long soak period, I had inconsistent melting of the solder; I wound up reflowing it a second time, rotated 180 degrees. And even then some of the solder joints on the FPGA looked a little rough. But no matter; it passed basic inspection for shorts and conductivity, which just left the question of "did I accidentally fry anything."
Not as such. Which doesn't mean it worked. But it also didn't explode, which I'm taking as a win.
Let's start counting the errors, though. Some are simple - the registration holes under the MicroSD card were in the wrong place, so it doesn't sit flush and doesn't have all the mechanical strength it should really have for a part that has something pushed into it. Easy enough to fix in the next revision, and doesn't really stop me from using it in r1. And I missed connecting the DB25 shell to ground, which is inconsequential here but weird.
Then we have the actual "it can't work that way" errors. The easier ones: the footprint for the SPI flash is for a narrow JEDEC SOIC-8, where I've got a wider part (EIAJ 208 mil). Half the pins made the pads, and the other half did not. The LEDs I picked had the same problem; wrong size footprint. Not a huge problem but ones I would have preferred to deal with before the first reflow. A little wire down the edge solves that one.
Worse: somehow, I completely bungled which pins were on which pads on two parts. This FPGA is a low-voltage part, and as such needs three voltage rails: 1.1v, 2.5v, and 3.3v. The buck regulators I used for 1.1v and 3.3v? Yeah, that's gonna leave a mark:
Not pretty, but it works.
And lastly we have the "that's... not great" errors. When the FPGA boots, it needs the flash to be ready at precisely the right time. And if you press the reset button it needs to make sure the button doesn't cause multiple different presses through the cycle. Both of those are solved with an extra 1uF capacitor across the switch. And all of that leads to this fun version of the PCB:
I've been telling a little fib for a while here. Or, more accurately, I've been letting you continue to believe something that isn't true.
This emulator isn't running on a Teensy 4.0.
Yes, it started that way; the hacked-together prototype used for debugging, which was useful through the proof of concept that I was, indeed, emulating a ProFile. But it had some timing issues and rather than pushing the brute-force "my CPU can out-CPU your CPU" race, I decided to take the more robust path.
It's on an FPGA, carried on a commercially made PCB, built so that anyone can build their own.
My first pass was on an Alchitry Cu dev board - which I quickly outgrew, and moved up to the Alchitry Au. Overkill, but functional.
Then I rebuilt it on the ULX3S-12F, which meant building another adapter in the middle to keep the wiring I'd built for the Au. Not pretty, but functional just the same (shown here with my bus sniffer in the middle of the wiring, and the original Teensy ProFile reader on the left).
The '12F is a better choice. The Au is based on the Artix-7, which is somewhere shy of $100 per part; the LFE5U-12F is a much more sane $20. To make my life easier I also built it around the only non-BGA version of the 12F.
This was challenging to route - the TQFP-144 is really dense. But I'd rather have the difficult routing than have to deal with ensuring a ball grid array device gets properly soldered to pads I can't even see.
The verdict? Still pending! The boards have arrived, they're waiting for me to build one and test it out.
This feels like we're mostly done. The card fixed, the architecture understood, and we've pushed past limits of the original intended system pretty far already. But can we go further?
Yes. Yes, we can.
Let's start with the logical swap. In theory, if I had two ProFile hard drives I could boot off of one, swap the cable to the other, and read it just fine. There's no hard drive cache for us to defeat; we'd just have to chance that the electrical swap didn't accidentally fry anything. (Yes, that's a real possibility; I would definitely not perform this swap if I were you. But theoretically it's possible.)
Yes, I built it - specifically for ProDOS, it's possible to hot-swap volumes. And something more that I'll save for the end.
When thinking about whether or not this is possible, the first question is - what is the interface that allows us to do this? The BMOW FloppyEmu does the same, when you press buttons on its screen; but the current emulator has no buttons nor screen, nor do I want it to have that complexity. My first thought was that maybe we could build a new command to pass through to the emulator. It has a simple command byte structure - 00 for read, and so on; perhaps I could add some magical $ED (mnumonic for "eject disk") command that the emulator would honor, and be able to do something with. But the ProFile ROM defeats this. For us to send that command, I'd either have to write a complex program that directly drove the card, or I'd have to write a replacement ROM. Neither of those sounded very appealing (but both very possible).
Instead, I opted for a bit of modern trickery: knocking.
A ProDOS volume is, at most, $FFFF blocks (16-bit); where the ROM turns anything $FF00 or over into $FFFFxx on its assumption that you're asking for some sort of control block data. So I built a pattern: if you read the four blocks $FF41 $FF32 $FF45 $FF44 in that sequence, over a short time, the emulator notices that it happens and triggers ... some behavior. This is safe for a physical ProFile drive - it should calmly return I/O out-of-range errors. And it's unlikely that someone will ask for those four specific blocks from an arbitrary hard drive image (although it is possible, and if it were to happen, I could extend the knock sequence). A little bit of assembly...
kloop: (re-point BUFL/BUFH to INFOBUF) lda ktab,x ; $41 $32 $45 $44 sta BLKL jsr drvcall ; errors ignored: a real drive refuses these ... x4 (re-point) lda #$C0 / sta BLKL jsr drvcall ; info block bcs nodev lda INFOBUF+4 cmp #$A5 ; the emulator's signature byte bne nodev lda #$C1 / sta BLKL ; fetch the boot blob... lda #$00 / sta BUFL lda #$08 / sta BUFH ; ...to $0800, where the ROM would have put block 0 jsr drvcall bcs nodev ldx unitsv ; X = slot*16, the boot-block entry convention jmp $0801 ; and enter it exactly as a boot would
... performs the knock, checks that the returned data looks like it should, and then runs the same picker menu that the bootloader uses. Boom - in-OS, runtime, re-selection of what drive is connected.
That's great if we're doing it by hand, but the actual use case I have is: moving files between two images. And to do that, it needs to be automated in some way. ProDOS really isn't command-line-ish, but how close can we get?
This is an excerpt of PICK, a binary that performs the knock, transfers file names from the emulator as data blocks, prints them onscreen, takes input to choose one of them, and then performs the swap.
After the knock, the info block ($FFC0) gives a count; and each catalog page ($FF80 + (idx>>3)) is an ordinary 512-byte block payload with 64-byte records. Those records were cached by the emulator at its startup, so there's no extra overhead; it's just data transfer couched in special terms, overloading block numbers near the end of the device within a short time frame. Wait too long and you get the normal disk block instead.
Great; I can now...
Read more »As mentioned in Act 4: I've got a bootable 32MB-capable system - except when I don't. I've found two published hard drive images that just won't boot at all: the Total Replay image, and Nox Archaist. Both absolutely worth the time to figure out why not.
The symptom is clear. Both images completely hang when you try to boot them, very unsatisfyingly like this -
Without a distinct crash, this is harder than just disassembling what's happening. This means we need to take a look at the boot block for Total Replay and walk the whole boot cycle through the ROM. Given that these images are 32MB, it's possible that they're trying to load something from the end of the disk and failing - given my geometry hack and the $FF00 magic block rewrite override. Maybe THAT'S my bug!
We can start with the debug output of the Teensy ProFile emulator, which dumps this more than a dozen times a second:
rd blk=$000000 block 0 rd blk=$00D848 "next directory block" = bytes +2/+3 of block 0 = 48 D8 (the loader's own TXA/PHA) rd blk=$000002 block $D848's bytes +2/+3 are 02 00, so back to block 2 rd blk=$000003 rd blk=$000004 rd blk=$000005 null next pointer -> read block 0 "as a directory" -> repeat
So that hypothesis is dead; that's not my bug. It's looping, not getting anywhere near the end of the disk, in a very sane boot pattern that just never completes. We're going to have to go deeper - down in the Total Replay boot block disassembly:
$0867 LDA #$1E $0869 STA $45 ; driver buffer hi = $1E00 $086B STA $3F ; scan pointer hi = $1E $086D JSR READBLOCK ; $08C3: ReadBlock via the card's ProDOS driver $0870 LDA #$04 $0872 STA $3E ; scan pointer = $1E04 (first dirent) $0874 LDY #$00 $0876 LDA ($3E),Y ; entry's storage/length byte <-- walks the pointer $0878 AND #$0F ; name length $087A TAX $087B INX $087C CMP $09A4,Y ; compare against "LAUNCHER.SYSTEM" $087F BEQ $0898 $0881 CLC $0882 LDA $3E ; next entry: pointer += $27 (39 bytes) $0884 ADC #$27 $0886 STA $3E $0888 BCC $088C $088A INC $3F $088C CMP #$FF ; end of this block's entries? $088E BNE $0874 $0890 LDX $1E02 ; next directory block, ABSOLUTE from the buffer $0893 LDY $1E03 ; (this is why the hops work but the walk doesn't) $0896 BCS $0867 ; read it, resetting only the high byte $0898 INY $0899 LDA ($3E),Y ; match the name, character by character $089B DEX $089C BNE $087C $089E STX $FF $08A0 LDY #$11 $08A2 LDA ($3E),Y ; key pointer (index block) lo $08A4 TAX $08A5 INY $08A6 LDA ($3E),Y ; key pointer hi $08A8 TAY $08A9 JSR READBLOCK ; read the file's index block READBLOCK: $08C3 STX $46 ; block number lo (caller passes the block in X/Y) $08C5 STY $47 ; block number hi $08C7 LDA #$01 $08C9 STA $42 ; command 1 = READ $08CB LDA $45 ; save the buffer high byte... $08CD PHA $08CE JSR $D1D1 ; PLACEHOLDER: rewritten at boot to JSR $CnEA $08D1 PLA $08D2 STA $45 ; ...and restore it: the card's read loop advances $45 by 2 $08D4 RTS
What I see there is a ton of zero-page magic variable addresses, self-modifying code, and cleverness.
The 6502 has distinct opcodes that make it faster, easier, and the code smaller when using the first $100 bytes of memory for variables ("zero-page addresses"). This memory space is, then, incredibly valuable: if you have a program running, which is using an operating system, which talks with drivers, all of them will need some set of it and - as with most hardware of this era - there's no real way to stake your claim that your special zero-page address is or is not already claimed. Handy charts like this existed in the 80s to help programmers figure out where to stash their variables, and what to avoid.
Lo Nibble of Address Hi Nib 0 1 2 3 4 5 6 7 8 9 A B C D E F --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 0 | A~ A~ A A A A - - - - A A A AI A A 1 | A A A A A A A A A A A A A A - M* 2 | M M M M M3 M MA3 MA3 M3 M3 M3 M3 MA3 MA3 M3 MA3 3 | MA M MA MA3 M M3 M3B M3B M3B M3B M~ M~ MA3~MA3~MA3~MA3~ 4 | M3~...Read more »
This is where things start really becoming fun.
With the difficult repair behind me, now I can ask a new question: "What can I do with this card?" With the actual ProFile still enjoying its second retirement, and this Teensy stand-in sitting in its place, I have the opportunity to consider what this project has promised from its title - a modern emulator that performs some novel tricks.
But let's start at the beginning.
We've gotten to the point where the emulator is capable of booting the machine. I sort of hand waved over the end of the debugging cycle because it was already very long. In reality the end of the debugging cycle didn't go nearly as smoothly; once I got good bus conversations flowing through the card, I still had some minor trouble. I had to properly rewire part of it, and remove/rework quite a lot of debugging code so that it worked fast enough to meet the ROM's timing expectations. That process is really what netted me the flow diagrams in the previous entry; they were each painstakingly built out while debugging the emulator itself.
But we got there! A booting hard drive is literally the minimum requirement I had. And I started thinking about how cards like the CFFA card work. There are multiple volumes on some reasonably modern media, with a ROM hook at startup to detect a keypress and invoke a selection menu. Cards like this are built around older IDE drive expectations, so they tend do use the partition table and thus can only support 4 volumes. And when I'm talking about a relatively tiny CF card, maybe the wasted space is okay; but when we're talking about a 16GB piece of actual modern media, it seems absolutely crazy to be stuck with only one 10MB volume.
Here's my wish list, then. I want to be able to create an arbitrary number of volumes on my media, limited only by the size of the media itself, easily selected at boot time; with full 32MB volume support (as this is the maximum ProDOS volume size).
Which brings us to the first real obstacle. While I can obviously create whatever files I want on a microSD card, the boot-time selector is, typically, a function of the boot ROM. When you're designing a card from scratch you have that luxury. While I could write a replacement ROM, that feels like "a step too far" - for anyone that has a ProFile interface card in their Apple ][, where the card actually works, and they're looking to build/install some sort of hard drive replacement, it's reasonable to consider a peripheral that plugs in to the other end fo the cable; but it's not really consumer-grade product if they have to pop out a ROM chip along the way. Especially if that's likely to cause leaf socket fatigue failures.
No, I need another path. And I'm unreasonably excited by the one I came up with.
At boot time the ProFile card looks to the hard drive to retrieve state information from the drive itself, reading block $FFFFFF. This magic block number (and block $FFFFFE which is scratch RAM) was never in contention for actual drive space, because the drives in question were 5 MB or 10 MB - and with 512 byte blocks, that means we only need to address $4BFF blocks, at most.
After reading $FFFFFF, block 0 is read - the boot block of the device, which then loads whatever it needs to continue the boot process.
So what if we got sneaky here? The ProFile card, out of ROM necessity, always checks block $FFFFFF before it starts booting. What if, after seeing a $FFFFFF read, our emulator slipped in its own boot block that invoked its keydown press check?
Well, if we did that, we'd wind up with something like this...
... nice. Once our fake boot block has loaded, it treats follow-up reads of block 0 as normal, allowing the device to boot once something is selected. With clever code in the emulator to send filenames to the Apple ][, we can run whatever we want - it's fairly trivial as long as we can keep it within block 0 (512...
Read more »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?
Read more »The Apple ProFile service manual is generous in technical detail. You want to know what you'll see on your scope when a drive read is happening? There's a picture in there. But alas, the schematic in it is for the Apple /// interface card and I need the Apple ]['s. If I want to trace it out, I'm going to have to do it myself.
And that is what I did. I pulled the card and traced it, pin by pin, net by net. This is slow, meditative work: ohm out a connection, find both ends, decide what the part actually is, and draw it. Rather than lean on generic library symbols, I
drew custom ones so that every pin carries its real function instead of a factory label. When a pin's job is "D0 read strobe," the symbol says so.
This was also my first foray in to KiCad. I've used Eagle for many many years - but their licensing changes a few years ago made it unrealistic for me to keep shelling out dough for a product I use a couple times a year. I decided to bite the bullet and figure out how to move on.
Which gave me another opportunity - scripting the state of the card. As I identified parts and interconnections, it was easy enough to build netlists in a script and have that script regenerate the whole schematic.
It took about a week in total. A lot of beeping, dragging, sweeping, head-scratching, double-checking and documenting. Pull out the netlists and build actual wiring, because that's easier for me to see while tracing something (instead of a netlist's invisible connection to three other places, I can follow the lines).
Now I wouldn't want to build a replacement from this. I wouldn't say that it's verified 100% accurate - there were definitely late nights where I was mis-documenting something that I had to come back and fix later! - but it worked for its purpose here.
This part was genuinely hard, mentally. I know that I've made absolutely zero progress on solving my problem. I decided that the tangential problem of not having the schematic was important, and it's now tackled well enough for me to move on; but my original issue is none the better for it.
From my ProFile 10MB reader project you can see where I started: a 10MB hard drive and Apple //e which did not want to talk with each other. That reader was the "divide the problem in half" project: is the failure the drive, or the interface card in the Apple //e?
It was the card.
And having passed that hurdle, the next question was: can I repair the card?
In 2021 I did a brute force IC replacement of all of the components I could easily source, just as a "if this fixes it, fine, I'm done" - but it didn't. So I swapped the components back out, put the whole lot in anti-static bags, and shelved it as a future project.
But here in 2026 it *is* the future! How lucky.
Now, I had no idea how I was going to find the problem; and once I did find the problem, I wasn't sure what I was going to do with the card. A ProFile card is a rare thing to have these days and it's compatible with precisely one line of hardware that has precisely two choices of what to connect to it. And while I've got the bigger of them - my whopping 10MB hard drive - I also know from my 2021 work that the drive is magnetically failing. The blocks are being spared out every time I spin up the drive.
Right. First things first: "what will I do with it once it's fixed" is a yet further future problem (for which you probably know the answer, given the title of this project). The one to focus on right now is: how do I find what's wrong?
Let's list the issues.
And the assets.
A lot to unpack, but this feels very tractable.
Create an account to leave a comment. Already have an account? Log In.
Become a member to follow this project and never miss any updates