{"id":433,"date":"2026-06-13T10:15:06","date_gmt":"2026-06-13T10:15:06","guid":{"rendered":"https:\/\/www.r3tr0.net\/?p=433"},"modified":"2026-06-13T10:30:30","modified_gmt":"2026-06-13T10:30:30","slug":"wondermca-part-4-from-silence-to-tada-making-sound-blaster-dma-work-on-an-mca-ps-2","status":"publish","type":"post","link":"https:\/\/www.r3tr0.net\/index.php\/2026\/06\/13\/wondermca-part-4-from-silence-to-tada-making-sound-blaster-dma-work-on-an-mca-ps-2\/","title":{"rendered":"WonderMCA part 4 &#8211; From silence to TADA &#8211; making Sound Blaster DMA work on an MCA PS\/2"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Why this was hard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When I started WonderMCA \u2014 a PicoMEM-style RP2350-based card for the IBM PS\/2 MCA bus \u2014 the obvious &#8220;killer app&#8221; was Sound Blaster emulation. FreddyV has worked hard to bring the PicoMEM a working SBDSP emulator for ISA Bus; on MCA we get the same software base.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Except for one little detail: the&nbsp;<strong>DMA<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The whole point of a Sound Blaster emulation is that it streams 8-bit PCM sample data from RAM to the DAC without the CPU babysitting every sample. That stream goes through DMA (Direct Memory Acces). On an ISA Sound Blaster the DMA is pretty simple \u2014 there&#8217;s a single 8237 DMA controller on the chassis, you wire the card&#8217;s DREQ\/DACK pins to one of its channels, and the controller&#8217;s hardware does the rest. Total mental load: 30 seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">MCA is not ISA. MCA has no 8237. MCA has&nbsp;<strong>CACP<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">MCA is not ISA. MCA has no 8237. MCA has&nbsp;<strong>CACP<\/strong>.<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Central Arbitration Control Point is what IBM substituted for the 8237 when they designed MCA in 1987 \u2014 a fully multi-master bus arbiter. Every device that wants the bus (including CPU, refresh, floppy controller, DMA-using card) participates in a 4-cycle priority arbitration. The winner gets the bus for one or a handful of cycles, then has to release. There&#8217;s a per-arbitration&nbsp;<strong>7.8 \u00b5s deadline<\/strong>: if a card holds the bus longer than that, the chassis fires NMI POST 113 and the OS halts. Trust me I know pretty well the error 113. On MCA Bus, DMA is complex, picky, undocumented, specific to PS\/2 model. A pure concentrate of nightmare to have something to work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the WonderMCA case, the Sound Blaster emulation will be managed with our card to be DMA Slave (and not DMA Master). It means that the WonderMCA never owns the BUS, it request and capture. simple no ?&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On the WonderMCA, we have the following actors:&nbsp;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The CACP &#8211; Central Arbitration Control Point on the chassis<\/li>\n\n\n\n<li>The MCA Bus &#8211; with the Address line, Data line, and control lines,<\/li>\n\n\n\n<li>The WonderMCA CPLD<\/li>\n\n\n\n<li>The WonderMCA Control SW (4xDIP Switch)<\/li>\n\n\n\n<li>The Wonder MCA RP2350B<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">After many (many many) attempt, and RP2350 PIO trial, I decided to shift from pure RP2350 and to combine it with a CPLD. The Arbitration process is not complex but combinational and requires a processing time that the RP2350 can not provide without losing the MEM \/ IO Path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I started a small proof of concept with and ATF22V10C, with the algorithm provided in the IBM specifications. Using my preferred Logic analyser, I was able to see the mecanism of the arbitration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Given the need for CHRDY signal based on Address line decoding, tight timeframe for raising from M\/IO, S0, S1, I decide to combined both CHRDY and DMA in a single CPLD with enough macrocell for my needs.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The CPLD would provides enough GPIO (macrocells) to manage: 24x Address lines, 16 MCA Bus signals (\/ADL, \/CMD, M\/IO, \/S0, \/S1, \/PREEMPT, ARB\/BNT, MADE24, SBHE, CHRDY, ARB0-3, BURST, TC), and interfaced GPIO between the RP2350. Plus I need a CPLD easy to reprogram meaning with dedicatd GPIO. Last but not least, we need a CPLD 5V compliant.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Putting all this into the shaker and you get very few option, and one good option is the ATF1508 that exist in PLCC84 format. The bad news, it is not produced anymore or difficult to source.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ATF1508 and a TagConnect 2050 is the perfect combo to be able to reprogram the CPLD using WinCUPL without removing the CPLD from the WonderMCA.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The ATF1508 exists in 7ns, 10ns, 15 ns, 20ns, 25ns grade. After doing some test, beyond 10ns on some chassis (IBM P70) you start getting mis assertion of CHRDY.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I bought a stock of ATF1508-10 on aliexpress that seems to work so far&#8230;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Going back to the DMA process, this is the step performed between the PS\/2 and the WonderMCA:&nbsp;<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The system, like PrinceOfPersia is programming the DMA Controller and provides a buffer in memory,<\/li>\n\n\n\n<li>It starts the Sound Blaster Emulation by issuing via IOW on port 0x220 CMD to be executed,<\/li>\n\n\n\n<li>The WonderMCA triggers a DMA request to get the next Byte on the buffer at 22 KHz frequency, roughly ~45 \u00b5s,\n<ol class=\"wp-block-list\">\n<li>The WonderMCA RP2350 assert \/RP_DMA_REQ (Interfaced GPIO between the RP2350 and the ATF1508),<\/li>\n\n\n\n<li>The CPLD assert \/PREEMPT when not in \/CMD phase, not in ARBITRATION phase,<\/li>\n\n\n\n<li>The CPLD waits for \/ARB_GNT to go High (Arbitration phase)<\/li>\n\n\n\n<li>The CPLD starts the arbitration providing ARB0-3 signal on the bus based on the 4xDIP switch configuration to enable DMA Channel 1-4<\/li>\n\n\n\n<li>The CPLD wins the arbitration, and deassert the \/PREEMPT signal,<\/li>\n<\/ol>\n<\/li>\n\n\n\n<li>The Chassis goes in grant phase \/ARB_GNT low<\/li>\n\n\n\n<li>The Chassis starts a IOW cycle<\/li>\n\n\n\n<li>The CPLD assert \/CHRDY<\/li>\n\n\n\n<li>The RP2350 capture the bytes, important to notice that there is no address lines signal on this IOW cycle,<\/li>\n\n\n\n<li>The RP2350 release \/CHRDY via \/RP_REL signal to the CPLD<\/li>\n\n\n\n<li>The RP2350 release \/RP_DMA_REQ,<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Sounds simple, on the paper yes, but in reality it is more than painful. Every single timing mistake or \/CHRDY holding the bus too long trigger a NMI&#8230; What is funny with error 113 (DMA timeout), it wait for the current DMA cycle end to trigger, so you never know which DMA req has pulled the trigger&#8230; If you keep \/PREEMPT more than 50 ns asserted after the GRANT phase -&gt; 113. And there are tons of undocumented case like these ones. And every error 113, you have to do a cold start, boot the system, prepare for the test and do it again and again&#8230; Nice ride.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s what we had to build, in hardware (CPLD), firmware (RP2350), and just enough copper on the PCB to make it all reach the bus.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Early on we made a key design call:&nbsp;<strong>the CPLD handles arbitration,<br>the RP2350 just streams data<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The RP2350 is a 150 MHz dual-core M33 with PIO. It&#8217;s plenty fast for sustained 22 kHz audio (\u224845 \u00b5s per sample = 6750 PIO cycles), but its GPIOs are way too slow to participate in MCA&#8217;s nanosecond-scale arbitration timing directly. The ATF1508 CPLD is a 10 ns PLD \u2014 perfect for handshakes; awful for streaming.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first PCB version of the WonderMCA has to ATF1508-10 and no PLCC84 footprint&#8230; However I put MCA Bus signal replication via pin-header on the side of the board. This is not ideal in term of capacitance, but very useful (mandatory) to debug via Logic Analyzer.&nbsp;<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"768\" height=\"1024\" src=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/IMG_1797-Large-768x1024.jpeg\" alt=\"\" class=\"wp-image-429\" style=\"width:400px\" srcset=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/IMG_1797-Large-768x1024.jpeg 768w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/IMG_1797-Large-225x300.jpeg 225w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/IMG_1797-Large-600x800.jpeg 600w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/IMG_1797-Large.jpeg 960w\" sizes=\"auto, (max-width: 768px) 100vw, 768px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To validate the use of the CPLD, I have create a small daughter board and asked JLCPCB 5x sample. I bridged the pin Header with the daughter board. I had to put a few bodge wire between the RP2350 and the CPLD.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"970\" src=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-11-at-11.14.16-1024x970.png\" alt=\"\" class=\"wp-image-432\" style=\"width:400px\" srcset=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-11-at-11.14.16-1024x970.png 1024w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-11-at-11.14.16-300x284.png 300w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-11-at-11.14.16-768x727.png 768w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-11-at-11.14.16-600x568.png 600w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-11-at-11.14.16.png 1136w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The first prototype now looks like a spaghetti plate and I have time to time connectivity issues, but still good for testing.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Division of labor:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>CPLD<\/strong>: watch the sample-rate timer fire (via&nbsp;<code>RP_DMA_REQ<\/code>), raise&nbsp;<code>\/PREEMPT<\/code>, drive arbitration code, claim grant, drive&nbsp;<code>\/ADL<\/code>\/<code>\/CMD<\/code>&nbsp;for one IOR\/IOW cycle, release.<\/li>\n\n\n\n<li><strong>RP2350<\/strong>: on each cycle the CPLD opens, present the next sample byte (for SBDSP playback) or capture the byte (for ADC).<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That keeps the latency-critical handshake inside 10 ns silicon and the bulk-data path in software. Clean.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three GPIOs link the two:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>RP_DMA_REQ<\/code>&nbsp;(out): RP2350 raises HIGH when it wants the next byte<\/li>\n\n\n\n<li><code>RP_DMA_GRANT<\/code>&nbsp;(in): CPLD pulses HIGH when arbitration wins<\/li>\n\n\n\n<li><code>RP_TC<\/code>&nbsp;(in): CPLD pulses HIGH at terminal count<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Bodge wire #1 \u2014&nbsp;<code>\/PREEMPT<\/code>&nbsp;doesn&#8217;t go anywhere<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I plugged in the first WonderMCA WM10 PCB, programmed the CPLD with a<br>freshly-written&nbsp;<code>at1508-wincpl.pld<\/code>, fired up&nbsp;<code>dmatest<\/code>, and watched the logic analyzer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The chassis&#8217;s&nbsp;<code>\/PREEMPT<\/code>&nbsp;line: rock solid HIGH. Never moved.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The CPLD&#8217;s pin 49: pulsing correctly when REQ went high. Output was fine, just not reaching the chassis. I traced the PCB and found a mistake in the schematic:&nbsp;<strong>CPLD pin 49 (the&nbsp;<code>\/PREEMPT<\/code>&nbsp;output) was not routed to MCA edge connector pin A21<\/strong>. The trace ended in a via that went nowhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Out came the soldering iron. A 30-gauge enamelled wire from CPLD pin 49 to MCA A21 on the back of the card, soldered in by hand under a microscope. Forty-five seconds of work, one of the more important pieces of metal in the project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After the bodge:&nbsp;<code>\/PREEMPT<\/code>&nbsp;started reaching the chassis. Now we could actually request the bus.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">CPLD revision purgatory<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The CPLD source is compiled with WinCUPL. Between May 12 and June 4, the .pld went through more than 30 revisions trying to make the DMA path work cleanly across all chassis (8550Z \/ 8555 \/ 8556 \/ 8557 \/ 8570 \/ P70 \/ 8580). Most of those revisions did one thing each. Three of them taught me something I didn&#8217;t already know.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rev 40 \u2014 the blk_220 decode bug<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Symptom: SBDSP I\/O reads at base 0x220 always returned&nbsp;<code>FF<\/code>. From the<br>RP2350 side, the byte WAS being driven onto the bus correctly. Something between us and the chassis was masking it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The CPLD source had:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>blk_220 = !A11 &amp; !A10 &amp;  A9 &amp;  A8 &amp;  A7 &amp;  A6 &amp; !A5 &amp; !A4;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That looked right. Except. Four of those signal polarities were&nbsp;<em>inverted on the actual silicon<\/em>&nbsp;compared to my mental model \u2014 the WinCUPL fitter had selected a target product term that needed inversion, but the original CUPL equation read the bare signals. Net effect:&nbsp;<code>blk_220<\/code>&nbsp;was decoding&nbsp;<code>0x180<\/code>&nbsp;instead of&nbsp;<code>0x220<\/code>. Our &#8220;SBDSP at 0x220&#8221; was actually &#8220;nothing at 0x220, something else mysterious at 0x180&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fixed in rev 40 by re-deriving the equation against the actual chassis bus signals (rather than what I&#8217;d assumed the bare CPLD inputs looked like). Lesson: the CUPL source isn&#8217;t the silicon \u2014<br>always verify the decode with an LA capture before assuming logic errors.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rev 48 \u2014 the WinCUPL output-enable footgun<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">CACP needs&nbsp;<code>\/PREEMPT<\/code>&nbsp;to be open-drain \u2014 multiple masters share the line, anyone can pull it LOW, no one drives it HIGH. The ATF1508 has no native OD pin mode, so you emulate OD with the&nbsp;<code>.OE<\/code>&nbsp;(output enable) attribute: drive&nbsp;<code>0<\/code>when you want LOW, tri-state when you want HIGH, and the bus has external pull-ups.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I tried the obvious:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PIN 49 = !PREMPT_OUT;\nPREMPT_OUT.OE = PREMPT_OUT;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It compiled clean. It didn&#8217;t work.&nbsp;<code>\/PREEMPT<\/code>&nbsp;either floated or stayed HIGH \u2014 never asserted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After hours staring at LA captures, I read the WinCUPL docs more carefully and found the issue.&nbsp;<strong>You cannot use the same signal as both the input to a pin equation AND as its&nbsp;<code>.OE<\/code>&nbsp;controller.<\/strong>&nbsp;The fitter silently produces dead logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The pattern that works:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PIN 49 = PREMPT_INT;        \/* drive whatever this signal is *\/\nPREMPT_INT    = &#039;b&#039;0;       \/* always-LOW intent *\/\nPREMPT_INT.OE = some_cond;  \/* but only enable when some_cond *\/\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two named signals \u2014 one for the value, one for the OE control, no self-reference. That&#8217;s the OD-emulation pattern.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While I was at it, rev 48 also added a&nbsp;<strong>10 k\u03a9 external pull-down on&nbsp;<code>RP_DMA_REQ<\/code><\/strong>. Without it, between RP2350 boots and CPLD program, the line floated and CACP saw spurious REQ assertions. Pull-down fixed it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rev 48 was the first build where single-byte AND multi-byte DMA both worked end-to-end. I ran&nbsp;<code>sbdma 384<\/code>&nbsp;(384 samples at 22 kHz) and got my first burst of &#8220;real&#8221; audio: a 16 ms square wave tone through the I2S DAC. Tinny, brief, and the most satisfying noise I&#8217;ve heard in months.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Hours of debugging what POST 113 actually means<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Between rev 48 and rev 57, something kept tripping us up:&nbsp;<strong>NMI 113<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Anyone working on MCA cards knows the dread of NMI POST 113 \u2014 &#8220;Card arbitration timeout, system halted.&#8221; For days I assumed POST 113 fires&nbsp;<em>during<\/em>&nbsp;a too-slow cycle: the chassis sees you holding the bus past the 7.8 \u00b5s deadline mid-arbitration, gives up, NMIs. So I went after every microsecond of latency inside the cycle handler. Tightened the data drive. Pre-computed responses before REP_REL assertion. Bisected the firmware path. Recoded the CPLD wait-state logic three different ways. Each round I felt sure I&#8217;d fixed it; each round POST 113 came back, often after hundreds of perfectly clean cycles had already streamed through.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The breakthrough came when I finally instrumented the chassis side<br>properly \u2014 capturing the exact tick at which the chassis port 0x90 bit<br>5 (the CACP &#8220;arb timeout&#8221; status bit) went HIGH and tracing back along<br>the LA capture.&nbsp;<strong>POST 113 wasn&#8217;t firing during a slow cycle. It was firing AFTER a full, otherwise-clean cycle that should have ended.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The 7.8 \u00b5s CACP budget isn&#8217;t a deadline you have to finish&nbsp;<em>each<\/em>&nbsp;cycle by. It&#8217;s a deadline by which you have to&nbsp;<strong>release the bus<\/strong>. The cycle itself can be fast \u2014 sub-microsecond even \u2014 but if you hold on for too long&nbsp;<em>after<\/em>&nbsp;the cycle&#8217;s data transfer is over, the chassis decides you&#8217;ve timed out. Whether you&#8217;re actively driving data or not is irrelevant. What CACP cares about is &#8220;have you given the bus back yet.&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So we had a release problem, not a cycle-speed problem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With that reframing, the actual bug fell out in twenty minutes. I captured a long burst of streaming IOWs and noticed something specific: one IOW out of every few hundred wasn&#8217;t dropping CHRDY at the end. The data byte got through. The chassis even latched it. But our CPLD&#8217;s&nbsp;<code>CHRDY_IO<\/code>&nbsp;signal stayed asserted (LOW) past the cycle&#8217;s natural end \u2014 adding a phantom 8-10 \u00b5s wait state to the&nbsp;<em>next<\/em>&nbsp;arb window. Those extra microseconds, added to the normal ~5 \u00b5s cycle time, were just enough to push the total bus-hold past CACP&#8217;s 7.8 \u00b5s<br>release deadline. POST 113 fired&nbsp;<em>several<\/em>&nbsp;cycles later, because that was when the chassis&#8217;s next attempted re-arbitration noticed we hadn&#8217;t actually let go.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The proximate cause was a race in&nbsp;<code>CHRDY_IO<\/code>&#8216;s self-latching feedback<br>term \u2014 under specific timing conditions it would re-assert itself a<br>clock after&nbsp;<code>HANDLED_IO<\/code>&nbsp;cleared, briefly stretching its release. Killing the self-feedback in rev 56 closed that window. After rev 56: 30-minute Wolf3D sessions with no POST 113.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The general lesson sat there glowing:&nbsp;<strong>POST 113 tells you what<br>condition was violated, but not when or why.<\/strong>&nbsp;Always trace back from the chassis-side status bit, not forward from the cycle you think caused it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rev 57 \u2014 Wolf3D&#8217;s gun, and the bus-handler contamination<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rev 56 fixed POST 113. For a moment everything was great \u2014 TADA<br>played, test waveforms streamed cleanly, the chassis stopped NMIing.<br>Then I tried Wolf3D.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wolf3D&#8217;s first level loads level data off disk on demand. It also plays a digital gun-shot sample (~50 ms at ~11 kHz) every time you fire. The sample loops through the same SBDSP DMA path we&#8217;d just made stable. By itself: fine. Walk through a door, fire the gun, hear it cleanly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But when the player fired&nbsp;<em>while the next room was loading off the HDD<\/em>, the chassis crashed within seconds. Not POST 113. Not silence. A hard freeze, sometimes preceded by a Wolf3D error message about corrupted level data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It took me longer than I&#8217;d like to admit to realize the twosymptoms were one bug.&nbsp;<strong>The disk reads were returning garbage, and the DMA cycle immediately before each disk read was the cause.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s what was happening, in the order the bus saw it:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>SBDSP timer fires. Pico raises&nbsp;<code>RP_DMA_REQ<\/code>.<\/li>\n\n\n\n<li>CPLD wins arbitration, runs an IOW to&nbsp;<code>0xFFFC<\/code>&nbsp;(the CDMA data port). Per rev 49-53&#8217;s &#8220;clean dreq_gated&#8221; change,&nbsp;<strong>CHRDY was no longer asserted on this DMA IOW<\/strong>. The cycle still completed \u2014 the chassis CDMA latched its byte off D0..D7 \u2014 but our RP2350 bus handler never got the CHRDY-released event it was waiting on to advance its internal handshake state machine.<\/li>\n\n\n\n<li>The handler exited the IOW path with its state half-updated: <code>HANDLED_IO<\/code>&nbsp;cleared on time, but&nbsp;<code>RP_REL<\/code>&nbsp;(the &#8220;I&#8217;ve released the bus&#8221; gate) hadn&#8217;t been driven by the CHRDY-release path that normally co-fires it.<\/li>\n\n\n\n<li>Within ~5-10 \u00b5s, the chassis BIOS (in the middle of servicing a disk INT 13h read for Wolf3D) issued the next MEMR to fetch a sector byte from RAM emulated on our card.<\/li>\n\n\n\n<li>The bus handler entered the MEMR path with stale&nbsp;<code>RP_REL<\/code>&nbsp;state. The address-latch sequence read a previous cycle&#8217;s address bits instead of the new MEMR&#8217;s, so we drove the wrong byte onto the bus.<\/li>\n\n\n\n<li>Wolf3D&#8217;s disk reader got a wrong byte. After a few hundred of those, the level data was hopelessly corrupted. Wolf3D either detected the corruption and threw a &#8220;bad data&#8221; error, or just loaded the garbage as level geometry and tried to render it, crashing.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">So the chain was:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">POST 113 fix in rev 49-53 \u2192 CHRDY removed from DMA path \u2192 bus-handler state-machine never sees its expected CHRDY-release event on DMA IOWs \u2192 leaves stale state \u2192 next non-DMA cycle (typically the disk-read MEMR) inherits the bad state \u2192 wrong byte on the bus \u2192 silent data corruption \u2192 eventual app or OS failure.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">The disk crashes only showed up under Wolf3D because Wolf3D was the first thing I tried that&nbsp;<strong>mixed sustained DMA with concurrent disk activity<\/strong>. SBDIAG plays TADA but does nothing else on the bus \u2014 no disk, no other I\/O. So SBDIAG looked clean while the bug<br>was latent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rev 57 fixed it by restoring CHRDY for DMA IOW cycles specifically, via a parallel&nbsp;<code>io_match_chrdy<\/code>&nbsp;signal that includes the DMA terms while keeping the REP_REL path clean:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>io_match       = blk_2A0 # blk_100 # blk_320 # blk_300 # blk_220 # blk_388;\nio_match_chrdy = io_match\n              # (blk_FFFC &amp; active_grant)\n              # (active_grant &amp; MIO);\n\nsfdbk_internal = (io_match &amp; valid_io) # (mem_match &amp; valid_mem);\nCHRDY_IO       = !RP_REL &amp; !HANDLED_IO\n               &amp; ((io_match_chrdy &amp; valid_io) # CHRDY_IO);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two separate decoders, fed from the same logic but with different<br>selectivity. SFDBK stays clean (DMA grants don&#8217;t trigger card SFDBK responses, which would confuse the chassis). CHRDY extends DMA IOW cycles so the firmware bus handler sees its expected handshake sequence and leaves clean state for the next MEMR\/MEMW to inherit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After rev 57: gun shot played, disk reads stayed clean, level loaded, Wolf3D ran. The chassis didn&#8217;t NMI. The cross-contamination between the DMA path and the rest of the bus handler was finally gone.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"857\" src=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-13-at-12.05.36-1024x857.png\" alt=\"\" class=\"wp-image-440\" style=\"width:600px\" srcset=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-13-at-12.05.36-1024x857.png 1024w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-13-at-12.05.36-300x251.png 300w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-13-at-12.05.36-768x642.png 768w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-13-at-12.05.36-600x502.png 600w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/Screen-Shot-2026-06-13-at-12.05.36.png 1334w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The deeper lesson here, beyond CHRDY-on-DMA-IOW:&nbsp;<strong>the RP2350 bus handler is a state machine that spans multiple cycles<\/strong>. If one cycle leaves it in an unexpected state \u2014 even a cycle that looks &#8220;complete enough&#8221; from the chassis&#8217;s perspective \u2014 the next cycle inherits the contamination. CHRDY isn&#8217;t just a wait state for the chassis; it&#8217;s also the firmware&#8217;s &#8220;OK I&#8217;m done, you can advance&#8221; synchronization signal. Drop it on one cycle type and you corrupt the next.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The firmware side<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hardware was now stable. The remaining mile was firmware pacing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The SBDSP emulator on the RP2350 uses a Pico hardware timer to fire at the configured sample rate (8 kHz \/ 11 kHz \/ 22 kHz \/ 44 kHz). Each timer fire calls&nbsp;<code>wm_dma_start_write()<\/code>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Load the next sample byte into the bus handler&#8217;s tx register.<\/li>\n\n\n\n<li>Set&nbsp;<code>req_pending = true<\/code>.<\/li>\n\n\n\n<li>Raise&nbsp;<code>RP_DMA_REQ<\/code>&nbsp;HIGH (which the CPLD sees as &#8220;next byte ready,<br>please arbitrate&#8221;).<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The CPLD then runs the full arbitration + IOR\/IOW cycle and the firmware&#8217;s&nbsp;<code>iow_handler_done<\/code>&nbsp;runs after the cycle completes:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Drop&nbsp;<code>RP_DMA_REQ<\/code>&nbsp;LOW (one byte done, no more for now).<\/li>\n\n\n\n<li><strong>Pulse&nbsp;<code>\/PREEMPT<\/code>&nbsp;HIGH for ~20 \u00b5s<\/strong>&nbsp;(= 920 NOPs at 150 MHz).<\/li>\n\n\n\n<li>Return to main loop.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">That&nbsp;<code>\/PREEMPT<\/code>&nbsp;post-cycle pulse is critical. CACP&#8217;s 7.8 \u00b5s arbitration deadline isn&#8217;t a per-cycle limit \u2014 it&#8217;s a per-grant limit. If we hold the bus continuously across multiple cycles, we<br>exceed it. The 20 \u00b5s pulse forces CACP to release us at the end of every IOW, run a fresh arbitration round (which the CPU usually wins on idle systems, but only briefly), then re-grant us for the next sample.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A&nbsp;<code>tc_latched<\/code>&nbsp;back-pressure check in&nbsp;<code>wm_dma_start_write<\/code>&nbsp;and a<br>3-miss watchdog in&nbsp;<code>pm_cmd.cpp<\/code>&nbsp;close the loop: if the IOW never fires (CDMA stalled, bus contention, anything), we don&#8217;t infinitely re-raise REQ.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The full picture: SBDSP DMA cycle from app to ear<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s worth drawing the whole loop end-to-end, because the IRQ at the end is what closes it. Without the IRQ the DOS app has no idea its buffer played; without the streaming the IRQ never fires; without the release pulse CACP NMIs you. All three have to land in order.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1016\" src=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-1024x1016.png\" alt=\"\" class=\"wp-image-450\" srcset=\"https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-1024x1016.png 1024w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-300x298.png 300w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-150x150.png 150w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-768x762.png 768w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-1536x1524.png 1536w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-600x595.png 600w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow-100x100.png 100w, https:\/\/www.r3tr0.net\/wp-content\/uploads\/2026\/06\/sbdsp_dma_flow.png 1568w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code><\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step-by-step<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>App configures sample rate.<\/strong>\u00a0DOS app does\u00a0<code>OUT 0x22C, 40h<\/code>\u00a0(set time-constant command) then a second\u00a0<code>OUT 0x22C<\/code>\u00a0with the rate byte. Pico&#8217;s SBDSP emulator translates that to a hardware-timer frequency.<\/li>\n\n\n\n<li><strong>App starts DMA playback.<\/strong>\u00a0<code>OUT 0x22C, 14h<\/code>\u00a0(DSP command 14h = 8-bit single-cycle DMA output) followed by two bytes for the buffer length (-1, low + high). The emulator does NOT touch CDMA directly here \u2014 it just records the playback parameters and arms its internal timer. The app is also responsible for having already programmed the chassis CDMA controller (via\u00a0<code>OUT<\/code>\u00a0to ports 0x18-0x1F) with the source memory address + count + mode.<\/li>\n\n\n\n<li><strong>Per-sample timer fire.<\/strong>\u00a0At each sample interval the SBDSP emu loads the next byte from its FIFO into the bus-handler&#8217;s TX register and raises\u00a0<code>RP_DMA_REQ<\/code> to tell the CPLD &#8220;I want one bus cycle now.&#8221;<\/li>\n\n\n\n<li><strong>CPLD asks for the bus.<\/strong>\u00a0Pulses\u00a0<code>\/PREEMPT<\/code>\u00a0LOW. Chassis sees the request and queues an arbitration round.<\/li>\n\n\n\n<li><strong>CACP runs arbitration.<\/strong>\u00a0All competing masters drive their priority codes onto\u00a0<code>ARB[3:0]<\/code>\u00a0in parallel; the chassis grants the bus to the highest-priority. Our card uses a low priority (CPU-friendly), but on an idle chassis we win quickly.<\/li>\n\n\n\n<li><strong>The IOW cycle.<\/strong>\u00a0CPLD drives\u00a0<code>M\/-IO=0<\/code>, pulls\u00a0<code>\/ADL<\/code>\u00a0then<code> \/CMD<\/code>\u00a0LOW, holds CHRDY LOW for a few extra ns (rev 57&#8217;s wait state) so the chassis CDMA controller has guaranteed data setup time, then watches CDMA latch the byte off D0..D7.<\/li>\n\n\n\n<li><strong>CPLD releases.<\/strong>\u00a0<code>\/CMD<\/code>\u00a0HIGH,\u00a0<code>\/ADL<\/code>\u00a0HIGH, CHRDY HIGH, byte delivered.<\/li>\n\n\n\n<li><strong>Post-cycle cleanup.<\/strong>\u00a0Firmware&#8217;s\u00a0<code>iow_handler_done<\/code>\u00a0drops <code>RP_DMA_REQ<\/code>\u00a0LOW (no more bytes pending right now) and pulses<code>\/PREEMPT<\/code>\u00a0HIGH for ~20 \u00b5s, forcing CACP to release us and re-arbitrate. The CPU usually wins the next round on an otherwise-idle chassis; we wait for the next timer fire.<\/li>\n\n\n\n<li><strong>Terminal count.<\/strong>\u00a0When CDMA&#8217;s down-counter reaches zero, it asserts\u00a0<code>\/TC<\/code>\u00a0on the bus for one cycle. CPLD latches that into <code>RP_TC<\/code>\u00a0and the Pico&#8217;s SBDSP emulator knows the buffer is done.<\/li>\n\n\n\n<li><strong>DSP IRQ.<\/strong>\u00a0SBDSP emulator raises the configured SB IRQ (typically IRQ 5 on SB \/ SB2 \/ SB Pro). The IRQ flows through PMBIOS&#8217;s PM_Int multiplexer to the chassis 8259, which dispatches\u00a0<code>INT 0Dh<\/code>\u00a0(or whatever the SB IRQ vector maps to) to the DOS app&#8217;s registered ISR.<\/li>\n\n\n\n<li><strong>App ACKs + repeats.<\/strong>\u00a0The app reads\u00a0<code>0x22E<\/code>\u00a0to clear the DSP&#8217;s IRQ flag, then either programs the next buffer (single-cycle mode) or leaves the DSP to keep streaming (auto-init mode where the DSP re-loads count\/address itself).<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The whole loop runs continuously for the duration of audio playback. At 22 kHz with a 4096-byte buffer, step 3 through step 8 fire 4096 times before step 9-11 fire once. At 44 kHz with the same buffer, it&#8217;s 4096 times in ~93 ms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each of those steps has a window where things can break:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Step 4: CPLD output not actually reaching MCA edge (bodge wire fix)<\/li>\n\n\n\n<li>Step 5: priority code wrong or arbitration timing off<\/li>\n\n\n\n<li>Step 6: CHRDY not extending enough (Wolf3D gun shot regression)<\/li>\n\n\n\n<li>Step 7: CHRDY self-latching, holding bus past CACP deadline (POST 113)<\/li>\n\n\n\n<li>Step 8: pulse too short \u2192 CACP doesn&#8217;t re-arbitrate<\/li>\n\n\n\n<li>Step 9: \/TC ignored \u2192 endless playback or missed buffer boundary<\/li>\n\n\n\n<li>Step 10: IRQ multiplexer drops the SB IRQ \u2192 app&#8217;s ISR never fires<\/li>\n\n\n\n<li>Step 11: app&#8217;s ACK not reaching DSP \u2192 DSP holds IRQ asserted, locks<br>up next cycle<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Every one of those windows produced a unique class of debugging sessions, with its own LA capture and its own forehead-on-desk moment. Getting all 11 right at the same time is what &#8220;MCA audio works&#8221; actually means.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">First TADA<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">June 3, 2026. The 8556 ran\u00a0<code>SBDIAG \/A220 \/I5 \/D1<\/code>. The autodetect passed. The Sound Blaster Diagnostic test played TADA. I had to play it three times to convince myself it was real audio and not a test pattern I&#8217;d memorized from the LA capture.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That same evening Wolf3D&#8217;s gun fired cleanly.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The chassis matrix turned green:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>8550Z (286, 10 MHz): SBDIAG \u2713, Wolf3D \u2713<\/li>\n\n\n\n<li>8555 (286, 16 MHz): \u2713<\/li>\n\n\n\n<li>8556 (286, 16 MHz): \u2713<\/li>\n\n\n\n<li>8557 (286, 16 MHz): \u2713<\/li>\n\n\n\n<li>8570 (386, 25 MHz): \u2713 (after the unrelated MADE24 fix)<\/li>\n\n\n\n<li>P70 (gas-plasma 386): \u2713<\/li>\n\n\n\n<li>8580 (386): \u2713<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">First sustained MCA audio on a Pico-based card. Probably 30 years<br>since IBM gave any thought to whether such a thing was possible.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>MCA is not a more-complicated ISA<\/strong>. It&#8217;s a different bus architecture. CACP is not a more-complicated 8237. If you try to port ISA DMA thinking to MCA you&#8217;ll spend a week confused. Read the HITRA02 spec section on extended-DMA before writing a single line of CUPL.<\/li>\n\n\n\n<li><strong>The CPLD source is not the silicon<\/strong>. Always verify with a logic analyzer that the decode matches what you wrote. The blk_220 bug would have eaten a week longer if I&#8217;d kept staring at <code>.pld<\/code>&nbsp;source instead of the LA.<\/li>\n\n\n\n<li><strong>WinCUPL has silent footguns<\/strong>. The OD-emulation pattern <code>PIN N = !X; X.OE = X<\/code>&nbsp;looks right, compiles clean, and produces dead logic. The right pattern is two named signals.<\/li>\n\n\n\n<li><strong>You can&#8217;t have it all on a clock-by-clock basis<\/strong>. Rev 49\u201353 tried to keep&nbsp;<code>dreq_gated<\/code>&nbsp;perfectly clean by removing DMA terms from&nbsp;<code>io_match<\/code>. That broke CHRDY on DMA writes, which corrupted audio. Rev 57 split into two decoders \u2014 one clean for SFDBK, one inclusive for CHRDY \u2014 and got both. Sometimes the right answer is two equations, not one cleverer one.<\/li>\n\n\n\n<li><strong>Patch wires are part of the design<\/strong>&nbsp;until proven otherwise. The&nbsp;<code>\/PREEMPT<\/code>&nbsp;bodge wire stayed in for six weeks before the WM11 board revision picked it up in copper. Patch wires aren&#8217;t ugly \u2014 they&#8217;re the fastest way to move forward when copper is wrong.<\/li>\n\n\n\n<li><strong>CACP timing budgets are forgiving for paced traffic, brutal for bursts.<\/strong>&nbsp;SBDSP at 22 kHz = one cycle per 45 \u00b5s, well below the per-grant deadline. A na\u00efve &#8220;DMA as fast as possible&#8221; loop blows past it in microseconds. Pace your DMA to the audio rate; let CACP rest between cycles; the chassis is happy.<\/li>\n\n\n\n<li><strong>POST 113 fires for &#8220;you didn&#8217;t release&#8221;, not &#8220;your cycle was<br>slow&#8221;.<\/strong>&nbsp;I spent days chasing nanoseconds inside the cycle handler before I realized the chassis was unhappy about a CHRDY self-latch that briefly re-asserted after the cycle&#8217;s natural end \u2014 extending the&nbsp;<em>release<\/em>&nbsp;window past the deadline. The cycle itself was fine. Don&#8217;t optimize what the symptom doesn&#8217;t actually blame. When you see POST 113, trace back from the chassis-side status bit (port 0x90 bit 5), not forward from the most recently-completed cycle.<\/li>\n\n\n\n<li><strong>First sound is a milestone worth celebrating<\/strong>&nbsp;even if it&#8217;s a square wave. The gap between &#8220;DMA is theoretically working&#8221; and &#8220;music plays&#8221; is bigger than you think; finishing it changes how you debug the rest.<\/li>\n\n\n\n<li><strong>First-pass instrumentation lies.<\/strong>&nbsp;I had POST tracing on the serial UART from day one, and it told me POST 113 fired &#8220;after the last DMA cycle&#8221; \u2014 which I correctly read as &#8220;near the end of a cycle&#8221;. What it actually meant was &#8220;8-10 \u00b5s after the last cycle&#8217;s apparent end, because something didn&#8217;t release&#8221;. Until you wire the LA to the chassis-side status bit you&#8217;re guessing. The half-day spent on the instrumentation paid for itself many times over.<\/li>\n\n\n\n<li><strong>Bus cycles aren&#8217;t independent \u2014 they share a state machine.<\/strong> The RP2350 bus handler keeps state across cycles (<code>RP_REL<\/code>, <code>HANDLED_IO<\/code>, address-latch registers). If one cycle leaves that state half-set \u2014 because we silenced a signal it was waiting on, like CHRDY on DMA IOW \u2014 the&nbsp;<em>next<\/em>&nbsp;cycle inherits the contamination, and the wrongness shows up there instead of on the cycle that actually caused it. Wolf3D&#8217;s &#8220;disk read corruption&#8221; was really &#8220;the DMA IOW immediately before each disk MEMR left the handler in a bad state, so the disk MEMR read the wrong byte.&#8221; Always trace one cycle backward when debugging &#8220;this cycle is wrong&#8221; \u2014 the cause is often the one before.<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>Why this was hard When I started WonderMCA \u2014 a PicoMEM-style RP2350-based card for the IBM PS\/2 MCA bus \u2014 the obvious &#8220;killer app&#8221; was Sound Blaster emulation. FreddyV has worked hard to bring the PicoMEM a working SBDSP emulator for ISA Bus; on MCA we get the same software base. Except for one little [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-433","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/posts\/433","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/comments?post=433"}],"version-history":[{"count":6,"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/posts\/433\/revisions"}],"predecessor-version":[{"id":452,"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/posts\/433\/revisions\/452"}],"wp:attachment":[{"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/media?parent=433"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/categories?post=433"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.r3tr0.net\/index.php\/wp-json\/wp\/v2\/tags?post=433"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}