Where we were
Since the summer WonderMCA has worked on every PS/2 I own… as long as you did not look too closely at the 386DX machines. On the 286 and 386SX boxes (8550Z, 8555, 8556, 8557) the card was solid. On the Model 70 (8570), the same card, the same firmware and the same CPLD produced a slow drip of problems that never pointed at anything:
- at power-on the BIOS sometimes said “Memory change: Reset the RAM Config”, meaning the card’s configuration block had been changed behind its back;
DIR C:could reboot the machine;- loading a 67 KB
.EXEfrom the emulated floppy rebooted the PC before the program even started.
This post is the story of the last few weeks. It covers five things that turned out to be linked more than I expected:
- finding the real cause of the 8570 instability (spoiler: SmartDrive, and four address lines the firmware never looked at);
- a new feature: 4 MB of extended memory (XMS) served from the card’s PSRAM. Slower than the planar’s own RAM, but it works;
- updating the CPLD from the microSD card: the RP2350 now does the JTAG itself, so future updates no longer need a cable and a Windows PC;
- validating the rev 8 board, including the long hunt for the USB panics;
- the Server 95 (9595), which now runs DOOM after a DMA arbitration problem was solved.
It is a long one. Grab a coffee.
A quick map of the card
You need a picture of the card to follow the rest, so here is one.
MCA edge connector
A0-A23 D0-D15 /ADL /CMD S0 S1 M/IO CHRDY ARB0-3 /PREEMPT /BURST
| | | ^ ^
| | | | |
| | v | |
| | +--------------------------------------+
| | | ATF1508AS CPLD |
| | | - address decode (all 24 bits) |
| | | - CHRDY: "wait, I am not ready" |
| | | - card-selected feedback, DS16 |
| | | - DMA arbitration |
| | +--------------------------------------+
| | ^ RP_REL ("release the cycle")
v v |
+-------------+ +---------------------------------------+
| 74LVC257 |---->| RP2350B |
| address mux | | Core 1: the bus handler (assembly), |
| (A_AB pin | | serves every cycle |
| selects | | Core 0: the emulated devices |
| which half)| | (disk, Sound Blaster, USB, |
+-------------+ | network, screenpad...) |
+---------------------------------------+
| | | |
8 MB microSD USB host WiFi / W5500
PSRAM
The important idea is that two chips decide whether a bus cycle belongs to the card. The CPLD is fast and decodes the address in hardware: it tells the PS/2 “this is mine, wait for me” by pulling CHRDY. The RP2350’s Core 1 runs a tight assembly loop that watches for the start of every cycle (/ADL), reads the address through the mux and serves the data. When it has finished it pulses RP_REL so the CPLD lets the cycle end.
Two decoders. Keep that in mind.
Part 1: the SmartDrive ghost
Two months of wrong answers
The 8570 problems started in July. Here is what I tried, in order. I am listing it because every item seemed perfectly reasonable at the time.
| When | Theory | What I did | Result |
|---|---|---|---|
| July | The Model 70 reserves D7000 |
Moved the DPRAM window from D6000 to D5000 |
The hang moved somewhere else |
| July | The Model 80 claims D6000 |
Shrank the ROM, moved the window to D4000 |
“Memory change” at POST instead |
| Aug–Sep | 386DX bus timing: dword splits, data steering, a late DS16 | Timing proposals, logic analyser traces | Nothing conclusive |
| Sep | Stale address from the mux | CPLD rev 101, timed CHRDY on memory cycles | Built, never needed |
| Sep | Something writes the config block | A “canary” in the firmware + x86 tools: cfgdump, memstres, dwtest, latchtst, diskrd, made24 |
The first real evidence |
On 10 September I withdrew the two July “SOLVED” entries in the development log
publicly: the PS/2 does not reserve anything at D6000 or D7000. Every
window move had simply changed which part of the DPRAM got damaged. The good news is that I thought I had bad memory RAM and of course it was not the case, I was way more subtle and it was my mistake.
The canary sings
The canary was a 768-byte shadow copy of the configuration block, compared continuously, printing every byte that changed. On the 8570 it caught the damage in the act:
- right after POST 52/53 (the planar’s memory test above 1 MB): 50 bytes
changed, in 50 runs of exactly one byte, every one on a 4-byte grid, holding
test patterns
55,AA,00; - in DOS, during disk reads: bytes on a 4-byte grid holding sector data.
One byte in four, always on a doubleword grid, always holding data that was being moved around somewhere else in memory.
The one-minute test I should have done first
On 11 September I booted the 8570 clean (F5: no HIMEM, no SmartDrive) and tried again.
- Clean boot: no problem.
DIR C:fine,.EXEloads fine. - HIMEM alone: fine.
- HIMEM + SmartDrive: the problems come back.
Weeks of timing theories, and the trigger was a disk cache.
What SmartDrive does, and why it hurts
SmartDrive keeps its cache in extended memory, above 1 MB, starting just after
the HMA at about 0x110000. A cache of 800 KB or more therefore covers
0x1D4000–0x1D5FFF.
Now look at that address: 0x1D4000 is 1 MB + D4000. And D4000 is
exactly where WonderMCA’s DPRAM (with the config block) lives.
Physical memory on the 8570
0x000000 +------------------------+
| conventional 640 KB |
0x0A0000 +------------------------+
| video, ROMs |
0x0D0000 +------------------------+ <-- WonderMCA ROM D0000-D3FFF
0x0D4000 |########################| <-- WonderMCA DPRAM D4000-D5FFF
0x0D6000 +------------------------+ (config block, disk buffers)
| ... |
0x100000 +------------------------+ 1 MB
| HMA |
0x110000 +------------------------+
| SmartDrive XMS cache |
| |
0x1D4000 |########################| <-- 1 MB + D4000: SAME LOW 20 BITS
0x1D6000 | | as the DPRAM window
| |
+------------------------+
Two decoders, two widths
This is where “two decoders” matters. I finally found it on the night of 11 September.
- The CPLD decodes all 24 address bits. For
0x1D4000it correctly says “not mine”: no CHRDY, no card-selected feedback. The planar’s real memory owns that cycle. - The Core 1 handler built its page index from A12–A19 only. That is a
1 MB address space. So for Core 1,
0x1D4000and0x0D4000were the same page,0xD4.
Address bit: 23 22 21 20 | 19 18 17 16 15 14 13 12 | 11 ........ 0
------------+-------------------------+--------------
CPLD decode: [ used ] | [ used ] | [ used ]
Core 1 page: [ IGNORED ] | [ page index 0-255 ] | [ offset ]
0x0D4000 = 0000 | 1101 0100 | ... -> page 0xD4 -> DPRAM (correct)
0x1D4000 = 0001 | 1101 0100 | ... -> page 0xD4 -> DPRAM (WRONG)
^
A20 set, but Core 1 never looked at it
So every time SmartDrive wrote its cache at 0x1D4000, Core 1 also stored
the data into the DPRAM. On reads it drove the bus while the planar’s own RAM
was answering too. Because the CPLD had not claimed the cycle, there was no
CHRDY: the planar ran the cycles at full memory speed and Core 1 caught only
the first byte of each doubleword. That is exactly the one byte in four the
canary saw.
It also explains the rest:
- Why POST 52/53: the planar’s memory test walks the same 1 MB alias with
55/AApatterns. - Why reboots: SmartDrive served executables from a cache the card had been corrupting. The PC ran garbage.
- Why only the 8570: on the 386SX and 286 machines, memory above 1 MB stays on the planar and never reaches the channel. On the 386DX Model 70 it does.
The made24 tool confirmed it: with A20 verified ON, a write at 0x1D4000
came back at D400:1800.
The fix: two instructions
A20–A23 were already on the mux, on GPIO 24–27, and read by the same poll as A12–A19. The fix is a test before the page lookup in the memory read and memory write paths:
tst ADDR, #0x0F000000 @ any of A20..A23 set?
bne 7f @ yes: not ours, leave the cycle alone
ubfx ... @ page index from A12..A19, as before
That costs two cycles, taken from settle time the handler was already spending. On 12 September the 8570 ran HIMEM + DOS=HIGH + SmartDrive with no config corruption and no reboot. An arc that had started in July closed with two instructions.
Lesson: when a fault only shows under a particular software stack, boot without that stack before doing anything else. That test takes one minute. I spent weeks on bus timing for a bug that lived in the memory-manager layer.
During all that period, I was also hunting a mouse (COM2) emulation issue (chassis freezing) on particular game like DUNE or WARCRAFT. In the end, it was already the same HMA issue with RP2350 core1 taking the wrong address.
Why this bug, it was my fault, I blindly ported the code from the famous Picomem from FreddyV. FreddyV helped me doing this port and explained me how to manage MEMW and MEMR with tight timing but in the end I did not challenge the fact that ISA bus is only 0-19 Address line wide where the PS/2 is 0-23 Wide (considering that everything above is left aside with Made24 signal).
Part 2: 4 MB of extended memory from the PSRAM
Why
Many PS/2s have 1 or 2 MB of RAM, and DOOM wants 4 MB, this is the case for the 8550 and 8550Z without any slot expansion capabilities. The card carries an 8 MB PSRAM chip. Could WonderMCA serve some of it as real, bus-mapped extended memory? Not an emulated XMS driver but actual RAM that the POST counts and HIMEM uses, like a memory card?
The problem: PSRAM is slow
Serving a memory read from the RP2350’s internal SRAM (the ROM, the DPRAM) leaves about 80 ns between the start of the cycle and the moment the data must be on the bus. One byte from the QSPI PSRAM takes 150–250 ns. It cannot fit.
The MCA bus has an answer: CHRDY. A slave that is not ready pulls CHRDY low and the planar waits. WonderMCA already did this for I/O cycles. The new part is doing it for memory, on a window above the planar’s RAM:
Ordinary memory read (ROM / DPRAM, from SRAM): no wait
/ADL ‾‾‾\__/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
/CMD ‾‾‾‾‾‾‾‾\______________/‾‾‾‾‾‾‾‾‾‾‾‾
CHRDY ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ always ready
DATA ----------------< byte >----------
|<- ~80 ns ->|
Held memory read (XMS, from PSRAM)
/ADL ‾‾‾\__/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
/CMD ‾‾‾‾‾‾‾‾\__________________________________/‾‾‾‾‾
CHRDY ‾‾‾‾‾‾\___________________________/‾‾‾‾‾‾‾‾‾‾‾‾‾‾ CPLD says "wait"
PSRAM [== fetch 150-250 ns ==]
DATA ------------------------------< byte >---------
RP_REL ________________________________/‾\_____________ release = LAST
^
Core 1 must be back at the poll
before the next cycle starts
The window has to start above the planar’s RAM
A PS/2 with 1 MB keeps 640 KB low and remaps the other 384 KB to
0x100000–0x15FFFF. Claim any cycle there and the planar’s memory test fails
(POST 201). So the window starts at the planar’s top, and the CPLD has to do
that skip itself: any CHRDY on planar RAM breaks it, and doing the skip in
firmware only was not enough. The start is set with two DIP switches (CPLD rev
123):
| SW_CFG3 : SW_CFG4 | Planar RAM | Card window |
|---|---|---|
| off : on | 1 MB | 0x160000 + 4 MB |
| on : on | 2 MB | 0x240000 + 4 MB |
| on : off | 4 MB | 0x440000 + 4 MB |
| off : off | — | XMS off |
The same code goes into config.txt as XMS 1, XMS 2 or XMS 4, because
the firmware cannot read those switches.
The Model 70 had a trap: with 2 MB it reports 1920 KB, not 2048. It keeps
128 KB for itself. The rule became: planar top = n MB + 0x40000.
Everything that went wrong, and the rules it left behind
XMS took about three days and roughly twenty CPLD revisions (105 to 123). Each failure left a rule behind:
-
The init was dead code. On the first test all cycles were declined. The init call sat in a code path that the WM10 board never reaches, so the compiler dropped it. Rule: anything the WM10 needs must be on the live path. Check the ELF.
-
The second byte of every word read vanished. After releasing a cycle, Core 1 spent about 100 ns on counters and tracing before it got back to watching /ADL. The PS/2 starts the second byte of a word 10–100 ns after the first one ends. Rule: release is the last instruction. Do the bookkeeping while the cycle is still held, since that time is free.
-
Data shifted by one byte in streams. The stub read A0–A11 at T+21, after the page table lookup. By then the planar had already put the next address on the bus. Rule: sample the bus address in the T+17…T+21 window. Holding the cycle buys time for everything except the address.
-
POST stalled at 704, 832 or 960 KB. An address transient at a boundary (
0xFFFFF→0x100000) matched the window decode for a few nanoseconds and latched CHRDY for a cycle nobody would ever release. Rule: every self-holding CHRDY latch needs its hold gated by a decode sampled at /ADL (theXM_LATandIO_LATguards). -
“It works when tracing is on.” A detector for a stuck bus sampled /CMD once per millisecond. During the POST sweep over XMS almost every cycle is held, so 50 “asserted” samples in a row happened by chance, and the “recovery” then broke a live transfer. Rule: never conclude “bus stuck” from sparse samples of a level signal.
-
The 8570 would not boot at all. A self-test in the Core 1 init drove
0xAAonto the data bus while the planar was still coming out of reset. On the 8550Z the bus is idle at that moment, so it never showed there. -
DOOM: “Internal stack overflow. System halted.” Moving a single heartbeat counter increment to after the release made Core 1 late for the next cycle often enough to corrupt DOOM’s stack. One store, in the wrong place. (Rule 2 again.)
Results
On the 8550Z the POST counted 4736 KB (640 KB + 4096 KB). On the 8570 with a 4 MB planar it counted 8064 KB (3968 KB + 4096 KB), and HIMEM accepted it. XMSTEST, a tool I wrote for this, runs ten suites of patterns through the BIOS block move and passes on both machines.
Then I added 16-bit transfers: one bus cycle per word instead of two byte
cycles. XMSBENCH measures throughput with the BIOS block move (INT 15h, 87h),
8570, window at 0x240000, in KB/s:
| Transfer | 8-bit card | 16-bit card | Gain |
|---|---|---|---|
| Write, even address | 1033 | 1814 | +76% |
| Read, even address | 995 | 1814 | +82% |
| Write, odd address (byte lanes) | 969 | 1014 | |
| Read, odd address (byte lanes) | 896 | 928 | |
| Reference: conventional → conventional | 3548 | 3548 |
So the card’s XMS runs at about half the speed of the planar’s own RAM. That is slow for memory and fine for what it is used for: HIMEM, SmartDrive, DOS extenders, DOOM’s level data. DOOM runs on the 8570 with XMS, Sound Blaster and the USB keyboard all enabled.
One honest note on optimisation: I tried DMA prefetching from the PSRAM. It measured slower (1616 vs 1718 KB/s), so it is switched off. The numbers decide.
The 16-bit build had a surprise of its own. It froze on an I/O cycle, just
like an old snooping bug. The cause was a register allocation detail of GCC’s
asm goto: an output written early in the assembly shared its register with
an input that was still needed. One & fixed it ("=&l", “early clobber”).
Part 3: the CPLD reprograms itself
Why
Until now, every CPLD update meant a JTAG cable, the ATMISP software on a Windows PC, and taking the card out of the machine. That is fine for me and a barrier for anyone else who owns a WonderMCA. The RP2350 is right next to the CPLD with GPIOs to spare, so why not let it do the JTAG itself?
How it works
microSD RP2350 ATF1508AS
0:/UPGRADE/ bit-banged JTAG CPLD
+----------------+ +------------------+ SW1 +---------+
| cpld_erase.svf |---->| libxsvf player |---TCK-->| |
| cpld_upgrade_ | | | SW2 | JTAG |
| 123.svf | | GPIO 42 = TCK |---TMS-->| TAP |
| cpld_verif.svf | | GPIO 43 = TMS | SW3 | |
+----------------+ | GPIO 47 = TDI |---TDI-->| |
| GPIO 46 = TDO |<--TDO---| |
+------------------+ SW4 +---------+
| (via a buffer)
v
screenpad: progress bar, LEDs, result
- ATMISP produces standard SVF files: erase, program, verify. Copy them
into
UPGRADEon the microSD. The_123in the name is the CPLD revision, shown everywhere during the update. - At boot, the RP2350 plays them into the CPLD using libxsvf (Clifford Wolf’s SVF/XSVF player), clocking TCK at about 250 kHz from four GPIOs.
- The screenpad shows a progress bar with the yellow LED lit, then the result (revision, IDCODE, “POWER CYCLE NOW”). The board LED blinks during the update and finishes with three long flashes.
- With
CPLD_UPDATE_AUTO 0inconfig.txt, the screenpad lists the files and you play them one at a time by hand.
The DIP switches, and why they matter so much
The RP2350 does not have four spare pins. TCK and TMS share GPIO 42 and 43 with IRQ6 and IRQ7. Four DIP switches select which one each pin connects to.
That creates a real danger. If the bus handler were running with the switches in the JTAG position, every IRQ6 or IRQ7 the card raised would clock the CPLD’s JTAG state machine with a random TMS. So:
At power-on, BEFORE the bus handler starts:
read the switch positions from the pull resistors
|
+-- none set ---------> normal boot
|
+-- all set ----------> bus handler NOT started
| -> CPLD update
|
+-- some set, some not -> STOP. Red LED, switch
positions on the screenpad.
The card can do neither job.
A partial setting now halts the card deliberately. The first version let it boot on with no bus handler: alive, answering nothing. That is the worst kind of failure to diagnose.
The difficulties
-
TDO came back inverted. The first IDCODE read was
FEAF7FC0where the file expected0150803F: the exact bitwise complement. The board has an inverting buffer on TDO. The JTAG standard requires bit 0 of every IDCODE to be 1, so the firmware checks that bit and corrects the polarity by itself. -
“Out of memory” halfway through programming. libxsvf reallocates its buffers constantly, shrinking and growing them in turn, which fragmented the RP2350’s heap until an allocation failed. The fix keeps one grow-only buffer per purpose.
-
Files not found. My own file was named
cpld_ugrade_116.svf. The firmware now uses its own case-insensitive matcher, lists the whole folder and accepts that misspelling with a note. -
SW4 cannot be seen. The switch positions are read by fighting each external pull resistor with the RP2350’s internal pull. That works for TCK, TMS and TDI. TDO sits behind a buffer that drives the pin whichever way the switch is set. My first test said “off” when it was on, and my second said “on” when it was off. The honest answer is that SW4 cannot be read, so the firmware no longer judges it. A disconnected TDO shows up anyway: the IDCODE read fails and nothing is written.
Don’t lock the door behind you
The ATF1508’s JTAG pins can also be used as ordinary I/O. If a CPLD image ever
went out with JTAG disabled, or with the security fuse set, that card could
never be reprogrammed again, by this feature or by a cable. The .fit report
must say JTAG = ON and Security_mode = OFF. I checked the current build,
and the rule is now written into the reference document.
Status
The JTAG path reads the correct IDCODE, the SVF player runs, and programming reached the row writes before hitting the heap problem, which is now fixed. The first complete erase → program → verify run on the bench is the next step.
Part 4: board rev 8 and the USB panics
Everything above ran on board rev 8: RP2350B, ATF1508AS CPLD, 74LVC257 address mux, 8 MB QSPI PSRAM, microSD, USB host, W5500 Ethernet, WiFi, RTC, and the screenpad (OLED + keypad + four LEDs). Validating rev 8 meant, above all, making USB reliable.
Panic 1: “ep was already available”, and a voltage
Sustained USB mass-storage reads panicked, but only while Core 1 was serving the MCA bus. With Core 1 idle, the same stick read 73 MB flawlessly.
That load dependence pointed straight at Core 1, and I spent a long time there: dummy Core 1, poll-only Core 1, per-cycle-type stubs, code in flash versus SRAM, every bus-priority setting. All of it chased symptoms.
Panic 2: the orphaned AVAIL bit
The USB Ethernet dongles brought a new panic. The host scheduler shares one hardware endpoint between several transfers and aborts a transfer that keeps answering NAK. When that abort races a packet that is arriving, the buffer’s “available” bit is left set with nobody to clear it. The stock code could only wait and then panic.
Problem 3: exactly 50% packet loss
DHCP failed on most boots, and one ping in two was lost. One aborted transfer left the host’s data toggle (the DATA0/DATA1 sequence bit) one step behind the dongle. From then on, every other packet arrived with the “wrong” toggle and was dropped, until re-enumeration.
Problem 4: double buffering
The host’s bulk IN double buffering dropped the second buffer when a short packet arrived. It is forced to single buffering now.
All these USB issues had the same cause, the rev 5 of the board was a 2 layer PCB with small traces below the RP2350 crystal. This design was really not smart as the crystal was creating false edge on the rest of the PCB. Moving to 4 PCB layer and creating a clean zone around the main crystal solved the issue after endless days of debugging
Part 5: the Server 95 runs DOOM
01291300
The IBM PS/2 Server 95 (9595) is the fastest machine in my collection: a 486DX2 at 66 MHz. WonderMCA booted on it, but DMA sound stopped with error 01291300.
IBM’s Hardware Maintenance Manual calls 01291300 a “cache error”. It is not. IBM’s own field service tip database (the RETAIN tips) says it is a bus master arbitration error. It also says two things that made everything clear:
- On PS/2 systems at 66 MHz and above, adapters with marginal DMA timing time out. IBM’s fix for its own Multiprotocol Adapter was a hardware rework of the adapter, not of the system.
- The 9595’s High Speed Parallel Port arbitrates on the bus (by default at shared level 7) and is a known cause of 01291300. The standard remedy is to disable the arbitrable serial and parallel ports in the configuration.
How MCA DMA arbitration works (very short version)
WonderMCA is a DMA slave. To play a Sound Blaster sample it asks for the bus; the planar’s DMA controller then moves the byte on its behalf.
card wants a byte arbitration phase grant phase
-----------------> /PREEMPT low ARB/-GNT low: winner owns bus
ARB/-GNT high planar DMA: MEMR from RAM,
every requester drives then IOW to the card
its level on ARB0-3
(wire-AND: lowest wins)
/BURST -- tells the arbiter "I am doing a burst, don't take the bus back"
WonderMCA never bursts, so it must NEVER pull this low
Two things were wrong on the 9595:
- The parallel port was competing. With the High Speed Parallel Port disabled in the configuration, the arbitration conflict went away.
- /BURST was left to chance. The CPLD pin wired to MCA /BURST was declared
but had no equation, so its state was whatever the fitter chose. A /BURST
stuck low tells the arbiter a burst is in progress, which is precisely an
arbitration error. The smaller PS/2s do not care. The Server 95’s arbiter
does. CPLD rev 104 releases /BURST permanently (open-collector, high
impedance), the same way the reference Sound Blaster design
mcsb.vdoes.
With both fixes Prince of Persia played with sound on the 9595, and now DOOM runs too.
What I take away from these weeks
- Boot clean before theorising. One F5 test would have saved weeks on SmartDrive.
- Two decoders must agree. The CPLD and the RP2350 each decide whether a cycle is the card’s. If they disagree by even one address bit, you get ghosts.
- Release last. Anything Core 1 does after freeing a bus cycle is time stolen from the next one: a lost byte, a DOOM stack overflow.
- A lower voltage that fixes a bug means power, not logic.
- Don’t trust a pin you can’t see. If the hardware cannot tell you something (SW4), say so and prove it another way.
- The fastest machine finds the sloppiest signal. An undriven /BURST was harmless on six chassis and fatal on the seventh.
- Read IBM’s field notes. The RETAIN tips knew about 01291300 thirty years ago.
- Measure. The prefetch “optimisation” was slower. Only the benchmark told me.
What’s next
Beta testing opening 😉